visage-app 0.3.3 → 0.9.0.pre1

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