elasticsearch-paramedic-rack 0.1.0

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 (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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";