Merge lp:~richardw/jarmon/customisable-charts into lp:jarmon

Proposed by Richard Wall
Status: Merged
Approved by: Richard Wall
Approved revision: 95
Merge reported by: Richard Wall
Merged at revision: not available
Proposed branch: lp:~richardw/jarmon/customisable-charts
Merge into: lp:jarmon
Diff against target: 1640 lines (+1026/-336)
7 files modified
docs/examples/assets/css/style.css (+37/-4)
docs/examples/index.html (+18/-159)
docs/examples/jarmon_example_recipes.js (+72/-76)
jarmon/jarmon.js (+695/-76)
jarmon/jarmon.test.js (+178/-0)
jarmonbuild/commands.py (+25/-17)
test.html (+1/-4)
To merge this branch: bzr merge lp:~richardw/jarmon/customisable-charts
Reviewer Review Type Date Requested Status
Richard Wall Approve
Review via email: mp+64335@code.launchpad.net

Commit message

Various changes and re-factoring towards implementation of a customisable interface. Old and unfinished but needs merging before starting work on Jquery 1.5 compatibility work.

To post a comment you must log in.
Revision history for this message
Richard Wall (richardw) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'docs/examples/assets/css/style.css'
--- docs/examples/assets/css/style.css 2010-07-22 11:35:48 +0000
+++ docs/examples/assets/css/style.css 2011-06-12 15:34:30 +0000
@@ -9,11 +9,7 @@
9}9}
1010
11h2 {11h2 {
12 padding: 0 0 0 55px;
13 margin: 20px auto 5px auto;
14 font-size: 14px;12 font-size: 14px;
15 text-align: left;
16 clear: both;
17}13}
1814
19p, li, dt, dd, td, th, div {15p, li, dt, dd, td, th, div {
@@ -36,6 +32,7 @@
36 height:200px;32 height:200px;
37 width: 850px;33 width: 850px;
38 margin: 0 auto 0 auto;34 margin: 0 auto 0 auto;
35 clear: both;
39}36}
4037
41.tickLabel {38.tickLabel {
@@ -75,6 +72,11 @@
75 border: none;72 border: none;
76}73}
7774
75input[type=text] {
76 padding: 3px;
77 border: 1px solid #EEE;
78}
79
78.notice {80.notice {
79 border: 1px solid Green;81 border: 1px solid Green;
80 background: #FFDDFF;82 background: #FFDDFF;
@@ -85,3 +87,34 @@
85#calroot {87#calroot {
86 z-index: 2;88 z-index: 2;
87}89}
90
91.chart-header {
92 width: 790px;
93 padding: 5px 0 5px 0;
94 margin: 20px auto 0 auto;
95 position: relative;
96 left: 25px;
97}
98
99.chart-header:AFTER {
100 content: ''
101}
102
103.chart-container h2{
104 float: left;
105 margin: 0;
106}
107
108.chart-container .chart-controls{
109 float: right;
110 margin: 0;
111}
112
113.tab-controls {
114 width: 790px;
115 padding: 5px 0 5px 0;
116 margin: 20px auto 0 auto;
117 text-align: right;
118 position: relative;
119 left: 25px;
120}
88121
=== modified file 'docs/examples/index.html'
--- docs/examples/index.html 2010-10-03 23:54:48 +0000
+++ docs/examples/index.html 2011-06-12 15:34:30 +0000
@@ -16,151 +16,15 @@
16 <script type="text/javascript" src="../../jarmon/jarmon.js"></script>16 <script type="text/javascript" src="../../jarmon/jarmon.js"></script>
17 <script type="text/javascript" src="jarmon_example_recipes.js"></script>17 <script type="text/javascript" src="jarmon_example_recipes.js"></script>
18 <script type="text/javascript">18 <script type="text/javascript">
19 // Recipes for the charts on this page
20
21 var application_recipes = [
22 {
23 title: 'Jarmon Webserver TCP Stats',
24 data: [
25 ['data/tcpconns-8080-local/tcp_connections-CLOSE_WAIT.rrd', 0, 'CLOSE_WAIT', ''],
26 ['data/tcpconns-8080-local/tcp_connections-SYN_RECV.rrd', 0, 'SYN_RECV', ''],
27 ['data/tcpconns-8080-local/tcp_connections-TIME_WAIT.rrd', 0, 'TIME_WAIT', ''],
28 ['data/tcpconns-8080-local/tcp_connections-CLOSED.rrd', 0, 'CLOSED', ''],
29 ['data/tcpconns-8080-local/tcp_connections-FIN_WAIT2.rrd', 0, 'FIN_WAIT2', ''],
30 ['data/tcpconns-8080-local/tcp_connections-FIN_WAIT1.rrd', 0, 'FIN_WAIT1', ''],
31 ['data/tcpconns-8080-local/tcp_connections-ESTABLISHED.rrd', 0, 'ESTABLISHED', ''],
32 ['data/tcpconns-8080-local/tcp_connections-LAST_ACK.rrd', 0, 'LAST_ACK', ''],
33 ['data/tcpconns-8080-local/tcp_connections-LISTEN.rrd', 0, 'LISTEN', ''],
34 ['data/tcpconns-8080-local/tcp_connections-SYN_SENT.rrd', 0, 'SYN_SENT', ''],
35 ['data/tcpconns-8080-local/tcp_connections-CLOSING.rrd', 0, 'CLOSING', '']
36 ],
37 options: jQuery.extend(true, {yaxis: {tickDecimals: 0}}, jarmon.Chart.BASE_OPTIONS, jarmon.Chart.STACKED_OPTIONS)
38 }
39 ];
40
41
42 function initialiseCharts() {
43 /**
44 * Setup chart date range controls and all charts
45 **/
46
47 var p = new jarmon.Parallimiter(1);
48 function serialDownloader(url) {
49 return p.addCallable(jarmon.downloadBinary, [url]);
50 }
51
52 // Extract the chart template from the page
53 var chartTemplate = $('.chart-container').remove();
54
55 function templateFactory(parentEl) {
56 return function() {
57 // The chart template must be appended to the page early, so
58 // that flot can calculate chart dimensions etc.
59 return chartTemplate.clone().appendTo(parentEl);
60 }
61 }
62
63 var cc = new jarmon.ChartCoordinator($('.chartRangeControl'));
64 var t;
65 // Initialise tabs and update charts when tab is clicked
66 $(".css-tabs:first").bind('click', function(i) {
67 // XXX: Hack to give the tab just enough time to become visible
68 // so that flot can calculate chart dimensions.
69 window.clearTimeout(t);
70 t = window.setTimeout(function() { cc.update(); }, 100);
71 });
72
73 cc.charts = [].concat(
74 jarmon.Chart.fromRecipe(
75 [].concat(
76 jarmon.COLLECTD_RECIPES.cpu,
77 jarmon.COLLECTD_RECIPES.memory,
78 jarmon.COLLECTD_RECIPES.load),
79 templateFactory('.system-charts'), serialDownloader),
80 jarmon.Chart.fromRecipe(
81 jarmon.COLLECTD_RECIPES.interface,
82 templateFactory('.network-charts'), serialDownloader),
83 jarmon.Chart.fromRecipe(
84 jarmon.COLLECTD_RECIPES.dns,
85 templateFactory('.dns-charts'), serialDownloader),
86 jarmon.Chart.fromRecipe(
87 application_recipes,
88 templateFactory('.application-charts'), serialDownloader)
89 );
90
91 // Initialise all the charts
92 cc.init();
93 }
9419
95 $(function() {20 $(function() {
96 // Add dhtml calendars to the date input fields21 jarmon.buildTabbedChartUi(
97 $(".timerange_control img")22 $('.chart-container').remove(),
98 .dateinput({23 jarmon.CHART_RECIPES_COLLECTD,
99 'format': 'dd mmm yyyy 00:00:00',24 $('.tabbed-chart-interface'),
100 'max': +1,25 jarmon.TAB_RECIPES_STANDARD,
101 'css': {'input': 'jquerytools_date'}})26 $('.chartRangeControl')
102 .bind('onBeforeShow', function(e) {27 );
103 var classes = $(this).attr('class').split(' ');
104 var currentDate, input_selector;
105 for(var i=0; i<=classes.length; i++) {
106 input_selector = '[name="' + classes[i] + '"]';
107 // Look for a neighboring input element whose name matches the
108 // class name of this calendar
109 // Parse the value as a date if the returned date.getTime
110 // returns NaN we know it's an invalid date
111 // XXX: is there a better way to check for valid date?
112 currentDate = new Date($(this).siblings(input_selector).val());
113 if(currentDate.getTime() != NaN) {
114 $(this).data('dateinput')._input_selector = input_selector;
115 $(this).data('dateinput')._initial_val = currentDate.getTime();
116 $(this).data('dateinput').setValue(currentDate);
117 break;
118 }
119 }
120 })
121 .bind('onHide', function(e) {
122 // Called after a calendar date has been chosen by the user.
123
124 // Use the sibling selector that we generated above before opening
125 // the calendar
126 var input_selector = $(this).data('dateinput')._input_selector;
127 var oldStamp = $(this).data('dateinput')._initial_val;
128 var newDate = $(this).data('dateinput').getValue();
129 // Only update the form field if the date has changed.
130 if(oldStamp != newDate.getTime()) {
131 $(this).siblings(input_selector).val(
132 newDate.toString().split(' ').slice(1,5).join(' '));
133 // Trigger a change event which should automatically update the
134 // graphs and change the timerange drop down selector to
135 // "custom"
136 $(this).siblings(input_selector).trigger('change');
137 }
138 });
139
140 // Avoid overlaps between the calendars
141 // XXX: This is a bit of hack, what if there's more than one set of calendar
142 // controls on a page?
143 $(".timerange_control img.from_custom").bind('onBeforeShow',
144 function() {
145 var otherVal = new Date(
146 $('.timerange_control [name="to_custom"]').val());
147
148 $(this).data('dateinput').setMax(otherVal);
149 }
150 );
151 $(".timerange_control img.to_custom").bind('onBeforeShow',
152 function() {
153 var otherVal = new Date(
154 $('.timerange_control [name="from_custom"]').val());
155
156 $(this).data('dateinput').setMin(otherVal);
157 }
158 );
159
160 // Setup dhtml tabs
161 $(".css-tabs").tabs(".css-panes > div", {history: true});
162
163 initialiseCharts();
164 });28 });
165 </script>29 </script>
166 </head>30 </head>
@@ -194,24 +58,19 @@
194 <div class="range-preview"58 <div class="range-preview"
195 title="Time range preview - click and drag to select a custom timerange" ></div>59 title="Time range preview - click and drag to select a custom timerange" ></div>
196 </form>60 </form>
197 <ul class="css-tabs">61 </div>
198 <li><a href="#system">System</a></li>62 <div class="tabbed-chart-interface"></div>
199 <li><a href="#network">Network</a></li>63 <div class="chart-container">
200 <li><a href="#dns">DNS</a></li>64 <div class="chart-header">
201 <li><a href="#application">Application</a></li>
202 </ul>
203 <div class="css-panes charts">
204 <div class="system-charts"></div>
205 <div class="network-charts"></div>
206 <div class="dns-charts"></div>
207 <div class="application-charts"></div>
208 </div>
209 <div class="chart-container">
210 <h2 class="title"></h2>65 <h2 class="title"></h2>
211 <div class="error"></div>66 <div class="chart-controls">
212 <div class="chart"></div>67 <input type="button" name="chart_edit" value="edit">
213 <div class="graph-legend"></div>68 <input type="button" name="chart_delete" value="delete">
69 </div>
214 </div>70 </div>
71 <div class="error"></div>
72 <div class="chart"></div>
73 <div class="graph-legend"></div>
215 </div>74 </div>
216 </body>75 </body>
217</html>76</html>
21877
=== modified file 'docs/examples/jarmon_example_recipes.js'
--- docs/examples/jarmon_example_recipes.js 2010-08-22 13:41:26 +0000
+++ docs/examples/jarmon_example_recipes.js 2011-06-12 15:34:30 +0000
@@ -9,80 +9,76 @@
9 var jarmon = {};9 var jarmon = {};
10}10}
1111
12jarmon.COLLECTD_RECIPES = {12jarmon.TAB_RECIPES_STANDARD = [
13 'cpu': [13 ['System', ['cpu', 'memory','load']],
14 {14 ['Network', ['interface']],
15 title: 'CPU Usage',15 ['DNS', ['dns_query_types', 'dns_return_codes']]
16 data: [16];
17 ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'],17
18 ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'],18jarmon.CHART_RECIPES_COLLECTD = {
19 ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'],19 'cpu': {
20 ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'],20 title: 'CPU Usage',
21 ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'],21 data: [
22 ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%']22 ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'],
23 ],23 ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'],
24 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS,24 ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'],
25 jarmon.Chart.STACKED_OPTIONS)25 ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'],
26 }26 ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'],
27 ],27 ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%']
2828 ],
29 'memory': [29 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS,
30 {30 jarmon.Chart.STACKED_OPTIONS)
31 title: 'Memory',31 },
32 data: [32
33 ['data/memory/memory-buffered.rrd', 0, 'Buffered', 'B'],33 'memory': {
34 ['data/memory/memory-used.rrd', 0, 'Used', 'B'],34 title: 'Memory',
35 ['data/memory/memory-cached.rrd', 0, 'Cached', 'B'],35 data: [
36 ['data/memory/memory-free.rrd', 0, 'Free', 'B']36 ['data/memory/memory-buffered.rrd', 0, 'Buffered', 'B'],
37 ],37 ['data/memory/memory-used.rrd', 0, 'Used', 'B'],
38 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS,38 ['data/memory/memory-cached.rrd', 0, 'Cached', 'B'],
39 jarmon.Chart.STACKED_OPTIONS)39 ['data/memory/memory-free.rrd', 0, 'Free', 'B']
40 }40 ],
41 ],41 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS,
4242 jarmon.Chart.STACKED_OPTIONS)
43 'dns': [43 },
44 {44
45 title: 'DNS Query Types',45 'dns_query_types': {
46 data: [46 title: 'DNS Query Types',
47 ['data/dns/dns_qtype-A.rrd', 0, 'A', 'Q/s'],47 data: [
48 ['data/dns/dns_qtype-PTR.rrd', 0, 'PTR', 'Q/s'],48 ['data/dns/dns_qtype-A.rrd', 0, 'A', 'Q/s'],
49 ['data/dns/dns_qtype-SOA.rrd', 0, 'SOA', 'Q/s'],49 ['data/dns/dns_qtype-PTR.rrd', 0, 'PTR', 'Q/s'],
50 ['data/dns/dns_qtype-SRV.rrd', 0, 'SRV', 'Q/s']50 ['data/dns/dns_qtype-SOA.rrd', 0, 'SOA', 'Q/s'],
51 ],51 ['data/dns/dns_qtype-SRV.rrd', 0, 'SRV', 'Q/s']
52 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)52 ],
53 },53 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
5454 },
55 {55
56 title: 'DNS Return Codes',56 'dns_return_codes': {
57 data: [57 title: 'DNS Return Codes',
58 ['data/dns/dns_rcode-NOERROR.rrd', 0, 'NOERROR', 'Q/s'],58 data: [
59 ['data/dns/dns_rcode-NXDOMAIN.rrd', 0, 'NXDOMAIN', 'Q/s'],59 ['data/dns/dns_rcode-NOERROR.rrd', 0, 'NOERROR', 'Q/s'],
60 ['data/dns/dns_rcode-SERVFAIL.rrd', 0, 'SERVFAIL', 'Q/s']60 ['data/dns/dns_rcode-NXDOMAIN.rrd', 0, 'NXDOMAIN', 'Q/s'],
61 ],61 ['data/dns/dns_rcode-SERVFAIL.rrd', 0, 'SERVFAIL', 'Q/s']
62 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)62 ],
63 }63 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
64 ],64 },
6565
66 'load': [66 'load': {
67 {67 title: 'Load Average',
68 title: 'Load Average',68 data: [
69 data: [69 ['data/load/load.rrd', 'shortterm', 'Short Term', ''],
70 ['data/load/load.rrd', 'shortterm', 'Short Term', ''],70 ['data/load/load.rrd', 'midterm', 'Medium Term', ''],
71 ['data/load/load.rrd', 'midterm', 'Medium Term', ''],71 ['data/load/load.rrd', 'longterm', 'Long Term', '']
72 ['data/load/load.rrd', 'longterm', 'Long Term', '']72 ],
73 ],73 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
74 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)74 },
75 }75
76 ],76 'interface': {
7777 title: 'Wlan0 Throughput',
78 'interface': [78 data: [
79 {79 ['data/interface/if_octets-wlan0.rrd', 'tx', 'Transmit', 'b/s'],
80 title: 'Wlan0 Throughput',80 ['data/interface/if_octets-wlan0.rrd', 'rx', 'Receive', 'b/s']
81 data: [81 ],
82 ['data/interface/if_octets-wlan0.rrd', 'tx', 'Transmit', 'b/s'],82 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
83 ['data/interface/if_octets-wlan0.rrd', 'rx', 'Receive', 'b/s']83 }
84 ],
85 options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
86 }
87 ]
88};84};
8985
=== modified file 'jarmon/jarmon.js'
--- jarmon/jarmon.js 2010-10-04 22:59:04 +0000
+++ jarmon/jarmon.js 2011-06-12 15:34:30 +0000
@@ -256,6 +256,18 @@
256 'lastUpdated': lastUpdated*1000.0};256 'lastUpdated': lastUpdated*1000.0};
257};257};
258258
259
260jarmon.RrdQuery.prototype.getDSNames = function() {
261 /**
262 * Return a list of RRD Data Source names
263 *
264 * @method getDSNames
265 * @return {Array} An array of DS names.
266 **/
267 return this.rrd.getDSNames();
268};
269
270
259/**271/**
260 * A wrapper around RrdQuery which provides asynchronous access to the data in a272 * A wrapper around RrdQuery which provides asynchronous access to the data in a
261 * remote RRD file.273 * remote RRD file.
@@ -275,23 +287,11 @@
275 this._download = null;287 this._download = null;
276};288};
277289
278jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId) {
279 /**
280 * Return a Flot compatible data series asynchronously.
281 *
282 * @method getData
283 * @param startTime {Number} The start timestamp
284 * @param endTime {Number} The end timestamp
285 * @param dsId {Variant} identifier of the RRD datasource (string or number)
286 * @return {Object} A Deferred which calls back with a flot data series.
287 **/
288 var endTimestamp = endTime/1000;
289290
290 // Download the rrd if there has never been a download or if the last291jarmon.RrdQueryRemote.prototype._callRemote = function(methodName, args) {
291 // completed download had a lastUpdated timestamp less than the requested292 // Download the rrd if there has never been a download and don't start
292 // end time.293 // another download if one is already in progress.
293 // Don't start another download if one is already in progress.294 if(!this._download) {
294 if(!this._download || (this._download.fired > -1 && this.lastUpdate < endTimestamp )) {
295 this._download = this.downloader(this.url)295 this._download = this.downloader(this.url)
296 .addCallback(296 .addCallback(
297 function(self, binary) {297 function(self, binary) {
@@ -307,9 +307,10 @@
307 // Set up a deferred which will call getData on the local RrdQuery object307 // Set up a deferred which will call getData on the local RrdQuery object
308 // returning a flot compatible data object to the caller.308 // returning a flot compatible data object to the caller.
309 var ret = new MochiKit.Async.Deferred().addCallback(309 var ret = new MochiKit.Async.Deferred().addCallback(
310 function(self, startTime, endTime, dsId, rrd) {310 function(self, methodName, args, rrd) {
311 return new jarmon.RrdQuery(rrd, self.unit).getData(startTime, endTime, dsId);311 var rq = new jarmon.RrdQuery(rrd, self.unit);
312 }, this, startTime, endTime, dsId);312 return rq[methodName].apply(rq, args);
313 }, this, methodName, args);
313314
314 // Add a pair of callbacks to the current download which will callback the315 // Add a pair of callbacks to the current download which will callback the
315 // result which we setup above.316 // result which we setup above.
@@ -326,6 +327,35 @@
326 return ret;327 return ret;
327};328};
328329
330
331jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId, cfName) {
332 /**
333 * Return a Flot compatible data series asynchronously.
334 *
335 * @method getData
336 * @param startTime {Number} The start timestamp
337 * @param endTime {Number} The end timestamp
338 * @param dsId {Variant} identifier of the RRD datasource (string or number)
339 * @return {Object} A Deferred which calls back with a flot data series.
340 **/
341 if(this.lastUpdate < endTime/1000) {
342 this._download = null;
343 }
344 return this._callRemote('getData', [startTime, endTime, dsId, cfName]);
345};
346
347
348jarmon.RrdQueryRemote.prototype.getDSNames = function() {
349 /**
350 * Return a list of RRD Data Source names
351 *
352 * @method getDSNames
353 * @return {Object} A Deferred which calls back with an array of DS names.
354 **/
355 return this._callRemote('getDSNames');
356};
357
358
329/**359/**
330 * Wraps RrdQueryRemote to provide access to a different RRD DSs within a360 * Wraps RrdQueryRemote to provide access to a different RRD DSs within a
331 * single RrdDataSource.361 * single RrdDataSource.
@@ -364,12 +394,17 @@
364 * @param options {Object} Flot options which control how the chart should be394 * @param options {Object} Flot options which control how the chart should be
365 * drawn.395 * drawn.
366 **/396 **/
367jarmon.Chart = function(template, options) {397jarmon.Chart = function(template, recipe, downloader) {
368 this.template = template;398 this.template = template;
369 this.options = jQuery.extend(true, {yaxis: {}}, options);399 this.recipe = recipe;
400 this.downloader = downloader;
401
402 this.options = jQuery.extend(true, {yaxis: {}}, recipe.options);
370403
371 this.data = [];404 this.data = [];
372405
406 this.setup();
407
373 var self = this;408 var self = this;
374409
375410
@@ -380,7 +415,6 @@
380 self.draw();415 self.draw();
381 });416 });
382417
383
384 this.options['yaxis']['ticks'] = function(axis) {418 this.options['yaxis']['ticks'] = function(axis) {
385 /*419 /*
386 * Choose a suitable SI multiplier based on the min and max values from420 * Choose a suitable SI multiplier based on the min and max values from
@@ -442,6 +476,29 @@
442 };476 };
443};477};
444478
479jarmon.Chart.prototype.setup = function() {
480 this.template.find('.title').text(this.recipe['title']);
481 this.data = [];
482 var recipe = this.recipe;
483 var dataDict = {};
484 for(var j=0; j<recipe['data'].length; j++) {
485 var rrd = recipe['data'][j][0];
486 var ds = recipe['data'][j][1];
487 // Test for integer DS index as opposed to DS name
488 var dsi = parseInt(ds);
489 if(ds.toString() == dsi.toString()) {
490 ds = dsi;
491 }
492 var label = recipe['data'][j][2];
493 var unit = recipe['data'][j][3];
494
495 if(typeof dataDict[rrd] == 'undefined') {
496 dataDict[rrd] = new jarmon.RrdQueryRemote(rrd, unit, this.downloader);
497 }
498 this.addData(label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds));
499 }
500};
501
445jarmon.Chart.prototype.addData = function(label, db, enabled) {502jarmon.Chart.prototype.addData = function(label, db, enabled) {
446 /**503 /**
447 * Add details of a remote RRD data source whose data will be added to this504 * Add details of a remote RRD data source whose data will be added to this
@@ -568,13 +625,15 @@
568 // to accomodate the color box625 // to accomodate the color box
569 var legend = self.template.find('.graph-legend').show();626 var legend = self.template.find('.graph-legend').show();
570 legend.empty();627 legend.empty();
571 self.template.find('.legendLabel')628 self.template.find('.legendLabel').each(
572 .each(function(i, el) {629 function(i, el) {
573 var orig = $(el);630 var orig = $(el);
574 var label = orig.text();631 var label = orig.text();
575 var newEl = $('<div />')632 var newEl = $('<div />', {
576 .attr('class', 'legendItem')633 'class': 'legendItem',
577 .attr('title', 'Data series switch - click to turn this data series on or off')634 'title': 'Data series switch - click to turn \
635 this data series on or off'
636 })
578 .width(orig.width()+20)637 .width(orig.width()+20)
579 .text(label)638 .text(label)
580 .prepend(orig.prev().find('div div').clone().addClass('legendColorBox'))639 .prepend(orig.prev().find('div div').clone().addClass('legendColorBox'))
@@ -585,8 +644,8 @@
585 if( $.inArray(label, disabled) > -1 ) {644 if( $.inArray(label, disabled) > -1 ) {
586 newEl.addClass('disabled');645 newEl.addClass('disabled');
587 }646 }
588 })647 }
589 .remove();648 ).remove();
590 legend.append($('<div />').css('clear', 'both'));649 legend.append($('<div />').css('clear', 'both'));
591 self.template.find('.legend').remove();650 self.template.find('.legend').remove();
592651
@@ -608,49 +667,513 @@
608};667};
609668
610669
611jarmon.Chart.fromRecipe = function(recipes, templateFactory, downloader) {670/**
671 * Generate a form through which to choose a data source from a remote RRD file
672 *
673 * @class jarmon.RrdChooser
674 * @constructor
675 **/
676jarmon.RrdChooser = function($tpl) {
677 this.$tpl = $tpl;
678 this.data = {
679 rrdUrl: '',
680 dsName: '',
681 dsLabel: '',
682 dsUnit:''
683 };
684};
685
686jarmon.RrdChooser.prototype.drawRrdUrlForm = function() {
687 var self = this;
688 this.$tpl.empty();
689
690 $('<form/>').append(
691 $('<div/>').append(
692 $('<p/>').text('Enter the URL of an RRD file'),
693 $('<label/>').append(
694 'URL: ',
695 $('<input/>', {
696 type: 'text',
697 name: 'rrd_url',
698 value: this.data.rrdUrl
699 })
700 ),
701 $('<input/>', {type: 'submit', value: 'download'}),
702 $('<div/>', {class: 'next'})
703 )
704 ).submit(
705 function(e) {
706 self.data.rrdUrl = this['rrd_url'].value;
707 $placeholder = $(this).find('.next').empty();
708 new jarmon.RrdQueryRemote(self.data.rrdUrl).getDSNames().addCallback(
709 function($placeholder, dsNames) {
710 if(dsNames.length > 1) {
711 $('<p/>').text(
712 'The RRD file contains multiple data sources. \
713 Choose one:').appendTo($placeholder);
714
715 $(dsNames).map(
716 function(i, el) {
717 return $('<input/>', {
718 type: 'button',
719 value: el
720 }
721 ).click(
722 function(e) {
723 self.data.dsName = this.value;
724 self.drawDsLabelForm();
725 }
726 );
727 }).appendTo($placeholder);
728 } else {
729 self.data.dsName = dsNames[0];
730 self.drawDsLabelForm();
731 }
732 }, $placeholder
733 ).addErrback(
734 function($placeholder, err) {
735 $('<p/>', {'class': 'error'}).text(err.toString()).appendTo($placeholder);
736 }, $placeholder
737 );
738 return false;
739 }
740 ).appendTo(this.$tpl);
741}
742
743jarmon.RrdChooser.prototype.drawDsLabelForm = function() {
744 var self = this;
745 this.$tpl.empty();
746
747 $('<form/>').append(
748 $('<p/>').text('Choose a label and unit for this data source.'),
749 $('<div/>').append(
750 $('<label/>').append(
751 'Label: ',
752 $('<input/>', {
753 type: 'text',
754 name: 'dsLabel',
755 value: this.data.dslabel || this.data.dsName
756 })
757 )
758 ),
759 $('<div/>').append(
760 $('<label/>').append(
761 'Unit: ',
762 $('<input/>', {
763 type: 'text',
764 name: 'dsUnit',
765 value: this.data.dsUnit
766 })
767 )
768 ),
769 $('<input/>', {type: 'button', value: 'back'}).click(
770 function(e) {
771 self.drawRrdUrlForm();
772 }
773 ),
774 $('<input/>', {type: 'submit', value: 'save'}),
775 $('<div/>', {class: 'next'})
776 ).submit(
777 function(e) {
778 self.data.dsLabel = this['dsLabel'].value;
779 self.data.dsUnit = this['dsUnit'].value;
780 self.drawDsSummary();
781 return false;
782 }
783 ).appendTo(this.$tpl);
784};
785
786
787jarmon.RrdChooser.prototype.drawDsSummary = function() {
788 var self = this;
789 this.$tpl.empty();
790
791 jQuery.each(this.data, function(i, el) {
792 $('<p/>').append(
793 $('<strong/>').text(i),
794 [': ', el].join('')
795 ).appendTo(self.$tpl);
796 });
797
798 this.$tpl.append(
799 $('<input/>', {type: 'button', value: 'back'}).click(
800 function(e) {
801 self.drawDsLabelForm();
802 }
803 ),
804 $('<input/>', {type: 'button', value: 'finish'})
805 );
806};
807
808
809jarmon.ChartEditor = function($tpl, chart) {
810 this.$tpl = $tpl;
811 this.chart = chart;
812
813 $('form', this.$tpl[0]).live(
814 'submit',
815 {self: this},
816 function(e) {
817 var self = e.data.self;
818 self.chart.recipe.title = this['title'].value;
819 self.chart.recipe.data = $(this).find('.datasources tbody tr').map(
820 function(i, el) {
821 return $(el).find('input[type=text]').map(
822 function(i, el) {
823 return el.value;
824 }
825 );
826 }
827 );
828 self.chart.setup();
829 self.chart.draw();
830 return false;
831 }
832 );
833
834 $('form', this.$tpl[0]).live(
835 'reset',
836 {self: this},
837 function(e) {
838 var self = e.data.self;
839 self.draw();
840 return false;
841 }
842 );
843
844 $('form input[name=datasource_delete]', this.$tpl[0]).live(
845 'click',
846 function(e) {
847 $(this).closest('tr').remove();
848 }
849 );
850
851 $('form input[name=datasource_add]', this.$tpl[0]).live(
852 'click',
853 {self: this},
854 function(e) {
855 var self = e.data.self;
856 self._addDatasourceRow(
857 self._extractRowValues(
858 $(this).closest('tr')
859 )
860 );
861 $(this).closest('tr').find('input[type=text]').val('');
862 }
863 );
864};
865
866jarmon.ChartEditor.prototype.draw = function() {
867 var self = this;
868 this.$tpl.empty();
869
870 $('<form/>').append(
871 $('<div/>').append(
872 $('<label/>').append(
873 'Title: ',
874 $('<input/>', {
875 type: 'text',
876 name: 'title',
877 value: this.chart.recipe.title
878 })
879 )
880 ),
881 $('<fieldset/>').append(
882 $('<legend/>').text('Data Sources'),
883 $('<table/>', {'class': 'datasources'}).append(
884 $('<thead/>').append(
885 $('<tr/>').append(
886 $('<th/>').text('RRD File'),
887 $('<th/>').text('DS Name'),
888 $('<th/>').text('DS Label'),
889 $('<th/>').text('DS Unit'),
890 $('<th/>')
891 )
892 ),
893 $('<tfoot/>').append(
894 $('<tr/>').append(
895 $('<td/>').append(
896 $('<input/>', {type: 'text'})
897 ),
898 $('<td/>').append(
899 $('<input/>', {type: 'text'})
900 ),
901 $('<td/>').append(
902 $('<input/>', {type: 'text'})
903 ),
904 $('<td/>').append(
905 $('<input/>', {type: 'text'})
906 ),
907 $('<td/>').append(
908 $('<input/>', {
909 type: 'button',
910 value: 'add',
911 name: 'datasource_add'
912 })
913 )
914 )
915 ),
916 $('<tbody/>')
917 )
918 ),
919 $('<input/>', {type: 'submit', value: 'save'}),
920 $('<input/>', {type: 'reset', value: 'reset'})
921 ).appendTo(this.$tpl);
922
923 for(var i=0; i<this.chart.recipe.data.length; i++) {
924 this._addDatasourceRow(this.chart.recipe.data[i]);
925 }
926};
927
928
929jarmon.ChartEditor.prototype._extractRowValues = function($row) {
930 return $row.find('input[type=text]').map(
931 function(i, el) {
932 return el.value;
933 }
934 )
935};
936
937
938jarmon.ChartEditor.prototype._addDatasourceRow = function(record) {
939 $('<tr/>').append(
940 $('<td/>').append(
941 $('<input/>', {type: 'text', value: record[0]})
942 ),
943 $('<td/>').append(
944 $('<input/>', {type: 'text', value: record[1]})
945 ),
946 $('<td/>').append(
947 $('<input/>', {type: 'text', value: record[2]})
948 ),
949 $('<td/>').append(
950 $('<input/>', {type: 'text', value: record[3]})
951 ),
952 $('<td/>').append(
953 $('<input/>', {
954 type: 'button',
955 value: 'delete',
956 name: 'datasource_delete'
957 })
958 )
959 ).appendTo(this.$tpl.find('.datasources tbody'));
960};
961
962
963jarmon.TabbedInterface = function($tpl, recipe) {
964 this.$tpl = $tpl;
965 this.recipe = recipe;
966 this.placeholders = [];
967
968 this.$tabBar = $('<ul/>', {'class': 'css-tabs'}).appendTo($tpl);
969
970 // Icon and hidden input box for adding new tabs. See event handlers below.
971 this.$newTabControls = $('<li/>', {
972 'class': 'newTabControls',
973 'title': 'Add new tab'
974 }).append(
975 $('<img/>', {src: 'assets/icons/next.gif'}),
976 $('<input/>', {'type': 'text'}).hide()
977 ).appendTo(this.$tabBar);
978
979 this.$tabPanels = $('<div/>', {'class': 'css-panes charts'}).appendTo($tpl);
980 var tabName, $tabPanel, placeNames;
981 for(var i=0; i<recipe.length; i++) {
982 tabName = recipe[i][0];
983 placeNames = recipe[i][1];
984
985 $tabPanel = this.newTab(tabName);
986
987 for(var j=0; j<placeNames.length; j++) {
988 this.placeholders.push([
989 placeNames[j], $('<div/>').appendTo($tabPanel)]);
990 }
991 }
992
993 this.setup();
994
995 // Show the new tab name input box when the user clicks the new tab icon
996 $('ul.css-tabs > li.newTabControls > img', $tpl[0]).live(
997 'click',
998 function(e) {
999 $(this).hide().siblings().show().focus();
1000 }
1001 );
1002
1003 // When the "new" tab input loses focus, use its value to create a new
1004 // tab.
1005 // XXX: Due to event bubbling, this event seems to be triggered twice, but
1006 // only when the input is forcefully blurred by the "keypress" event handler
1007 // below. To prevent two tabs, we blank the input field value. Tried
1008 // preventing event bubbling, but there seems to be some subtle difference
1009 // with the use of jquery live event handlers.
1010 $('ul.css-tabs > li.newTabControls > input', $tpl[0]).live(
1011 'blur',
1012 {self: this},
1013 function(e) {
1014 var self = e.data.self;
1015 var value = this.value;
1016 this.value = '';
1017 $(this).hide().siblings().show();
1018 if(value) {
1019 self.newTab(value);
1020 self.setup();
1021 self.$tabBar.data("tabs").click(value);
1022 }
1023 }
1024 );
1025
1026 // Unfocus the input element when return key is pressed. Triggers a
1027 // blur event which then replaces the input with a tab
1028 $('ul.css-tabs > li > input', $tpl[0]).live(
1029 'keypress',
1030 function(e) {
1031 if(e.which == 13) {
1032 $(this).blur();
1033 }
1034 }
1035 );
1036
1037 // Show tab name input box when tab is double clicked.
1038 $('ul.css-tabs > li > a', $tpl[0]).live(
1039 'dblclick',
1040 {self: this},
1041 function(e) {
1042 var $originalLink = $(this);
1043 var $input = $('<input/>', {
1044 'value': $originalLink.text(),
1045 'name': 'editTabTitle',
1046 'type': 'text'
1047 })
1048 $originalLink.replaceWith($input);
1049 $input.focus();
1050 }
1051 );
1052
1053 // Handle the updating of the tab when its name is edited.
1054 $('ul.css-tabs > li > input[name=editTabTitle]', $tpl[0]).live(
1055 'blur',
1056 {self: this},
1057 function(e) {
1058 var self = e.data.self;
1059 $(this).replaceWith(
1060 $('<a/>', {
1061 href: ['#', this.value].join('')
1062 }).text(this.value)
1063 )
1064 self.setup();
1065 self.$tabBar.data("tabs").click(this.value);
1066 }
1067 );
1068
1069 $('input[name=add_new_chart]', $tpl[0]).live(
1070 'click',
1071 {self: this},
1072 function(e) {
1073 console.log(e);
1074 }
1075 );
1076};
1077
1078jarmon.TabbedInterface.prototype.newTab = function(tabName) {
1079 // Add a tab
1080 $('<li/>').append(
1081 $('<a/>', {href: ['#', tabName].join('')}).text(tabName)
1082 ).appendTo(this.$tabBar);
1083 var $placeholder = $('<div/>');
1084 // Add tab panel
1085 $('<div/>').append(
1086 $placeholder,
1087 $('<div/>', {'class': 'tab-controls'}).append(
1088 $('<input/>', {
1089 type: 'button',
1090 value: 'Add new chart',
1091 name: 'add_new_chart'
1092 })
1093 )
1094 ).appendTo(this.$tabPanels);
1095
1096 return $placeholder;
1097};
1098
1099jarmon.TabbedInterface.prototype.setup = function() {
1100 this.$newTabControls.remove();
1101 // Destroy then re-initialise the jquerytools tabs plugin
1102 var api = this.$tabBar.data("tabs");
1103 if(api) {
1104 api.destroy();
1105 }
1106 this.$tabBar.tabs(this.$tabPanels.children('div'));
1107 this.$newTabControls.appendTo(this.$tabBar);
1108};
1109
1110
1111jarmon.buildTabbedChartUi = function ($chartTemplate, chartRecipes,
1112 $tabTemplate, tabRecipes,
1113 $controlPanelTemplate) {
612 /**1114 /**
613 * A static factory method to generate a list of I{Chart} from a list of1115 * Setup chart date range controls and all charts
614 * recipes and a list of available rrd files in collectd path format.
615 *
616 * @method fromRecipe
617 * @param recipes {Array} A list of recipe objects.
618 * @param templateFactory {Function} A callable which generates an html
619 * template for a chart.
620 * @param downloader {Function} A download function which returns a Deferred
621 * @return {Array} A list of Chart objects
622 **/1116 **/
6231117 var p = new jarmon.Parallimiter(1);
624 var charts = [];1118 function serialDownloader(url) {
625 var dataDict = {};1119 return p.addCallable(jarmon.downloadBinary, [url]);
626
627 var recipe, chartData, template, c, i, j, ds, label, rrd, unit, re, match;
628
629 for(i=0; i<recipes.length; i++) {
630 recipe = recipes[i];
631 chartData = [];
632
633 for(j=0; j<recipe['data'].length; j++) {
634 rrd = recipe['data'][j][0];
635 ds = recipe['data'][j][1];
636 label = recipe['data'][j][2];
637 unit = recipe['data'][j][3];
638 if(typeof dataDict[rrd] == 'undefined') {
639 dataDict[rrd] = new jarmon.RrdQueryRemote(rrd, unit, downloader);
640 }
641 chartData.push([label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds)]);
642 }
643 if(chartData.length > 0) {
644 template = templateFactory();
645 template.find('.title').text(recipe['title']);
646 c = new jarmon.Chart(template, recipe['options']);
647 for(j=0; j<chartData.length; j++) {
648 c.addData.apply(c, chartData[j]);
649 }
650 charts.push(c);
651 }
652 }1120 }
653 return charts;1121
1122 var ti = new jarmon.TabbedInterface($tabTemplate, tabRecipes);
1123
1124 var charts = jQuery.map(
1125 ti.placeholders,
1126 function(el, i) {
1127 var chart = new jarmon.Chart(
1128 $chartTemplate.clone().appendTo(el[1]),
1129 chartRecipes[el[0]],
1130 serialDownloader
1131 );
1132
1133 $('input[name=chart_edit]', el[1][0]).live(
1134 'click',
1135 {chart: chart},
1136 function(e) {
1137 var chart = e.data.chart;
1138 new jarmon.ChartEditor(
1139 chart.template.find('.graph-legend'), chart).draw();
1140 }
1141 );
1142
1143 $('input[name=chart_delete]', el[1][0]).live(
1144 'click',
1145 {chart: chart},
1146 function(e) {
1147 var chart = e.data.chart;
1148 chart.template.remove();
1149 }
1150 );
1151
1152 return chart;
1153 }
1154 );
1155
1156 var cc = new jarmon.ChartCoordinator($controlPanelTemplate, charts);
1157 // Update charts when tab is clicked
1158 ti.$tpl.find(".css-tabs:first").bind(
1159 'click',
1160 {'cc': cc},
1161 function(e) {
1162 var cc = e.data.cc;
1163 // XXX: Hack to give the tab just enough time to become visible
1164 // so that flot can calculate chart dimensions.
1165 window.clearTimeout(cc.t);
1166 cc.t = window.setTimeout(
1167 function() {
1168 cc.update();
1169 }, 100);
1170 }
1171 );
1172
1173 // Initialise all the charts
1174 cc.init();
1175
1176 return [charts, ti, cc];
654};1177};
6551178
6561179
@@ -720,10 +1243,10 @@
720 * @param ui {Object} A one element jQuery containing an input form and1243 * @param ui {Object} A one element jQuery containing an input form and
721 * placeholders for the timeline and for the series of charts.1244 * placeholders for the timeline and for the series of charts.
722 **/1245 **/
723jarmon.ChartCoordinator = function(ui) {1246jarmon.ChartCoordinator = function(ui, charts) {
724 var self = this;1247 var self = this;
725 this.ui = ui;1248 this.ui = ui;
726 this.charts = [];1249 this.charts = charts;
7271250
728 // Style and configuration of the range timeline1251 // Style and configuration of the range timeline
729 this.rangePreviewOptions = {1252 this.rangePreviewOptions = {
@@ -817,11 +1340,107 @@
8171340
818 // When a selection is made on the range timeline, or any of my charts1341 // When a selection is made on the range timeline, or any of my charts
819 // redraw all the charts.1342 // redraw all the charts.
820 this.ui.bind("plotselected", function(event, ranges) {1343 $(document).bind(
821 self.ui.find('[name="from_standard"]').val('custom');1344 'plotselected',
822 self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to);1345 {self: this},
823 self.update();1346 function(e, ranges) {
824 });1347 var self = e.data.self;
1348 var eventSourceIsMine = false;
1349
1350 // plotselected event may be from my range selector chart or
1351 if( self.ui.has(e.target) ) {
1352 eventSourceIsMine = true;
1353 } else {
1354 // ...it may come from one of the charts under my supervision
1355 for(var i=0; i<self.charts.length; i++) {
1356 if(self.charts[i].template.has(e.target).length > 0) {
1357 eventSourceIsMine = true;
1358 break;
1359 }
1360 }
1361 }
1362
1363 if(eventSourceIsMine) {
1364 // Update the prepared time range select box to value "custom"
1365 self.ui.find('[name="from_standard"]').val('custom');
1366
1367 // Update all my charts
1368 self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to);
1369 self.update();
1370 }
1371 }
1372 );
1373
1374 // Add dhtml calendars to the date input fields
1375 this.ui.find(".timerange_control img")
1376 .dateinput({
1377 'format': 'dd mmm yyyy 00:00:00',
1378 'max': +1,
1379 'css': {'input': 'jquerytools_date'}})
1380 .bind('onBeforeShow', function(e) {
1381 var classes = $(this).attr('class').split(' ');
1382 var currentDate, input_selector;
1383 for(var i=0; i<=classes.length; i++) {
1384 input_selector = '[name="' + classes[i] + '"]';
1385 // Look for a neighboring input element whose name matches the
1386 // class name of this calendar
1387 // Parse the value as a date if the returned date.getTime
1388 // returns NaN we know it's an invalid date
1389 // XXX: is there a better way to check for valid date?
1390 currentDate = new Date($(this).siblings(input_selector).val());
1391 if(currentDate.getTime() != NaN) {
1392 $(this).data('dateinput')._input_selector = input_selector;
1393 $(this).data('dateinput')._initial_val = currentDate.getTime();
1394 $(this).data('dateinput').setValue(currentDate);
1395 break;
1396 }
1397 }
1398 })
1399 .bind('onHide', function(e) {
1400 // Called after a calendar date has been chosen by the user.
1401
1402 // Use the sibling selector that we generated above before opening
1403 // the calendar
1404 var input_selector = $(this).data('dateinput')._input_selector;
1405 var oldStamp = $(this).data('dateinput')._initial_val;
1406 var newDate = $(this).data('dateinput').getValue();
1407 // Only update the form field if the date has changed.
1408 if(oldStamp != newDate.getTime()) {
1409 $(this).siblings(input_selector).val(
1410 newDate.toString().split(' ').slice(1,5).join(' '));
1411 // Trigger a change event which should automatically update the
1412 // graphs and change the timerange drop down selector to
1413 // "custom"
1414 $(this).siblings(input_selector).trigger('change');
1415 }
1416 });
1417
1418 // Avoid overlaps between the calendars
1419 // XXX: This is a bit of hack, what if there's more than one set of calendar
1420 // controls on a page?
1421 this.ui.find(".timerange_control img.from_custom").bind(
1422 'onBeforeShow',
1423 {self: this},
1424 function(e) {
1425 var self = e.data.self;
1426 var otherVal = new Date(
1427 self.ui.find('.timerange_control [name="to_custom"]').val());
1428
1429 $(this).data('dateinput').setMax(otherVal);
1430 }
1431 );
1432 this.ui.find(".timerange_control img.to_custom").bind(
1433 'onBeforeShow',
1434 {self: this},
1435 function(e) {
1436 var self = e.data.self;
1437 var otherVal = new Date(
1438 self.ui.find('.timerange_control [name="from_custom"]').val());
1439
1440 $(this).data('dateinput').setMin(otherVal);
1441 }
1442 );
1443
825};1444};
8261445
8271446
8281447
=== modified file 'jarmon/jarmon.test.js'
--- jarmon/jarmon.test.js 2010-11-28 14:50:38 +0000
+++ jarmon/jarmon.test.js 2011-06-12 15:34:30 +0000
@@ -277,6 +277,105 @@
277277
278278
279 Y.Test.Runner.add(new Y.Test.Case({279 Y.Test.Runner.add(new Y.Test.Case({
280 name: "jarmon.RrdQueryRemote",
281
282 setUp: function() {
283 this.rq = new jarmon.RrdQueryRemote('build/test.rrd', '');
284 },
285
286 test_getDataTimeRangeOverlapError: function () {
287 /**
288 * The starttime must be less than the endtime
289 **/
290 this.rq.getData(1, 0).addBoth(
291 function(self, res) {
292 self.resume(function() {
293 Y.Assert.isInstanceOf(RangeError, res);
294 });
295 }, this);
296 this.wait();
297 },
298
299
300 test_getDataUnknownCfError: function () {
301 /**
302 * Error is raised if the rrd file doesn't contain an RRA with the
303 * requested consolidation function (CF)
304 **/
305 this.rq.getData(RRD_STARTTIME, RRD_ENDTIME, 0, 'FOO').addBoth(
306 function(self, res) {
307 self.resume(function() {
308 Y.Assert.isInstanceOf(TypeError, res);
309 });
310 }, this);
311 this.wait();
312 },
313
314
315 test_getData: function () {
316 /**
317 * The generated rrd file should have values 0-9 at 300s intervals
318 * starting at 1980-01-01 00:00:00
319 * Result should include a data points with times > starttime and
320 * <= endTime
321 **/
322 this.rq.getData(RRD_STARTTIME + (RRD_STEP+1) * 1000,
323 RRD_ENDTIME - (RRD_STEP-1) * 1000).addBoth(
324 function(self, data) {
325 self.resume(function() {
326 // We request data starting 1 STEP +1s after the RRD file
327 // first val and ending 1 STEP -1s before the RRD last val
328 // ie one step within the RRD file, but 1s away from the
329 // step boundary to test the quantisation of the
330 // requested time range.
331
332 // so we expect two less rows than the total rows in the
333 // file.
334 Y.Assert.areEqual(RRD_RRAROWS-2, data.data.length);
335
336 // The value of the first returned row should be the
337 // second value in the RRD file (starts at value 0)
338 Y.Assert.areEqual(1, data.data[0][1]);
339
340 // The value of the last returned row should be the
341 // 10 value in the RRD file (starts at value 0)
342 Y.Assert.areEqual(10, data.data[data.data.length-1][1]);
343
344 // The timestamp of the first returned row should be
345 // exactly one step after the start of the RRD file
346 Y.Assert.areEqual(
347 RRD_STARTTIME+RRD_STEP*1000, data.data[0][0]);
348
349 // RRD_ENDTIME is on a step boundary and is therfore
350 // actually the start time of a new row
351 // So when we ask for endTime = RRD_ENDTIME-STEP-1 we
352 // actually get data up to the 2nd to last RRD row.
353 Y.Assert.areEqual(
354 RRD_ENDTIME-RRD_STEP*1000*2,
355 data.data[data.data.length-1][0]);
356 });
357 }, this);
358 this.wait();
359 },
360
361 test_getDataUnknownValues: function () {
362 /**
363 * If the requested time range is outside the range of the RRD file
364 * we should not get any values back
365 **/
366 this.rq.getData(RRD_ENDTIME, RRD_ENDTIME+1000).addBoth(
367 function(self, data) {
368 self.resume(function() {
369 Y.Assert.areEqual(0, data.data.length);
370 });
371 }, this);
372 this.wait();
373 }
374
375 }));
376
377
378 Y.Test.Runner.add(new Y.Test.Case({
280 name: "jarmon.Chart",379 name: "jarmon.Chart",
281380
282 test_draw: function () {381 test_draw: function () {
@@ -301,6 +400,85 @@
301 }));400 }));
302401
303402
403 Y.Test.Runner.add(new Y.Test.Case({
404 name: "jarmon.RrdChooser",
405
406 setUp: function() {
407 this.$tpl = $('<div/>').appendTo($('body'))
408 var c = new jarmon.RrdChooser(this.$tpl);
409 c.drawRrdUrlForm();
410 },
411
412 test_drawInitialForm: function () {
413 /**
414 * Test that the initial config form contains an rrd form field
415 **/
416 Y.Assert.areEqual(
417 this.$tpl.find('form input[name=rrd_url]').size(), 1);
418 },
419
420 test_drawUrlErrorMessage: function () {
421 /**
422 * Test that submitting the form with an incorrect url results in
423 * an error message
424 **/
425 var self = this;
426 this.$tpl.find('form input[name=rrd_url]').val('Foo/Bar').submit();
427 this.wait(
428 function() {
429 Y.Assert.areEqual(self.$tpl.find('.error').size(), 1);
430 }, 1000
431 );
432 },
433
434 test_drawUrlListDatasources: function () {
435 /**
436 * Test that submitting the form with an correct rrd url results in
437 * list of further DS label fields
438 **/
439 var self = this;
440 this.$tpl.find('form input[name=rrd_url]').val('build/test.rrd').submit();
441 this.wait(
442 function() {
443 Y.Assert.areEqual(self.$tpl.find('input[name=rrd_ds_label]').size(), 1);
444 }, 1000
445 );
446 },
447 }));
448
449
450 Y.Test.Runner.add(new Y.Test.Case({
451 name: "jarmon.ChartEditor",
452
453 setUp: function() {
454 this.$tpl = $('<div/>').appendTo($('body'))
455 var c = new jarmon.ChartEditor(
456 this.$tpl,
457 {
458 title: 'Foo',
459 datasources: [
460 ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'],
461 ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'],
462 ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'],
463 ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'],
464 ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'],
465 ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%']
466 ]
467 }
468 );
469 c.draw();
470 },
471
472 test_drawInitialForm: function () {
473 /**
474 * Test that the initial config form contains an rrd form field
475 **/
476 Y.Assert.areEqual(
477 this.$tpl.find('form input[name=rrd_url]').size(), 1);
478 }
479 }));
480
481
304 //initialize the console482 //initialize the console
305 var yconsole = new Y.Console({483 var yconsole = new Y.Console({
306 newestOnTop: false,484 newestOnTop: false,
307485
=== modified file 'jarmonbuild/commands.py'
--- jarmonbuild/commands.py 2010-08-30 14:22:25 +0000
+++ jarmonbuild/commands.py 2011-06-12 15:34:30 +0000
@@ -8,9 +8,7 @@
8import os8import os
9import shutil9import shutil
10import sys10import sys
11import time
1211
13from datetime import datetime
14from optparse import OptionParser12from optparse import OptionParser
15from subprocess import check_call, PIPE13from subprocess import check_call, PIPE
16from tempfile import gettempdir14from tempfile import gettempdir
@@ -20,8 +18,8 @@
20import pkg_resources18import pkg_resources
2119
2220
23JARMON_PROJECT_TITLE='Jarmon'21JARMON_PROJECT_TITLE = 'Jarmon'
24JARMON_PROJECT_URL='http://www.launchpad.net/jarmon'22JARMON_PROJECT_URL = 'http://www.launchpad.net/jarmon'
2523
26YUIDOC_URL = 'http://yuilibrary.com/downloads/yuidoc/yuidoc_1.0.0b1.zip'24YUIDOC_URL = 'http://yuilibrary.com/downloads/yuidoc/yuidoc_1.0.0b1.zip'
27YUIDOC_MD5 = 'cd5545d2dec8f7afe3d18e793538162c'25YUIDOC_MD5 = 'cd5545d2dec8f7afe3d18e793538162c'
@@ -91,21 +89,27 @@
91 yuizip_path = os.path.join(tmpdir, os.path.basename(YUIDOC_URL))89 yuizip_path = os.path.join(tmpdir, os.path.basename(YUIDOC_URL))
92 if os.path.exists(yuizip_path):90 if os.path.exists(yuizip_path):
93 self.log.debug('Using cached YUI doc')91 self.log.debug('Using cached YUI doc')
94 def producer():92
93 def producer_local():
95 yield open(yuizip_path).read()94 yield open(yuizip_path).read()
95
96 producer = producer_local
96 else:97 else:
97 self.log.debug('Downloading YUI Doc')98 self.log.debug('Downloading YUI Doc')
98 def producer():99
100 def producer_remote():
99 with open(yuizip_path, 'w') as yuizip:101 with open(yuizip_path, 'w') as yuizip:
100 download = urlopen(YUIDOC_URL)102 download = urlopen(YUIDOC_URL)
101 while True:103 while True:
102 bytes = download.read(1024*10)104 bytes = download.read(1024 * 10)
103 if not bytes:105 if not bytes:
104 break106 break
105 else:107 else:
106 yuizip.write(bytes)108 yuizip.write(bytes)
107 yield bytes109 yield bytes
108110
111 producer = producer_remote
112
109 checksum = hashlib.md5()113 checksum = hashlib.md5()
110 for bytes in producer():114 for bytes in producer():
111 checksum.update(bytes)115 checksum.update(bytes)
@@ -114,7 +118,8 @@
114 if actual_md5 != YUIDOC_MD5:118 if actual_md5 != YUIDOC_MD5:
115 raise BuildError(119 raise BuildError(
116 'YUI Doc checksum error. File: %s, '120 'YUI Doc checksum error. File: %s, '
117 'Expected: %s, Got: %s' % (yuizip_path, YUIDOC_MD5, actual_md5))121 'Expected: %s, '
122 'Got: %s' % (yuizip_path, YUIDOC_MD5, actual_md5))
118 else:123 else:
119 self.log.debug('YUI Doc checksum verified')124 self.log.debug('YUI Doc checksum verified')
120125
@@ -145,7 +150,7 @@
145 workingbranch_dir, 'jarmonbuild', 'yuidoc_template'),),150 workingbranch_dir, 'jarmonbuild', 'yuidoc_template'),),
146 '--version=%s' % (buildversion,),151 '--version=%s' % (buildversion,),
147 '--project=%s' % (JARMON_PROJECT_TITLE,),152 '--project=%s' % (JARMON_PROJECT_TITLE,),
148 '--projecturl=%s' % (JARMON_PROJECT_URL,)153 '--projecturl=%s' % (JARMON_PROJECT_URL,),
149 ), stdout=PIPE, stderr=PIPE,)154 ), stdout=PIPE, stderr=PIPE,)
150155
151 shutil.rmtree(yuidoc_dir)156 shutil.rmtree(yuidoc_dir)
@@ -181,21 +186,21 @@
181 if status != 0:186 if status != 0:
182 raise BuildError('bzr export failure. Status: %r' % (status,))187 raise BuildError('bzr export failure. Status: %r' % (status,))
183188
184
185 self.log.debug('Record the branch version')189 self.log.debug('Record the branch version')
186 from bzrlib.branch import Branch190 from bzrlib.branch import Branch
187 from bzrlib.version_info_formats import format_python191 from bzrlib.version_info_formats import format_python
188 v = format_python.PythonVersionInfoBuilder(192 v = format_python.PythonVersionInfoBuilder(
189 Branch.open(workingbranch_dir))193 Branch.open(workingbranch_dir))
190 versionfile_path = os.path.join(build_dir, 'jarmonbuild', '_version.py')194
195 versionfile_path = os.path.join(
196 build_dir, 'jarmonbuild', '_version.py')
197
191 with open(versionfile_path, 'w') as f:198 with open(versionfile_path, 'w') as f:
192 v.generate(f)199 v.generate(f)
193200
194
195 self.log.debug('Generate apidocs')201 self.log.debug('Generate apidocs')
196 BuildApidocsCommand().main([buildversion])202 BuildApidocsCommand().main([buildversion])
197203
198
199 self.log.debug('Generate archive')204 self.log.debug('Generate archive')
200 archive_root = 'jarmon-%s' % (buildversion,)205 archive_root = 'jarmon-%s' % (buildversion,)
201 prefix_len = len(build_dir) + 1206 prefix_len = len(build_dir) + 1
@@ -205,7 +210,7 @@
205 for file in files:210 for file in files:
206 z.write(211 z.write(
207 os.path.join(root, file),212 os.path.join(root, file),
208 os.path.join(archive_root, root[prefix_len:], file)213 os.path.join(archive_root, root[prefix_len:], file),
209 )214 )
210 finally:215 finally:
211 z.close()216 z.close()
@@ -233,14 +238,17 @@
233 rows = 12238 rows = 12
234 step = 10239 step = 10
235240
236 dss.append(DataSource(dsName='speed', dsType='GAUGE', heartbeat=2*step))241 dss.append(
242 DataSource(dsName='speed', dsType='GAUGE', heartbeat=2 * step))
237 rras.append(RRA(cf='AVERAGE', xff=0.5, steps=1, rows=rows))243 rras.append(RRA(cf='AVERAGE', xff=0.5, steps=1, rows=rows))
238 rras.append(RRA(cf='AVERAGE', xff=0.5, steps=12, rows=rows))244 rras.append(RRA(cf='AVERAGE', xff=0.5, steps=12, rows=rows))
239 my_rrd = RRD(filename, ds=dss, rra=rras, start=start, step=step)245 my_rrd = RRD(filename, ds=dss, rra=rras, start=start, step=step)
240 my_rrd.create()246 my_rrd.create()
241247
242 for i, t in enumerate(range(start+step, start+step+(rows*step), step)):248 for i, t in enumerate(
243 self.log.debug('DATA: %s %s (%s)' % (t, i, datetime.fromtimestamp(t)))249 range(start + step, start + step + (rows * step), step)):
250 self.log.debug(
251 'DATA: %s %s (%s)' % (t, i, datetime.fromtimestamp(t)))
244 my_rrd.bufferValue(t, i)252 my_rrd.bufferValue(t, i)
245253
246 # Add further data 1 second later to demonstrate that the rrd254 # Add further data 1 second later to demonstrate that the rrd
247255
=== modified file 'test.html'
--- test.html 2010-10-03 23:27:06 +0000
+++ test.html 2011-06-12 15:34:30 +0000
@@ -3,10 +3,7 @@
3 <head>3 <head>
4 <meta charset="utf-8">4 <meta charset="utf-8">
5 <title>Jarmon Unit Test Runner</title>5 <title>Jarmon Unit Test Runner</title>
6 <link rel="stylesheet" type="text/css"6
7 href="http://developer.yahoo.com/yui/3/assets/yui.css"/>
8 <link rel="stylesheet" type="text/css"
9 href="http://yui.yahooapis.com/3.1.1/build/cssfonts/fonts-min.css"/>
10 <style type='text/css'>7 <style type='text/css'>
11 .chart {8 .chart {
12 width: 500px;9 width: 500px;

Subscribers

People subscribed via source and target branches

to all changes: