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
@@ -13,36 +13,42 @@ module Visage
13
13
  @rrddir ||= Visage::Config.rrddir
14
14
  end
15
15
 
16
+ # Returns a list of hosts that match the supplied glob, or array of names.
16
17
  def hosts(opts={})
17
- case
18
- when opts[:hosts].blank?
19
- glob = "*"
20
- when opts[:hosts] =~ /,/
21
- glob = "{#{opts[:hosts].strip.gsub(/\s*/, '').gsub(/,$/, '')}}"
18
+ hosts = opts[:hosts]
19
+ case hosts
20
+ when String && /,/
21
+ glob = "{#{hosts}}"
22
+ when Array
23
+ glob = "{#{opts[:hosts].join(',')}}"
22
24
  else
23
- glob = opts[:hosts]
25
+ glob = "*"
24
26
  end
25
27
 
26
28
  Dir.glob("#{rrddir}/#{glob}").map {|e| e.split('/').last }.sort.uniq
27
29
  end
28
30
 
29
31
  def metrics(opts={})
30
- case
31
- when opts[:metrics].blank?
32
- glob = "*/*"
33
- when opts[:metrics] =~ /,/
34
- glob = "{" + opts[:metrics].split(/\s*,\s*/).map { |m|
35
- m =~ /\// ? m : ["*/#{m}", "#{m}/*"]
36
- }.join(',').gsub(/,$/, '') + "}"
37
- when opts[:metrics] !~ /\//
38
- glob = "#{opts[:metrics]}/#{opts[:metrics]}"
32
+ selected_hosts = hosts(opts)
33
+
34
+ metrics = opts[:metrics]
35
+ case metrics
36
+ when String && /,/
37
+ metric_glob = "{#{metrics}}"
38
+ when Array
39
+ metric_glob = "{#{opts[:metrics].join(',')}}"
39
40
  else
40
- glob = opts[:metrics]
41
+ metric_glob = "*/*"
41
42
  end
42
43
 
43
- host_glob = (opts[:host] || "*")
44
-
45
- Dir.glob("#{rrddir}/#{host_glob}/#{glob}.rrd").map {|e| e.split('/')[-2..-1].join('/').gsub(/\.rrd$/, '')}.sort.uniq
44
+ selected_hosts.map { |host|
45
+ Dir.glob("#{rrddir}/#{host}/#{metric_glob}.rrd").map {|filename|
46
+ filename[/#{rrddir}\/#{host}\/(.*)\.rrd/, 1]
47
+ }
48
+ }.reduce(:&)
49
+ #else
50
+ # Dir.glob("#{rrddir}/#{host_glob}/#{glob}.rrd").map {|e| e.split('/')[-2..-1].join('/').gsub(/\.rrd$/, '')}.sort.uniq
51
+ #end
46
52
  end
47
53
 
48
54
  end
@@ -33,4 +33,21 @@ module Sinatra
33
33
  @page_title ? "#{@page_title} | Visage" : "Visage"
34
34
  end
35
35
  end
36
+
37
+ module RequireJSHelper
38
+ def require_js(filename)
39
+ @js_filenames ||= []
40
+ @js_filenames << filename
41
+ end
42
+
43
+ def include_required_js
44
+ if @js_filenames
45
+ @js_filenames.map { |filename|
46
+ "<script type='text/javascript' src='#{link_to("/javascripts/#{filename}.js")}'></script>"
47
+ }.join("\n")
48
+ else
49
+ ""
50
+ end
51
+ end
52
+ end
36
53
  end
@@ -11,6 +11,15 @@ module Visage
11
11
  attr_reader :options, :selected_hosts, :hosts, :selected_metrics, :metrics,
12
12
  :name, :errors
13
13
 
14
+ def self.old_format?
15
+ profiles = Visage::Config::File.load('profiles.yaml', :create => true, :ignore_bundled => true) || {}
16
+ profiles.each_pair do |name, attrs|
17
+ return true if attrs[:hosts] =~ /\*/ || attrs[:metrics] =~ /\*/
18
+ end
19
+
20
+ false
21
+ end
22
+
14
23
  def self.load
15
24
  Visage::Config::File.load('profiles.yaml', :create => true, :ignore_bundled => true) || {}
16
25
  end
@@ -32,25 +41,8 @@ module Visage
32
41
  @options = opts
33
42
  @options[:url] = @options[:profile_name] ? @options[:profile_name].downcase.gsub(/[^\w]+/, "+") : nil
34
43
  @errors = {}
35
-
36
- # FIXME: this is nasty
37
- # FIXME: doesn't work if there's only one host
38
- # FIXME: add regex matching option
39
- if @options[:hosts].blank?
40
- @selected_hosts = []
41
- @hosts = Visage::Collectd::RRDs.hosts
42
- else
43
- @selected_hosts = Visage::Collectd::RRDs.hosts(:hosts => @options[:hosts])
44
- @hosts = Visage::Collectd::RRDs.hosts - @selected_hosts
45
- end
46
-
47
- if @options[:metrics].blank?
48
- @selected_metrics = []
49
- @metrics = Visage::Collectd::RRDs.metrics
50
- else
51
- @selected_metrics = Visage::Collectd::RRDs.metrics(:metrics => @options[:metrics])
52
- @metrics = Visage::Collectd::RRDs.metrics - @selected_metrics
53
- end
44
+ @options[:hosts] = @options[:hosts].values if @options[:hosts].class == Hash
45
+ @options[:metrics] = @options[:metrics].values if @options[:metrics].class == Hash
54
46
  end
55
47
 
56
48
  # Hashed based access to @options.
@@ -61,16 +53,17 @@ module Visage
61
53
  def save
62
54
  if valid?
63
55
  # Construct record.
64
- attrs = { :hosts => @options[:hosts],
65
- :metrics => @options[:metrics],
56
+ attrs = { :hosts => @options[:hosts],
57
+ :metrics => @options[:metrics],
66
58
  :profile_name => @options[:profile_name],
67
- :url => @options[:profile_name].downcase.gsub(/[^\w]+/, "+") }
59
+ :url => @options[:profile_name].downcase.gsub(/[^\w]+/, "+") }
68
60
 
69
61
  # Save it.
70
62
  profiles = self.class.load
71
63
  profiles[attrs[:url]] = attrs
72
64
 
73
65
  Visage::Config::File.open('profiles.yaml') do |file|
66
+ file.truncate(0)
74
67
  file << profiles.to_yaml
75
68
  end
76
69
 
@@ -85,10 +78,10 @@ module Visage
85
78
  end
86
79
 
87
80
  def graphs
88
- graphs = []
89
-
90
- hosts = Visage::Collectd::RRDs.hosts(:hosts => @options[:hosts])
81
+ graphs = []
82
+ hosts = @options[:hosts]
91
83
  metrics = @options[:metrics]
84
+
92
85
  hosts.each do |host|
93
86
  attrs = {}
94
87
  globs = Visage::Collectd::RRDs.metrics(:host => host, :metrics => metrics)
@@ -0,0 +1,607 @@
1
+ var SearchToken = new Class({
2
+ Implements: [ Options, Events ],
3
+ initialize: function(wrapper, options) {
4
+ this.wrapper = wrapper;
5
+ this.setOptions(options);
6
+ this.element = new Element("div", { 'class': 'token' });
7
+ this.input = new Element("input", { 'class': 'token', 'autocomplete': 'off' });
8
+
9
+ this.element.grab(this.input);
10
+ // Trigger the data collection callback.
11
+
12
+ this.setupInputEvents();
13
+ this.setupFinalizedEvents();
14
+ },
15
+ value: function() {
16
+ return this.element.get('text')
17
+ },
18
+ setupFinalizedEvents: function() {
19
+ this.element.addEvent('click', function(e) {
20
+ e.stop();
21
+
22
+ this.wrapper.tokens.each(function(token) {
23
+ $(token).removeClass('selected');
24
+ });
25
+ var token = e.target;
26
+ if (token.hasClass('finalized')) {
27
+ token.addClass('selected');
28
+ var input = token.getElement('input.delete');
29
+ if (!input) {
30
+ var input = new Element('input', {
31
+ 'class': 'delete',
32
+ 'styles': {
33
+ 'width': '0px',
34
+ 'height': '0px',
35
+ 'padding': '0px',
36
+ 'margin': '0px',
37
+ 'z-index': '-1',
38
+ 'position': 'absolute',
39
+ 'left': '-100px'
40
+ },
41
+ 'events': {
42
+ 'keyup': function(e) {
43
+ e.stop();
44
+ if (["backspace", "delete"].contains(e.key)) {
45
+ this.destroy();
46
+ this.wrapper.tokens[this.wrapper.tokens.length - 1].takeFocus();
47
+ }
48
+ }.bind(this)
49
+ }
50
+ })
51
+ token.grab(input);
52
+ }
53
+ input.focus()
54
+ }
55
+ }.bind(this));
56
+ },
57
+ setupInputEvents: function() {
58
+ this.input.addEvent('focus', function(e) {
59
+ this.options.data.pass(null,this)();
60
+ }.bind(this));
61
+
62
+ /* Autocomplete menu */
63
+ this.input.addEvent('keyup', function(e) {
64
+ /* These keys are trigger actions on the autocomplete menu. */
65
+ var reservedKeys = [ "down", "up",
66
+ "enter",
67
+ "pageup", "pagedown",
68
+ "esc" ];
69
+ if (reservedKeys.contains(e.key)) { return };
70
+
71
+ /* signal to destroyPreviousToken() if this input has been edited */
72
+ if (e.target.get('value').length > 0) {
73
+ e.target.addClass('edited')
74
+ }
75
+
76
+ var query = e.target.get('value');
77
+ this.showResults(query);
78
+
79
+ }.bind(this));
80
+
81
+ /* Stop webkit from paging up/down */
82
+ this.input.addEvent('keydown', function(e) {
83
+ var reservedKeys = [ "pageup", "pagedown" ];
84
+ if (reservedKeys.contains(e.key)) { e.stop() };
85
+ });
86
+
87
+ /* Tab == enter for autocomplete */
88
+ this.input.addEvent('blur', function(e) {
89
+ var input = e.target.get('value');
90
+ if (input.length > 0) {
91
+ e.stop();
92
+ this.select();
93
+ }
94
+ }.bind(this));
95
+
96
+ /* Autocomplete menu navigation */
97
+ this.input.addEvent('keyup', function(e) {
98
+ switch(e.key) {
99
+ case "down":
100
+ if (this.input.get('value').length != 0) {
101
+ this.down();
102
+ } else {
103
+ if (this.resultSet().getChildren("li").length > 0) {
104
+ this.down();
105
+ } else {
106
+ var query = e.target.get('value');
107
+ this.showResults(query);
108
+ }
109
+ }
110
+ break;
111
+ case "up":
112
+ this.up();
113
+ break;
114
+ case "enter":
115
+ this.select();
116
+ break;
117
+ case "pageup":
118
+ this.up('top');
119
+ break;
120
+ case "pagedown":
121
+ this.down('bottom');
122
+ break;
123
+ case "esc":
124
+ this.hideResults();
125
+ break;
126
+ case "backspace":
127
+ this.destroyPreviousToken();
128
+ break;
129
+ default:
130
+ //console.log(e.key);
131
+ }
132
+
133
+ }.bind(this));
134
+
135
+ },
136
+ showResults: function(query) {
137
+ var resultSet = this.resultSet(),
138
+ data = this.data,
139
+ existing = this.wrapper.tokens.map(function(token) { return token.value() }),
140
+ results = data.filter(function(item) {
141
+ return (item.test(query, 'i') && !existing.contains(item) )
142
+ }).sort();
143
+
144
+ /* Build the result set to display */
145
+ resultSet.empty();
146
+ results.each(function(host, index) {
147
+ var result = new TokenSearchResult({'html': host});
148
+ if (index == 0) { result.active() };
149
+ resultSet.grab(result);
150
+ });
151
+ /* Catchall entry */
152
+ if (results.length > 1) {
153
+ var all = new TokenSearchResult({
154
+ 'html': '&uarr; all of the above',
155
+ 'class': 'result all',
156
+ });
157
+ resultSet.grab(all);
158
+ }
159
+ },
160
+ toElement: function() {
161
+ return this.element;
162
+ },
163
+ setValue: function(value) {
164
+ this.element.set('html', value);
165
+ },
166
+ finalize: function() {
167
+ this.element.addClass('finalized');
168
+ this.rehashURL({add: this.value()})
169
+ },
170
+ takeFocus: function() {
171
+ this.input.focus();
172
+ },
173
+ resultSet: function() {
174
+ return this.element.getParent('div.search').getElement('ul.results');
175
+ },
176
+ getActive: function() {
177
+ return this.resultSet().getElement('li.active');
178
+ },
179
+ down: function(position) {
180
+ var resultSet = this.resultSet();
181
+ active = this.getActive();
182
+
183
+ if (position == "bottom") {
184
+ down = resultSet.getLast('li.result');
185
+ } else {
186
+ down = active.getNext('li.result');
187
+ }
188
+
189
+ if (down) {
190
+ active.toggleClass('active');
191
+ down.toggleClass('active');
192
+ }
193
+ },
194
+ up: function(position) {
195
+ var resultSet = this.resultSet(),
196
+ active = this.getActive();
197
+
198
+ if (position == "top") {
199
+ up = resultSet.getFirst('li.result');
200
+ } else {
201
+ up = active.getPrevious('li.result');
202
+ }
203
+
204
+ if (up) {
205
+ active.toggleClass('active');
206
+ up.toggleClass('active');
207
+ }
208
+ },
209
+ destroy: function() {
210
+ this.wrapper.tokens.erase(this);
211
+ this.wrapper.destroyToken(this);
212
+ this.wrapper.resize();
213
+
214
+ this.rehashURL({remove: this.value()})
215
+ },
216
+ destroyPreviousToken: function() {
217
+ var input = this.input.get('value');
218
+
219
+ /* Only delete the previous token if:
220
+ * - the active TokenInput is empty,
221
+ * - and was empty on the last keystroke.
222
+ */
223
+ if ((input.length == 0 && this.previousInputLength > 0)
224
+ || input.length > 0
225
+ || this.input.hasClass('edited')
226
+ ) {
227
+ this.previousInputLength = input.length;
228
+ return
229
+ } else {
230
+ var token = this.wrapper.tokens[this.wrapper.tokens.length - 2];
231
+ if (token) {
232
+ token.destroy();
233
+ this.wrapper.destroyToken(token);
234
+ this.wrapper.resize();
235
+ this.hideResults();
236
+ };
237
+ }
238
+
239
+ },
240
+ hideResults: function() {
241
+ var results = this.resultSet();
242
+ results.empty();
243
+ },
244
+ select: function() {
245
+ var resultSet = this.resultSet(),
246
+ selected = this.getActive();
247
+
248
+ if ($chk(selected) && selected.hasClass('all')) {
249
+ var token = this;
250
+ this.wrapper.destroyToken(token);
251
+
252
+ /* Create a token for each result. */
253
+ resultSet.getElements('li.result').each(function(result) {
254
+ if (result.hasClass('all')) { return };
255
+
256
+ var text = result.get('html');
257
+ var token = this.wrapper.newToken()
258
+ token.setValue(text);
259
+ token.finalize();
260
+ }, this);
261
+ } else {
262
+ var token = this.element,
263
+ input = this.input,
264
+ text = selected.get('html');
265
+
266
+ input.destroy();
267
+ this.setValue(text);
268
+ this.finalize();
269
+ }
270
+
271
+ // IDEA: do selected.destroy() to remove just the entry?
272
+ resultSet.empty();
273
+
274
+ this.wrapper.newToken();
275
+
276
+ this.wrapper.resize();
277
+ },
278
+ rehashURL: function(options) {
279
+ // Setup the URL
280
+ var parameters = window.location.hash.slice(1).split('|');
281
+ if (parameters.length == 1) {
282
+ var parameters = ["hosts=", "metrics="]
283
+ }
284
+
285
+ var parameters = parameters.map(function(parameter) {
286
+ if (!$chk(parameter)) { return parameter }
287
+
288
+ var parts = parameter.split('='),
289
+ key = parts[0],
290
+ values = parts[1].split(','),
291
+ value = options.add || options.remove;
292
+
293
+ if (value.contains('/') && key == "hosts") { return parameter }
294
+ if (!value.contains('/') && key == "metrics") { return parameter }
295
+
296
+ if (options.add) {
297
+ values.include(value)
298
+ } else {
299
+ values.erase(value)
300
+ }
301
+ values.erase("")
302
+
303
+ var string = key + '=' + values.join(',');
304
+ return string
305
+ }.bind(this));
306
+
307
+ var hash = parameters.join('|');
308
+ window.location.hash = hash;
309
+ },
310
+ });
311
+
312
+ var TokenSearchResult = new Class({
313
+ Implements: [ Options, Events ],
314
+ options: {
315
+ 'class': 'result',
316
+ 'events': {
317
+ 'mouseenter': function(e) {
318
+ var element = e.target,
319
+ currentActive = element.getParent('ul').getElement('li.active');
320
+
321
+ if (currentActive) {
322
+ currentActive.removeClass('active');
323
+ }
324
+ element.addClass('active');
325
+ },
326
+ 'mouseleave': function(e) {
327
+ var element = e.target;
328
+ element.removeClass('active');
329
+ },
330
+ }
331
+ },
332
+ initialize: function(options) {
333
+ this.setOptions(options);
334
+ this.element = new Element('li', this.options);
335
+ },
336
+ // http://mootools.net/blog/2010/03/19/a-better-way-to-use-elements/
337
+ toElement: function() {
338
+ return this.element;
339
+ },
340
+ active: function() {
341
+ this.element.addClass('active');
342
+ }
343
+
344
+ });
345
+
346
+
347
+ var TokenSearch = new Class({
348
+ Implements: [ Options, Events ],
349
+ options: {
350
+ focus: true
351
+ },
352
+ initialize: function(parent, options) {
353
+ this.setOptions(options);
354
+ this.parent = $(parent);
355
+
356
+ this.element = new Element('div', {'class': 'tokenWrapper'});
357
+ this.results = new Element('ul', {'class': 'results'});
358
+ this.parent.grab(this.element);
359
+ this.parent.grab(this.results);
360
+ this.tokens = [];
361
+
362
+ if (this.options.tokens) {
363
+ this.options.tokens.each(function(text) {
364
+ var token = this.newToken()
365
+ token.setValue(text);
366
+ token.finalize();
367
+ }.bind(this));
368
+ }
369
+ this.newToken(this.options.focus);
370
+
371
+ /* Clicks within the contain focus input on the editable token. */
372
+ this.element.addEvent('click', function() {
373
+ this.tokens.getLast().takeFocus();
374
+ }.bind(this));
375
+ },
376
+ toElement: function() {
377
+ return this.element;
378
+ },
379
+ newToken: function(focus) {
380
+ var token = new SearchToken(this, { 'data': this.options.data });
381
+
382
+ this.tokens.include(token);
383
+ this.element.grab(token);
384
+
385
+ if (focus != false) {
386
+ token.takeFocus();
387
+ }
388
+
389
+ this.resize();
390
+
391
+ return token;
392
+ },
393
+ destroyToken: function(token) {
394
+ this.tokens.erase(token);
395
+ $(token).destroy()
396
+ },
397
+ tokenValues: function() {
398
+ return this.tokens.map(function(t) {
399
+ return t.value()
400
+ }).slice(0,-1);
401
+ },
402
+ resize: function() {
403
+ var firstToken = $(this.tokens[0]),
404
+ lastToken = $(this.tokens[this.tokens.length - 1]),
405
+ lastTokenHeight = lastToken.getDimensions().height,
406
+ baseY = this.element.getPosition().y,
407
+ minY = firstToken.getPosition().y,
408
+ maxY = lastToken.getPosition().y;
409
+
410
+ if (minY != maxY) {
411
+ var newHeight = maxY - baseY + lastTokenHeight;
412
+ } else {
413
+ var newHeight = minY - baseY + lastTokenHeight;
414
+ }
415
+ this.element.setStyle('height', newHeight);
416
+ },
417
+ });
418
+
419
+ var ChartBuilder = new Class({
420
+ Implements: [ Options, Events ],
421
+ initialize: function(element, options) {
422
+ this.setOptions(options);
423
+ this.builder = $(element);
424
+
425
+ var parameters = window.location.hash.slice(1).split('|');
426
+
427
+ parameters.each(function(parameter) {
428
+ if (!$chk(parameter)) { return }
429
+
430
+ var parts = parameter.split('='),
431
+ key = parts[0],
432
+ values = parts[1].split(',');
433
+
434
+ values.erase("")
435
+ this.options[key] = values;
436
+ }.bind(this));
437
+
438
+ this.searchers = new Object;
439
+ this.setupHostSearch();
440
+ this.setupMetricSearch();
441
+ this.setupShow();
442
+
443
+ /* Display graphs if hosts + metrics have been selected */
444
+ if (this.options.hosts && this.options.metrics) {
445
+ this.showGraphs();
446
+ }
447
+ },
448
+ setupHostSearch: function() {
449
+ var container = this.builder.getElement("div#hosts div.search"),
450
+ searcher = new TokenSearch(container, {
451
+ 'tokens': this.options.hosts,
452
+ 'data': this.getHosts
453
+ });
454
+ this.searchers.host = searcher;
455
+ },
456
+ setupMetricSearch: function() {
457
+ var container = this.builder.getElement("div#metrics div.search"),
458
+ searcher = new TokenSearch(container, {
459
+ 'tokens': this.options.metrics,
460
+ 'data': this.getMetrics,
461
+ 'focus': false
462
+ });
463
+ this.searchers.metric = searcher;
464
+ },
465
+ setupSave: function() {
466
+ if (!this.save) {
467
+ var profile_name = this.profile_name = new Element('input', {
468
+ 'id': 'profile_name',
469
+ 'type': 'text',
470
+ 'class': 'text',
471
+ 'value': $('name') ? $('name').get('text') : ''
472
+ });
473
+
474
+ var show = this.builder.getElement('input#show');
475
+ var save = this.save = new Element('input', {
476
+ 'id': 'save',
477
+ 'type': 'button',
478
+ 'class': 'button',
479
+ 'value': 'Save profile',
480
+ 'events': {
481
+ 'click': function() {
482
+ var hosts = this.searchers.host.tokenValues(),
483
+ metrics = this.searchers.metric.tokenValues();
484
+
485
+ var jsonRequest = new Request.JSON({
486
+ method: 'post',
487
+ url: '/builder',
488
+ onSuccess: function(response) {
489
+ new Message({
490
+ title: 'Profile saved',
491
+ message: '"' + profile_name.get('value') + '"',
492
+ top: true,
493
+ iconPath: '/images/',
494
+ icon: 'ok.png',
495
+ }).say();
496
+ },
497
+ }).send({'data': {
498
+ 'hosts': hosts,
499
+ 'metrics': metrics,
500
+ 'profile_name': profile_name.get('value')
501
+ }});
502
+
503
+ }.bind(this)
504
+ }
505
+ });
506
+
507
+ save.fade('hide')
508
+ show.grab(save, 'after');
509
+
510
+ profile_name.fade('hide')
511
+ save.grab(profile_name, 'after');
512
+ }
513
+ },
514
+ setupShow: function() {
515
+ var show = this.builder.getElement('input#show');
516
+
517
+ /* Button to save profile */
518
+ show.addEvent('click', function(e) {
519
+ this.setupSave();
520
+ }.bind(this));
521
+
522
+ /* Display the graphs */
523
+ show.addEvent('click', function(e) {
524
+ e.stop();
525
+
526
+ this.showGraphs();
527
+
528
+ // Fade the save button once graphs have been rendered.
529
+ save.fade.delay(1500, save, 'in')
530
+ profile_name.fade.delay(1500, profile_name, 'in')
531
+ }.bind(this));
532
+
533
+ },
534
+ showGraphs: function() {
535
+ window.Graphs = [];
536
+
537
+ var hosts = $(this.searchers.host).getElements("div.token.finalized"),
538
+ metrics = $(this.searchers.metric).getElements("div.token.finalized");
539
+
540
+ var hosts = hosts.map(function(el) { return el.get('text') }),
541
+ metrics = metrics.map(function(el) { return el.get('text') }),
542
+ graphs = $('graphs'),
543
+ save = this.save;
544
+ profile_name = this.profile_name;
545
+
546
+ graphs.empty();
547
+ hosts.each(function(host) {
548
+ /* Separate each plugin onto its own graph */
549
+ var plugins = {};
550
+ metrics.each(function(m) {
551
+ var parts = m.split('/'),
552
+ plugin = parts[0],
553
+ metric = parts[1];
554
+
555
+ if (! plugins[plugin]) {
556
+ plugins[plugin] = []
557
+ }
558
+
559
+ plugins[plugin].push(metric)
560
+ });
561
+
562
+ /* Create the graphs */
563
+ $each(plugins, function(metrics, plugin) {
564
+ var element = new Element('div', {'class': 'graph'});
565
+ graphs.grab(element);
566
+
567
+ var graph = new VisageGraph(element, host, plugin, {
568
+ pluginInstance: metrics.join(',')
569
+ });
570
+
571
+ window.Graphs.include(graph);
572
+ });
573
+ }.bind(this));
574
+ },
575
+ getHosts: function() {
576
+ var request = new Request.JSONP({
577
+ url: "/data",
578
+ method: "get",
579
+ onRequest: function(json) {
580
+ this.data = [];
581
+ },
582
+ onComplete: function(json) {
583
+ this.data = json.hosts;
584
+ }.bind(this)
585
+ }).send();
586
+
587
+ return request;
588
+ },
589
+ getMetrics: function(hosts) {
590
+ var builder = $(this.wrapper).getParent('div#chart-builder'),
591
+ tokens = builder.getElement("div#hosts div.tokenWrapper"),
592
+ hosts = tokens.getElements("div.token.finalized").map(function(token) {
593
+ return token.get('text');
594
+ });
595
+
596
+ var url = "/data/" + hosts.join(',');
597
+ var request = new Request.JSONP({
598
+ url: url,
599
+ method: "get",
600
+ onComplete: function(json) {
601
+ this.data = json.metrics;
602
+ }.bind(this)
603
+ }).send();
604
+
605
+ return request;
606
+ }
607
+ });