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