visage-app 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+