visage-app 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. data/.gitignore +10 -0
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile +1 -15
  4. data/Gemfile.lock +44 -42
  5. data/README.md +123 -49
  6. data/Rakefile +16 -26
  7. data/bin/visage-app +17 -4
  8. data/features/cli.feature +10 -3
  9. data/features/json.feature +37 -0
  10. data/features/step_definitions/{visage_steps.rb → cli_steps.rb} +1 -1
  11. data/features/step_definitions/json_steps.rb +50 -8
  12. data/features/step_definitions/site_steps.rb +1 -1
  13. data/features/support/config/default/profiles.yaml +335 -0
  14. data/features/{data → support}/config/with_no_profiles/.stub +0 -0
  15. data/features/support/config/with_no_profiles/profiles.yaml +0 -0
  16. data/features/support/config/with_old_profile_yaml/profiles.yaml +116 -0
  17. data/features/support/env.rb +2 -3
  18. data/lib/visage-app.rb +35 -25
  19. data/lib/visage-app/collectd/json.rb +115 -118
  20. data/lib/visage-app/collectd/rrds.rb +25 -19
  21. data/lib/visage-app/helpers.rb +17 -0
  22. data/lib/visage-app/profile.rb +18 -25
  23. data/lib/visage-app/public/images/caution.png +0 -0
  24. data/lib/visage-app/public/images/ok.png +0 -0
  25. data/lib/visage-app/public/images/questions.png +0 -0
  26. data/lib/visage-app/public/javascripts/builder.js +607 -0
  27. data/lib/visage-app/public/javascripts/graph.js +179 -142
  28. data/lib/visage-app/public/javascripts/message.js +520 -0
  29. data/lib/visage-app/public/javascripts/mootools-core-1.4.0-full-compat.js +6285 -0
  30. data/lib/visage-app/public/javascripts/mootools-more-1.4.0.1.js +6399 -0
  31. data/lib/visage-app/public/stylesheets/message.css +61 -0
  32. data/lib/visage-app/public/stylesheets/screen.css +149 -38
  33. data/lib/visage-app/version.rb +5 -0
  34. data/lib/visage-app/views/builder.haml +38 -49
  35. data/lib/visage-app/views/builder_form.haml +14 -0
  36. data/lib/visage-app/views/layout.haml +5 -2
  37. data/lib/visage-app/views/profile.haml +44 -25
  38. data/visage-app.gemspec +29 -132
  39. metadata +93 -150
  40. data/VERSION +0 -1
  41. data/features/builder.feature +0 -16
  42. data/lib/visage-app/collectd/profile.rb +0 -36
@@ -121,14 +121,12 @@ function formatValue(value, options) {
121
121
  break
122
122
  }
123
123
 
124
- var rounded = label.round(precision)
125
-
126
- return rounded + unit
124
+ return label.format({decimals: precision, suffix: unit})
127
125
  }
128
126
 
129
127
  function formatDate(d) {
130
- var datetime = new Date(d * 1000)
131
- return datetime.format("%Y-%m-%d %H:%M:%S UTC%T")
128
+ var datetime = new Date(d)
129
+ return datetime.format("%Y-%m-%d %H:%M:%S UTC%z")
132
130
  }
133
131
 
134
132
  function formatPluginName(name) {
@@ -140,13 +138,13 @@ function formatPluginName(name) {
140
138
 
141
139
 
142
140
  /*
143
- * visageBase()
141
+ * VisageBase()
144
142
  *
145
143
  * Base class for fetching data and setting graph options.
146
144
  * Should be used by other classes to build specialised graphing behaviour.
147
145
  *
148
146
  */
149
- var visageBase = new Class({
147
+ var VisageBase = new Class({
150
148
  Implements: [ Options, Events ],
151
149
  options: {
152
150
  secureJSON: false,
@@ -154,30 +152,28 @@ var visageBase = new Class({
154
152
  live: false
155
153
  },
156
154
  initialize: function(element, host, plugin, options) {
157
- this.parentElement = element
158
- this.options.host = host
159
- this.options.plugin = plugin
160
- this.setOptions(options)
161
-
162
- var data = new Hash()
163
- if($chk(this.options.start)) {
164
- data.set('start', this.options.start)
165
- }
166
- if($chk(this.options.finish)) {
167
- data.set('finish', this.options.finish)
168
- }
155
+ this.parentElement = element;
156
+ this.options.host = host;
157
+ this.options.plugin = plugin;
158
+ this.query = window.location.search.slice(1).parseQueryString();
159
+ this.options = Object.merge(this.options, this.query);
160
+
161
+ this.setOptions(options);
162
+
163
+ this.requestData = new Object();
164
+ this.requestData.start = this.options.start;
165
+ this.requestData.finish = this.options.finish;
169
166
 
170
- this.requestData = data;
171
167
  this.getData(); // calls graphData
172
168
  },
173
169
  dataURL: function() {
174
- var url = ['data', this.options.host, this.options.plugin]
170
+ var url = ['data', this.options.host, this.options.plugin];
175
171
  // if the data exists on another host (useful for embedding)
176
- if ($defined(this.options.baseurl)) {
172
+ if (this.options.baseurl) {
177
173
  url.unshift(this.options.baseurl.replace(/\/$/, ''))
178
174
  }
179
175
  // for specific plugin instances
180
- if ($chk(this.options.pluginInstance)) {
176
+ if (this.options.pluginInstance) {
181
177
  url.push(this.options.pluginInstance)
182
178
  }
183
179
  // if no url is specified
@@ -202,30 +198,30 @@ var visageBase = new Class({
202
198
 
203
199
  this.request.send();
204
200
  },
205
- graphName: function() {
201
+ title: function() {
206
202
  if ($chk(this.options.name)) {
207
- var name = this.options.name
203
+ var title = this.options.name
208
204
  } else {
209
- var name = [ formatPluginName(this.options.plugin),
210
- 'on',
211
- this.options.host ].join(' ')
205
+ var title = [ formatPluginName(this.options.plugin),
206
+ 'on',
207
+ this.options.host ].join(' ')
212
208
  }
213
- return name
209
+ return title
214
210
  },
215
211
  });
216
212
 
217
213
 
218
214
  /*
219
- * visageGraph()
215
+ * VisageGraph()
220
216
  *
221
217
  * General purpose graph for rendering data from a single plugin
222
218
  * with multiple plugin instances.
223
219
  *
224
- * Builds upon visageBase().
220
+ * Builds upon VisageBase().
225
221
  *
226
222
  */
227
- var visageGraph = new Class({
228
- Extends: visageBase,
223
+ var VisageGraph = new Class({
224
+ Extends: VisageBase,
229
225
  Implements: Chain,
230
226
  // assemble data to graph, then draw it
231
227
  graphData: function(data) {
@@ -266,20 +262,21 @@ var visageGraph = new Class({
266
262
  var plugin = this.options.plugin
267
263
  var data = data ? data : this.response
268
264
 
269
- $each(data[host][plugin], function(instance, iname) {
270
- $each(instance, function(metric, mname) {
265
+ $each(data[host][plugin], function(instance, instanceName) {
266
+ $each(instance, function(metric, metricName) {
271
267
  var start = metric.start,
272
268
  finish = metric.finish,
273
269
  interval = (finish - start) / metric.data.length;
274
270
 
275
271
  var data = metric.data.map(function(value, index) {
276
- var x = start + index * interval,
272
+ var x = (start + index * interval) * 1000,
277
273
  y = value;
274
+
278
275
  return [ x, y ];
279
276
  });
280
277
 
281
278
  var set = {
282
- name: [ host, plugin, iname, mname ],
279
+ name: [ host, plugin, instanceName, metricName ],
283
280
  data: data,
284
281
  };
285
282
 
@@ -318,7 +315,7 @@ var visageGraph = new Class({
318
315
  },
319
316
  drawChart: function() {
320
317
  var series = this.series,
321
- title = this.graphName(),
318
+ title = this.title(),
322
319
  element = this.parentElement,
323
320
  ytitle = formatPluginName(this.options.plugin),
324
321
  min,
@@ -331,19 +328,20 @@ var visageGraph = new Class({
331
328
  max = meta.max;
332
329
 
333
330
  this.chart = new Highcharts.Chart({
331
+ series: series,
334
332
  chart: {
335
- renderTo: element,
336
- defaultSeriesType: 'line',
337
- marginRight: 200,
338
- marginBottom: 25,
339
- zoomType: 'xy',
340
- height: 300,
333
+ renderTo: element,
334
+ type: 'line',
335
+ marginRight: 0,
336
+ marginBottom: 60,
337
+ zoomType: 'xy',
338
+ height: 300,
341
339
  events: {
342
340
  load: function(e) {
343
341
  setInterval(function() {
344
342
  if (this.options.live) {
345
- var data = { 'start': this.lastFinish,
346
- 'finish': this.lastFinish + 10,
343
+ var data = { 'start': this.lastFinish / 1000,
344
+ 'finish': this.lastFinish / 1000 + 10,
347
345
  'live': true };
348
346
  this.requestData = data;
349
347
  this.getData()
@@ -353,37 +351,48 @@ var visageGraph = new Class({
353
351
  }
354
352
  },
355
353
  title: {
356
- text: title,
357
- style: {
358
- fontSize: '20px',
359
- fontWeight: 'bold',
360
- color: "#333333"
361
- }
354
+ text: title,
355
+ style: {
356
+ 'fontSize': '18px',
357
+ 'fontWeight': 'bold',
358
+ 'color': '#333333',
359
+ 'font-family': 'Bitstream Vera Sans, Helvetica Neue, sans-serif',
360
+ }
362
361
  },
362
+ /*
363
+ colors: [
364
+ '#204a87', '#4e9a06', '#cc0000', '#5c3566', '#f57900', '#e9b96e', '#ad7fa8', '#888a85', '#8ae234', '#75507b', '#c17d11', '#729fcf', '#73d216', '#ef2929', '#edd400', '#8f5902', '#555753', '#fce94f', '#2e3436', '#babdb6', '#3465a4', '#a40000', '#c4a000', '#ce5c00', '#d3d7cf', '#fcaf3e', '#eeeeec',
365
+ ],
366
+ */
363
367
  xAxis: {
364
- type: 'datetime',
365
- labels: {
366
- y: 20,
367
- formatter: function() {
368
- var d = new Date(this.value * 1000)
369
- return d.format("%H:%M")
370
- }
371
- },
372
368
  title: {
373
369
  text: null
370
+ },
371
+ lineColor: "#aaa",
372
+ tickColor: "#aaa",
373
+ type: 'datetime',
374
+ dateTimeLabelFormats: {
375
+ second: '%H:%M:%S',
376
+ minute: '%H:%M',
377
+ hour: '%H:%M',
378
+ day: '%d/%m',
379
+ week: '%d/%m',
380
+ month: '%m/%Y',
381
+ year: '%Y'
374
382
  }
383
+
375
384
  },
376
385
  yAxis: {
377
- title: {
378
- text: ytitle
379
- },
380
- startOnTick: false,
381
- minPadding: 0.065,
382
- max: max,
383
- endOnTick: true,
384
- labels: {
386
+ title: {
387
+ text: null
388
+ },
389
+ startOnTick: false,
390
+ minPadding: 0.065,
391
+ endOnTick: false,
392
+ gridLineColor: "#dddddd",
393
+ labels: {
385
394
  formatter: function() {
386
- var precision = min - max < 1000 ? 2 : 0,
395
+ var precision = 1,
387
396
  value = formatValue(this.value, {
388
397
  'precision': precision,
389
398
  'min': min,
@@ -391,50 +400,55 @@ var visageGraph = new Class({
391
400
  });
392
401
  return value
393
402
  }
394
- }
403
+ }
395
404
  },
396
405
  plotOptions: {
397
- series: {
398
- shadow: false,
399
- marker: {
400
- enabled: false,
401
- stacking: 'normal',
402
- states: {
403
- hover: {
404
- enabled: true
406
+ series: {
407
+ shadow: false,
408
+ lineWidth: 1,
409
+ marker: {
410
+ enabled: false,
411
+ states: {
412
+ hover: {
413
+ enabled: true,
414
+ radius: 4,
415
+ },
416
+ },
417
+ },
418
+ states: {
419
+ hover: {
420
+ enabled: true,
421
+ lineWidth: 1,
422
+ },
405
423
  }
406
- }
407
424
  }
408
- }
409
425
  },
410
426
  tooltip: {
411
- formatter: function() {
412
- var tip;
413
- tip = '<strong>'
414
- tip += formatSeriesLabel(this.series.name).trim()
415
- tip += '</strong>' + ' -> '
416
- tip += '<span style="font-family: monospace; font-size: 14px;">'
417
- tip += formatValue(this.y, { 'precision': 2, 'min': min, 'max': max })
418
- tip += '<span style="font-size: 9px; color: #777">'
419
- tip += ' (' + this.y + ')'
420
- tip += '</span>'
421
- tip += '</span>'
422
- tip += '<br/>'
423
- tip += '<span style="font-family: monospace">'
424
- tip += formatDate(this.x)
425
- tip += '</span>'
426
-
427
- return tip
428
- }
427
+ formatter: function() {
428
+ var tip;
429
+ tip = '<strong>'
430
+ tip += formatSeriesLabel(this.series.name).trim()
431
+ tip += '</strong>' + ' -> '
432
+ tip += '<span style="font-family: monospace; font-size: 14px;">'
433
+ tip += formatValue(this.y, { 'precision': 2, 'min': min, 'max': max })
434
+ tip += '<span style="font-size: 9px; color: #777">'
435
+ tip += ' (' + this.y + ')'
436
+ tip += '</span>'
437
+ tip += '</span>'
438
+ tip += '<br/>'
439
+ tip += '<span style="font-family: monospace">'
440
+ tip += formatDate(this.x)
441
+ tip += '</span>'
442
+
443
+ return tip
444
+ }
429
445
  },
430
446
  legend: {
431
- layout: 'vertical',
432
- align: 'right',
447
+ layout: 'horizontal',
448
+ align: 'center',
433
449
  verticalAlign: 'top',
434
- x: -10,
435
- y: 60,
450
+ y: 275,
436
451
  borderWidth: 0,
437
- itemWidth: 186,
438
452
  labelFormatter: function() {
439
453
  return formatSeriesLabel(this.name)
440
454
  },
@@ -443,13 +457,12 @@ var visageGraph = new Class({
443
457
  color: '#333333'
444
458
  },
445
459
  itemHoverStyle: {
446
- color: '#777777'
460
+ color: '#888'
447
461
  }
448
462
 
449
463
  },
450
- series: series,
451
464
  credits: {
452
- enabled: false
465
+ enabled: false
453
466
  }
454
467
  });
455
468
 
@@ -462,53 +475,71 @@ var visageGraph = new Class({
462
475
  * - form
463
476
  * \
464
477
  * - select
465
- * | \
466
- * | - option
467
- * | |
468
- * | - option
469
- * |
470
- * - submit
478
+ * \
479
+ * - option
480
+ * |
481
+ * - option
471
482
  */
472
483
  var currentDate = new Date;
473
484
  var currentUnixTime = parseInt(currentDate.getTime() / 1000);
474
485
 
475
486
  var container = $(this.parentElement);
476
- var form = new Element('form', {
487
+ var form = this.form = new Element('form', {
477
488
  'method': 'get',
489
+ 'styles': {
490
+ 'text-align': 'right',
491
+ },
478
492
  'events': {
479
- 'submit': function(e, foo) {
480
- e.stop();
481
- e.target.getElement('select').getSelected().each(function(option) {
482
- value = parseInt(option.value.split('=')[1])
483
- data = { 'start': value }
484
- });
485
- this.requestData = data;
486
-
493
+ 'submit': function(e) {
494
+ this.requestData = this.form.getElement('select').getSelected()[0].value.parseQueryString()
487
495
  /* Draw everything again. */
488
496
  this.getData();
489
497
  }.bind(this)
490
498
  }
491
499
  });
492
500
 
493
- var select = new Element('select', { 'class': 'date timescale' });
494
- var timescales = new Hash({ 'hour': 1, '2 hours': 2, '6 hours': 6, '12 hours': 12,
495
- 'day': 24, '2 days': 48, '3 days': 72,
496
- 'week': 168, '2 weeks': 336, 'month': 672 });
501
+ /* Select dropdown */
502
+ var select = this.select = new Element('select', {
503
+ 'class': 'date timescale',
504
+ 'styles': {
505
+ 'margin-bottom': '3px',
506
+ 'border': '1px solid #aaa',
507
+ },
508
+ 'events': {
509
+ 'change': function(e) {
510
+ e.target.form.fireEvent('submit', e)
511
+ }
512
+ }
513
+ });
514
+
515
+ /* Timescales available in the dropdown */
516
+ var timescales = new Hash({ '1 hour': 1,
517
+ '2 hours': 2,
518
+ '6 hours': 6,
519
+ '12 hours': 12,
520
+ '24 hours': 24,
521
+ '3 days': 72,
522
+ '7 days': 168,
523
+ '2 weeks': 336,
524
+ '1 month': 774,
525
+ '3 month': 2322,
526
+ '6 months': 4368,
527
+ '1 year': 8760,
528
+ '2 years': 17520 });
529
+
497
530
  timescales.each(function(hour, label) {
498
531
  var current = this.currentTimePeriod == 'last {label}'.substitute({'label': label });
499
- var value = "start={start}".substitute({'start': currentUnixTime - (hour * 3600)});
500
- var html = 'last {label}'.substitute({'label': label });
532
+ var value = "start={start}".substitute({'start': currentUnixTime - (hour * 3600)});
533
+ var html = 'last {label}'.substitute({'label': label });
501
534
 
502
535
  var option = new Element('option', {
503
- html: html,
504
- value: value,
505
- selected: (current ? 'selected' : '')
536
+ 'html': html,
537
+ 'value': value,
538
+ 'selected': (current ? 'selected' : ''),
506
539
  });
507
540
  select.grab(option)
508
541
  });
509
542
 
510
- var submit = new Element('input', { 'type': 'submit', 'value': 'show' });
511
-
512
543
  var liveToggler = new Element('input', {
513
544
  'type': 'checkbox',
514
545
  'id': this.parentElement + '-live',
@@ -520,7 +551,7 @@ var visageGraph = new Class({
520
551
  }.bind(this)
521
552
  },
522
553
  'styles': {
523
- 'margin-left': '4px',
554
+ 'margin-right': '4px',
524
555
  'cursor': 'pointer'
525
556
  }
526
557
  });
@@ -531,7 +562,7 @@ var visageGraph = new Class({
531
562
  'styles': {
532
563
  'font-family': 'sans-serif',
533
564
  'font-size': '11px',
534
- 'margin-left': '8px',
565
+ 'margin-right': '8px',
535
566
  'cursor': 'pointer'
536
567
  }
537
568
  });
@@ -540,9 +571,10 @@ var visageGraph = new Class({
540
571
  'href': this.dataURL(),
541
572
  'html': 'Export data',
542
573
  'styles': {
543
- 'font-family': 'sans-serif',
544
- 'font-size': '11px',
545
- 'margin-left': '8px',
574
+ 'font-family': 'sans-serif',
575
+ 'font-size': '11px',
576
+ 'margin-right': '8px',
577
+ 'color': '#2F5A92',
546
578
  },
547
579
  'events': {
548
580
  'mouseover': function(e) {
@@ -561,16 +593,21 @@ var visageGraph = new Class({
561
593
  }
562
594
  });
563
595
 
564
- form.grab(select)
565
- form.grab(submit)
596
+ form.grab(exportLink)
566
597
  form.grab(liveToggler)
567
598
  form.grab(liveLabel)
568
- form.grab(exportLink)
599
+ form.grab(select)
569
600
  container.grab(form, 'top')
570
601
  },
602
+ setTimePeriodTo: function(selected) {
603
+ var option = this.select.getElements('option').filter(function(opt) {
604
+ return opt.text == selected.text
605
+ })[0];
571
606
 
607
+ option.set('selected', 'selected');
572
608
 
573
-
609
+ this.form.fireEvent('submit')
610
+ }
574
611
  });
575
612
 
576
613
  // buildEmbedder: function() {
@@ -610,7 +647,7 @@ var visageGraph = new Class({
610
647
  // baseurl = "{protocol}//{host}".substitute({'host': window.location.host, 'protocol': window.location.protocol});
611
648
  // code = "<script src='{baseurl}/javascripts/visage.js' type='text/javascript'></script>".substitute({'baseurl': baseurl});
612
649
  // code += "<div id='graph'></div>"
613
- // code += "<script type='text/javascript'>window.addEvent('domready', function() { var graph = new visageGraph('graph', '{host}', '{plugin}', ".substitute({'host': this.options.host, 'plugin': this.options.plugin});
650
+ // code += "<script type='text/javascript'>window.addEvent('domready', function() { var graph = new VisageGraph('graph', '{host}', '{plugin}', ".substitute({'host': this.options.host, 'plugin': this.options.plugin});
614
651
  // code += "{"
615
652
  // code += "width: 900, height: 220, gridWidth: 800, gridHeight: 200, baseurl: '{baseurl}'".substitute({'baseurl': baseurl});
616
653
  // code += "}); });</script>"