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
1=== modified file 'docs/examples/assets/css/style.css'
2--- docs/examples/assets/css/style.css 2010-07-22 11:35:48 +0000
3+++ docs/examples/assets/css/style.css 2011-06-12 15:34:30 +0000
4@@ -9,11 +9,7 @@
5 }
6
7 h2 {
8- padding: 0 0 0 55px;
9- margin: 20px auto 5px auto;
10 font-size: 14px;
11- text-align: left;
12- clear: both;
13 }
14
15 p, li, dt, dd, td, th, div {
16@@ -36,6 +32,7 @@
17 height:200px;
18 width: 850px;
19 margin: 0 auto 0 auto;
20+ clear: both;
21 }
22
23 .tickLabel {
24@@ -75,6 +72,11 @@
25 border: none;
26 }
27
28+input[type=text] {
29+ padding: 3px;
30+ border: 1px solid #EEE;
31+}
32+
33 .notice {
34 border: 1px solid Green;
35 background: #FFDDFF;
36@@ -85,3 +87,34 @@
37 #calroot {
38 z-index: 2;
39 }
40+
41+.chart-header {
42+ width: 790px;
43+ padding: 5px 0 5px 0;
44+ margin: 20px auto 0 auto;
45+ position: relative;
46+ left: 25px;
47+}
48+
49+.chart-header:AFTER {
50+ content: ''
51+}
52+
53+.chart-container h2{
54+ float: left;
55+ margin: 0;
56+}
57+
58+.chart-container .chart-controls{
59+ float: right;
60+ margin: 0;
61+}
62+
63+.tab-controls {
64+ width: 790px;
65+ padding: 5px 0 5px 0;
66+ margin: 20px auto 0 auto;
67+ text-align: right;
68+ position: relative;
69+ left: 25px;
70+}
71
72=== modified file 'docs/examples/index.html'
73--- docs/examples/index.html 2010-10-03 23:54:48 +0000
74+++ docs/examples/index.html 2011-06-12 15:34:30 +0000
75@@ -16,151 +16,15 @@
76 <script type="text/javascript" src="../../jarmon/jarmon.js"></script>
77 <script type="text/javascript" src="jarmon_example_recipes.js"></script>
78 <script type="text/javascript">
79- // Recipes for the charts on this page
80-
81- var application_recipes = [
82- {
83- title: 'Jarmon Webserver TCP Stats',
84- data: [
85- ['data/tcpconns-8080-local/tcp_connections-CLOSE_WAIT.rrd', 0, 'CLOSE_WAIT', ''],
86- ['data/tcpconns-8080-local/tcp_connections-SYN_RECV.rrd', 0, 'SYN_RECV', ''],
87- ['data/tcpconns-8080-local/tcp_connections-TIME_WAIT.rrd', 0, 'TIME_WAIT', ''],
88- ['data/tcpconns-8080-local/tcp_connections-CLOSED.rrd', 0, 'CLOSED', ''],
89- ['data/tcpconns-8080-local/tcp_connections-FIN_WAIT2.rrd', 0, 'FIN_WAIT2', ''],
90- ['data/tcpconns-8080-local/tcp_connections-FIN_WAIT1.rrd', 0, 'FIN_WAIT1', ''],
91- ['data/tcpconns-8080-local/tcp_connections-ESTABLISHED.rrd', 0, 'ESTABLISHED', ''],
92- ['data/tcpconns-8080-local/tcp_connections-LAST_ACK.rrd', 0, 'LAST_ACK', ''],
93- ['data/tcpconns-8080-local/tcp_connections-LISTEN.rrd', 0, 'LISTEN', ''],
94- ['data/tcpconns-8080-local/tcp_connections-SYN_SENT.rrd', 0, 'SYN_SENT', ''],
95- ['data/tcpconns-8080-local/tcp_connections-CLOSING.rrd', 0, 'CLOSING', '']
96- ],
97- options: jQuery.extend(true, {yaxis: {tickDecimals: 0}}, jarmon.Chart.BASE_OPTIONS, jarmon.Chart.STACKED_OPTIONS)
98- }
99- ];
100-
101-
102- function initialiseCharts() {
103- /**
104- * Setup chart date range controls and all charts
105- **/
106-
107- var p = new jarmon.Parallimiter(1);
108- function serialDownloader(url) {
109- return p.addCallable(jarmon.downloadBinary, [url]);
110- }
111-
112- // Extract the chart template from the page
113- var chartTemplate = $('.chart-container').remove();
114-
115- function templateFactory(parentEl) {
116- return function() {
117- // The chart template must be appended to the page early, so
118- // that flot can calculate chart dimensions etc.
119- return chartTemplate.clone().appendTo(parentEl);
120- }
121- }
122-
123- var cc = new jarmon.ChartCoordinator($('.chartRangeControl'));
124- var t;
125- // Initialise tabs and update charts when tab is clicked
126- $(".css-tabs:first").bind('click', function(i) {
127- // XXX: Hack to give the tab just enough time to become visible
128- // so that flot can calculate chart dimensions.
129- window.clearTimeout(t);
130- t = window.setTimeout(function() { cc.update(); }, 100);
131- });
132-
133- cc.charts = [].concat(
134- jarmon.Chart.fromRecipe(
135- [].concat(
136- jarmon.COLLECTD_RECIPES.cpu,
137- jarmon.COLLECTD_RECIPES.memory,
138- jarmon.COLLECTD_RECIPES.load),
139- templateFactory('.system-charts'), serialDownloader),
140- jarmon.Chart.fromRecipe(
141- jarmon.COLLECTD_RECIPES.interface,
142- templateFactory('.network-charts'), serialDownloader),
143- jarmon.Chart.fromRecipe(
144- jarmon.COLLECTD_RECIPES.dns,
145- templateFactory('.dns-charts'), serialDownloader),
146- jarmon.Chart.fromRecipe(
147- application_recipes,
148- templateFactory('.application-charts'), serialDownloader)
149- );
150-
151- // Initialise all the charts
152- cc.init();
153- }
154
155 $(function() {
156- // Add dhtml calendars to the date input fields
157- $(".timerange_control img")
158- .dateinput({
159- 'format': 'dd mmm yyyy 00:00:00',
160- 'max': +1,
161- 'css': {'input': 'jquerytools_date'}})
162- .bind('onBeforeShow', function(e) {
163- var classes = $(this).attr('class').split(' ');
164- var currentDate, input_selector;
165- for(var i=0; i<=classes.length; i++) {
166- input_selector = '[name="' + classes[i] + '"]';
167- // Look for a neighboring input element whose name matches the
168- // class name of this calendar
169- // Parse the value as a date if the returned date.getTime
170- // returns NaN we know it's an invalid date
171- // XXX: is there a better way to check for valid date?
172- currentDate = new Date($(this).siblings(input_selector).val());
173- if(currentDate.getTime() != NaN) {
174- $(this).data('dateinput')._input_selector = input_selector;
175- $(this).data('dateinput')._initial_val = currentDate.getTime();
176- $(this).data('dateinput').setValue(currentDate);
177- break;
178- }
179- }
180- })
181- .bind('onHide', function(e) {
182- // Called after a calendar date has been chosen by the user.
183-
184- // Use the sibling selector that we generated above before opening
185- // the calendar
186- var input_selector = $(this).data('dateinput')._input_selector;
187- var oldStamp = $(this).data('dateinput')._initial_val;
188- var newDate = $(this).data('dateinput').getValue();
189- // Only update the form field if the date has changed.
190- if(oldStamp != newDate.getTime()) {
191- $(this).siblings(input_selector).val(
192- newDate.toString().split(' ').slice(1,5).join(' '));
193- // Trigger a change event which should automatically update the
194- // graphs and change the timerange drop down selector to
195- // "custom"
196- $(this).siblings(input_selector).trigger('change');
197- }
198- });
199-
200- // Avoid overlaps between the calendars
201- // XXX: This is a bit of hack, what if there's more than one set of calendar
202- // controls on a page?
203- $(".timerange_control img.from_custom").bind('onBeforeShow',
204- function() {
205- var otherVal = new Date(
206- $('.timerange_control [name="to_custom"]').val());
207-
208- $(this).data('dateinput').setMax(otherVal);
209- }
210- );
211- $(".timerange_control img.to_custom").bind('onBeforeShow',
212- function() {
213- var otherVal = new Date(
214- $('.timerange_control [name="from_custom"]').val());
215-
216- $(this).data('dateinput').setMin(otherVal);
217- }
218- );
219-
220- // Setup dhtml tabs
221- $(".css-tabs").tabs(".css-panes > div", {history: true});
222-
223- initialiseCharts();
224+ jarmon.buildTabbedChartUi(
225+ $('.chart-container').remove(),
226+ jarmon.CHART_RECIPES_COLLECTD,
227+ $('.tabbed-chart-interface'),
228+ jarmon.TAB_RECIPES_STANDARD,
229+ $('.chartRangeControl')
230+ );
231 });
232 </script>
233 </head>
234@@ -194,24 +58,19 @@
235 <div class="range-preview"
236 title="Time range preview - click and drag to select a custom timerange" ></div>
237 </form>
238- <ul class="css-tabs">
239- <li><a href="#system">System</a></li>
240- <li><a href="#network">Network</a></li>
241- <li><a href="#dns">DNS</a></li>
242- <li><a href="#application">Application</a></li>
243- </ul>
244- <div class="css-panes charts">
245- <div class="system-charts"></div>
246- <div class="network-charts"></div>
247- <div class="dns-charts"></div>
248- <div class="application-charts"></div>
249- </div>
250- <div class="chart-container">
251+ </div>
252+ <div class="tabbed-chart-interface"></div>
253+ <div class="chart-container">
254+ <div class="chart-header">
255 <h2 class="title"></h2>
256- <div class="error"></div>
257- <div class="chart"></div>
258- <div class="graph-legend"></div>
259+ <div class="chart-controls">
260+ <input type="button" name="chart_edit" value="edit">
261+ <input type="button" name="chart_delete" value="delete">
262+ </div>
263 </div>
264+ <div class="error"></div>
265+ <div class="chart"></div>
266+ <div class="graph-legend"></div>
267 </div>
268 </body>
269 </html>
270
271=== modified file 'docs/examples/jarmon_example_recipes.js'
272--- docs/examples/jarmon_example_recipes.js 2010-08-22 13:41:26 +0000
273+++ docs/examples/jarmon_example_recipes.js 2011-06-12 15:34:30 +0000
274@@ -9,80 +9,76 @@
275 var jarmon = {};
276 }
277
278-jarmon.COLLECTD_RECIPES = {
279- 'cpu': [
280- {
281- title: 'CPU Usage',
282- data: [
283- ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'],
284- ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'],
285- ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'],
286- ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'],
287- ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'],
288- ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%']
289- ],
290- options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS,
291- jarmon.Chart.STACKED_OPTIONS)
292- }
293- ],
294-
295- 'memory': [
296- {
297- title: 'Memory',
298- data: [
299- ['data/memory/memory-buffered.rrd', 0, 'Buffered', 'B'],
300- ['data/memory/memory-used.rrd', 0, 'Used', 'B'],
301- ['data/memory/memory-cached.rrd', 0, 'Cached', 'B'],
302- ['data/memory/memory-free.rrd', 0, 'Free', 'B']
303- ],
304- options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS,
305- jarmon.Chart.STACKED_OPTIONS)
306- }
307- ],
308-
309- 'dns': [
310- {
311- title: 'DNS Query Types',
312- data: [
313- ['data/dns/dns_qtype-A.rrd', 0, 'A', 'Q/s'],
314- ['data/dns/dns_qtype-PTR.rrd', 0, 'PTR', 'Q/s'],
315- ['data/dns/dns_qtype-SOA.rrd', 0, 'SOA', 'Q/s'],
316- ['data/dns/dns_qtype-SRV.rrd', 0, 'SRV', 'Q/s']
317- ],
318- options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
319- },
320-
321- {
322- title: 'DNS Return Codes',
323- data: [
324- ['data/dns/dns_rcode-NOERROR.rrd', 0, 'NOERROR', 'Q/s'],
325- ['data/dns/dns_rcode-NXDOMAIN.rrd', 0, 'NXDOMAIN', 'Q/s'],
326- ['data/dns/dns_rcode-SERVFAIL.rrd', 0, 'SERVFAIL', 'Q/s']
327- ],
328- options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
329- }
330- ],
331-
332- 'load': [
333- {
334- title: 'Load Average',
335- data: [
336- ['data/load/load.rrd', 'shortterm', 'Short Term', ''],
337- ['data/load/load.rrd', 'midterm', 'Medium Term', ''],
338- ['data/load/load.rrd', 'longterm', 'Long Term', '']
339- ],
340- options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
341- }
342- ],
343-
344- 'interface': [
345- {
346- title: 'Wlan0 Throughput',
347- data: [
348- ['data/interface/if_octets-wlan0.rrd', 'tx', 'Transmit', 'b/s'],
349- ['data/interface/if_octets-wlan0.rrd', 'rx', 'Receive', 'b/s']
350- ],
351- options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
352- }
353- ]
354+jarmon.TAB_RECIPES_STANDARD = [
355+ ['System', ['cpu', 'memory','load']],
356+ ['Network', ['interface']],
357+ ['DNS', ['dns_query_types', 'dns_return_codes']]
358+];
359+
360+jarmon.CHART_RECIPES_COLLECTD = {
361+ 'cpu': {
362+ title: 'CPU Usage',
363+ data: [
364+ ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'],
365+ ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'],
366+ ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'],
367+ ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'],
368+ ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'],
369+ ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%']
370+ ],
371+ options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS,
372+ jarmon.Chart.STACKED_OPTIONS)
373+ },
374+
375+ 'memory': {
376+ title: 'Memory',
377+ data: [
378+ ['data/memory/memory-buffered.rrd', 0, 'Buffered', 'B'],
379+ ['data/memory/memory-used.rrd', 0, 'Used', 'B'],
380+ ['data/memory/memory-cached.rrd', 0, 'Cached', 'B'],
381+ ['data/memory/memory-free.rrd', 0, 'Free', 'B']
382+ ],
383+ options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS,
384+ jarmon.Chart.STACKED_OPTIONS)
385+ },
386+
387+ 'dns_query_types': {
388+ title: 'DNS Query Types',
389+ data: [
390+ ['data/dns/dns_qtype-A.rrd', 0, 'A', 'Q/s'],
391+ ['data/dns/dns_qtype-PTR.rrd', 0, 'PTR', 'Q/s'],
392+ ['data/dns/dns_qtype-SOA.rrd', 0, 'SOA', 'Q/s'],
393+ ['data/dns/dns_qtype-SRV.rrd', 0, 'SRV', 'Q/s']
394+ ],
395+ options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
396+ },
397+
398+ 'dns_return_codes': {
399+ title: 'DNS Return Codes',
400+ data: [
401+ ['data/dns/dns_rcode-NOERROR.rrd', 0, 'NOERROR', 'Q/s'],
402+ ['data/dns/dns_rcode-NXDOMAIN.rrd', 0, 'NXDOMAIN', 'Q/s'],
403+ ['data/dns/dns_rcode-SERVFAIL.rrd', 0, 'SERVFAIL', 'Q/s']
404+ ],
405+ options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
406+ },
407+
408+ 'load': {
409+ title: 'Load Average',
410+ data: [
411+ ['data/load/load.rrd', 'shortterm', 'Short Term', ''],
412+ ['data/load/load.rrd', 'midterm', 'Medium Term', ''],
413+ ['data/load/load.rrd', 'longterm', 'Long Term', '']
414+ ],
415+ options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
416+ },
417+
418+ 'interface': {
419+ title: 'Wlan0 Throughput',
420+ data: [
421+ ['data/interface/if_octets-wlan0.rrd', 'tx', 'Transmit', 'b/s'],
422+ ['data/interface/if_octets-wlan0.rrd', 'rx', 'Receive', 'b/s']
423+ ],
424+ options: jQuery.extend(true, {}, jarmon.Chart.BASE_OPTIONS)
425+ }
426 };
427
428=== modified file 'jarmon/jarmon.js'
429--- jarmon/jarmon.js 2010-10-04 22:59:04 +0000
430+++ jarmon/jarmon.js 2011-06-12 15:34:30 +0000
431@@ -256,6 +256,18 @@
432 'lastUpdated': lastUpdated*1000.0};
433 };
434
435+
436+jarmon.RrdQuery.prototype.getDSNames = function() {
437+ /**
438+ * Return a list of RRD Data Source names
439+ *
440+ * @method getDSNames
441+ * @return {Array} An array of DS names.
442+ **/
443+ return this.rrd.getDSNames();
444+};
445+
446+
447 /**
448 * A wrapper around RrdQuery which provides asynchronous access to the data in a
449 * remote RRD file.
450@@ -275,23 +287,11 @@
451 this._download = null;
452 };
453
454-jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId) {
455- /**
456- * Return a Flot compatible data series asynchronously.
457- *
458- * @method getData
459- * @param startTime {Number} The start timestamp
460- * @param endTime {Number} The end timestamp
461- * @param dsId {Variant} identifier of the RRD datasource (string or number)
462- * @return {Object} A Deferred which calls back with a flot data series.
463- **/
464- var endTimestamp = endTime/1000;
465
466- // Download the rrd if there has never been a download or if the last
467- // completed download had a lastUpdated timestamp less than the requested
468- // end time.
469- // Don't start another download if one is already in progress.
470- if(!this._download || (this._download.fired > -1 && this.lastUpdate < endTimestamp )) {
471+jarmon.RrdQueryRemote.prototype._callRemote = function(methodName, args) {
472+ // Download the rrd if there has never been a download and don't start
473+ // another download if one is already in progress.
474+ if(!this._download) {
475 this._download = this.downloader(this.url)
476 .addCallback(
477 function(self, binary) {
478@@ -307,9 +307,10 @@
479 // Set up a deferred which will call getData on the local RrdQuery object
480 // returning a flot compatible data object to the caller.
481 var ret = new MochiKit.Async.Deferred().addCallback(
482- function(self, startTime, endTime, dsId, rrd) {
483- return new jarmon.RrdQuery(rrd, self.unit).getData(startTime, endTime, dsId);
484- }, this, startTime, endTime, dsId);
485+ function(self, methodName, args, rrd) {
486+ var rq = new jarmon.RrdQuery(rrd, self.unit);
487+ return rq[methodName].apply(rq, args);
488+ }, this, methodName, args);
489
490 // Add a pair of callbacks to the current download which will callback the
491 // result which we setup above.
492@@ -326,6 +327,35 @@
493 return ret;
494 };
495
496+
497+jarmon.RrdQueryRemote.prototype.getData = function(startTime, endTime, dsId, cfName) {
498+ /**
499+ * Return a Flot compatible data series asynchronously.
500+ *
501+ * @method getData
502+ * @param startTime {Number} The start timestamp
503+ * @param endTime {Number} The end timestamp
504+ * @param dsId {Variant} identifier of the RRD datasource (string or number)
505+ * @return {Object} A Deferred which calls back with a flot data series.
506+ **/
507+ if(this.lastUpdate < endTime/1000) {
508+ this._download = null;
509+ }
510+ return this._callRemote('getData', [startTime, endTime, dsId, cfName]);
511+};
512+
513+
514+jarmon.RrdQueryRemote.prototype.getDSNames = function() {
515+ /**
516+ * Return a list of RRD Data Source names
517+ *
518+ * @method getDSNames
519+ * @return {Object} A Deferred which calls back with an array of DS names.
520+ **/
521+ return this._callRemote('getDSNames');
522+};
523+
524+
525 /**
526 * Wraps RrdQueryRemote to provide access to a different RRD DSs within a
527 * single RrdDataSource.
528@@ -364,12 +394,17 @@
529 * @param options {Object} Flot options which control how the chart should be
530 * drawn.
531 **/
532-jarmon.Chart = function(template, options) {
533+jarmon.Chart = function(template, recipe, downloader) {
534 this.template = template;
535- this.options = jQuery.extend(true, {yaxis: {}}, options);
536+ this.recipe = recipe;
537+ this.downloader = downloader;
538+
539+ this.options = jQuery.extend(true, {yaxis: {}}, recipe.options);
540
541 this.data = [];
542
543+ this.setup();
544+
545 var self = this;
546
547
548@@ -380,7 +415,6 @@
549 self.draw();
550 });
551
552-
553 this.options['yaxis']['ticks'] = function(axis) {
554 /*
555 * Choose a suitable SI multiplier based on the min and max values from
556@@ -442,6 +476,29 @@
557 };
558 };
559
560+jarmon.Chart.prototype.setup = function() {
561+ this.template.find('.title').text(this.recipe['title']);
562+ this.data = [];
563+ var recipe = this.recipe;
564+ var dataDict = {};
565+ for(var j=0; j<recipe['data'].length; j++) {
566+ var rrd = recipe['data'][j][0];
567+ var ds = recipe['data'][j][1];
568+ // Test for integer DS index as opposed to DS name
569+ var dsi = parseInt(ds);
570+ if(ds.toString() == dsi.toString()) {
571+ ds = dsi;
572+ }
573+ var label = recipe['data'][j][2];
574+ var unit = recipe['data'][j][3];
575+
576+ if(typeof dataDict[rrd] == 'undefined') {
577+ dataDict[rrd] = new jarmon.RrdQueryRemote(rrd, unit, this.downloader);
578+ }
579+ this.addData(label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds));
580+ }
581+};
582+
583 jarmon.Chart.prototype.addData = function(label, db, enabled) {
584 /**
585 * Add details of a remote RRD data source whose data will be added to this
586@@ -568,13 +625,15 @@
587 // to accomodate the color box
588 var legend = self.template.find('.graph-legend').show();
589 legend.empty();
590- self.template.find('.legendLabel')
591- .each(function(i, el) {
592+ self.template.find('.legendLabel').each(
593+ function(i, el) {
594 var orig = $(el);
595 var label = orig.text();
596- var newEl = $('<div />')
597- .attr('class', 'legendItem')
598- .attr('title', 'Data series switch - click to turn this data series on or off')
599+ var newEl = $('<div />', {
600+ 'class': 'legendItem',
601+ 'title': 'Data series switch - click to turn \
602+ this data series on or off'
603+ })
604 .width(orig.width()+20)
605 .text(label)
606 .prepend(orig.prev().find('div div').clone().addClass('legendColorBox'))
607@@ -585,8 +644,8 @@
608 if( $.inArray(label, disabled) > -1 ) {
609 newEl.addClass('disabled');
610 }
611- })
612- .remove();
613+ }
614+ ).remove();
615 legend.append($('<div />').css('clear', 'both'));
616 self.template.find('.legend').remove();
617
618@@ -608,49 +667,513 @@
619 };
620
621
622-jarmon.Chart.fromRecipe = function(recipes, templateFactory, downloader) {
623+/**
624+ * Generate a form through which to choose a data source from a remote RRD file
625+ *
626+ * @class jarmon.RrdChooser
627+ * @constructor
628+ **/
629+jarmon.RrdChooser = function($tpl) {
630+ this.$tpl = $tpl;
631+ this.data = {
632+ rrdUrl: '',
633+ dsName: '',
634+ dsLabel: '',
635+ dsUnit:''
636+ };
637+};
638+
639+jarmon.RrdChooser.prototype.drawRrdUrlForm = function() {
640+ var self = this;
641+ this.$tpl.empty();
642+
643+ $('<form/>').append(
644+ $('<div/>').append(
645+ $('<p/>').text('Enter the URL of an RRD file'),
646+ $('<label/>').append(
647+ 'URL: ',
648+ $('<input/>', {
649+ type: 'text',
650+ name: 'rrd_url',
651+ value: this.data.rrdUrl
652+ })
653+ ),
654+ $('<input/>', {type: 'submit', value: 'download'}),
655+ $('<div/>', {class: 'next'})
656+ )
657+ ).submit(
658+ function(e) {
659+ self.data.rrdUrl = this['rrd_url'].value;
660+ $placeholder = $(this).find('.next').empty();
661+ new jarmon.RrdQueryRemote(self.data.rrdUrl).getDSNames().addCallback(
662+ function($placeholder, dsNames) {
663+ if(dsNames.length > 1) {
664+ $('<p/>').text(
665+ 'The RRD file contains multiple data sources. \
666+ Choose one:').appendTo($placeholder);
667+
668+ $(dsNames).map(
669+ function(i, el) {
670+ return $('<input/>', {
671+ type: 'button',
672+ value: el
673+ }
674+ ).click(
675+ function(e) {
676+ self.data.dsName = this.value;
677+ self.drawDsLabelForm();
678+ }
679+ );
680+ }).appendTo($placeholder);
681+ } else {
682+ self.data.dsName = dsNames[0];
683+ self.drawDsLabelForm();
684+ }
685+ }, $placeholder
686+ ).addErrback(
687+ function($placeholder, err) {
688+ $('<p/>', {'class': 'error'}).text(err.toString()).appendTo($placeholder);
689+ }, $placeholder
690+ );
691+ return false;
692+ }
693+ ).appendTo(this.$tpl);
694+}
695+
696+jarmon.RrdChooser.prototype.drawDsLabelForm = function() {
697+ var self = this;
698+ this.$tpl.empty();
699+
700+ $('<form/>').append(
701+ $('<p/>').text('Choose a label and unit for this data source.'),
702+ $('<div/>').append(
703+ $('<label/>').append(
704+ 'Label: ',
705+ $('<input/>', {
706+ type: 'text',
707+ name: 'dsLabel',
708+ value: this.data.dslabel || this.data.dsName
709+ })
710+ )
711+ ),
712+ $('<div/>').append(
713+ $('<label/>').append(
714+ 'Unit: ',
715+ $('<input/>', {
716+ type: 'text',
717+ name: 'dsUnit',
718+ value: this.data.dsUnit
719+ })
720+ )
721+ ),
722+ $('<input/>', {type: 'button', value: 'back'}).click(
723+ function(e) {
724+ self.drawRrdUrlForm();
725+ }
726+ ),
727+ $('<input/>', {type: 'submit', value: 'save'}),
728+ $('<div/>', {class: 'next'})
729+ ).submit(
730+ function(e) {
731+ self.data.dsLabel = this['dsLabel'].value;
732+ self.data.dsUnit = this['dsUnit'].value;
733+ self.drawDsSummary();
734+ return false;
735+ }
736+ ).appendTo(this.$tpl);
737+};
738+
739+
740+jarmon.RrdChooser.prototype.drawDsSummary = function() {
741+ var self = this;
742+ this.$tpl.empty();
743+
744+ jQuery.each(this.data, function(i, el) {
745+ $('<p/>').append(
746+ $('<strong/>').text(i),
747+ [': ', el].join('')
748+ ).appendTo(self.$tpl);
749+ });
750+
751+ this.$tpl.append(
752+ $('<input/>', {type: 'button', value: 'back'}).click(
753+ function(e) {
754+ self.drawDsLabelForm();
755+ }
756+ ),
757+ $('<input/>', {type: 'button', value: 'finish'})
758+ );
759+};
760+
761+
762+jarmon.ChartEditor = function($tpl, chart) {
763+ this.$tpl = $tpl;
764+ this.chart = chart;
765+
766+ $('form', this.$tpl[0]).live(
767+ 'submit',
768+ {self: this},
769+ function(e) {
770+ var self = e.data.self;
771+ self.chart.recipe.title = this['title'].value;
772+ self.chart.recipe.data = $(this).find('.datasources tbody tr').map(
773+ function(i, el) {
774+ return $(el).find('input[type=text]').map(
775+ function(i, el) {
776+ return el.value;
777+ }
778+ );
779+ }
780+ );
781+ self.chart.setup();
782+ self.chart.draw();
783+ return false;
784+ }
785+ );
786+
787+ $('form', this.$tpl[0]).live(
788+ 'reset',
789+ {self: this},
790+ function(e) {
791+ var self = e.data.self;
792+ self.draw();
793+ return false;
794+ }
795+ );
796+
797+ $('form input[name=datasource_delete]', this.$tpl[0]).live(
798+ 'click',
799+ function(e) {
800+ $(this).closest('tr').remove();
801+ }
802+ );
803+
804+ $('form input[name=datasource_add]', this.$tpl[0]).live(
805+ 'click',
806+ {self: this},
807+ function(e) {
808+ var self = e.data.self;
809+ self._addDatasourceRow(
810+ self._extractRowValues(
811+ $(this).closest('tr')
812+ )
813+ );
814+ $(this).closest('tr').find('input[type=text]').val('');
815+ }
816+ );
817+};
818+
819+jarmon.ChartEditor.prototype.draw = function() {
820+ var self = this;
821+ this.$tpl.empty();
822+
823+ $('<form/>').append(
824+ $('<div/>').append(
825+ $('<label/>').append(
826+ 'Title: ',
827+ $('<input/>', {
828+ type: 'text',
829+ name: 'title',
830+ value: this.chart.recipe.title
831+ })
832+ )
833+ ),
834+ $('<fieldset/>').append(
835+ $('<legend/>').text('Data Sources'),
836+ $('<table/>', {'class': 'datasources'}).append(
837+ $('<thead/>').append(
838+ $('<tr/>').append(
839+ $('<th/>').text('RRD File'),
840+ $('<th/>').text('DS Name'),
841+ $('<th/>').text('DS Label'),
842+ $('<th/>').text('DS Unit'),
843+ $('<th/>')
844+ )
845+ ),
846+ $('<tfoot/>').append(
847+ $('<tr/>').append(
848+ $('<td/>').append(
849+ $('<input/>', {type: 'text'})
850+ ),
851+ $('<td/>').append(
852+ $('<input/>', {type: 'text'})
853+ ),
854+ $('<td/>').append(
855+ $('<input/>', {type: 'text'})
856+ ),
857+ $('<td/>').append(
858+ $('<input/>', {type: 'text'})
859+ ),
860+ $('<td/>').append(
861+ $('<input/>', {
862+ type: 'button',
863+ value: 'add',
864+ name: 'datasource_add'
865+ })
866+ )
867+ )
868+ ),
869+ $('<tbody/>')
870+ )
871+ ),
872+ $('<input/>', {type: 'submit', value: 'save'}),
873+ $('<input/>', {type: 'reset', value: 'reset'})
874+ ).appendTo(this.$tpl);
875+
876+ for(var i=0; i<this.chart.recipe.data.length; i++) {
877+ this._addDatasourceRow(this.chart.recipe.data[i]);
878+ }
879+};
880+
881+
882+jarmon.ChartEditor.prototype._extractRowValues = function($row) {
883+ return $row.find('input[type=text]').map(
884+ function(i, el) {
885+ return el.value;
886+ }
887+ )
888+};
889+
890+
891+jarmon.ChartEditor.prototype._addDatasourceRow = function(record) {
892+ $('<tr/>').append(
893+ $('<td/>').append(
894+ $('<input/>', {type: 'text', value: record[0]})
895+ ),
896+ $('<td/>').append(
897+ $('<input/>', {type: 'text', value: record[1]})
898+ ),
899+ $('<td/>').append(
900+ $('<input/>', {type: 'text', value: record[2]})
901+ ),
902+ $('<td/>').append(
903+ $('<input/>', {type: 'text', value: record[3]})
904+ ),
905+ $('<td/>').append(
906+ $('<input/>', {
907+ type: 'button',
908+ value: 'delete',
909+ name: 'datasource_delete'
910+ })
911+ )
912+ ).appendTo(this.$tpl.find('.datasources tbody'));
913+};
914+
915+
916+jarmon.TabbedInterface = function($tpl, recipe) {
917+ this.$tpl = $tpl;
918+ this.recipe = recipe;
919+ this.placeholders = [];
920+
921+ this.$tabBar = $('<ul/>', {'class': 'css-tabs'}).appendTo($tpl);
922+
923+ // Icon and hidden input box for adding new tabs. See event handlers below.
924+ this.$newTabControls = $('<li/>', {
925+ 'class': 'newTabControls',
926+ 'title': 'Add new tab'
927+ }).append(
928+ $('<img/>', {src: 'assets/icons/next.gif'}),
929+ $('<input/>', {'type': 'text'}).hide()
930+ ).appendTo(this.$tabBar);
931+
932+ this.$tabPanels = $('<div/>', {'class': 'css-panes charts'}).appendTo($tpl);
933+ var tabName, $tabPanel, placeNames;
934+ for(var i=0; i<recipe.length; i++) {
935+ tabName = recipe[i][0];
936+ placeNames = recipe[i][1];
937+
938+ $tabPanel = this.newTab(tabName);
939+
940+ for(var j=0; j<placeNames.length; j++) {
941+ this.placeholders.push([
942+ placeNames[j], $('<div/>').appendTo($tabPanel)]);
943+ }
944+ }
945+
946+ this.setup();
947+
948+ // Show the new tab name input box when the user clicks the new tab icon
949+ $('ul.css-tabs > li.newTabControls > img', $tpl[0]).live(
950+ 'click',
951+ function(e) {
952+ $(this).hide().siblings().show().focus();
953+ }
954+ );
955+
956+ // When the "new" tab input loses focus, use its value to create a new
957+ // tab.
958+ // XXX: Due to event bubbling, this event seems to be triggered twice, but
959+ // only when the input is forcefully blurred by the "keypress" event handler
960+ // below. To prevent two tabs, we blank the input field value. Tried
961+ // preventing event bubbling, but there seems to be some subtle difference
962+ // with the use of jquery live event handlers.
963+ $('ul.css-tabs > li.newTabControls > input', $tpl[0]).live(
964+ 'blur',
965+ {self: this},
966+ function(e) {
967+ var self = e.data.self;
968+ var value = this.value;
969+ this.value = '';
970+ $(this).hide().siblings().show();
971+ if(value) {
972+ self.newTab(value);
973+ self.setup();
974+ self.$tabBar.data("tabs").click(value);
975+ }
976+ }
977+ );
978+
979+ // Unfocus the input element when return key is pressed. Triggers a
980+ // blur event which then replaces the input with a tab
981+ $('ul.css-tabs > li > input', $tpl[0]).live(
982+ 'keypress',
983+ function(e) {
984+ if(e.which == 13) {
985+ $(this).blur();
986+ }
987+ }
988+ );
989+
990+ // Show tab name input box when tab is double clicked.
991+ $('ul.css-tabs > li > a', $tpl[0]).live(
992+ 'dblclick',
993+ {self: this},
994+ function(e) {
995+ var $originalLink = $(this);
996+ var $input = $('<input/>', {
997+ 'value': $originalLink.text(),
998+ 'name': 'editTabTitle',
999+ 'type': 'text'
1000+ })
1001+ $originalLink.replaceWith($input);
1002+ $input.focus();
1003+ }
1004+ );
1005+
1006+ // Handle the updating of the tab when its name is edited.
1007+ $('ul.css-tabs > li > input[name=editTabTitle]', $tpl[0]).live(
1008+ 'blur',
1009+ {self: this},
1010+ function(e) {
1011+ var self = e.data.self;
1012+ $(this).replaceWith(
1013+ $('<a/>', {
1014+ href: ['#', this.value].join('')
1015+ }).text(this.value)
1016+ )
1017+ self.setup();
1018+ self.$tabBar.data("tabs").click(this.value);
1019+ }
1020+ );
1021+
1022+ $('input[name=add_new_chart]', $tpl[0]).live(
1023+ 'click',
1024+ {self: this},
1025+ function(e) {
1026+ console.log(e);
1027+ }
1028+ );
1029+};
1030+
1031+jarmon.TabbedInterface.prototype.newTab = function(tabName) {
1032+ // Add a tab
1033+ $('<li/>').append(
1034+ $('<a/>', {href: ['#', tabName].join('')}).text(tabName)
1035+ ).appendTo(this.$tabBar);
1036+ var $placeholder = $('<div/>');
1037+ // Add tab panel
1038+ $('<div/>').append(
1039+ $placeholder,
1040+ $('<div/>', {'class': 'tab-controls'}).append(
1041+ $('<input/>', {
1042+ type: 'button',
1043+ value: 'Add new chart',
1044+ name: 'add_new_chart'
1045+ })
1046+ )
1047+ ).appendTo(this.$tabPanels);
1048+
1049+ return $placeholder;
1050+};
1051+
1052+jarmon.TabbedInterface.prototype.setup = function() {
1053+ this.$newTabControls.remove();
1054+ // Destroy then re-initialise the jquerytools tabs plugin
1055+ var api = this.$tabBar.data("tabs");
1056+ if(api) {
1057+ api.destroy();
1058+ }
1059+ this.$tabBar.tabs(this.$tabPanels.children('div'));
1060+ this.$newTabControls.appendTo(this.$tabBar);
1061+};
1062+
1063+
1064+jarmon.buildTabbedChartUi = function ($chartTemplate, chartRecipes,
1065+ $tabTemplate, tabRecipes,
1066+ $controlPanelTemplate) {
1067 /**
1068- * A static factory method to generate a list of I{Chart} from a list of
1069- * recipes and a list of available rrd files in collectd path format.
1070- *
1071- * @method fromRecipe
1072- * @param recipes {Array} A list of recipe objects.
1073- * @param templateFactory {Function} A callable which generates an html
1074- * template for a chart.
1075- * @param downloader {Function} A download function which returns a Deferred
1076- * @return {Array} A list of Chart objects
1077+ * Setup chart date range controls and all charts
1078 **/
1079-
1080- var charts = [];
1081- var dataDict = {};
1082-
1083- var recipe, chartData, template, c, i, j, ds, label, rrd, unit, re, match;
1084-
1085- for(i=0; i<recipes.length; i++) {
1086- recipe = recipes[i];
1087- chartData = [];
1088-
1089- for(j=0; j<recipe['data'].length; j++) {
1090- rrd = recipe['data'][j][0];
1091- ds = recipe['data'][j][1];
1092- label = recipe['data'][j][2];
1093- unit = recipe['data'][j][3];
1094- if(typeof dataDict[rrd] == 'undefined') {
1095- dataDict[rrd] = new jarmon.RrdQueryRemote(rrd, unit, downloader);
1096- }
1097- chartData.push([label, new jarmon.RrdQueryDsProxy(dataDict[rrd], ds)]);
1098- }
1099- if(chartData.length > 0) {
1100- template = templateFactory();
1101- template.find('.title').text(recipe['title']);
1102- c = new jarmon.Chart(template, recipe['options']);
1103- for(j=0; j<chartData.length; j++) {
1104- c.addData.apply(c, chartData[j]);
1105- }
1106- charts.push(c);
1107- }
1108+ var p = new jarmon.Parallimiter(1);
1109+ function serialDownloader(url) {
1110+ return p.addCallable(jarmon.downloadBinary, [url]);
1111 }
1112- return charts;
1113+
1114+ var ti = new jarmon.TabbedInterface($tabTemplate, tabRecipes);
1115+
1116+ var charts = jQuery.map(
1117+ ti.placeholders,
1118+ function(el, i) {
1119+ var chart = new jarmon.Chart(
1120+ $chartTemplate.clone().appendTo(el[1]),
1121+ chartRecipes[el[0]],
1122+ serialDownloader
1123+ );
1124+
1125+ $('input[name=chart_edit]', el[1][0]).live(
1126+ 'click',
1127+ {chart: chart},
1128+ function(e) {
1129+ var chart = e.data.chart;
1130+ new jarmon.ChartEditor(
1131+ chart.template.find('.graph-legend'), chart).draw();
1132+ }
1133+ );
1134+
1135+ $('input[name=chart_delete]', el[1][0]).live(
1136+ 'click',
1137+ {chart: chart},
1138+ function(e) {
1139+ var chart = e.data.chart;
1140+ chart.template.remove();
1141+ }
1142+ );
1143+
1144+ return chart;
1145+ }
1146+ );
1147+
1148+ var cc = new jarmon.ChartCoordinator($controlPanelTemplate, charts);
1149+ // Update charts when tab is clicked
1150+ ti.$tpl.find(".css-tabs:first").bind(
1151+ 'click',
1152+ {'cc': cc},
1153+ function(e) {
1154+ var cc = e.data.cc;
1155+ // XXX: Hack to give the tab just enough time to become visible
1156+ // so that flot can calculate chart dimensions.
1157+ window.clearTimeout(cc.t);
1158+ cc.t = window.setTimeout(
1159+ function() {
1160+ cc.update();
1161+ }, 100);
1162+ }
1163+ );
1164+
1165+ // Initialise all the charts
1166+ cc.init();
1167+
1168+ return [charts, ti, cc];
1169 };
1170
1171
1172@@ -720,10 +1243,10 @@
1173 * @param ui {Object} A one element jQuery containing an input form and
1174 * placeholders for the timeline and for the series of charts.
1175 **/
1176-jarmon.ChartCoordinator = function(ui) {
1177+jarmon.ChartCoordinator = function(ui, charts) {
1178 var self = this;
1179 this.ui = ui;
1180- this.charts = [];
1181+ this.charts = charts;
1182
1183 // Style and configuration of the range timeline
1184 this.rangePreviewOptions = {
1185@@ -817,11 +1340,107 @@
1186
1187 // When a selection is made on the range timeline, or any of my charts
1188 // redraw all the charts.
1189- this.ui.bind("plotselected", function(event, ranges) {
1190- self.ui.find('[name="from_standard"]').val('custom');
1191- self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to);
1192- self.update();
1193- });
1194+ $(document).bind(
1195+ 'plotselected',
1196+ {self: this},
1197+ function(e, ranges) {
1198+ var self = e.data.self;
1199+ var eventSourceIsMine = false;
1200+
1201+ // plotselected event may be from my range selector chart or
1202+ if( self.ui.has(e.target) ) {
1203+ eventSourceIsMine = true;
1204+ } else {
1205+ // ...it may come from one of the charts under my supervision
1206+ for(var i=0; i<self.charts.length; i++) {
1207+ if(self.charts[i].template.has(e.target).length > 0) {
1208+ eventSourceIsMine = true;
1209+ break;
1210+ }
1211+ }
1212+ }
1213+
1214+ if(eventSourceIsMine) {
1215+ // Update the prepared time range select box to value "custom"
1216+ self.ui.find('[name="from_standard"]').val('custom');
1217+
1218+ // Update all my charts
1219+ self.setTimeRange(ranges.xaxis.from, ranges.xaxis.to);
1220+ self.update();
1221+ }
1222+ }
1223+ );
1224+
1225+ // Add dhtml calendars to the date input fields
1226+ this.ui.find(".timerange_control img")
1227+ .dateinput({
1228+ 'format': 'dd mmm yyyy 00:00:00',
1229+ 'max': +1,
1230+ 'css': {'input': 'jquerytools_date'}})
1231+ .bind('onBeforeShow', function(e) {
1232+ var classes = $(this).attr('class').split(' ');
1233+ var currentDate, input_selector;
1234+ for(var i=0; i<=classes.length; i++) {
1235+ input_selector = '[name="' + classes[i] + '"]';
1236+ // Look for a neighboring input element whose name matches the
1237+ // class name of this calendar
1238+ // Parse the value as a date if the returned date.getTime
1239+ // returns NaN we know it's an invalid date
1240+ // XXX: is there a better way to check for valid date?
1241+ currentDate = new Date($(this).siblings(input_selector).val());
1242+ if(currentDate.getTime() != NaN) {
1243+ $(this).data('dateinput')._input_selector = input_selector;
1244+ $(this).data('dateinput')._initial_val = currentDate.getTime();
1245+ $(this).data('dateinput').setValue(currentDate);
1246+ break;
1247+ }
1248+ }
1249+ })
1250+ .bind('onHide', function(e) {
1251+ // Called after a calendar date has been chosen by the user.
1252+
1253+ // Use the sibling selector that we generated above before opening
1254+ // the calendar
1255+ var input_selector = $(this).data('dateinput')._input_selector;
1256+ var oldStamp = $(this).data('dateinput')._initial_val;
1257+ var newDate = $(this).data('dateinput').getValue();
1258+ // Only update the form field if the date has changed.
1259+ if(oldStamp != newDate.getTime()) {
1260+ $(this).siblings(input_selector).val(
1261+ newDate.toString().split(' ').slice(1,5).join(' '));
1262+ // Trigger a change event which should automatically update the
1263+ // graphs and change the timerange drop down selector to
1264+ // "custom"
1265+ $(this).siblings(input_selector).trigger('change');
1266+ }
1267+ });
1268+
1269+ // Avoid overlaps between the calendars
1270+ // XXX: This is a bit of hack, what if there's more than one set of calendar
1271+ // controls on a page?
1272+ this.ui.find(".timerange_control img.from_custom").bind(
1273+ 'onBeforeShow',
1274+ {self: this},
1275+ function(e) {
1276+ var self = e.data.self;
1277+ var otherVal = new Date(
1278+ self.ui.find('.timerange_control [name="to_custom"]').val());
1279+
1280+ $(this).data('dateinput').setMax(otherVal);
1281+ }
1282+ );
1283+ this.ui.find(".timerange_control img.to_custom").bind(
1284+ 'onBeforeShow',
1285+ {self: this},
1286+ function(e) {
1287+ var self = e.data.self;
1288+ var otherVal = new Date(
1289+ self.ui.find('.timerange_control [name="from_custom"]').val());
1290+
1291+ $(this).data('dateinput').setMin(otherVal);
1292+ }
1293+ );
1294+
1295 };
1296
1297
1298
1299=== modified file 'jarmon/jarmon.test.js'
1300--- jarmon/jarmon.test.js 2010-11-28 14:50:38 +0000
1301+++ jarmon/jarmon.test.js 2011-06-12 15:34:30 +0000
1302@@ -277,6 +277,105 @@
1303
1304
1305 Y.Test.Runner.add(new Y.Test.Case({
1306+ name: "jarmon.RrdQueryRemote",
1307+
1308+ setUp: function() {
1309+ this.rq = new jarmon.RrdQueryRemote('build/test.rrd', '');
1310+ },
1311+
1312+ test_getDataTimeRangeOverlapError: function () {
1313+ /**
1314+ * The starttime must be less than the endtime
1315+ **/
1316+ this.rq.getData(1, 0).addBoth(
1317+ function(self, res) {
1318+ self.resume(function() {
1319+ Y.Assert.isInstanceOf(RangeError, res);
1320+ });
1321+ }, this);
1322+ this.wait();
1323+ },
1324+
1325+
1326+ test_getDataUnknownCfError: function () {
1327+ /**
1328+ * Error is raised if the rrd file doesn't contain an RRA with the
1329+ * requested consolidation function (CF)
1330+ **/
1331+ this.rq.getData(RRD_STARTTIME, RRD_ENDTIME, 0, 'FOO').addBoth(
1332+ function(self, res) {
1333+ self.resume(function() {
1334+ Y.Assert.isInstanceOf(TypeError, res);
1335+ });
1336+ }, this);
1337+ this.wait();
1338+ },
1339+
1340+
1341+ test_getData: function () {
1342+ /**
1343+ * The generated rrd file should have values 0-9 at 300s intervals
1344+ * starting at 1980-01-01 00:00:00
1345+ * Result should include a data points with times > starttime and
1346+ * <= endTime
1347+ **/
1348+ this.rq.getData(RRD_STARTTIME + (RRD_STEP+1) * 1000,
1349+ RRD_ENDTIME - (RRD_STEP-1) * 1000).addBoth(
1350+ function(self, data) {
1351+ self.resume(function() {
1352+ // We request data starting 1 STEP +1s after the RRD file
1353+ // first val and ending 1 STEP -1s before the RRD last val
1354+ // ie one step within the RRD file, but 1s away from the
1355+ // step boundary to test the quantisation of the
1356+ // requested time range.
1357+
1358+ // so we expect two less rows than the total rows in the
1359+ // file.
1360+ Y.Assert.areEqual(RRD_RRAROWS-2, data.data.length);
1361+
1362+ // The value of the first returned row should be the
1363+ // second value in the RRD file (starts at value 0)
1364+ Y.Assert.areEqual(1, data.data[0][1]);
1365+
1366+ // The value of the last returned row should be the
1367+ // 10 value in the RRD file (starts at value 0)
1368+ Y.Assert.areEqual(10, data.data[data.data.length-1][1]);
1369+
1370+ // The timestamp of the first returned row should be
1371+ // exactly one step after the start of the RRD file
1372+ Y.Assert.areEqual(
1373+ RRD_STARTTIME+RRD_STEP*1000, data.data[0][0]);
1374+
1375+ // RRD_ENDTIME is on a step boundary and is therfore
1376+ // actually the start time of a new row
1377+ // So when we ask for endTime = RRD_ENDTIME-STEP-1 we
1378+ // actually get data up to the 2nd to last RRD row.
1379+ Y.Assert.areEqual(
1380+ RRD_ENDTIME-RRD_STEP*1000*2,
1381+ data.data[data.data.length-1][0]);
1382+ });
1383+ }, this);
1384+ this.wait();
1385+ },
1386+
1387+ test_getDataUnknownValues: function () {
1388+ /**
1389+ * If the requested time range is outside the range of the RRD file
1390+ * we should not get any values back
1391+ **/
1392+ this.rq.getData(RRD_ENDTIME, RRD_ENDTIME+1000).addBoth(
1393+ function(self, data) {
1394+ self.resume(function() {
1395+ Y.Assert.areEqual(0, data.data.length);
1396+ });
1397+ }, this);
1398+ this.wait();
1399+ }
1400+
1401+ }));
1402+
1403+
1404+ Y.Test.Runner.add(new Y.Test.Case({
1405 name: "jarmon.Chart",
1406
1407 test_draw: function () {
1408@@ -301,6 +400,85 @@
1409 }));
1410
1411
1412+ Y.Test.Runner.add(new Y.Test.Case({
1413+ name: "jarmon.RrdChooser",
1414+
1415+ setUp: function() {
1416+ this.$tpl = $('<div/>').appendTo($('body'))
1417+ var c = new jarmon.RrdChooser(this.$tpl);
1418+ c.drawRrdUrlForm();
1419+ },
1420+
1421+ test_drawInitialForm: function () {
1422+ /**
1423+ * Test that the initial config form contains an rrd form field
1424+ **/
1425+ Y.Assert.areEqual(
1426+ this.$tpl.find('form input[name=rrd_url]').size(), 1);
1427+ },
1428+
1429+ test_drawUrlErrorMessage: function () {
1430+ /**
1431+ * Test that submitting the form with an incorrect url results in
1432+ * an error message
1433+ **/
1434+ var self = this;
1435+ this.$tpl.find('form input[name=rrd_url]').val('Foo/Bar').submit();
1436+ this.wait(
1437+ function() {
1438+ Y.Assert.areEqual(self.$tpl.find('.error').size(), 1);
1439+ }, 1000
1440+ );
1441+ },
1442+
1443+ test_drawUrlListDatasources: function () {
1444+ /**
1445+ * Test that submitting the form with an correct rrd url results in
1446+ * list of further DS label fields
1447+ **/
1448+ var self = this;
1449+ this.$tpl.find('form input[name=rrd_url]').val('build/test.rrd').submit();
1450+ this.wait(
1451+ function() {
1452+ Y.Assert.areEqual(self.$tpl.find('input[name=rrd_ds_label]').size(), 1);
1453+ }, 1000
1454+ );
1455+ },
1456+ }));
1457+
1458+
1459+ Y.Test.Runner.add(new Y.Test.Case({
1460+ name: "jarmon.ChartEditor",
1461+
1462+ setUp: function() {
1463+ this.$tpl = $('<div/>').appendTo($('body'))
1464+ var c = new jarmon.ChartEditor(
1465+ this.$tpl,
1466+ {
1467+ title: 'Foo',
1468+ datasources: [
1469+ ['data/cpu-0/cpu-wait.rrd', 0, 'CPU-0 Wait', '%'],
1470+ ['data/cpu-1/cpu-wait.rrd', 0, 'CPU-1 Wait', '%'],
1471+ ['data/cpu-0/cpu-system.rrd', 0, 'CPU-0 System', '%'],
1472+ ['data/cpu-1/cpu-system.rrd', 0, 'CPU-1 System', '%'],
1473+ ['data/cpu-0/cpu-user.rrd', 0, 'CPU-0 User', '%'],
1474+ ['data/cpu-1/cpu-user.rrd', 0, 'CPU-1 User', '%']
1475+ ]
1476+ }
1477+ );
1478+ c.draw();
1479+ },
1480+
1481+ test_drawInitialForm: function () {
1482+ /**
1483+ * Test that the initial config form contains an rrd form field
1484+ **/
1485+ Y.Assert.areEqual(
1486+ this.$tpl.find('form input[name=rrd_url]').size(), 1);
1487+ }
1488+ }));
1489+
1490+
1491 //initialize the console
1492 var yconsole = new Y.Console({
1493 newestOnTop: false,
1494
1495=== modified file 'jarmonbuild/commands.py'
1496--- jarmonbuild/commands.py 2010-08-30 14:22:25 +0000
1497+++ jarmonbuild/commands.py 2011-06-12 15:34:30 +0000
1498@@ -8,9 +8,7 @@
1499 import os
1500 import shutil
1501 import sys
1502-import time
1503
1504-from datetime import datetime
1505 from optparse import OptionParser
1506 from subprocess import check_call, PIPE
1507 from tempfile import gettempdir
1508@@ -20,8 +18,8 @@
1509 import pkg_resources
1510
1511
1512-JARMON_PROJECT_TITLE='Jarmon'
1513-JARMON_PROJECT_URL='http://www.launchpad.net/jarmon'
1514+JARMON_PROJECT_TITLE = 'Jarmon'
1515+JARMON_PROJECT_URL = 'http://www.launchpad.net/jarmon'
1516
1517 YUIDOC_URL = 'http://yuilibrary.com/downloads/yuidoc/yuidoc_1.0.0b1.zip'
1518 YUIDOC_MD5 = 'cd5545d2dec8f7afe3d18e793538162c'
1519@@ -91,21 +89,27 @@
1520 yuizip_path = os.path.join(tmpdir, os.path.basename(YUIDOC_URL))
1521 if os.path.exists(yuizip_path):
1522 self.log.debug('Using cached YUI doc')
1523- def producer():
1524+
1525+ def producer_local():
1526 yield open(yuizip_path).read()
1527+
1528+ producer = producer_local
1529 else:
1530 self.log.debug('Downloading YUI Doc')
1531- def producer():
1532+
1533+ def producer_remote():
1534 with open(yuizip_path, 'w') as yuizip:
1535 download = urlopen(YUIDOC_URL)
1536 while True:
1537- bytes = download.read(1024*10)
1538+ bytes = download.read(1024 * 10)
1539 if not bytes:
1540 break
1541 else:
1542 yuizip.write(bytes)
1543 yield bytes
1544
1545+ producer = producer_remote
1546+
1547 checksum = hashlib.md5()
1548 for bytes in producer():
1549 checksum.update(bytes)
1550@@ -114,7 +118,8 @@
1551 if actual_md5 != YUIDOC_MD5:
1552 raise BuildError(
1553 'YUI Doc checksum error. File: %s, '
1554- 'Expected: %s, Got: %s' % (yuizip_path, YUIDOC_MD5, actual_md5))
1555+ 'Expected: %s, '
1556+ 'Got: %s' % (yuizip_path, YUIDOC_MD5, actual_md5))
1557 else:
1558 self.log.debug('YUI Doc checksum verified')
1559
1560@@ -145,7 +150,7 @@
1561 workingbranch_dir, 'jarmonbuild', 'yuidoc_template'),),
1562 '--version=%s' % (buildversion,),
1563 '--project=%s' % (JARMON_PROJECT_TITLE,),
1564- '--projecturl=%s' % (JARMON_PROJECT_URL,)
1565+ '--projecturl=%s' % (JARMON_PROJECT_URL,),
1566 ), stdout=PIPE, stderr=PIPE,)
1567
1568 shutil.rmtree(yuidoc_dir)
1569@@ -181,21 +186,21 @@
1570 if status != 0:
1571 raise BuildError('bzr export failure. Status: %r' % (status,))
1572
1573-
1574 self.log.debug('Record the branch version')
1575 from bzrlib.branch import Branch
1576 from bzrlib.version_info_formats import format_python
1577 v = format_python.PythonVersionInfoBuilder(
1578 Branch.open(workingbranch_dir))
1579- versionfile_path = os.path.join(build_dir, 'jarmonbuild', '_version.py')
1580+
1581+ versionfile_path = os.path.join(
1582+ build_dir, 'jarmonbuild', '_version.py')
1583+
1584 with open(versionfile_path, 'w') as f:
1585 v.generate(f)
1586
1587-
1588 self.log.debug('Generate apidocs')
1589 BuildApidocsCommand().main([buildversion])
1590
1591-
1592 self.log.debug('Generate archive')
1593 archive_root = 'jarmon-%s' % (buildversion,)
1594 prefix_len = len(build_dir) + 1
1595@@ -205,7 +210,7 @@
1596 for file in files:
1597 z.write(
1598 os.path.join(root, file),
1599- os.path.join(archive_root, root[prefix_len:], file)
1600+ os.path.join(archive_root, root[prefix_len:], file),
1601 )
1602 finally:
1603 z.close()
1604@@ -233,14 +238,17 @@
1605 rows = 12
1606 step = 10
1607
1608- dss.append(DataSource(dsName='speed', dsType='GAUGE', heartbeat=2*step))
1609+ dss.append(
1610+ DataSource(dsName='speed', dsType='GAUGE', heartbeat=2 * step))
1611 rras.append(RRA(cf='AVERAGE', xff=0.5, steps=1, rows=rows))
1612 rras.append(RRA(cf='AVERAGE', xff=0.5, steps=12, rows=rows))
1613 my_rrd = RRD(filename, ds=dss, rra=rras, start=start, step=step)
1614 my_rrd.create()
1615
1616- for i, t in enumerate(range(start+step, start+step+(rows*step), step)):
1617- self.log.debug('DATA: %s %s (%s)' % (t, i, datetime.fromtimestamp(t)))
1618+ for i, t in enumerate(
1619+ range(start + step, start + step + (rows * step), step)):
1620+ self.log.debug(
1621+ 'DATA: %s %s (%s)' % (t, i, datetime.fromtimestamp(t)))
1622 my_rrd.bufferValue(t, i)
1623
1624 # Add further data 1 second later to demonstrate that the rrd
1625
1626=== modified file 'test.html'
1627--- test.html 2010-10-03 23:27:06 +0000
1628+++ test.html 2011-06-12 15:34:30 +0000
1629@@ -3,10 +3,7 @@
1630 <head>
1631 <meta charset="utf-8">
1632 <title>Jarmon Unit Test Runner</title>
1633- <link rel="stylesheet" type="text/css"
1634- href="http://developer.yahoo.com/yui/3/assets/yui.css"/>
1635- <link rel="stylesheet" type="text/css"
1636- href="http://yui.yahooapis.com/3.1.1/build/cssfonts/fonts-min.css"/>
1637+
1638 <style type='text/css'>
1639 .chart {
1640 width: 500px;

Subscribers

People subscribed via source and target branches

to all changes: