riemann-dash 0.2.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +7 -0
  3. data/Gemfile.lock +52 -0
  4. data/README.markdown +29 -5
  5. data/Rakefile.rb +11 -0
  6. data/bin/riemann-dash +2 -2
  7. data/example/config.rb +17 -0
  8. data/lib/riemann/dash/app.rb +32 -0
  9. data/lib/riemann/dash/config.rb +154 -0
  10. data/lib/riemann/dash/controller/css.rb +1 -1
  11. data/lib/riemann/dash/controller/index.rb +6 -36
  12. data/lib/riemann/dash/public/dash.js +44 -18
  13. data/lib/riemann/dash/public/format.js +3 -3
  14. data/lib/riemann/dash/public/persistence.js +2 -2
  15. data/lib/riemann/dash/public/subs.js +63 -48
  16. data/lib/riemann/dash/public/util.js +37 -44
  17. data/lib/riemann/dash/public/vendor/backbone.js +1571 -0
  18. data/lib/riemann/dash/public/vendor/jquery/jquery-1.9.1.min.js +5 -0
  19. data/lib/riemann/dash/public/{jquery-ui-1.9.0.custom.min.js → vendor/jquery/jquery-ui-1.9.0.custom.min.js} +0 -0
  20. data/lib/riemann/dash/public/{jquery.quickfit.js → vendor/jquery/jquery.quickfit.js} +0 -0
  21. data/lib/riemann/dash/public/vendor/jquery/jquery.simplemodal.1.4.4.min.js +26 -0
  22. data/lib/riemann/dash/public/vendor/lodash.min.js +40 -0
  23. data/lib/riemann/dash/public/vendor/smoothie.js +376 -0
  24. data/lib/riemann/dash/public/{toastr.css → vendor/toastr/toastr.css} +1 -1
  25. data/lib/riemann/dash/public/{toastr.js → vendor/toastr/toastr.js} +0 -0
  26. data/lib/riemann/dash/public/views/gauge.js +8 -5
  27. data/lib/riemann/dash/public/views/grid.js +138 -67
  28. data/lib/riemann/dash/public/views/timeseries.js +230 -0
  29. data/lib/riemann/dash/public/views/title.js +6 -3
  30. data/lib/riemann/dash/version.rb +2 -2
  31. data/lib/riemann/dash/views/css.scss +52 -2
  32. data/lib/riemann/dash/views/index.erubis +38 -192
  33. data/lib/riemann/dash.rb +3 -97
  34. data/riemann-dash.gemspec +28 -0
  35. data/sh/c +1 -0
  36. data/sh/env.rb +2 -0
  37. data/sh/test +1 -0
  38. data/test/config_test.rb +114 -0
  39. data/test/fixtures/config/basic_config.rb +2 -0
  40. data/test/fixtures/config/ws_config.rb +1 -0
  41. data/test/fixtures/ws_config/dummy_config.json +1 -0
  42. data/test/fixtures/ws_config/pretty_printed_config.json +6 -0
  43. data/test/test_helper.rb +10 -0
  44. metadata +43 -18
  45. data/lib/riemann/dash/public/jquery-1.7.2.min.js +0 -4
  46. data/lib/riemann/dash/public/jquery.json-2.2.min.js +0 -31
  47. data/lib/riemann/dash/public/jquery.simplemodal.1.4.3.min.js +0 -26
  48. data/lib/riemann/dash/public/mustache.js +0 -597
  49. data/lib/riemann/dash/public/underscore-min.js +0 -5
@@ -6,6 +6,7 @@
6
6
  this.query = json.query;
7
7
  this.title = json.title;
8
8
  this.max = json.max || "all";
9
+ this.by = json.by || "host";
9
10
  this.clickFocusable = true;
10
11
 
11
12
  // Initial display
@@ -18,8 +19,12 @@
18
19
  this.el.find('h2').text(this.title);
19
20
 
20
21
  // State
22
+ this.columns = [];
23
+ this.rows = [];
24
+
21
25
  this.hosts = [];
22
26
  this.services = [];
27
+
23
28
  this.events = {};
24
29
  if (this.max === "service" || this.max === "host") {
25
30
  this.currentMax = {};
@@ -44,26 +49,33 @@
44
49
  title: this.title,
45
50
  query: this.query,
46
51
  max: this.max,
52
+ by: this.by
47
53
  });
48
54
  }
55
+
56
+ var editTemplate = _.template(
57
+ "<label for='title'>Title</label>" +
58
+ "<input type='text' name='title' value='{{title}}' /><br />" +
59
+ "<label for='query'>Query</label><br />" +
60
+ "<textarea name='query' class='query'>{{query}}</textarea><br />" +
61
+ "<label for='by'>group by</label>" +
62
+ "<input type='text' name='by' value='{{by}}' /><br />" +
63
+ "<span class='desc'>'host' or 'service'</span><br />" +
64
+ "<label for='max'>Max</label>" +
65
+ "<input type='text' name='max' value='{{max}}' /><br />" +
66
+ "<span class='desc'>'all', 'host', 'service', or any number.</span>"
67
+ )
49
68
 
50
69
  Grid.prototype.editForm = function() {
51
- return Mustache.render('<label for="title">Title</label>' +
52
- '<input type="text" name="title" value="{{title}}" /><br />' +
53
- '<label for="query">Query</label><br />' +
54
- '<textarea name="query" class="query">{{query}}</textarea><br />' +
55
- '<label for="max">Max</label>' +
56
- '<input type="text" name="max" value="{{max}}" /><br />' +
57
- '<span class="desc">"all", "host", "service", or any number.</span>',
58
- this)
70
+ return editTemplate(this);
59
71
  }
60
72
 
61
73
  // Returns all events, flat.
62
74
  Grid.prototype.allEvents = function() {
63
75
  var events = [];
64
- for (host in this.events) {
65
- for (service in this.events[host]) {
66
- events.push(this.events[host][service]);
76
+ for (row in this.events) {
77
+ for (column in this.events[row]) {
78
+ events.push(this.events[row][column]);
67
79
  }
68
80
  }
69
81
  }
@@ -96,9 +108,16 @@
96
108
  event.description);
97
109
 
98
110
  // Bar chart
99
- if (event.metric) {
111
+ if (event.metric == 0) {
112
+ // Zero
113
+ element.find('.bar').css('width', 0);
114
+ } else if (0 < event.metric) {
115
+ // Positive
100
116
  element.find('.bar').css('width',
101
117
  (event.metric / this.eventMax(event) * 100) + "%");
118
+ } else {
119
+ // Nil or negative
120
+ element.find('.bar').css('width', 0);
102
121
  }
103
122
  }
104
123
 
@@ -106,10 +125,15 @@
106
125
  // Render a single event if there's been no change to table structure.
107
126
  Grid.prototype.partialRender = function(event) {
108
127
  var table = this.el.find('table');
109
- var hostIndex = this.hosts.indexOf(event.host);
110
- var serviceIndex = this.services.indexOf(event.service);
111
- var row = this.el.find('tbody tr')[hostIndex];
112
- var td = $($(row).find('td')[serviceIndex]);
128
+ if (this.by === "host") {
129
+ var rowIndex = this.rows.indexOf(event.host);
130
+ var columnIndex = this.columns.indexOf(event.service);
131
+ } else {
132
+ var rowIndex = this.rows.indexOf(event.service);
133
+ var columnIndex = this.columns.indexOf(event.host)
134
+ }
135
+ var row = this.el.find('tbody tr')[rowIndex];
136
+ var td = $($(row).find('td')[columnIndex]);
113
137
 
114
138
  this.renderElement(td, event);
115
139
  }
@@ -122,18 +146,18 @@
122
146
  // Header
123
147
  table.append("<thead><tr><th></th></tr></thead>");
124
148
  var row = table.find("thead tr");
125
- this.services.forEach(function(service) {
149
+ this.columns.forEach(function(name) {
126
150
  var element = $('<th>');
127
- element.text(service);
151
+ element.text(name);
128
152
  row.append(element);
129
153
  });
130
154
 
131
- this.hosts.forEach(function(host) {
155
+ this.rows.forEach(function(name) {
132
156
  row = $("<tr><th></th>");
133
157
  table.append(row);
134
- row.find('th').text(host);
135
- this.services.forEach(function(service) {
136
- var event = this.events[host][service];
158
+ row.find('th').text(name);
159
+ this.columns.forEach(function(subName) {
160
+ var event = this.events[name][subName];
137
161
  var element = $('<td><span class="bar"><span class="metric"/></span></td>');
138
162
  this.renderElement(element, event);
139
163
  row.append(element);
@@ -151,30 +175,53 @@
151
175
  }
152
176
 
153
177
  var e;
178
+
154
179
  if (this.max === "all") {
155
180
  this.currentMax = -1/0;
156
- for (host in this.events) {
157
- for (service in this.events[host]) {
158
- e = this.events[host][service];
181
+ for (name in this.events) {
182
+ for (subName in this.events[name]) {
183
+ e = this.events[name][subName];
159
184
  this.currentMax = Math.max(e.metric, this.currentMax || -1/0);
160
185
  }
161
186
  }
162
- } else if (this.max === "host" ) {
163
- this.currentMax = {};
164
- for (host in this.events) {
165
- for (service in this.events[host]) {
166
- e = this.events[host][service];
167
- this.currentMax[e.host] =
168
- Math.max(e.metric, this.currentMax[e.host] || -1/0)
187
+ } else if (this.by == "host") {
188
+ if (this.max == "host") {
189
+ this.currentMax = {};
190
+ for (host in this.events) {
191
+ for (service in this.events[host]) {
192
+ e = this.events[host][service];
193
+ this.currentMax[e.host] =
194
+ Math.max(e.metric, this.currentMax[e.host] || -1/0)
195
+ }
196
+ }
197
+ } else if (this.max === "service") {
198
+ this.currentMax = {};
199
+ for (host in this.events) {
200
+ for (service in this.events[host]) {
201
+ e = this.events[host][service];
202
+ this.currentMax[e.service] =
203
+ Math.max(e.metric, this.currentMax[e.service] || -1/0);
204
+ }
169
205
  }
170
206
  }
171
- } else if (this.max === "service") {
172
- this.currentMax = {};
173
- for (host in this.events) {
174
- for (service in this.events[host]) {
175
- e = this.events[host][service];
176
- this.currentMax[e.service] =
177
- Math.max(e.metric, this.currentMax[e.service] || -1/0);
207
+ } else if (this.by === "service") {
208
+ if (this.max == "host") {
209
+ this.currentMax = {};
210
+ for (service in this.events) {
211
+ for (host in this.events[service]) {
212
+ e = this.events[service][host];
213
+ this.currentMax[e.host] =
214
+ Math.max(e.metric, this.currentMax[e.host] || -1/0)
215
+ }
216
+ }
217
+ } else if (this.max === "service") {
218
+ this.currentMax = {};
219
+ for (service in this.events) {
220
+ for (host in this.events[service]) {
221
+ e = this.events[service][host];
222
+ this.currentMax[e.service] =
223
+ Math.max(e.metric, this.currentMax[e.service] || -1/0);
224
+ }
178
225
  }
179
226
  }
180
227
  } else {
@@ -188,34 +235,58 @@
188
235
  // Stores an event in the internal state tables. Returns true if we
189
236
  // haven't seen this host/service before.
190
237
  Grid.prototype.saveEvent = function(e) {
191
- // Host list
192
- if (this.hosts.indexOf(e.host) === -1) {
193
- this.hosts.push(e.host);
194
- this.hosts = _.uniq(this.hosts.sort(), true);
195
- }
238
+ if (this.by === "host") {
239
+ // Host list
240
+ if (this.rows.indexOf(e.host) === -1) {
241
+ this.rows.push(e.host);
242
+ this.rows = _.uniq(this.rows.sort(), true);
243
+ }
196
244
 
197
- // Services list
198
- if (this.services.indexOf(e.service) === -1) {
199
- this.services.push(e.service);
200
- this.services = _.uniq(this.services.sort(), true);
201
- }
245
+ // Services list
246
+ if (this.columns.indexOf(e.service) === -1) {
247
+ this.columns.push(e.service);
248
+ this.columns = _.uniq(this.columns.sort(), true);
249
+ }
202
250
 
203
- // Events map
204
- if (this.events[e.host] === undefined) {
205
- // New host
206
- this.events[e.host] = {};
207
- }
208
- if (this.events[e.host][e.service] === undefined) {
209
- // New event
210
- var newEvent = true;
211
- } else {
212
- var newEvent = false;
213
- }
251
+ // Events map
252
+ if (this.events[e.host] === undefined) {
253
+ // New host
254
+ this.events[e.host] = {};
255
+ }
256
+ var newEvent = (this.events[e.host][e.service] === undefined);
257
+
258
+ // Store event
259
+ this.events[e.host][e.service] = e;
260
+
261
+ return newEvent;
262
+
263
+ } else if (this.by === "service") {
264
+ // Services list
265
+ if (this.rows.indexOf(e.service) === -1) {
266
+ this.rows.push(e.service);
267
+ this.rows = _.uniq(this.rows.sort(), true);
268
+ }
214
269
 
215
- // Store event
216
- this.events[e.host][e.service] = e;
270
+ // Host list
271
+ if (this.columns.indexOf(e.host) === -1) {
272
+ this.columns.push(e.host);
273
+ this.columns = _.uniq(this.columns.sort(), true);
274
+ }
275
+
276
+ // Events map
277
+ if (this.events[e.service] === undefined) {
278
+ // New host
279
+ this.events[e.service] = {};
280
+ }
281
+ var newEvent = (this.events[e.service][e.host] === undefined);
282
+
283
+ // Store event
284
+ this.events[e.service][e.host] = e;
285
+
286
+ return newEvent;
287
+
288
+ }
217
289
 
218
- return newEvent;
219
290
  }
220
291
 
221
292
  // Add an event.
@@ -263,10 +334,10 @@
263
334
  }
264
335
 
265
336
  Grid.prototype.reflow = function() {
266
- // this.el.find('table').height(
267
- // this.height() -
268
- // this.el.find('h2').height()
269
- // );
337
+ // this.el.find('table').height(
338
+ // this.height() -
339
+ // this.el.find('h2').height()
340
+ // );
270
341
  }
271
342
 
272
343
  Grid.prototype.delete = function() {
@@ -0,0 +1,230 @@
1
+ (function() {
2
+ var fitopts = {min: 6, max: 1000};
3
+
4
+ var nameFor = function(event) {
5
+ return event.host ? event.host + ":" + event.service : event.service
6
+ return event.host + ":" + event.service;
7
+ };
8
+
9
+ var hextoRGB = function(hex) { // converts hex string to rgb triple
10
+ // only works for full 6 char hex, not shorthand
11
+ if (hex[0]=="#") hex=hex.substr(1);
12
+ var hexp = "([a-f0-9]{2})";
13
+ var colorGroups = RegExp('^'+hexp+hexp+hexp+'$', 'i').exec(hex).slice(1);
14
+ return _.map(colorGroups, function(color) { return parseInt(color, 16) });
15
+ }
16
+
17
+ var rateLimit = 500; // ms
18
+
19
+ var TimeSeriesView = function(json) {
20
+
21
+ var self = this;
22
+
23
+ self.smoothie = new SmoothieChart({
24
+ grid: {strokeStyle:'#ccc', fillStyle:'rgba(255, 255, 255, 0.0)', lineWidth: 1, millisPerLine: 1000},
25
+ labels: { fillStyle:'#262626' }
26
+ });
27
+
28
+ // colors
29
+ var colorTemplate = _.template("rgba({{red}},{{green}},{{blue}},{{alpha}})");
30
+
31
+ var colorPallette = function() {
32
+ // should be customizable in future...
33
+ // default color pallette borrowed from http://code.shutterstock.com/rickshaw/examples/colors.html
34
+ // Array.prototype.reduce.call(svg.childNodes, function(accum, path) { accum.push(path.getAttribute("fill")); return accum; }, [])
35
+ var DEFAULT = ["#57306f", "#514c76", "#646583", "#738394",
36
+ "#6b9c7d", "#84b665", "#a7ca50", "#bfe746",
37
+ "#e2f528", "#fff726", "#ecdd00", "#d4b11d",
38
+ "#de8800", "#de4800", "#c91515", "#9a0000",
39
+ "#7b0429", "#580839", "#31082b"];
40
+ return DEFAULT;
41
+ };
42
+ this.pallette = colorPallette();
43
+
44
+ var takeColor = function() {
45
+ // pops a color off the pallette stack, or regenerates the
46
+ // stack if we're out of colors
47
+ var color = self.pallette.shift();
48
+ if (color) {
49
+ return color;
50
+ } else {
51
+ self.pallette = colorPallette();
52
+ return self.pallette.shift();
53
+ }
54
+ }
55
+
56
+ // map event names to colors
57
+ var colorMap = {};
58
+
59
+ var colorFromString = function(s) {
60
+ // caches a color in the color table
61
+ var color = colorMap[s];
62
+ if (color) return color;
63
+ color = colorMap[s] = takeColor()
64
+ return color;
65
+ }
66
+
67
+ var RGBfromString = function(s) {
68
+ // returns an RGB triple from a string
69
+ return hextoRGB(colorFromString(s));
70
+ };
71
+
72
+ var rgbaFromTriple = function(rgb, alpha) {
73
+ return colorTemplate({
74
+ red: rgb[0],
75
+ green: rgb[1],
76
+ blue: rgb[2],
77
+ alpha: alpha
78
+ });
79
+ }
80
+
81
+ // smoothiecharts timeseries
82
+ var seriesCollection = {};
83
+
84
+ // throttle appends to graph
85
+ var appendEvent = function(series, event) {
86
+ series.append(event);
87
+ return series.append(new Date(event.time).getTime(), format.float(event.metric));
88
+ };
89
+
90
+ var createTimeSeries = function(name, event) {
91
+ var seriesColor = RGBfromString(name),
92
+ series = new TimeSeries(),
93
+ color = rgbaFromTriple(seriesColor, 1),
94
+ seriesOpts = {lineWidth: self.lineWidth || 2,
95
+ strokeStyle: color,
96
+ fillStyle: rgbaFromTriple(seriesColor, self.opacity || 0)};
97
+
98
+ series.appendEvent = _.throttle(function(event) {
99
+ series.append(new Date(event.time).getTime(), format.float(event.metric));
100
+ }, rateLimit);;
101
+
102
+ self.smoothie.addTimeSeries(series, seriesOpts);
103
+ return series;
104
+ };
105
+
106
+ // stream data into series
107
+ var intoSeries = function(event) {
108
+ var seriesName = nameFor(event)
109
+ var cachedSeries = seriesCollection[seriesName];
110
+
111
+ if (cachedSeries) {
112
+ cachedSeries.appendEvent(event);
113
+ } else {
114
+ var newSeries = seriesCollection[seriesName] = createTimeSeries(seriesName, event);
115
+ newSeries.appendEvent(event);
116
+ };
117
+ };
118
+
119
+ var legendCollection = {};
120
+
121
+ var updateLegend = function(event) {
122
+ var eventName = nameFor(event),
123
+ cachedEl = legendCollection[eventName];
124
+
125
+ if (cachedEl) {
126
+ cachedEl.text(eventName + ": " + format.float(event.metric))
127
+ } else {
128
+ var $el = $("<div></div>");
129
+ var color = rgbaFromTriple(RGBfromString(eventName), 0.7);
130
+ $el.addClass('event-legend').css({"background-color": color});
131
+ $el.text = _.throttle($el.text, rateLimit);
132
+ setTimeout(function() {
133
+ if ($el) {
134
+ $el.text(eventName + ": " + format.float(event.metric));
135
+ }
136
+ }, +self.delay || 0);
137
+ legendCollection[eventName] = $el;
138
+ self.$legend.append($el)
139
+ }
140
+
141
+ }
142
+
143
+ view.View.call(this, json);
144
+ this.query = json.query;
145
+ this.title = json.title;
146
+ this.delay = json.delay;
147
+ this.opacity = json.opacity;
148
+ this.lineWidth = json.lineWidth;
149
+
150
+ this.clickFocusable = true;
151
+ this.el.append(
152
+ '<div class="time-series-container">' +
153
+ '<div class="legend"></div>' +
154
+ '<div class="title">' + this.title +
155
+ '</div>' +
156
+ '<canvas class="timeseries"></canvas>' +
157
+ '</div>'
158
+ );
159
+
160
+ this.$canvas = this.el.find(".timeseries");
161
+ this.$legend = this.el.find(".legend");
162
+ this.$titlecontainer = this.el.find("div.title");
163
+
164
+ this.$title = this.$titlecontainer.find("h2");
165
+ this.canvas = this.$canvas.get(0);
166
+
167
+
168
+ this.$legend = this.el.find(".legend");
169
+
170
+ this.reflow();
171
+
172
+ this.smoothie.streamTo(this.canvas, this.delay);
173
+
174
+ if (this.query) {
175
+ this.sub = subs.subscribe(this.query, function(event) {
176
+ updateLegend(event)
177
+ intoSeries(event)
178
+ });
179
+ }
180
+ }
181
+
182
+ view.inherit(view.View, TimeSeriesView);
183
+ view.TimeSeries = TimeSeriesView;
184
+ view.types.TimeSeries = TimeSeriesView;
185
+
186
+ TimeSeriesView.prototype.json = function() {
187
+ return $.extend(view.View.prototype.json.call(this), {
188
+ type: 'TimeSeries',
189
+ title: this.title,
190
+ delay: this.delay,
191
+ query: this.query,
192
+ opacity: this.opacity,
193
+ lineWidth: this.lineWidth,
194
+ });
195
+ }
196
+
197
+ var editTemplate = _.template(
198
+ '<label for="title">title</label>' +
199
+ '<input type="text" name="title" value="{{title}}" /><br />' +
200
+ '<label for="query">query</label>' +
201
+ '<textarea type="text" name="query">{{ query }}</textarea><br />' +
202
+ '<label for="lineWidth">line width</label>' +
203
+ '<input type="text" name="lineWidth" value="{{lineWidth}}" /><br />' +
204
+ '<label for="opacity">fill opacity</label>' +
205
+ '<input type="text" name="opacity" value="{{opacity}}" /><br />' +
206
+ '<label for="delay">delay (smoother animation)</label>' +
207
+ '<input type="text" name="delay" value="{{delay}}" />'
208
+ )
209
+
210
+ TimeSeriesView.prototype.editForm = function() {
211
+ return editTemplate(this);
212
+ }
213
+
214
+ TimeSeriesView.prototype.reflow = function() {
215
+ // Size metric
216
+ var width = this.el.width();
217
+ var height = this.el.height();
218
+ this.$canvas.attr("width", width - 10);
219
+ this.$canvas.attr("height", height - 10);
220
+
221
+ }
222
+
223
+ TimeSeriesView.prototype.delete = function() {
224
+ if (this.sub) {
225
+ subs.unsubscribe(this.sub);
226
+ }
227
+ view.View.prototype.delete.call(this);
228
+ }
229
+ })();
230
+
@@ -23,10 +23,13 @@
23
23
  });
24
24
  }
25
25
 
26
+ var editTemplate = _.template(
27
+ '<label for="title">Title</label>' +
28
+ '<input type="title" name="title" value="{{title}}" />'
29
+ );
30
+
26
31
  Title.prototype.editForm = function() {
27
- return Mustache.render('<label for="title">Title</label>' +
28
- '<input type="title" name="title" value="{{title}}" />',
29
- this);
32
+ return editTemplate(this);
30
33
  }
31
34
 
32
35
  Title.prototype.reflow = function() {
@@ -1,4 +1,4 @@
1
1
  module Riemann; end
2
- class Riemann::Dash
3
- VERSION = '0.2.1'
2
+ module Riemann::Dash
3
+ VERSION = '0.2.3'
4
4
  end
@@ -14,6 +14,13 @@ $grey: #888;
14
14
  $green: #06FA23;
15
15
  $yellow: #FEF206;
16
16
 
17
+ @mixin vendor-prefix($name, $argument) {
18
+ #{$name}: $argument;
19
+ -webkit-#{$name}: $argument;
20
+ -ms-#{$name}: $argument;
21
+ -moz-#{$name}: $argument;
22
+ }
23
+
17
24
  html {
18
25
  height: 100%;
19
26
  margin: 0;
@@ -153,7 +160,11 @@ html,table {
153
160
  border-radius: 3px;
154
161
  }
155
162
 
156
- .state.ok, .bar.ok {
163
+ .state {
164
+ @include vendor-prefix(transition, all 0.3s linear);
165
+ }
166
+
167
+ .state.ok, .state.okay, .bar.ok, .bar.okay {
157
168
  background: $green;
158
169
  color: #000;
159
170
  }
@@ -161,7 +172,7 @@ html,table {
161
172
  background: $amber;
162
173
  color: #000;
163
174
  }
164
- .state.critical, .bar.critical {
175
+ .state.critical, .state.failure, .bar.critical, .bar.failure {
165
176
  background: $orange;
166
177
  color: #000;
167
178
  }
@@ -380,3 +391,42 @@ h2 {
380
391
  width: 1px;
381
392
  }
382
393
  }
394
+
395
+ .time-series-container {
396
+ .title {
397
+ font-weight: 700;
398
+ font-size: 1.5em;
399
+ background-color: hsla(0, 0%, 100%, 0.8);
400
+ z-index: 10;
401
+ position: absolute;
402
+ bottom: -5px;
403
+ right: 10px;
404
+ padding: 2px;
405
+ display: block;
406
+ }
407
+ .legend {
408
+ height: 100%;
409
+ font-size: 16px;
410
+ text-shadow: hsl(0, 0%, 95%) 0px 0px 1px;
411
+ color: hsl(0, 0%, 5%);
412
+ position: absolute;
413
+ bottom: 0px;
414
+ display: -webkit-flex;
415
+ display: -moz-flex;
416
+ display: -ms-flex;
417
+ display: flex;
418
+ @include vendor-prefix(flex-flow, column wrap);
419
+ @include vendor-prefix(align-content, flex-start);
420
+ @include vendor-prefix(justify-content, flex-start);
421
+ @include vendor-prefix(align-items, stretch);
422
+ }
423
+ .event-legend {
424
+ padding: 4px;
425
+ @include vendor-prefix(flex, 1 1 auto);
426
+ @include vendor-prefix(transition, all 0.3s ease-out);
427
+ }
428
+ .event-legend:hover {
429
+ background-color: hsla(0, 0%, 100%, .8) !important;
430
+ }
431
+
432
+ }