elasticsearch-paramedic-rack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +38 -0
  6. data/Rakefile +24 -0
  7. data/elasticsearch-paramedic-rack.gemspec +24 -0
  8. data/lib/elasticsearch-paramedic-rack.rb +10 -0
  9. data/lib/elasticsearch-paramedic-rack/middelware.rb +23 -0
  10. data/lib/elasticsearch-paramedic-rack/version.rb +7 -0
  11. data/public/elasticsearch-paramedic/audio/alert-green.mp3 +0 -0
  12. data/public/elasticsearch-paramedic/audio/alert-red.mp3 +0 -0
  13. data/public/elasticsearch-paramedic/audio/alert-yellow.mp3 +0 -0
  14. data/public/elasticsearch-paramedic/css/app.css +312 -0
  15. data/public/elasticsearch-paramedic/css/libs/cubism.css +57 -0
  16. data/public/elasticsearch-paramedic/css/libs/style.css +499 -0
  17. data/public/elasticsearch-paramedic/img/apple-touch-icon.png +0 -0
  18. data/public/elasticsearch-paramedic/img/glyphicons-halflings-white.png +0 -0
  19. data/public/elasticsearch-paramedic/img/glyphicons-halflings.png +0 -0
  20. data/public/elasticsearch-paramedic/img/spinner.gif +0 -0
  21. data/public/elasticsearch-paramedic/index.html +196 -0
  22. data/public/elasticsearch-paramedic/js/app.js +474 -0
  23. data/public/elasticsearch-paramedic/js/cubism.js +109 -0
  24. data/public/elasticsearch-paramedic/js/libs/colorbrewer.min.js +1 -0
  25. data/public/elasticsearch-paramedic/js/libs/cubism.elasticsearch.js +273 -0
  26. data/public/elasticsearch-paramedic/js/libs/cubism.v1.js +986 -0
  27. data/public/elasticsearch-paramedic/js/libs/cubism.v1.min.js +1 -0
  28. data/public/elasticsearch-paramedic/js/libs/d3.v2.min.js +4 -0
  29. data/public/elasticsearch-paramedic/js/libs/ember-0.9.8.js +20150 -0
  30. data/public/elasticsearch-paramedic/js/libs/ember-0.9.8.min.js +14 -0
  31. data/public/elasticsearch-paramedic/js/libs/jquery-1.7.2.min.js +4 -0
  32. data/test/middelware_test.rb +63 -0
  33. data/test/test_helper.rb +12 -0
  34. metadata +134 -0
@@ -0,0 +1,196 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Paramedic</title>
5
+ <meta name="apple-mobile-web-app-capable" content="yes">
6
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
7
+ <link rel="apple-touch-icon" href="img/apple-touch-icon.png">
8
+ <link rel="stylesheet" href="css/libs/style.css">
9
+ <link rel="stylesheet" href="css/libs/cubism.css">
10
+ <link rel="stylesheet" href="css/app.css">
11
+ <!--[if lt IE 9]><script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
12
+ </head>
13
+ <body>
14
+ <header class="clearfix">
15
+ <script type="text/x-handlebars">
16
+ {{#with App.cluster}}
17
+
18
+ <section {{bindAttr class=":cluster_name status"}}>
19
+ <span class="label">Cluster Name</span>
20
+ <h1>{{cluster_name}}</h1>
21
+
22
+ <p>
23
+ <span class="label">Status</span>
24
+ <span {{bindAttr class=":status status"}}>{{status}}</span>
25
+ <span class="label">Nodes</span>
26
+ {{number_of_nodes}}
27
+ <span class="label">Docs</span>
28
+ {{#bind docs_count}}{{number_with_delimiter docs_count}}{{/bind}}
29
+ </p>
30
+ </section>
31
+
32
+ <section class="shards">
33
+ <p><span class="label">Shards</span></p>
34
+ <p><span class="label darker">Primary</span>
35
+ {{active_primary_shards}}</p>
36
+ <p><span class="label darker">Relocating</span>
37
+ {{relocating_shards}}</p>
38
+ </section>
39
+
40
+ <section class="shards">
41
+ <p><span class="label">&nbsp;</span></p>
42
+ <p><span class="label darker">Initializing</span>
43
+ {{initializing_shards}}</p>
44
+ <p><span class="label darker">Unassigned</span>
45
+ {{unassigned_shards}}</p>
46
+ </section>
47
+
48
+ {{#with App }}
49
+ <section {{bindAttr class=":endpoint refresh_allowed:polling-active refreshing:polling-in-progress"}}>
50
+ {{/with}}
51
+ <p title="Change ElasticSearch URL">
52
+ <span class="label">URL</span>
53
+ {{view Ember.TextField valueBinding="App.elasticsearch_url" id="elasticsearch_url"}}
54
+ </p>
55
+
56
+ <p class="refresh">
57
+ <span class="icon-refresh"></span>
58
+ <span class="refresh-label">Refresh every</span>
59
+ <span class="refresh-controls">
60
+ {{view Ember.Select
61
+ contentBinding="App.refresh_intervals"
62
+ selectionBinding="App.refresh_interval"
63
+ optionLabelPath="content.label"
64
+ optionValuePath="content.value"}}
65
+ <button {{action "toggle" target="App.toggleRefreshAllowedButton"}}>{{App.toggleRefreshAllowedButton.text}}</button>
66
+ </span>
67
+ </p>
68
+ <p class="alerts">
69
+ {{view Ember.Checkbox id="sound-enabled" checkedBinding="App.sounds_enabled"}}
70
+ <label for="sound-enabled" class="dimmed">Sounds?</label>
71
+ </p>
72
+ </section>
73
+
74
+
75
+ {{/with}}
76
+ </script>
77
+ </header>
78
+
79
+ <section id="cubism">
80
+ <script type="text/x-handlebars">
81
+ <h2 class="label clear">
82
+ Stats
83
+ <small><a {{action "toggle" target="App.toggleChart"}}>{{App.toggleChart.text}}</a></small>
84
+ </h2>
85
+ </script>
86
+ <div id="chart"></div>
87
+ </section>
88
+
89
+ <h2 class="label">Nodes</h2>
90
+ <script type="text/x-handlebars">
91
+ <div id="nodes" class="clearfix">
92
+ {{#each App.nodes}}
93
+ <div {{bindAttr class=":node master"}}>
94
+ <h3><span {{bindAttr class="master:icon-star"}}></span> {{name}}</h3>
95
+ <div class="meta">
96
+ <p><span class="label">ID: </span>{{id}}</p>
97
+ <p><span class="label">IP: </span>{{http_address}}</p>
98
+ <p><span class="label">Host: </span>{{hostname}}</p>
99
+ <p><span class="label">Load: </span>{{load}}</p>
100
+ <p><span class="label">Size: </span>{{disk}}</p>
101
+ <p><span class="label">Docs: </span>{{#bind docs}}{{number_with_delimiter docs}}{{/bind}}</p>
102
+ <p><span class="label">Heap: </span>{{jvm_heap_used}}
103
+ <small class="dimmed" title="Heap max">/{{jvm_heap_max}}</small></p>
104
+ </div>
105
+ </div>
106
+ {{/each}}
107
+ </div>
108
+ </script>
109
+ <br>
110
+
111
+ <h2 class="label">Indices</h2>
112
+ <script type="text/x-handlebars">
113
+ <div id="indices">
114
+ {{#each index in App.indices.sorted}}
115
+ {{#with index}}
116
+ <div {{bindAttr class=":index :clearfix state show_detail:expanded"}}>
117
+ <div class="basic-info clearfix">
118
+ <h3><a {{bindAttr href="url"}} title="Browse Index">{{name}}</a></h3>
119
+
120
+ <div class="buttons">
121
+ {{#unless closed}}
122
+ <button {{action "showDetail" target="App.indices"}}>
123
+ {{#if show_detail}}Hide details{{else}}Show details{{/if}}
124
+ </button>
125
+ {{/unless}}
126
+ </div>
127
+
128
+ <div class="shards">
129
+ {{#each shards}}
130
+ <div {{bindAttr class=":shard primary state recovery.stage" title="state"}}>
131
+ {{name}}
132
+ </div>
133
+ {{/each}}
134
+ </div>
135
+
136
+ <div class="meta">
137
+ <p>
138
+ {{settings.number_of_shards}} shards /
139
+ {{settings.number_of_replicas}} replicas /
140
+ {{#bind docs}}{{number_with_delimiter docs}}{{/bind}} docs /
141
+ {{size}} /
142
+ {{indexing.index_time}} indexing /
143
+ {{search.query_time}} querying /
144
+ {{state}}
145
+ </p>
146
+ </div>
147
+ </div>
148
+
149
+ <!-- Shard Allocation -->
150
+ {{#if show_detail}}
151
+ <div class="extra-info shards clearfix">
152
+ {{#unless show_detail_loaded}}
153
+ <div class="loading">Waiting for data...</div>
154
+ {{/unless}}
155
+ {{#each nodes}}
156
+ <div class="node">
157
+ <h3>{{name}} <small>{{hostname}}</small></h3>
158
+ <div class="clearfix">
159
+ {{#each shards}}
160
+ <div {{bindAttr class=":shard primary state recovery.stage" title="state"}}>
161
+ <h3>{{name}}</h3>
162
+ <div class="meta">
163
+ <p>{{state}}/{{recovery.stage}}</p>
164
+ <p>
165
+ {{recovery.time}}
166
+ {{recovery.size}}
167
+ </p>
168
+ </div>
169
+ </div>
170
+ {{/each}}
171
+ </div>
172
+ </div>
173
+ {{/each}}
174
+ </div>
175
+ {{/if}}
176
+ </div>
177
+ {{/with}}
178
+ {{/each}}
179
+ </ul>
180
+ </script>
181
+
182
+ <audio id="alert-green" src="audio/alert-green.mp3"></audio>
183
+ <audio id="alert-yellow" src="audio/alert-yellow.mp3"></audio>
184
+ <audio id="alert-red" src="audio/alert-red.mp3"></audio>
185
+
186
+ <script src="js/libs/jquery-1.7.2.min.js"></script>
187
+ <!-- <script src="js/libs/ember-0.9.8.min.js"></script> -->
188
+ <script src="js/libs/ember-0.9.8.js"></script>
189
+ <script src="js/libs/colorbrewer.min.js"></script>
190
+ <script src="js/libs/d3.v2.min.js"></script>
191
+ <script src="js/libs/cubism.v1.js"></script>
192
+ <script src="js/libs/cubism.elasticsearch.js"></script>
193
+ <script src="js/app.js"></script>
194
+ <script src="js/cubism.js"></script>
195
+ </body>
196
+ </html>
@@ -0,0 +1,474 @@
1
+ function l(m) { Ember.Logger.log(m); }
2
+
3
+ var App = Em.Application.create({
4
+ name: "Paramedic",
5
+
6
+ ready: function() {
7
+ l(App.name + ' (re)loaded.')
8
+ App.__initialize_page()
9
+ App.__perform_refresh()
10
+ App.__initialize_cubism()
11
+ return this._super()
12
+ },
13
+
14
+ elasticsearch_url: function() {
15
+ var location = window.location
16
+ return (/_plugin/.test(location.href.toString())) ? location.protocol + "//" + location.host : "http://localhost:9200"
17
+ }(),
18
+
19
+ refresh_intervals : Ember.ArrayController.create({
20
+ content: [
21
+ {label: '1 sec', value: 1000},
22
+ {label: '5 sec', value: 5000},
23
+ {label: '15 sec', value: 15*1000},
24
+ {label: '1 min', value: 60*1000},
25
+ {label: '5 min', value: 5*60*1000},
26
+ {label: '15 min', value: 15*60*1000}
27
+ ]
28
+ }),
29
+
30
+ refresh_allowed: true,
31
+ sounds_enabled: false,
32
+
33
+ __perform_refresh: function() {
34
+ App.cluster.__perform_refresh()
35
+ App.nodes.__perform_refresh()
36
+ App.indices.__perform_refresh()
37
+ },
38
+
39
+ __initialize_cubism: function() {
40
+ App.Cubism.setup()
41
+ },
42
+
43
+ __initialize_page: function() {
44
+ $("link[rel=apple-touch-icon]").attr("href", App.apple_touch_icon_b64)
45
+ }
46
+ });
47
+
48
+ App.refresh_interval = App.refresh_intervals.toArray()[1]
49
+
50
+ // ===== Models ===================================================================================
51
+
52
+ App.Cluster = Ember.Object.extend({
53
+ });
54
+
55
+ App.Node = Ember.Object.extend({
56
+ });
57
+
58
+ App.Index = Ember.Object.extend({
59
+ url: function() {
60
+ return App.elasticsearch_url + '/' + this.name + '/_search?pretty'
61
+ }.property("name").cacheable(),
62
+
63
+ closed: function() {
64
+ return (this.state && this.state == 'close')
65
+ }.property("state").cacheable()
66
+ });
67
+
68
+ App.Index.Shard = Ember.Object.extend({
69
+ });
70
+
71
+ // ===== Controllers ==============================================================================
72
+
73
+ App.cluster = Ember.Object.create({
74
+ content: App.Cluster.create({}),
75
+
76
+ refresh: function() {
77
+ clearTimeout(App.cluster.poller)
78
+ setTimeout(function() { App.set("refreshing", false) }, 1000)
79
+ App.cluster.poller = setTimeout( function() { App.cluster.__perform_refresh() }, App.refresh_interval.value )
80
+ },
81
+
82
+ __perform_refresh: function() {
83
+ if (!App.refresh_allowed) { return }
84
+ var self = this;
85
+
86
+ var __load_cluster_info = function(data) {
87
+ App.cluster.setProperties(data)
88
+ App.cluster.refresh();
89
+ }
90
+
91
+ App.set("refreshing", true)
92
+ $.getJSON(App.elasticsearch_url+"/_cluster/health", __load_cluster_info);
93
+ }
94
+ });
95
+
96
+ App.nodes = Ember.ArrayController.create({
97
+ content: [],
98
+
99
+ contains: function(item) {
100
+ return (Ember.typeOf(item) == 'string') ? this.mapProperty('id').contains(item) : this._super();
101
+ },
102
+
103
+ refresh: function() {
104
+ clearTimeout(App.nodes.poller)
105
+ setTimeout(function() { App.set("refreshing", false) }, 1000)
106
+ App.nodes.poller = setTimeout( function() { App.nodes.__perform_refresh() }, App.refresh_interval.value )
107
+ },
108
+
109
+ __perform_refresh: function() {
110
+ if (!App.refresh_allowed) { return }
111
+ var self = this;
112
+
113
+ var __load_nodes_info = function(data) {
114
+ for (var node_id in data.nodes) {
115
+ if ( !self.contains(node_id) ) self.addObject(App.Node.create({ id: node_id }))
116
+ var node = self.findProperty("id", node_id)
117
+ .set("name", data.nodes[node_id]['name'])
118
+ .set("hostname", data.nodes[node_id]['hostname'])
119
+ .set("http_address", data.nodes[node_id]['http_address'])
120
+ .set("jvm_heap_max", data.nodes[node_id]['jvm']['mem']['heap_max'])
121
+ .set("start_time", data.nodes[node_id]['jvm']['start_time'])
122
+ }
123
+
124
+ // Remove missing nodes from the collection
125
+ // TODO: Use model instance identity, contains(), etc
126
+ //
127
+ self.forEach(function(item) {
128
+ var loc = self.content.length || 0
129
+ while(--loc >= 0) {
130
+ var curObject = self.content.objectAt(loc)
131
+ if ( item && !Ember.keys(data.nodes).contains(item.id) && curObject.id === item.id) {
132
+ self.content.removeAt(loc)
133
+ }
134
+ }
135
+ })
136
+
137
+ App.nodes.refresh();
138
+ };
139
+
140
+ var __load_nodes_stats = function(data) {
141
+ for (var node_id in data.nodes) {
142
+ var node = self.findProperty("id", node_id)
143
+ if (node) {
144
+ node
145
+ .set("disk", data.nodes[node_id]['indices']['store']['size'])
146
+ .set("docs", data.nodes[node_id]['indices']['docs']['count'])
147
+ .set("load", data.nodes[node_id]['os']['load_average'][0].toFixed(3))
148
+ .set("cpu", data.nodes[node_id]['process']['cpu']['percent'])
149
+ .set("jvm_heap_used", data.nodes[node_id]['jvm']['mem']['heap_used'])
150
+ }
151
+ }
152
+ };
153
+
154
+ App.set("refreshing", true)
155
+ $.getJSON(App.elasticsearch_url+"/_cluster/nodes?jvm", __load_nodes_info);
156
+ $.getJSON(App.elasticsearch_url+"/_cluster/nodes/stats?indices&os&process&jvm", __load_nodes_stats);
157
+ }
158
+ });
159
+
160
+ App.indices = Ember.ArrayController.create({
161
+ content: [],
162
+
163
+ contains: function(item) {
164
+ return (Ember.typeOf(item) == 'string') ? this.mapProperty('name').contains(item) : this._super();
165
+ },
166
+
167
+ refresh: function() {
168
+ clearTimeout(App.indices.poller)
169
+ setTimeout(function() { App.set("refreshing", false) }, 1000)
170
+ App.indices.poller = setTimeout( function() { App.indices.__perform_refresh() }, App.refresh_interval.value )
171
+ },
172
+
173
+ sorted: function() {
174
+ return this.get("content")
175
+ .toArray()
176
+ .sort(function(a,b) { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; })
177
+ }.property("content.@each").cacheable(),
178
+
179
+ showDetail: function(event) {
180
+ // l(event.context.name)
181
+ // l(this)
182
+ event.context.toggleProperty("show_detail")
183
+ },
184
+
185
+ __perform_refresh: function() {
186
+ if (!App.refresh_allowed) { return }
187
+ var self = this;
188
+
189
+ var __load_cluster_state = function(data) {
190
+ for (var index_name in data.metadata.indices) {
191
+ // Mark master node
192
+ //
193
+ var master_node = App.nodes.content.findProperty("id", data.master_node)
194
+ if (master_node) master_node.set("master", true)
195
+
196
+ // Create or find an index
197
+ //
198
+ if ( !self.contains(index_name) ) self.addObject(App.Index.create({ name: index_name }))
199
+ var index = self.findProperty("name", index_name)
200
+
201
+ // Update index properties
202
+ //
203
+ index
204
+ .set("state", data.metadata.indices[index_name]['state'])
205
+
206
+ .set("settings", Ember.Object.create({
207
+ number_of_replicas: data.metadata.indices[index_name]['settings']['index.number_of_replicas'],
208
+ number_of_shards: data.metadata.indices[index_name]['settings']['index.number_of_shards']
209
+ }))
210
+
211
+ .set("aliases", data.metadata.indices[index_name]['aliases'])
212
+
213
+ // Shards
214
+ //
215
+ var shards = [],
216
+ primaries = [],
217
+ replicas = [],
218
+ unassigned = []
219
+
220
+ index
221
+ .set("shards", function() {
222
+ if (data.routing_table.indices[index_name]) {
223
+
224
+ for (var shard_name in data.routing_table.indices[index_name]['shards']) {
225
+
226
+ data.routing_table.indices[index_name]['shards'][shard_name].forEach(function(s) {
227
+
228
+ var shard = App.Index.Shard.create({name: shard_name})
229
+ shard.set("state", s.state)
230
+ .set("primary", s.primary)
231
+ .set("index", s.index)
232
+ .set("node_id", s.node)
233
+ .set("relocating_node_id", s.relocating_node)
234
+
235
+ if (s.primary) primaries .addObject(shard)
236
+ if (!s.primary && s.node) replicas .addObject(shard)
237
+ if (!s.primary && !s.node) unassigned.addObject(shard)
238
+ });
239
+
240
+ }
241
+ }
242
+
243
+ // Sort unassingned shards to series [0 .. n, 0 .. n]
244
+ // [0, 0, 1, 1, 2, 2] becomes: [0, 1, 2, 0, 1, 2]
245
+ //
246
+ var unassigned_sorted = []
247
+ unassigned_sorted.length = unassigned.length
248
+
249
+ var num_shards = primaries.length,
250
+ num_replicas = unassigned.length/num_shards;
251
+
252
+ for (var i = 0; i < num_shards; i++) {
253
+ // Create slices: [0, 0]; [1, 1]; [2, 2]
254
+ unassigned.slice(i*num_replicas, i*num_replicas+num_replicas).forEach(function(item,index) {
255
+ // Position for first slices: 0, 3
256
+ // Position for second slices: 1, 4
257
+ // Position for third slices: 2, 5
258
+ var position = i + num_shards * index
259
+ unassigned_sorted[position] = item
260
+ })
261
+ };
262
+
263
+ unassigned_sorted = unassigned_sorted.filter(function(i){return i != null})
264
+ return shards.concat(primaries, replicas, unassigned_sorted)
265
+ }())
266
+
267
+ if (index.show_detail) {
268
+ index.set("nodes", function() {
269
+ var nodes = []
270
+ if (data.routing_table.indices[index_name]) {
271
+ for (var shard_name in data.routing_table.indices[index_name]['shards']) {
272
+
273
+ data.routing_table.indices[index_name]['shards'][shard_name].forEach(function(shard_data) {
274
+ if (shard_data.node) {
275
+
276
+ // Find the node
277
+ // var node = App.nodes.content.findProperty("id", shard_data.node)
278
+ var node = nodes.findProperty("id", shard_data.node)
279
+ if (!node) {
280
+ var node = App.Node.create( App.nodes.content.findProperty("id", shard_data.node) )
281
+ nodes.addObject(node)
282
+ }
283
+
284
+ // Initialize node.shards
285
+ if (node && !node.shards) node.set("shards", [])
286
+
287
+ // Find shard in index.shards
288
+ var shard = index.shards.find(function(item) {
289
+ return item.name == shard_data.shard && item.node_id == shard_data.node && item.index == shard_data.index
290
+ })
291
+
292
+ // Remove shard from node.shards
293
+ node.shards.forEach(function(item, index) {
294
+ if (item.name == shard_data.shard && item.node_id == shard_data.node && item.index == shard_data.index) {
295
+ node.shards.removeAt(index)
296
+ }
297
+ })
298
+
299
+ // Add (possibly updated) shard back into collection
300
+ if (shard) { node.shards.addObject(shard) }
301
+
302
+ node.set("shards", node.shards.sort(function(a,b) { return a.name > b.name; }))
303
+ }
304
+ });
305
+ };
306
+ }
307
+ index.set("show_detail_loaded", true)
308
+ return nodes
309
+ }())
310
+ }
311
+
312
+ // Remove deleted indices from the collection
313
+ // TODO: Use model instance identity for this
314
+ //
315
+ self.forEach(function(item) {
316
+ // console.log(item.name)
317
+ var loc = self.content.length || 0
318
+ while(--loc >= 0) {
319
+ var curObject = self.content.objectAt(loc)
320
+ if ( item && !Ember.keys(data.metadata.indices).contains(item.name) && curObject.name === item.name) {
321
+ self.content.removeAt(loc)
322
+ }
323
+ }
324
+ })
325
+
326
+ }
327
+ };
328
+
329
+ var __load_indices_stats = function(data) {
330
+ App.cluster.set("docs_count",
331
+ data._all.primaries.docs ? data._all.primaries.docs.count : 0)
332
+
333
+ for (var index_name in data._all.indices) {
334
+ var index = self.findProperty("name", index_name)
335
+ if (!index) continue
336
+
337
+ index
338
+ .set("size", data._all.indices[index_name]['primaries']['store']['size'])
339
+ .set("size_in_bytes", data._all.indices[index_name]['primaries']['store']['size_in_bytes'])
340
+ .set("docs", data._all.indices[index_name]['primaries']['docs']['count'])
341
+ .set("indexing", data._all.indices[index_name]['primaries']['indexing'])
342
+ .set("search", data._all.indices[index_name]['primaries']['search'])
343
+ .set("get", data._all.indices[index_name]['primaries']['get'])
344
+ }
345
+ };
346
+
347
+ var __load_indices_status = function(data) {
348
+ for (var index_name in data.indices) {
349
+ var index = self.findProperty("name", index_name)
350
+ if (!index) continue
351
+ if (!index.show_detail) continue
352
+
353
+ for (var shard_name in data.indices[index_name]['shards']) {
354
+ // var shard = index.shards.findProperty("name", shard_name)
355
+
356
+ data.indices[index_name]['shards'][shard_name].forEach(function(shard_data) {
357
+ var shard = index.shards.find(function(item) {
358
+ return item.name == shard_name && item.node_id == shard_data['routing']['node']
359
+ })
360
+ // if (!shard) continue
361
+ if (shard) {
362
+
363
+ // l(shard_data)
364
+ shard
365
+ .set("size", shard_data.index.size)
366
+ // .set("docs", shard_data.docs.num_docs)
367
+ shard
368
+ .set("recovery", function() {
369
+ var recovery_type = shard_data['peer_recovery'] ? 'peer_recovery' : 'gateway_recovery'
370
+
371
+ return {
372
+ stage: shard_data[recovery_type].stage,
373
+ time: shard_data[recovery_type].time,
374
+ progress: shard_data[recovery_type].index.progress,
375
+ size: shard_data[recovery_type].index.size,
376
+ reused_size: shard_data[recovery_type].index.reused_size
377
+ }
378
+ }())
379
+ }
380
+ });
381
+ }
382
+ }
383
+ };
384
+
385
+ App.set("refreshing", true)
386
+ $.getJSON(App.elasticsearch_url+"/_cluster/state", __load_cluster_state);
387
+ $.getJSON(App.elasticsearch_url+"/_stats", __load_indices_stats);
388
+ $.getJSON(App.elasticsearch_url+"/_status?recovery=true", __load_indices_status);
389
+
390
+ // Schedule next run
391
+ //
392
+ App.indices.refresh();
393
+ }
394
+ });
395
+
396
+ // ===== Views ==================================================================================
397
+
398
+ App.toggleRefreshAllowedButton = Ember.View.create({
399
+ text: 'Stop',
400
+
401
+ toggle: function(event) {
402
+ this.set("text", ( App.refresh_allowed == true ) ? 'Start' : 'Stop')
403
+ App.toggleProperty("refresh_allowed")
404
+ }
405
+ });
406
+
407
+ App.toggleChart = Ember.View.create({
408
+ text: 'Hide',
409
+
410
+ toggle: function(event) {
411
+ var chart = $("#chart"),
412
+ visible = chart.is(":visible")
413
+
414
+ this.set("text", visible ? 'Show' : 'Hide')
415
+ visible ? chart.hide('fast') : chart.show('fast')
416
+ }
417
+ });
418
+
419
+ // ===== Observers ==============================================================================
420
+
421
+ App.addObserver('elasticsearch_url', function(event) {
422
+ // TODO: Use the `blur` event, so we're not trying to load partial URLs
423
+ Ember.Logger.log("ElasticSearch URL changed to " + this.get("elasticsearch_url"))
424
+ App.cluster.set("content", App.Cluster.create({}))
425
+ App.nodes.set("content", [])
426
+ App.indices.set("content", [])
427
+ App.ready()
428
+ App.Cubism.reset()
429
+ });
430
+
431
+ App.addObserver('refresh_interval', function() {
432
+ Ember.Logger.log("Refresh interval changed to " + App.refresh_interval.label)
433
+ App.ready()
434
+ });
435
+
436
+ App.addObserver('refresh_allowed', function() {
437
+ App.refresh_allowed ? App.Cubism.start() : App.Cubism.stop()
438
+ App.__perform_refresh()
439
+ });
440
+
441
+ App.nodes.addObserver('@each.name', function() {
442
+ // Wait until we have node names...
443
+ if ( !App.nodes.everyProperty("name") ) return;
444
+
445
+ Ember.Logger.log("Nodes changed to: " + App.nodes.mapProperty("name").join("; "))
446
+ App.Cubism.reset()
447
+ });
448
+
449
+ App.cluster.addObserver('cluster_name', function() {
450
+ $('title').text('Paramedic | ' + this.get('cluster_name'))
451
+ });
452
+
453
+ App.cluster.addObserver('status', function() {
454
+ if (App.get("sounds_enabled")) {
455
+ // FIXME: When running as a plugin, audio won't play again when `var a = $('#alert-'+this.get("status"))[0]`
456
+ var a = new Audio('audio/alert-'+this.get("status")+'.mp3')
457
+ a.volume=0.7
458
+ a.play()
459
+ }
460
+ });
461
+
462
+ // ===== Helpers ================================================================================
463
+
464
+ Handlebars.registerHelper('number_with_delimiter', function(property) {
465
+ var delimiter = ' '
466
+ , value = (isNaN(this)) ? Ember.getPath(this, property) : this.toString();
467
+ // console.log(this, property, value)
468
+ // Credit: http://stackoverflow.com/a/2254896/95696
469
+ return value ? value.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1"+delimiter) : value
470
+ });
471
+
472
+ // ===== Varia ==================================================================================
473
+
474
+ App.apple_touch_icon_b64 = "data:image/png;base64,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";