Merge lp:~richardw/jarmon/customisable-charts into lp:jarmon
- customisable-charts
- Merge into trunk
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 |
Related bugs: |
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.
Description of the change
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; |