visage-app 0.3.3 → 0.9.0.pre1

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/LICENCE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2009-2010 Lindsay Holmwood
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
21
+ Visage is distributed with Highcharts. Torstein Hønsi has kindly granted
22
+ permission to distribute Highcharts under the GPL as part of Visage.
23
+
24
+ If you ever need an excellent JavaScript charting library, please consider
25
+ purchasing a [commercial license](http://highcharts.com/license) for
26
+ Highcharts.
27
+
data/README.md CHANGED
@@ -152,3 +152,14 @@ Run all cucumber features:
152
152
 
153
153
  $ rake cucumber
154
154
 
155
+ Licencing
156
+ ---------
157
+
158
+ Visage is MIT licensed.
159
+
160
+ Visage is distributed with Highcharts. Torstein Hønsi has kindly granted
161
+ permission to distribute Highcharts under the GPL as part of Visage.
162
+
163
+ If you ever need an excellent JavaScript charting library, please consider
164
+ purchasing a [commercial license](http://highcharts.com/license) for
165
+ Highcharts.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.3
1
+ 0.9.0.pre1
data/lib/visage-app.rb CHANGED
@@ -25,13 +25,7 @@ module Visage
25
25
 
26
26
  configure do
27
27
  Visage::Config.use do |c|
28
- # Base configuration files.
29
- c['profiles'] = Visage::Config::File.load('profiles.yaml', :create => true, :ignore_bundled => true)
30
- c['plugin_colors'] = Visage::Config::File.load('plugin-colors.yaml')
31
- c['fallback_colors'] = Visage::Config::File.load('fallback-colors.yaml')
32
-
33
28
  # FIXME: make this configurable through file
34
- c['shade'] = false
35
29
  c['rrddir'] = ENV["RRDDIR"] ? Pathname.new(ENV["RRDDIR"]).expand_path : Pathname.new("/var/lib/collectd/rrd").expand_path
36
30
  end
37
31
  end
@@ -96,14 +90,12 @@ module Visage
96
90
  plugin = params[:captures][1].gsub("\0", "")
97
91
  plugin_instances = params[:captures][2].gsub("\0", "")
98
92
 
99
- collectd = CollectdJSON.new(:rrddir => Visage::Config.rrddir,
100
- :fallback_colors => Visage::Config.fallback_colors)
93
+ collectd = CollectdJSON.new(:rrddir => Visage::Config.rrddir)
101
94
  json = collectd.json(:host => host,
102
95
  :plugin => plugin,
103
96
  :plugin_instances => plugin_instances,
104
97
  :start => params[:start],
105
- :finish => params[:finish],
106
- :plugin_colors => Visage::Config.plugin_colors)
98
+ :finish => params[:finish])
107
99
  # if the request is cross-domain, we need to serve JSONP
108
100
  maybe_wrap_with_callback(json)
109
101
  end
@@ -9,14 +9,11 @@ require 'yajl'
9
9
  # Exposes RRDs as JSON.
10
10
  #
11
11
  # A loose shim onto RRDtool, with some extra logic to normalise the data.
12
- # Also provides a recommended color for rendering the data in a line graph.
13
12
  #
14
13
  class CollectdJSON
15
14
 
16
15
  def initialize(opts={})
17
16
  @rrddir = opts[:rrddir] || CollectdJSON.rrddir
18
- @fallback_colors = opts[:fallback_colors] || {}
19
- @used_fallbacks = []
20
17
  end
21
18
 
22
19
  # Entry point.
@@ -25,7 +22,6 @@ class CollectdJSON
25
22
  plugin = opts[:plugin]
26
23
  plugin_instances = opts[:plugin_instances][/\w.*/]
27
24
  instances = plugin_instances.blank? ? '*' : '{' + plugin_instances.split('/').join(',') + '}'
28
- @colors = opts[:plugin_colors]
29
25
  @plugin_names = []
30
26
 
31
27
  rrdglob = "#{@rrddir}/#{host}/#{plugin}/#{instances}.rrd"
@@ -66,18 +62,20 @@ class CollectdJSON
66
62
  # the same file). Separate the metrics.
67
63
  rrd_data.each_pair do |source, metric|
68
64
 
69
- # filter out NaNs, so yajl doesn't choke
65
+ # Filter out NaNs and weirdly massive values so yajl doesn't choke
70
66
  metric.map! do |datapoint|
71
- (!datapoint || datapoint.nan?) ? 0.0 : datapoint
67
+ case
68
+ when datapoint && datapoint.nan?
69
+ @tripped = true
70
+ @last_valid
71
+ when @tripped
72
+ @last_valid
73
+ else
74
+ @last_valid = datapoint
75
+ end
72
76
  end
73
77
 
74
- # Sometimes the last value from the RRD is ridiculously large.
75
- metric.slice!(-1)
76
-
77
- color = color_for(:host => data[:host],
78
- :plugin => data[:plugin],
79
- :instance => data[:instance],
80
- :metric => source)
78
+ metric[-1] = 0.0
81
79
 
82
80
  structure[data[:host]] ||= {}
83
81
  structure[data[:host]][data[:plugin]] ||= {}
@@ -86,7 +84,6 @@ class CollectdJSON
86
84
  structure[data[:host]][data[:plugin]][data[:instance]][source][:start] ||= data[:start]
87
85
  structure[data[:host]][data[:plugin]][data[:instance]][source][:finish] ||= data[:finish]
88
86
  structure[data[:host]][data[:plugin]][data[:instance]][source][:data] ||= metric
89
- structure[data[:host]][data[:plugin]][data[:instance]][source][:color] ||= color
90
87
  end
91
88
  end
92
89
 
@@ -94,34 +91,6 @@ class CollectdJSON
94
91
  encoder.encode(structure)
95
92
  end
96
93
 
97
- # We append the recommended line color onto data set, so the javascript
98
- # doesn't try and have to work it out. This lets us use all sorts of funky
99
- # fallback logic when determining what colours should be used.
100
- def color_for(opts={})
101
-
102
- plugin = opts[:plugin]
103
- instance = opts[:instance]
104
- metric = opts[:metric]
105
-
106
- return fallback_color unless plugin
107
- return color_for(opts.merge(:plugin => plugin[/(.+)-.+$/, 1])) unless @colors[plugin]
108
- return fallback_color unless instance
109
- return color_for(opts.merge(:instance => instance[/(.+)-.+$/, 1])) unless @colors[plugin][instance]
110
- return @colors[plugin][instance][metric] if @colors[plugin][instance]
111
- return fallback_color
112
- end
113
-
114
- def fallback_color
115
- fallbacks = @fallback_colors.to_a.sort_by {|pair| pair[1]['fallback_order'] }
116
- fallback = fallbacks.find { |color| !@used_fallbacks.include?(color) }
117
- unless fallback
118
- @used_fallbacks = []
119
- fallback = fallbacks.find { |color| !@used_fallbacks.include?(color) }
120
- end
121
- @used_fallbacks << fallback
122
- fallback[1]['color'] || "#000"
123
- end
124
-
125
94
  class << self
126
95
  attr_writer :rrddir
127
96
 
@@ -11,15 +11,19 @@ module Visage
11
11
  attr_reader :options, :selected_hosts, :hosts, :selected_metrics, :metrics,
12
12
  :name, :errors
13
13
 
14
+ def self.load
15
+ Visage::Config::File.load('profiles.yaml', :create => true, :ignore_bundled => true) || {}
16
+ end
17
+
14
18
  def self.get(id)
15
19
  url = id.downcase.gsub(/[^\w]+/, "+")
16
- profiles = Visage::Config.profiles || {}
20
+ profiles = self.load
17
21
  profiles[url] ? self.new(profiles[url]) : nil
18
22
  end
19
23
 
20
24
  def self.all(opts={})
21
25
  sort = opts[:sort]
22
- profiles = Visage::Config.profiles || {}
26
+ profiles = self.load
23
27
  profiles = sort == "name" ? profiles.sort.map {|i| i.last } : profiles.values
24
28
  profiles.map { |prof| self.new(prof) }
25
29
  end
@@ -63,7 +67,7 @@ module Visage
63
67
  :url => @options[:profile_name].downcase.gsub(/[^\w]+/, "+") }
64
68
 
65
69
  # Save it.
66
- profiles = Visage::Config.profiles || {}
70
+ profiles = self.load
67
71
  profiles[attrs[:url]] = attrs
68
72
 
69
73
  Visage::Config::File.open('profiles.yaml') do |file|
@@ -1,3 +1,127 @@
1
+ function formatSeriesLabel(labels) {
2
+ var host = labels[0],
3
+ plugin = labels[1],
4
+ instance = labels[2],
5
+ metric = labels[3],
6
+ name;
7
+
8
+ // Generic label building
9
+ name = instance
10
+ name = name.replace(plugin, '')
11
+ name = name.replace(plugin.split('-')[0], '')
12
+ name = name.replace('tcp_connections', '')
13
+ name = name.replace('ps_state', '')
14
+ name += metric == "value" ? "" : " (" + metric + ")"
15
+ name = name.replace(/^[-|_]*/, '')
16
+ name = name.trim().replace(/^\((.*)\)$/, '$1')
17
+ name = plugin == "irq" ? name.replace(/^/, 'irq ') : ''
18
+
19
+ // Plugin specific labeling
20
+ if (plugin == "interface") {
21
+ name += instance.replace(/^if_(.*)-(.*)/, '$2 $1') + ' (' + metric + ')'
22
+ }
23
+ if (["processes", "memory"].contains(plugin) || plugin.test(/^cpu-\d+/) ) {
24
+ name += instance.split('-')[1]
25
+ }
26
+ if (plugin == "swap") {
27
+ if (instance.test(/^swap_io/)) {
28
+ name += instance.replace(/^swap_(\w*)-(.*)$/, '$1 $2')
29
+ }
30
+ if (instance.test(/^swap-/)) {
31
+ name += instance.split('-')[1]
32
+ }
33
+ }
34
+ if (plugin == "load") {
35
+ name += metric.replace(/^\((.*)\)$/, '$1')
36
+ }
37
+ if (plugin.test(/^disk/)) {
38
+ name += instance.replace(/^disk_/, '') + ' (' + metric + ')'
39
+ }
40
+ if (["entropy","users"].contains(plugin)) {
41
+ name += metric
42
+ }
43
+ if (plugin == "uptime") {
44
+ name += instance
45
+ }
46
+ if (plugin == "ping") {
47
+ if (instance.test(/^ping_/)) {
48
+ name += instance.replace(/^ping_(.*)-(.*)$/, '$1 $2')
49
+ } else {
50
+ name += metric + ' ' + instance.split('-')[1]
51
+ }
52
+ }
53
+ if (plugin == "vmem") {
54
+ if (instance.test(/^vmpage_number-/)) {
55
+ name += instance.replace(/^vmpage_number-(.*)$/, '$1').replace('_', ' ')
56
+ }
57
+ if (instance.test(/^vmpage_io/)) {
58
+ name += instance.replace(/^vmpage_io-(.*)$/, '$1 ') + metric
59
+ }
60
+ if (instance.test(/^vmpage_faults/)) {
61
+ name += metric.trim() == "minflt" ? 'minor' : 'major'
62
+ name += ' faults'
63
+ }
64
+ }
65
+ if (plugin.test(/^tcpconns/)) {
66
+ name += instance.split('-')[1].replace('_', ' ')
67
+ }
68
+ if (plugin.test(/^tail/)) {
69
+ name += plugin.split('-').slice(1).join('-') + ' '
70
+ name += instance.split('-').slice(1).join('-')
71
+ }
72
+ if (plugin == "apache") {
73
+ var stash = instance.split('_')[1]
74
+ if (stash.test(/^scoreboard/)) {
75
+ name += 'connections: ' + stash.split('-')[1]
76
+ } else {
77
+ name += stash
78
+ }
79
+
80
+ }
81
+ return name.trim()
82
+ }
83
+
84
+ function formatValue(value, places) {
85
+ var places = places ? places : 0
86
+ switch(true) {
87
+ case (Math.abs(value) > 1125899906842624):
88
+ var label = value / 1125899906842624,
89
+ unit = 'P';
90
+ break
91
+ case (Math.abs(value) > 1099511627776):
92
+ var label = value / 1099511627776,
93
+ unit = 'T';
94
+ break
95
+ case (Math.abs(value) > 1073741824):
96
+ var label = value / 1073741824,
97
+ unit = 'G';
98
+ break
99
+ case (Math.abs(value) > 1048576):
100
+ var label = value / 1048576,
101
+ unit = 'M';
102
+ break
103
+ case (Math.abs(value) > 1024):
104
+ var label = value / 1024,
105
+ unit = 'K';
106
+ break
107
+ default:
108
+ var label = value,
109
+ unit = '';
110
+ break
111
+ }
112
+
113
+ var rounded = label.round(places)
114
+
115
+ return rounded + unit
116
+ }
117
+
118
+ function formatDate(d) {
119
+ var datetime = new Date(d * 1000)
120
+ return datetime.format("%Y-%m-%d %H:%M:%S UTC%T")
121
+ }
122
+
123
+
124
+
1
125
  /*
2
126
  * visageBase()
3
127
  *
@@ -8,28 +132,15 @@
8
132
  var visageBase = new Class({
9
133
  Implements: [Options, Events],
10
134
  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
135
  secureJSON: false,
22
136
  httpMethod: 'get',
23
- axis: "0 0 1 1"
137
+ live: false
24
138
  },
25
139
  initialize: function(element, host, plugin, options) {
26
- this.parentElement = element;
27
- this.setOptions(options);
28
- this.options.host = host;
29
- this.options.plugin = plugin;
30
- this.buildGraphHeader();
31
- this.buildGraphContainer();
32
- this.canvas = Raphael(this.graphContainer, this.options.width, this.options.height);
140
+ this.parentElement = element
141
+ this.setOptions(options)
142
+ this.options.host = host
143
+ this.options.plugin = plugin
33
144
  data = new Hash()
34
145
  if($chk(this.options.start)) {
35
146
  data.set('start', this.options.start)
@@ -37,7 +148,7 @@ var visageBase = new Class({
37
148
  if($chk(this.options.finish)) {
38
149
  data.set('finish', this.options.finish)
39
150
  }
40
- this.requestData = data
151
+ this.requestData = data;
41
152
  this.getData(); // calls graphData
42
153
  },
43
154
  dataURL: function() {
@@ -72,28 +183,10 @@ var visageBase = new Class({
72
183
 
73
184
  this.request.send();
74
185
  },
75
- buildGraphHeader: function() {
76
- header = $chk(this.options.name) ? this.options.name : this.options.plugin
77
- this.graphHeader = new Element('h3', {
78
- 'class': 'graph-title',
79
- 'html': header,
80
- 'styles': {
81
- 'color': "#121212"
82
- }
83
- });
84
- $(this.parentElement).grab(this.graphHeader);
186
+ graphName: function() {
187
+ name = $chk(this.options.name) ? this.options.name : this.options.plugin
188
+ return name
85
189
  },
86
- buildGraphContainer: function() {
87
- $(this.parentElement).set('style', 'padding-top: 1em');
88
-
89
- this.graphContainer = new Element('div', {
90
- 'class': 'graph container',
91
- 'styles': {
92
- 'margin-bottom': '24px'
93
- }
94
- });
95
- $(this.parentElement).grab(this.graphContainer)
96
- }
97
190
  });
98
191
 
99
192
 
@@ -111,453 +204,319 @@ var visageGraph = new Class({
111
204
  Implements: Chain,
112
205
  // assemble data to graph, then draw it
113
206
  graphData: function(data) {
207
+ this.response = data
208
+ this.buildDataStructures()
114
209
 
115
- this.ys = []
116
- this.colors = []
117
- this.instances = []
118
- this.metrics = []
119
-
120
- var host = this.options.host
121
- var plugin = this.options.plugin
210
+ if ( $defined(this.chart) ) {
211
+ this.series.each(function(series, index) {
212
+ this.chart.series[index].setData(series.data)
213
+ }, this);
214
+ } else {
215
+ this.drawChart()
216
+ }
217
+ },
218
+ buildDataStructures: function (data) {
219
+ var series = this.series = []
220
+ var host = this.options.host
221
+ var plugin = this.options.plugin
222
+ var data = data ? data : this.response
122
223
 
123
224
  $each(data[host][plugin], function(instance, iname) {
124
225
  $each(instance, function(metric, mname) {
125
- this.colors.push(metric.color)
126
- if ( !$defined(this.x) ) {
127
- this.x = this.buildXAxis(metric)
128
- }
129
- this.ys.push(metric.data)
130
- this.instances.push(iname) // labels
131
- this.metrics.push(mname) // labels
226
+ var set = {
227
+ name: [ host, plugin, iname, mname ],
228
+ data: metric.data,
229
+ pointStart: metric.start,
230
+ pointInterval: (metric.finish - metric.start) / metric.data.length
231
+ };
232
+
233
+ series.push(set)
132
234
  }, this);
133
235
  }, this);
134
236
 
135
- this.buildContainers();
136
- this.drawGraph();
137
-
138
- this.buildLabels();
139
- this.addSelectionInterface();
140
- this.addDebugInterface();
141
- this.buildDateSelector();
142
-
143
- /* disabling this for now for dramatic effect
144
- this.buildEmbedder();
145
- */
146
- },
147
- buildXAxis: function(metric) {
148
- var start = metric.start.toInt(),
149
- finish = metric.finish.toInt(),
150
- length = metric.data.length,
151
- interval = (finish - start) / length,
152
- counter = start,
153
- x = []
154
-
155
- while (counter < finish) {
156
- x.push(counter)
157
- counter += interval
158
- }
159
- return x
160
- },
161
- drawGraph: function() {
162
-
163
- var colors = this.colors;
164
- var left = this.options.leftEdge
165
- var top = this.options.topEdge
166
- var width = this.options.gridWidth
167
- var height = this.options.gridHeight
168
- var x = this.x // x axis
169
- var ys = this.ys // y axes
170
- var xstep = x.length / 20
171
- var shade = this.options.shade
172
- var axis = this.options.axis
173
-
174
- this.canvas.g.txtattr.font = "11px 'sans-serif'";
175
- this.graph = this.canvas.g.linechart(left, top, width, height, x, ys, {
176
- nostroke: false,
177
- width: 1.5,
178
- axis: axis,
179
- colors: colors,
180
- axisxstep: xstep,
181
- shade: shade
182
- });
183
-
184
- this.formatAxes();
237
+ return series
185
238
  },
186
- formatAxes: function() {
187
-
188
- /* clean up graph labels */
189
- this.graph.axis[0].text.items.getLast().hide()
190
- $each(this.graph.axis[0].text.items, function (time) {
191
-
192
- var unixTime = time.attr('text')
193
- var d = new Date(time.attr('text') * 1000);
194
- time.attr({'text': d.strftime("%H:%M")});
195
-
196
- time.mouseover(function () {
197
- this.attr({'text': d.strftime("%H:%M")});
198
- });
199
-
200
- /*
201
- time.mouseout(function () {
202
- this.attr({'text': d.strftime("%H:%M")});
203
- });
204
- */
205
- });
206
-
207
- $each(this.graph.axis[1].text.items, function (value) {
208
- // FIXME: no JS reference on train means awful rounding hacks!
209
- // if you are reading this, it's a bug!
210
- if (value.attr('text') > 1073741824) {
211
- var label = value.attr('text') / 1073741824;
212
- var unit = 'g'
213
- } else if (value.attr('text') > 1048576) {
214
- // and again :-(
215
- var label = value.attr('text') / 1048576;
216
- var unit = 'm'
217
- } else if (value.attr('text') > 1024) {
218
- var label = value.attr('text') / 1024;
219
- var unit = 'k';
220
- } else {
221
- var label = value.attr('text');
222
- var unit = ''
239
+ drawChart: function() {
240
+ var series = this.series,
241
+ title = this.graphName(),
242
+ element = this.parentElement,
243
+ ytitle = this.options.plugin
244
+ max = 0
245
+
246
+ /* Get the maximum value across all sets.
247
+ * Used later on to determine the decimal place in the label. */
248
+ series.each(function(set) {
249
+ var setMax = set.data.max()
250
+ if ( setMax > max ) {
251
+ max = setMax
223
252
  }
224
-
225
- var decimal = label.toString().split('.')
226
- if ($chk(this.previous) && this.previous.toString()[0] == label.toString()[0] && decimal.length > 1) {
227
- var round = '.' + decimal[1][0]
228
- } else {
229
- var round = ''
230
- }
231
-
232
- value.attr({'text': Math.floor(label) + round + unit})
233
- this.previous = value.attr('text')
234
253
  });
235
254
 
236
- },
237
- buildEmbedder: function() {
238
- var pre = new Element('textarea', {
239
- 'id': 'embedder',
240
- 'class': 'embedder',
241
- 'html': this.embedCode(),
242
- 'styles': {
243
- 'width': '500px',
244
- 'padding': '3px'
255
+ this.chart = new Highcharts.Chart({
256
+ chart: {
257
+ renderTo: element,
258
+ defaultSeriesType: 'line',
259
+ marginRight: 200,
260
+ marginBottom: 25,
261
+ zoomType: 'xy',
262
+ height: 300,
263
+ events: {
264
+ load: function(e) {
265
+ setInterval(function() {
266
+ if (this.options.live) {
267
+ this.getData()
268
+ }
269
+ }.bind(this), 10000);
270
+ }.bind(this)
271
+ }
272
+ },
273
+ title: {
274
+ text: title,
275
+ style: {
276
+ fontSize: '20px',
277
+ fontWeight: 'bold',
278
+ color: "#333333"
279
+ }
280
+ },
281
+ xAxis: {
282
+ type: 'datetime',
283
+ labels: {
284
+ y: 20,
285
+ formatter: function() {
286
+ var d = new Date(this.value * 1000)
287
+ return d.format("%H:%M")
288
+ }
289
+ },
290
+ title: {
291
+ text: null
292
+ }
293
+ },
294
+ yAxis: {
295
+ title: {
296
+ text: ytitle
297
+ },
298
+ maxPadding: 0,
299
+ plotLines: [{
300
+ width: 0.5,
301
+ }],
302
+ labels: {
303
+ formatter: function() {
304
+ var places = max < 10 ? 2 : 0
305
+ return formatValue(this.value, places)
306
+ }
307
+ }
308
+ },
309
+ plotOptions: {
310
+ series: {
311
+ stacking: 'normal',
312
+ marker: {
313
+ enabled: false
314
+ },
315
+ states: {
316
+ hover: {
317
+ enabled: true,
318
+ marker: {
319
+ symbol: 'triangle'
320
+ }
321
+ }
322
+ }
323
+ }
324
+ },
325
+ tooltip: {
326
+ formatter: function() {
327
+ var tip;
328
+ tip = '<b>' + formatSeriesLabel(this.series.name).trim() + '</b>-> '
329
+ tip += formatValue(this.y, 2) + ' <br/>'
330
+ tip += formatDate(this.x)
331
+
332
+ return tip
333
+ }
334
+ },
335
+ legend: {
336
+ layout: 'vertical',
337
+ align: 'right',
338
+ verticalAlign: 'top',
339
+ x: -10,
340
+ y: 60,
341
+ borderWidth: 0,
342
+ itemWidth: 186,
343
+ labelFormatter: function() {
344
+ return formatSeriesLabel(this.name)
345
+ },
346
+ itemStyle: {
347
+ cursor: 'pointer',
348
+ color: '#333333'
349
+ },
350
+ itemHoverStyle: {
351
+ color: '#777777'
245
352
  }
246
- });
247
- this.embedderContainer.grab(pre);
248
-
249
- var slider = new Fx.Slide(pre, {
250
- duration: 200
251
- });
252
353
 
253
- slider.hide();
354
+ },
355
+ series: series,
356
+ credits: {
357
+ enabled: false
358
+ }
359
+ });
254
360
 
255
- var toggler = new Element('a', {
256
- 'id': 'toggler',
257
- 'class': 'toggler',
258
- 'html': '(embed)',
259
- 'href': '#',
260
- 'styles': {
261
- 'font-size': '0.7em',
262
- }
263
- });
264
- toggler.addEvent('click', function(e) {
265
- e.stop();
266
- slider.toggle();
267
- });
268
- this.embedderTogglerContainer.grab(toggler);
269
- },
270
- embedCode: function() {
271
- baseurl = "{protocol}//{host}".substitute({'host': window.location.host, 'protocol': window.location.protocol});
272
- code = "<script src='{baseurl}/javascripts/visage.js' type='text/javascript'></script>".substitute({'baseurl': baseurl});
273
- code += "<div id='graph'></div>"
274
- 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});
275
- code += "{"
276
- code += "width: 900, height: 220, gridWidth: 800, gridHeight: 200, baseurl: '{baseurl}'".substitute({'baseurl': baseurl});
277
- code += "}); });</script>"
278
- return code.replace('<', '&lt;').replace('>', '&gt;')
361
+ this.buildDateSelector();
279
362
  },
280
- addDebugInterface: function() {
281
- var graph = this.graph;
363
+ buildDateSelector: function() {
282
364
  /*
283
- graph.hoverColumn(function () {
284
- });
285
- */
286
- },
287
- addSelectionInterface: function() {
288
- var graph = this.graph;
289
- var parentElement = this.parentElement
290
- var gridHeight = this.options.gridHeight
291
- graph.selectionMade = true
292
- this.graph.clickColumn(function () {
293
- if ($chk(graph.selectionMade) && graph.selectionMade) {
294
- if ($defined(graph.selection)) {
295
- graph.selection.remove();
296
- }
297
- graph.selectionMade = false
298
- graph.selection = this.paper.rect(this.x, 0, 1, gridHeight);
299
- graph.selection.toBack();
300
- graph.selection.attr({fill: '#555', stroke: '#555', opacity: 0.4});
301
- graph.selectionStart = this.axis.toInt()
302
- } else {
303
- graph.selectionMade = true
304
- graph.selectionFinish = this.axis.toInt()
305
- var select = $(parentElement).getElement('div.timescale.container select')
306
- var hasSelected = select.getChildren('option').some(function(option) {
307
- return option.get('html') == 'selected'
308
- });
309
- if (!hasSelected) {
310
- var option = new Element('option', {
311
- html: 'selected',
312
- value: '',
313
- selected: true
365
+ * container
366
+ * \
367
+ * - form
368
+ * \
369
+ * - select
370
+ * | \
371
+ * | - option
372
+ * | |
373
+ * | - option
374
+ * |
375
+ * - submit
376
+ */
377
+ var currentDate = new Date;
378
+ var currentUnixTime = parseInt(currentDate.getTime() / 1000);
379
+
380
+ var container = $(this.parentElement);
381
+ var form = new Element('form', {
382
+ 'method': 'get',
383
+ 'events': {
384
+ 'submit': function(e, foo) {
385
+ e.stop();
386
+ e.target.getElement('select').getSelected().each(function(option) {
387
+ data = new Hash()
388
+ split = option.value.split('=')
389
+ data.set(split[0], split[1])
314
390
  });
315
- select.grab(option)
316
- }
317
- }
318
- });
319
- this.graph.hoverColumn(function () {
320
- if ($chk(graph.selection) && !graph.selectionMade) {
321
- var width = this.x - graph.selection.attr('x');
322
- graph.selection.attr({'width': width});
391
+ this.requestData = data
392
+
393
+ /* Draw everything again. */
394
+ this.getData();
395
+ }.bind(this)
323
396
  }
324
397
  });
325
398
 
326
- },
327
- buildContainers: function() {
328
- this.embedderTogglerContainer = new Element('div', {
329
- 'class': 'embedder-toggler container',
330
- 'styles': {
331
- 'float': 'right',
332
- 'width': '20%',
333
- 'text-align': 'right',
334
- 'margin-right': '12px',
335
- 'padding-top': '4px'
336
- }
399
+ var select = new Element('select', { 'class': 'date timescale' });
400
+ var timescales = new Hash({ 'hour': 1, '2 hours': 2, '6 hours': 6, '12 hours': 12,
401
+ 'day': 24, '2 days': 48, '3 days': 72,
402
+ 'week': 168, '2 weeks': 336, 'month': 672 });
403
+ timescales.each(function(hour, label) {
404
+ var current = this.currentTimePeriod == 'last {label}'.substitute({'label': label });
405
+ var value = "start={start}".substitute({'start': currentUnixTime - (hour * 3600)});
406
+ var html = 'last {label}'.substitute({'label': label });
407
+
408
+ var option = new Element('option', {
409
+ html: html,
410
+ value: value,
411
+ selected: (current ? 'selected' : '')
412
+
413
+ });
414
+ select.grab(option)
337
415
  });
338
- $(this.parentElement).grab(this.embedderTogglerContainer, 'top')
339
416
 
340
- this.timescaleContainer = new Element('div', {
341
- 'class': 'timescale container',
417
+ var submit = new Element('input', { 'type': 'submit', 'value': 'show' });
418
+
419
+ var liveToggler = new Element('input', {
420
+ 'type': 'checkbox',
421
+ 'id': this.parentElement + '-live',
422
+ 'name': 'live',
423
+ 'events': {
424
+ 'click': function() {
425
+ this.options.live = !this.options.live
426
+ }.bind(this)
427
+ },
342
428
  'styles': {
343
- 'float': 'right',
344
- 'width': '20%'
429
+ 'margin-left': '4px',
430
+ 'cursor': 'pointer'
345
431
  }
346
432
  });
347
- $(this.parentElement).grab(this.timescaleContainer, 'top')
348
433
 
349
- this.labelsContainer = new Element('div', {
350
- 'class': 'labels container',
351
- 'title': 'click to hide',
434
+ var liveLabel = new Element('label', {
435
+ 'for': this.parentElement + '-live',
436
+ 'html': 'Live',
352
437
  'styles': {
353
- 'float': 'left',
354
- 'margin-left': '80px',
355
- 'padding-bottom': '1em'
438
+ 'font-family': 'sans-serif',
439
+ 'font-size': '11px',
440
+ 'margin-left': '8px',
441
+ 'cursor': 'pointer'
356
442
  }
357
443
  });
358
- $(this.parentElement).grab(this.labelsContainer)
359
444
 
360
- this.embedderContainer = new Element('div', {
361
- 'class': 'embedder container',
445
+ var exportLink = new Element('a', {
446
+ 'href': this.dataURL(),
447
+ 'html': 'Export data',
362
448
  'styles': {
363
- 'font-style': 'monospace',
364
- 'margin-left': '80px',
365
- 'font-size': '0.8em',
366
- 'clear': 'both'
367
- }
368
- });
369
- $(this.parentElement).grab(this.embedderContainer)
370
- },
371
- buildDateSelector: function() {
372
- /*
373
- * container
374
- * \
375
- * - form
376
- * \
377
- * - select
378
- * | \
379
- * | - option
380
- * | |
381
- * | - option
382
- * |
383
- * - submit
384
- */
385
- var currentDate = new Date;
386
- var currentUnixTime = parseInt(currentDate.getTime() / 1000);
387
-
388
- var container = $(this.timescaleContainer);
389
- var form = new Element('form', {
390
- 'method': 'get',
391
- 'events': {
392
- 'submit': function(e, foo) {
393
- e.stop();
394
-
395
- /*
396
- * Get the selected option, turn it into a hash for
397
- * getData() to use.
398
- */
399
- data = new Hash()
400
- if (e.target.getElement('select').getSelected().get('html') == 'selected') {
401
- data.set('start', this.graph.selectionStart);
402
- data.set('finish', this.graph.selectionFinish);
403
- } else {
404
- e.target.getElement('select').getSelected().each(function(option) {
405
- split = option.value.split('=')
406
- data.set(split[0], split[1])
407
- currentTimePeriod = option.get('html') // is this setting a global?
408
- }, this);
409
- }
410
- this.requestData = data
411
-
412
- /* Nuke graph + labels. */
413
- this.graph.remove();
414
- delete this.x;
415
- $(this.labelsContainer).empty();
416
- $(this.timescaleContainer).empty();
417
- $(this.embedderContainer).empty();
418
- $(this.embedderTogglerContainer).empty();
419
- if ($defined(this.graph.selection)) {
420
- this.graph.selection.remove();
421
- }
422
- /* Draw everything again. */
423
- this.getData();
424
- }.bind(this)
449
+ 'font-family': 'sans-serif',
450
+ 'font-size': '11px',
451
+ 'margin-left': '8px',
452
+ },
453
+ 'events': {
454
+ 'mouseover': function(e) {
455
+ var url = e.target.get('href')
456
+ var options = this.requestData.toQueryString()
457
+
458
+ if ( options != '' && ! url.contains('?') ) {
459
+ url += '?' + options
425
460
  }
426
- });
427
461
 
428
- var select = new Element('select', { 'class': 'date timescale' });
429
- var timescales = new Hash({ 'hour': 1, '2 hours': 2, '6 hours': 6, '12 hours': 12,
430
- 'day': 24, '2 days': 48, '3 days': 72,
431
- 'week': 168, '2 weeks': 336, 'month': 672 });
432
- timescales.each(function(hour, label) {
433
- var current = this.currentTimePeriod == 'last {label}'.substitute({'label': label });
434
- var value = "start={start}".substitute({'start': currentUnixTime - (hour * 3600)});
435
- var html = 'last {label}'.substitute({'label': label });
436
-
437
- var option = new Element('option', {
438
- html: html,
439
- value: value,
440
- selected: (current ? 'selected' : '')
441
-
442
- });
443
- select.grab(option)
444
- });
445
-
446
- var submit = new Element('input', { 'type': 'submit', 'value': 'show' });
462
+ e.target.set('href', url)
463
+ }.bind(this)
464
+ }
465
+ });
447
466
 
448
- form.grab(select);
449
- form.grab(submit);
450
- container.grab(form);
467
+ form.grab(select)
468
+ form.grab(submit)
469
+ form.grab(liveToggler)
470
+ form.grab(liveLabel)
471
+ form.grab(exportLink)
472
+ container.grab(form, 'top')
451
473
  },
452
- buildLabels: function() {
453
- //buildLabels: function(graphLines, instanceNames, dataSources, colors) {
454
-
455
- this.ys.each(function(set, index) {
456
- var path = this.graph.lines[index],
457
- color = this.colors[index]
458
- plugin = this.options.plugin
459
- instance = this.instances[index]
460
- metric = this.metrics[index]
461
-
462
- var container = new Element('div', {
463
- 'class': 'label plugin',
464
- 'styles': {
465
- 'padding': '0.2em 0.5em 0',
466
- 'float': 'left',
467
- 'width': '180px',
468
- 'font-size': '0.8em'
469
- },
470
- 'events': {
471
- 'mouseover': function(e) {
472
- e.stop();
473
- path.animate({'stroke-width': 3}, 300);
474
- //path.toFront();
475
- },
476
- 'mouseout': function(e) {
477
- e.stop();
478
- path.animate({'stroke-width': 1.5}, 300);
479
- //path.toBack();
480
- },
481
- 'click': function(e) {
482
- e.stop();
483
- path.attr('opacity') == 0 ? path.animate({'opacity': 1}, 350) : path.animate({'opacity': 0}, 350);
484
- }
485
- }
486
- });
487
-
488
- var box = new Element('div', {
489
- 'class': 'label plugin box ' + metric,
490
- 'html': '&nbsp;',
491
- 'styles': {
492
- 'background-color': color,
493
- 'width': '48px',
494
- 'height': '18px',
495
- 'float': 'left',
496
- 'margin-right': '0.5em'
497
- }
498
- });
499
-
500
- // plugin/instance/metrics names can be unmeaningful. make them pretty
501
- var name;
502
- name = instance.replace(plugin, '');
503
- name = name.replace('tcp_connections', '')
504
- name = name.replace('ps_state', '')
505
- name = name.replace(plugin.split('-')[0], '')
506
- name += metric == "value" ? "" : " (" + metric + ")"
507
- name = name.replace(/^[-|_]*/, '')
508
-
509
- var desc = new Element('span', {
510
- 'class': 'label plugin description ' + metric,
511
- 'html': name
512
- });
513
-
514
- container.grab(box);
515
- container.grab(desc);
516
- $(this.labelsContainer).grab(container);
517
-
518
- }, this);
519
- }
520
- })
521
474
 
522
- var visageSparkline = new Class({
523
- Extends: visageGraph,
524
- options: {
525
- width: 450,
526
- height: 80,
527
- leftEdge: 1,
528
- topEdge: 1,
529
- gridWidth: 449,
530
- gridHeight: 79,
531
- columns: 60,
532
- rows: 8,
533
- gridBorderColour: '#ccc',
534
- shade: false,
535
- secureJSON: false,
536
- httpMethod: 'get',
537
- axis: "0 0 0 0"
538
- },
539
- graphData: function(data) {
540
475
 
541
- this.ys = []
542
- this.colors = []
543
- this.instances = []
544
- this.metrics = []
545
476
 
546
- var host = this.options.host
547
- var plugin = this.options.plugin
477
+ });
548
478
 
549
- $each(data[host][plugin], function(instance, iname) {
550
- $each(instance, function(metric, mname) {
551
- this.colors.push(metric.color)
552
- if ( !$defined(this.x) ) {
553
- this.x = this.buildXAxis(metric)
554
- }
555
- this.ys.push(metric.data)
556
- this.instances.push(iname) // labels
557
- this.metrics.push(mname) // labels
558
- }, this);
559
- }, this);
479
+ // buildEmbedder: function() {
480
+ // var pre = new Element('textarea', {
481
+ // 'id': 'embedder',
482
+ // 'class': 'embedder',
483
+ // 'html': this.embedCode(),
484
+ // 'styles': {
485
+ // 'width': '500px',
486
+ // 'padding': '3px'
487
+ // }
488
+ // });
489
+ // this.embedderContainer.grab(pre);
490
+ //
491
+ // var slider = new Fx.Slide(pre, {
492
+ // duration: 200
493
+ // });
494
+ //
495
+ // slider.hide();
496
+ //
497
+ // var toggler = new Element('a', {
498
+ // 'id': 'toggler',
499
+ // 'class': 'toggler',
500
+ // 'html': '(embed)',
501
+ // 'href': '#',
502
+ // 'styles': {
503
+ // 'font-size': '0.7em',
504
+ // }
505
+ // });
506
+ // toggler.addEvent('click', function(e) {
507
+ // e.stop();
508
+ // slider.toggle();
509
+ // });
510
+ // this.embedderTogglerContainer.grab(toggler);
511
+ // },
512
+ // embedCode: function() {
513
+ // baseurl = "{protocol}//{host}".substitute({'host': window.location.host, 'protocol': window.location.protocol});
514
+ // code = "<script src='{baseurl}/javascripts/visage.js' type='text/javascript'></script>".substitute({'baseurl': baseurl});
515
+ // code += "<div id='graph'></div>"
516
+ // 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});
517
+ // code += "{"
518
+ // code += "width: 900, height: 220, gridWidth: 800, gridHeight: 200, baseurl: '{baseurl}'".substitute({'baseurl': baseurl});
519
+ // code += "}); });</script>"
520
+ // return code.replace('<', '&lt;').replace('>', '&gt;')
521
+ // },
560
522
 
561
- this.drawGraph();
562
- }
563
- });