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.
- data/.gitignore +10 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +1 -15
- data/Gemfile.lock +44 -42
- data/README.md +123 -49
- data/Rakefile +16 -26
- data/bin/visage-app +17 -4
- data/features/cli.feature +10 -3
- data/features/json.feature +37 -0
- data/features/step_definitions/{visage_steps.rb → cli_steps.rb} +1 -1
- data/features/step_definitions/json_steps.rb +50 -8
- data/features/step_definitions/site_steps.rb +1 -1
- data/features/support/config/default/profiles.yaml +335 -0
- data/features/{data → support}/config/with_no_profiles/.stub +0 -0
- data/features/support/config/with_no_profiles/profiles.yaml +0 -0
- data/features/support/config/with_old_profile_yaml/profiles.yaml +116 -0
- data/features/support/env.rb +2 -3
- data/lib/visage-app.rb +35 -25
- data/lib/visage-app/collectd/json.rb +115 -118
- data/lib/visage-app/collectd/rrds.rb +25 -19
- data/lib/visage-app/helpers.rb +17 -0
- data/lib/visage-app/profile.rb +18 -25
- data/lib/visage-app/public/images/caution.png +0 -0
- data/lib/visage-app/public/images/ok.png +0 -0
- data/lib/visage-app/public/images/questions.png +0 -0
- data/lib/visage-app/public/javascripts/builder.js +607 -0
- data/lib/visage-app/public/javascripts/graph.js +179 -142
- data/lib/visage-app/public/javascripts/message.js +520 -0
- data/lib/visage-app/public/javascripts/mootools-core-1.4.0-full-compat.js +6285 -0
- data/lib/visage-app/public/javascripts/mootools-more-1.4.0.1.js +6399 -0
- data/lib/visage-app/public/stylesheets/message.css +61 -0
- data/lib/visage-app/public/stylesheets/screen.css +149 -38
- data/lib/visage-app/version.rb +5 -0
- data/lib/visage-app/views/builder.haml +38 -49
- data/lib/visage-app/views/builder_form.haml +14 -0
- data/lib/visage-app/views/layout.haml +5 -2
- data/lib/visage-app/views/profile.haml +44 -25
- data/visage-app.gemspec +29 -132
- metadata +93 -150
- data/VERSION +0 -1
- data/features/builder.feature +0 -16
- 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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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 =
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
41
|
+
metric_glob = "*/*"
|
41
42
|
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
data/lib/visage-app/helpers.rb
CHANGED
@@ -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
|
data/lib/visage-app/profile.rb
CHANGED
@@ -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
|
-
|
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
|
65
|
-
:metrics
|
56
|
+
attrs = { :hosts => @options[:hosts],
|
57
|
+
:metrics => @options[:metrics],
|
66
58
|
:profile_name => @options[:profile_name],
|
67
|
-
:url
|
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)
|
Binary file
|
Binary file
|
Binary file
|
@@ -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': '↑ 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
|
+
});
|