visage-app 0.1.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 (40) hide show
  1. data/.gitignore +9 -0
  2. data/README.md +138 -0
  3. data/Rakefile +33 -0
  4. data/VERSION +1 -0
  5. data/bin/visage +17 -0
  6. data/config.ru +7 -0
  7. data/features/json.feature +66 -0
  8. data/features/site.feature +9 -0
  9. data/features/step_definitions/form_steps.rb +6 -0
  10. data/features/step_definitions/json_steps.rb +79 -0
  11. data/features/step_definitions/result_steps.rb +19 -0
  12. data/features/step_definitions/site_steps.rb +4 -0
  13. data/features/step_definitions/visage_steps.rb +20 -0
  14. data/features/step_definitions/webrat_steps.rb +42 -0
  15. data/features/support/env.rb +36 -0
  16. data/features/visage.feature +11 -0
  17. data/lib/visage/collectd/json.rb +142 -0
  18. data/lib/visage/collectd/profile.rb +36 -0
  19. data/lib/visage/config/fallback-colors.yaml +82 -0
  20. data/lib/visage/config/init.rb +33 -0
  21. data/lib/visage/config/plugin-colors.yaml +63 -0
  22. data/lib/visage/config/profiles.yaml +35 -0
  23. data/lib/visage/config/profiles.yaml.sample +33 -0
  24. data/lib/visage/config.rb +51 -0
  25. data/lib/visage/patches.rb +18 -0
  26. data/lib/visage/public/favicon.gif +0 -0
  27. data/lib/visage/public/javascripts/application.js +4 -0
  28. data/lib/visage/public/javascripts/g.line.js +217 -0
  29. data/lib/visage/public/javascripts/g.raphael.js +7 -0
  30. data/lib/visage/public/javascripts/graph.js +510 -0
  31. data/lib/visage/public/javascripts/mootools-1.2.3-core.js +4036 -0
  32. data/lib/visage/public/javascripts/mootools-1.2.3.1-more.js +104 -0
  33. data/lib/visage/public/javascripts/raphael-min.js +7 -0
  34. data/lib/visage/public/javascripts/raphael.js +3215 -0
  35. data/lib/visage/public/stylesheets/screen.css +96 -0
  36. data/lib/visage/views/index.haml +48 -0
  37. data/lib/visage/views/layout.haml +22 -0
  38. data/lib/visage/views/single.haml +43 -0
  39. data/lib/visage-app.rb +81 -0
  40. metadata +142 -0
@@ -0,0 +1,510 @@
1
+ /*
2
+ * visageBase()
3
+ *
4
+ * Base class for fetching data and setting graph options.
5
+ * Should be used by other classes to build specialised graphing behaviour.
6
+ *
7
+ */
8
+ var visageBase = new Class({
9
+ Implements: [Options, Events],
10
+ options: {
11
+ width: 900,
12
+ height: 220,
13
+ leftEdge: 100,
14
+ topEdge: 10,
15
+ gridWidth: 670,
16
+ gridHeight: 200,
17
+ columns: 60,
18
+ rows: 8,
19
+ gridBorderColour: '#ccc',
20
+ shade: false,
21
+ secureJSON: false,
22
+ httpMethod: 'get'
23
+ },
24
+ initialize: function(element, host, plugin, options) {
25
+ this.parentElement = element;
26
+ this.setOptions(options);
27
+ this.options.host = host;
28
+ this.options.plugin = plugin;
29
+ this.buildGraphHeader();
30
+ this.buildGraphContainer();
31
+ this.canvas = Raphael(this.graphContainer, this.options.width, this.options.height);
32
+ this.getData(); // calls graphData
33
+ },
34
+ dataURL: function() {
35
+ var url = ['data', this.options.host, this.options.plugin]
36
+ // if the data exists on another host (useful for embedding)
37
+ if ($defined(this.options.baseurl)) {
38
+ url.unshift(this.options.baseurl.replace(/\/$/, ''))
39
+ }
40
+ // for specific plugin instances
41
+ if ($chk(this.options.pluginInstance)) {
42
+ url.push(this.options.pluginInstance)
43
+ }
44
+ // if no url is specified
45
+ if (!url[0].match(/http\:\/\//)) {
46
+ url[0] = '/' + url[0]
47
+ }
48
+ return url.join('/')
49
+ },
50
+ getData: function() {
51
+ this.request = new Request.JSONP({
52
+ url: this.dataURL(),
53
+ data: this.requestData,
54
+ secure: this.options.secureJSON,
55
+ method: this.options.httpMethod,
56
+ onComplete: function(json) {
57
+ this.graphData(json);
58
+ }.bind(this),
59
+ onFailure: function(header, value) {
60
+ $(this.parentElement).set('html', header)
61
+ }.bind(this)
62
+ });
63
+
64
+ this.request.send();
65
+ },
66
+ buildGraphHeader: function() {
67
+ header = $chk(this.options.name) ? this.options.name : this.options.plugin
68
+ this.graphHeader = new Element('h3', {
69
+ 'class': 'graph-title',
70
+ 'html': header
71
+ });
72
+ $(this.parentElement).grab(this.graphHeader);
73
+ },
74
+ buildGraphContainer: function() {
75
+ $(this.parentElement).set('style', 'padding-top: 1em');
76
+
77
+ this.graphContainer = new Element('div', {
78
+ 'class': 'graph container',
79
+ 'styles': {
80
+ 'margin-bottom': '24px'
81
+ }
82
+ });
83
+ $(this.parentElement).grab(this.graphContainer)
84
+ }
85
+ });
86
+
87
+
88
+ /*
89
+ * visageGraph()
90
+ *
91
+ * General purpose graph for rendering data from a single plugin
92
+ * with multiple plugin instances.
93
+ *
94
+ * Builds upon visageBase().
95
+ *
96
+ */
97
+ var visageGraph = new Class({
98
+ Extends: visageBase,
99
+ Implements: Chain,
100
+ // assemble data to graph, then draw it
101
+ graphData: function(data) {
102
+
103
+ this.ys = []
104
+ this.colors = []
105
+ this.instances = []
106
+ this.metrics = []
107
+
108
+ var host = this.options.host
109
+ var plugin = this.options.plugin
110
+
111
+ $each(data[host][plugin], function(instance, iname) {
112
+ $each(instance, function(metric, mname) {
113
+ this.colors.push(metric.color)
114
+ if ( !$defined(this.x) ) {
115
+ this.x = this.buildXAxis(metric)
116
+ }
117
+ this.ys.push(metric.data)
118
+ this.instances.push(iname) // labels
119
+ this.metrics.push(mname) // labels
120
+ }, this);
121
+ }, this);
122
+
123
+ this.buildContainers();
124
+ this.drawGraph();
125
+
126
+ this.buildLabels();
127
+ this.addSelectionInterface();
128
+ this.addDebugInterface();
129
+ this.buildDateSelector();
130
+
131
+ /* disabling this for now for dramatic effect
132
+ this.buildEmbedder();
133
+ */
134
+ },
135
+ buildXAxis: function(metric) {
136
+ var start = metric.start.toInt(),
137
+ finish = metric.finish.toInt(),
138
+ length = metric.data.length,
139
+ interval = (finish - start) / length,
140
+ counter = start,
141
+ x = []
142
+
143
+ while (counter < finish) {
144
+ x.push(counter)
145
+ counter += interval
146
+ }
147
+ return x
148
+ },
149
+ drawGraph: function() {
150
+
151
+ var colors = this.colors;
152
+ var left = this.options.leftEdge
153
+ var top = this.options.topEdge
154
+ var width = this.options.gridWidth
155
+ var height = this.options.gridHeight
156
+ var x = this.x // x axis
157
+ var ys = this.ys // y axes
158
+ var xstep = x.length / 20
159
+ var shade = this.options.shade
160
+
161
+ this.canvas.g.txtattr.font = "11px 'sans-serif'";
162
+ this.graph = this.canvas.g.linechart(left, top, width, height, x, ys, {
163
+ nostroke: false,
164
+ width: 1.5,
165
+ axis: "0 0 1 1",
166
+ colors: colors,
167
+ axisxstep: xstep,
168
+ shade: shade
169
+ });
170
+
171
+ this.formatAxes();
172
+ },
173
+ formatAxes: function() {
174
+
175
+ /* clean up graph labels */
176
+ this.graph.axis[0].text.items.getLast().hide()
177
+ $each(this.graph.axis[0].text.items, function (time) {
178
+
179
+ var unixTime = time.attr('text')
180
+ var d = new Date(time.attr('text') * 1000);
181
+ time.attr({'text': d.strftime("%H:%M")});
182
+
183
+ time.mouseover(function () {
184
+ this.attr({'text': d.strftime("%H:%M")});
185
+ });
186
+
187
+ /*
188
+ time.mouseout(function () {
189
+ this.attr({'text': d.strftime("%H:%M")});
190
+ });
191
+ */
192
+ });
193
+
194
+ $each(this.graph.axis[1].text.items, function (value) {
195
+ // FIXME: no JS reference on train means awful rounding hacks!
196
+ // if you are reading this, it's a bug!
197
+ if (value.attr('text') > 1073741824) {
198
+ var label = value.attr('text') / 1073741824;
199
+ var unit = 'g'
200
+ } else if (value.attr('text') > 1048576) {
201
+ // and again :-(
202
+ var label = value.attr('text') / 1048576;
203
+ var unit = 'm'
204
+ } else if (value.attr('text') > 1024) {
205
+ var label = value.attr('text') / 1024;
206
+ var unit = 'k';
207
+ } else {
208
+ var label = value.attr('text');
209
+ var unit = ''
210
+ }
211
+
212
+ var decimal = label.toString().split('.')
213
+ if ($chk(this.previous) && this.previous.toString()[0] == label.toString()[0] && decimal.length > 1) {
214
+ var round = '.' + decimal[1][0]
215
+ } else {
216
+ var round = ''
217
+ }
218
+
219
+ value.attr({'text': Math.floor(label) + round + unit})
220
+ this.previous = value.attr('text')
221
+ });
222
+
223
+ },
224
+ buildEmbedder: function() {
225
+ var pre = new Element('textarea', {
226
+ 'id': 'embedder',
227
+ 'class': 'embedder',
228
+ 'html': this.embedCode(),
229
+ 'styles': {
230
+ 'width': '500px',
231
+ 'padding': '3px'
232
+ }
233
+ });
234
+ this.embedderContainer.grab(pre);
235
+
236
+ var slider = new Fx.Slide(pre, {
237
+ duration: 200
238
+ });
239
+
240
+ slider.hide();
241
+
242
+ var toggler = new Element('a', {
243
+ 'id': 'toggler',
244
+ 'class': 'toggler',
245
+ 'html': '(embed)',
246
+ 'href': '#',
247
+ 'styles': {
248
+ 'font-size': '0.7em',
249
+ }
250
+ });
251
+ toggler.addEvent('click', function(e) {
252
+ e.stop();
253
+ slider.toggle();
254
+ });
255
+ this.embedderTogglerContainer.grab(toggler);
256
+ },
257
+ embedCode: function() {
258
+ baseurl = "{protocol}//{host}".substitute({'host': window.location.host, 'protocol': window.location.protocol});
259
+ code = "<script src='{baseurl}/javascripts/visage.js' type='text/javascript'></script>".substitute({'baseurl': baseurl});
260
+ code += "<div id='graph'></div>"
261
+ 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});
262
+ code += "{"
263
+ code += "width: 900, height: 220, gridWidth: 800, gridHeight: 200, baseurl: '{baseurl}'".substitute({'baseurl': baseurl});
264
+ code += "}); });</script>"
265
+ return code.replace('<', '&lt;').replace('>', '&gt;')
266
+ },
267
+ addDebugInterface: function() {
268
+ var graph = this.graph;
269
+ /*
270
+ graph.hoverColumn(function () {
271
+ console.log([this.axis])
272
+ });
273
+ */
274
+ },
275
+ addSelectionInterface: function() {
276
+ var graph = this.graph;
277
+ var parentElement = this.parentElement
278
+ var gridHeight = this.options.gridHeight
279
+ graph.selectionMade = true
280
+ this.graph.clickColumn(function () {
281
+ if ($chk(graph.selectionMade) && graph.selectionMade) {
282
+ if ($defined(graph.selection)) {
283
+ graph.selection.remove();
284
+ }
285
+ graph.selectionMade = false
286
+ graph.selection = this.paper.rect(this.x, 0, 1, gridHeight);
287
+ graph.selection.toBack();
288
+ graph.selection.attr({fill: '#555', stroke: '#555', opacity: 0.4});
289
+ graph.selectionStart = this.axis.toInt()
290
+ } else {
291
+ graph.selectionMade = true
292
+ graph.selectionFinish = this.axis.toInt()
293
+ var select = $(parentElement).getElement('div.timescale.container select')
294
+ var hasSelected = select.getChildren('option').some(function(option) {
295
+ return option.get('html') == 'selected'
296
+ });
297
+ if (!hasSelected) {
298
+ var option = new Element('option', {
299
+ html: 'selected',
300
+ value: '',
301
+ selected: true
302
+ });
303
+ select.grab(option)
304
+ }
305
+ }
306
+ });
307
+ this.graph.hoverColumn(function () {
308
+ if ($chk(graph.selection) && !graph.selectionMade) {
309
+ var width = this.x - graph.selection.attr('x');
310
+ graph.selection.attr({'width': width});
311
+ }
312
+ });
313
+
314
+ },
315
+ buildContainers: function() {
316
+ this.embedderTogglerContainer = new Element('div', {
317
+ 'class': 'embedder-toggler container',
318
+ 'styles': {
319
+ 'float': 'right',
320
+ 'width': '20%',
321
+ 'text-align': 'right',
322
+ 'margin-right': '12px',
323
+ 'padding-top': '4px'
324
+ }
325
+ });
326
+ $(this.parentElement).grab(this.embedderTogglerContainer, 'top')
327
+
328
+ this.timescaleContainer = new Element('div', {
329
+ 'class': 'timescale container',
330
+ 'styles': {
331
+ 'float': 'right',
332
+ 'width': '20%'
333
+ }
334
+ });
335
+ $(this.parentElement).grab(this.timescaleContainer, 'top')
336
+
337
+ this.labelsContainer = new Element('div', {
338
+ 'class': 'labels container',
339
+ 'title': 'click to hide',
340
+ 'styles': {
341
+ 'float': 'left',
342
+ 'margin-left': '80px',
343
+ 'padding-bottom': '1em'
344
+ }
345
+ });
346
+ $(this.parentElement).grab(this.labelsContainer)
347
+
348
+ this.embedderContainer = new Element('div', {
349
+ 'class': 'embedder container',
350
+ 'styles': {
351
+ 'font-style': 'monospace',
352
+ 'margin-left': '80px',
353
+ 'font-size': '0.8em',
354
+ 'clear': 'both'
355
+ }
356
+ });
357
+ $(this.parentElement).grab(this.embedderContainer)
358
+ },
359
+ buildDateSelector: function() {
360
+ /*
361
+ * container
362
+ * \
363
+ * - form
364
+ * \
365
+ * - select
366
+ * | \
367
+ * | - option
368
+ * | |
369
+ * | - option
370
+ * |
371
+ * - submit
372
+ */
373
+ var currentDate = new Date;
374
+ var currentUnixTime = parseInt(currentDate.getTime() / 1000);
375
+
376
+ var container = $(this.timescaleContainer);
377
+ var form = new Element('form', {
378
+ 'action': this.dataURL(),
379
+ 'method': 'get',
380
+ 'events': {
381
+ 'submit': function(e, foo) {
382
+ e.stop();
383
+
384
+ /*
385
+ * Get the selected option, turn it into a hash for
386
+ * getData() to use.
387
+ */
388
+ data = new Hash()
389
+ if (e.target.getElement('select').getSelected().get('html') == 'selected') {
390
+ data.set('start', this.graph.selectionStart);
391
+ data.set('finish', this.graph.selectionFinish);
392
+ } else {
393
+ e.target.getElement('select').getSelected().each(function(option) {
394
+ split = option.value.split('=')
395
+ data.set(split[0], split[1])
396
+ currentTimePeriod = option.get('html') // is this setting a global?
397
+ }, this);
398
+ }
399
+ this.requestData = data
400
+
401
+ /* Nuke graph + labels. */
402
+ this.graph.remove();
403
+ delete this.x;
404
+ $(this.labelsContainer).empty();
405
+ $(this.timescaleContainer).empty();
406
+ $(this.embedderContainer).empty();
407
+ $(this.embedderTogglerContainer).empty();
408
+ if ($defined(this.graph.selection)) {
409
+ this.graph.selection.remove();
410
+ }
411
+ /* Draw everything again. */
412
+ this.getData();
413
+ }.bind(this)
414
+ }
415
+ });
416
+
417
+ var select = new Element('select', { 'class': 'date timescale' });
418
+ var timescales = new Hash({ 'hour': 1, '2 hours': 2, '6 hours': 6, '12 hours': 12,
419
+ 'day': 24, '2 days': 48, '3 days': 72,
420
+ 'week': 168, '2 weeks': 336, 'month': 672 });
421
+ timescales.each(function(hour, label) {
422
+ var current = this.currentTimePeriod == 'last {label}'.substitute({'label': label });
423
+ var value = "start={start}".substitute({'start': currentUnixTime - (hour * 3600)});
424
+ var html = 'last {label}'.substitute({'label': label });
425
+
426
+ var option = new Element('option', {
427
+ html: html,
428
+ value: value,
429
+ selected: (current ? 'selected' : '')
430
+
431
+ });
432
+ select.grab(option)
433
+ });
434
+
435
+ var submit = new Element('input', { 'type': 'submit', 'value': 'show' });
436
+
437
+ form.grab(select);
438
+ form.grab(submit);
439
+ container.grab(form);
440
+ },
441
+ buildLabels: function() {
442
+ //buildLabels: function(graphLines, instanceNames, dataSources, colors) {
443
+
444
+ this.ys.each(function(set, index) {
445
+ var path = this.graph.lines[index],
446
+ color = this.colors[index]
447
+ plugin = this.options.plugin
448
+ instance = this.instances[index]
449
+ metric = this.metrics[index]
450
+
451
+ var container = new Element('div', {
452
+ 'class': 'label plugin',
453
+ 'styles': {
454
+ 'padding': '0.2em 0.5em 0',
455
+ 'float': 'left',
456
+ 'width': '180px',
457
+ 'font-size': '0.8em'
458
+ },
459
+ 'events': {
460
+ 'mouseover': function(e) {
461
+ e.stop();
462
+ path.animate({'stroke-width': 3}, 300);
463
+ //path.toFront();
464
+ },
465
+ 'mouseout': function(e) {
466
+ e.stop();
467
+ path.animate({'stroke-width': 1.5}, 300);
468
+ //path.toBack();
469
+ },
470
+ 'click': function(e) {
471
+ e.stop();
472
+ path.attr('opacity') == 0 ? path.animate({'opacity': 1}, 350) : path.animate({'opacity': 0}, 350);
473
+ }
474
+ }
475
+ });
476
+
477
+ var box = new Element('div', {
478
+ 'class': 'label plugin box ' + metric,
479
+ 'html': '&nbsp;',
480
+ 'styles': {
481
+ 'background-color': color,
482
+ 'width': '48px',
483
+ 'height': '18px',
484
+ 'float': 'left',
485
+ 'margin-right': '0.5em'
486
+ }
487
+ });
488
+
489
+ // plugin/instance/metrics names can be unmeaningful. make them pretty
490
+ var name;
491
+ name = instance.replace(plugin, '');
492
+ name = name.replace('tcp_connections', '')
493
+ name = name.replace('ps_state', '')
494
+ name = name.replace(plugin.split('-')[0], '')
495
+ name += metric == "value" ? "" : " (" + metric + ")"
496
+ name = name.replace(/^[-|_]*/, '')
497
+
498
+ var desc = new Element('span', {
499
+ 'class': 'label plugin description ' + metric,
500
+ 'html': name
501
+ });
502
+
503
+ container.grab(box);
504
+ container.grab(desc);
505
+ $(this.labelsContainer).grab(container);
506
+
507
+ }, this);
508
+ }
509
+ })
510
+