riemann-dash 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
+ }