mouth 0.8.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 (41) hide show
  1. data/.gitignore +7 -0
  2. data/Capfile +26 -0
  3. data/Gemfile +3 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +138 -0
  6. data/Rakefile +19 -0
  7. data/TODO +32 -0
  8. data/bin/mouth +77 -0
  9. data/bin/mouth-console +18 -0
  10. data/bin/mouth-endoscope +18 -0
  11. data/lib/mouth.rb +61 -0
  12. data/lib/mouth/dashboard.rb +25 -0
  13. data/lib/mouth/endoscope.rb +120 -0
  14. data/lib/mouth/endoscope/public/222222_256x240_icons_icons.png +0 -0
  15. data/lib/mouth/endoscope/public/application.css +464 -0
  16. data/lib/mouth/endoscope/public/application.js +938 -0
  17. data/lib/mouth/endoscope/public/backbone.js +1158 -0
  18. data/lib/mouth/endoscope/public/d3.js +4707 -0
  19. data/lib/mouth/endoscope/public/d3.time.js +687 -0
  20. data/lib/mouth/endoscope/public/jquery-ui-1.8.16.custom.min.js +177 -0
  21. data/lib/mouth/endoscope/public/jquery.js +4 -0
  22. data/lib/mouth/endoscope/public/json2.js +480 -0
  23. data/lib/mouth/endoscope/public/keymaster.js +163 -0
  24. data/lib/mouth/endoscope/public/linen.js +46 -0
  25. data/lib/mouth/endoscope/public/seven.css +68 -0
  26. data/lib/mouth/endoscope/public/seven.js +291 -0
  27. data/lib/mouth/endoscope/public/underscore.js +931 -0
  28. data/lib/mouth/endoscope/views/dashboard.erb +67 -0
  29. data/lib/mouth/graph.rb +58 -0
  30. data/lib/mouth/instrument.rb +56 -0
  31. data/lib/mouth/record.rb +72 -0
  32. data/lib/mouth/runner.rb +89 -0
  33. data/lib/mouth/sequence.rb +284 -0
  34. data/lib/mouth/source.rb +76 -0
  35. data/lib/mouth/sucker.rb +235 -0
  36. data/lib/mouth/version.rb +3 -0
  37. data/mouth.gemspec +28 -0
  38. data/test/sequence_test.rb +163 -0
  39. data/test/sucker_test.rb +55 -0
  40. data/test/test_helper.rb +5 -0
  41. metadata +167 -0
@@ -0,0 +1,938 @@
1
+ (function($) {
2
+
3
+ // Some simple logging
4
+ var $log = function() { console.log.apply(console, arguments); };
5
+
6
+ //
7
+ // Models
8
+ //
9
+ var Dashboard = Backbone.Model.extend({
10
+ initialize: function(attrs) {
11
+ _.bindAll(this, 'onAddGraph');
12
+
13
+ var graphs = this.graphs = new GraphList();
14
+ _.each(attrs.graphs || [], function(g) {
15
+ graphs.add(g);
16
+ });
17
+ graphs.bind('add', this.onAddGraph);
18
+ },
19
+
20
+ // When a graph is added, fill in the association
21
+ onAddGraph: function(m) {
22
+ m.set({dashboard_id: this.get('id')});
23
+ },
24
+
25
+ // Intelligently place the graph on the dashboard. For now, just put it at the bottom left.
26
+ autoPlaceGraph: function(g) {
27
+ var pos = {left: 0, top: 0, width: 20, height: 7}
28
+ , bottoms = [];
29
+
30
+ this.graphs.each(function(gi) {
31
+ var gpos = gi.get('position');
32
+ bottoms.push((gpos.top || 0) + (gpos.height || 0));
33
+ });
34
+ bottoms.sort();
35
+ pos.top = bottoms[bottoms.length - 1] || 0;
36
+ g.set({position: pos});
37
+
38
+ this.graphs.add(g);
39
+ }
40
+ });
41
+
42
+ var Graph = Backbone.Model.extend({
43
+ defaults: {
44
+ kind: 'counters'
45
+ },
46
+
47
+ name: function() {
48
+ var s = this.get('sources') || []
49
+ , s0 = s[0]
50
+ , len = s.length
51
+ ;
52
+
53
+ if (s0) {
54
+ return this.get('kind') + (len > 1 ? 's' : '') + ": " + s0 + (len > 1 ? ', ...' : '');
55
+ }
56
+ return "No data defined yet."
57
+ },
58
+
59
+ setLocation: function(position) {
60
+ var pos = this.get('position');
61
+ _.extend(pos, position);
62
+
63
+ this.set({position: position});
64
+ },
65
+
66
+ // Assume all sources are of the same kind here
67
+ setSources: function(sources) {
68
+ var kind = (sources[0] && sources[0].kind) || "counter"
69
+ , sourceStrings = _.map(sources, function(r) { return r.source; })
70
+ ;
71
+
72
+ this.set({kind: kind, sources: sourceStrings});
73
+ },
74
+
75
+ addSource: function(s) {
76
+ var sources = this.get('sources');
77
+
78
+ if (!sources) {
79
+ sources = [s];
80
+ } else {
81
+ if (!_.include(sources, s)) {
82
+ sources.push(s);
83
+ }
84
+ }
85
+
86
+ this.set({sources: sources});
87
+ },
88
+
89
+ removeSource: function(source) {
90
+ var sources = this.get('sources');
91
+
92
+ if (sources) {
93
+ sources = _.reject(sources, function(el) { return el === source; });
94
+ this.set({sources: sources});
95
+ }
96
+ }
97
+ });
98
+
99
+ //
100
+ // Collections
101
+ //
102
+ var DashboardList = Backbone.Collection.extend({
103
+ model: Dashboard,
104
+ url: '/dashboards'
105
+ });
106
+
107
+ var GraphList = Backbone.Collection.extend({
108
+ model: Graph,
109
+ url: '/graphs'
110
+ });
111
+
112
+ //
113
+ // Views
114
+ //
115
+ var DashboardItemView = Backbone.View.extend({
116
+ tagName: 'li',
117
+ className: 'dashboard-item',
118
+
119
+ events: {
120
+ 'click .delete': 'onClickDelete',
121
+ 'keydown input': 'onKeyName',
122
+ 'click button': 'onClickSave',
123
+ 'click .cancel': 'onClickCancel'
124
+ },
125
+
126
+ initialize: function(opts) {
127
+ _.bindAll(this, 'render', 'onChangeModel', 'onRemoveModel', 'onClickDelete', 'onKeyName', 'onClickSave', 'onClickCancel', 'onChosen');
128
+
129
+ this.model.bind('change', this.onChangeModel);
130
+ this.model.bind('remove', this.onRemoveModel);
131
+ this.model.bind('chosen', this.onChosen);
132
+ this.mode = opts.mode || 'show'; // vs 'edit'
133
+ },
134
+
135
+ render: function() {
136
+ if (this.mode === 'show') {
137
+ this.renderShow();
138
+ } else {
139
+ this.renderEdit();
140
+ }
141
+ $(this.el).data('view', this);
142
+ return this;
143
+ },
144
+
145
+ renderShow: function() {
146
+ var name = this.model.get('name')
147
+ , html
148
+ ;
149
+
150
+ html = [
151
+ '<span class="chooser">' + name + '</span>'
152
+ ];
153
+
154
+ $(this.el).html(html.join(''));
155
+ },
156
+
157
+ renderEdit: function() {
158
+ var name = this.model.get('name')
159
+ , el = $(this.el)
160
+ , html
161
+ ;
162
+
163
+ html = [
164
+ '<div class="dashboard-item-edit">',
165
+ '<input type="text" value="' + name + '" /> <br />',
166
+ '<button>Save</button> or <a href="#" class="cancel">Cancel</a>',
167
+ '<a href="#" class="delete">Delete</a>',
168
+ '</div>'
169
+ ];
170
+
171
+ el.html(html.join(''));
172
+ el.find('input').select();
173
+
174
+ },
175
+
176
+ saveEditing: function() {
177
+ var newName = this.$('input').val();
178
+
179
+ if (newName.trim()) {
180
+ this.mode = 'show';
181
+ this.model.set({'name': newName});
182
+ this.model.save();
183
+ }
184
+ this.render();
185
+ },
186
+
187
+ onChangeModel: function() {
188
+ this.render();
189
+ },
190
+
191
+ onRemoveModel: function() {
192
+ $(this.el).remove();
193
+ },
194
+
195
+ onKeyName: function(evt) {
196
+ if (evt && evt.which === 13) {
197
+ this.saveEditing();
198
+ }
199
+ },
200
+
201
+ onClickSave: function(evt) {
202
+ this.saveEditing();
203
+ },
204
+
205
+ onClickCancel: function(evt) {
206
+ if (this.model.isNew()) {
207
+ this.model.destroy();
208
+ } else {
209
+ this.mode = 'show';
210
+ this.render();
211
+ }
212
+ },
213
+
214
+ onClickDelete: function(evt) {
215
+ evt && evt.preventDefault();
216
+ this.model.destroy();
217
+ },
218
+
219
+ onChosen: function() {
220
+ var el = $(this.el)
221
+ , wasChosen = el.hasClass('selected')
222
+ ;
223
+
224
+ if (wasChosen) {
225
+ this.mode = 'edit'
226
+ this.render();
227
+ } else {
228
+ $('.dashboard-item').removeClass('selected');
229
+ el.addClass('selected');
230
+ }
231
+ }
232
+ });
233
+
234
+ var DashboardListView = Backbone.View.extend({
235
+ events: {
236
+ 'click .add-dashboard': 'onClickAddDashboard',
237
+ 'click .chooser': 'onClickChooser'
238
+ },
239
+
240
+ initialize: function() {
241
+ _.bindAll(this, 'onClickAddDashboard', 'onAddDashboard', 'onResetDashboards', 'onClickChooser', 'onPopState');
242
+
243
+ this.collection = new DashboardList();
244
+ this.collection.bind('add', this.onAddDashboard);
245
+ this.collection.bind('reset', this.onResetDashboards);
246
+ this.collection.fetch();
247
+ window.onpopstate = this.onPopState;
248
+ },
249
+
250
+ setTitle: function(model) {
251
+ document.title = "Mouth: " + model.get("name");
252
+ },
253
+
254
+ onClickAddDashboard: function(evt) {
255
+ evt && evt.preventDefault();
256
+
257
+ this.collection.add(new Dashboard({name: "New Dashboard"}));
258
+ },
259
+
260
+ onClickChooser: function(evt) {
261
+ var target = $(evt.target).parent('.dashboard-item')
262
+ , view = target.data('view')
263
+ ;
264
+
265
+ this.trigger('current_change', view.model);
266
+ view.model.trigger("chosen");
267
+ window.history.pushState({dashboardId: view.model.id}, null, "/dashboards/" + view.model.id);
268
+ this.setTitle(view.model);
269
+ },
270
+
271
+ onAddDashboard: function(m, isDynamicAdd) {
272
+ var dbItemView = new DashboardItemView({model: m, mode: isDynamicAdd ? 'edit' : 'show'});
273
+ this.$('ul').append(dbItemView.render().el);
274
+ if (m.isNew()) {
275
+ this.trigger('current_change', m);
276
+ m.trigger('chosen');
277
+ }
278
+ return dbItemView;
279
+ },
280
+
281
+ onResetDashboards: function(col) {
282
+ var self = this
283
+ , currentModel = null
284
+ , currentView = null
285
+ , view
286
+ , path = window.location.pathname
287
+ , match
288
+ , targetDashboardId = null
289
+ ;
290
+
291
+ match = path.match(/\/dashboards\/(.+)/);
292
+ targetDashboardId = match && match[1];
293
+
294
+ col.each(function(m) {
295
+ currentModel = currentModel || m;
296
+ view = self.onAddDashboard(m);
297
+ currentView = currentView || view;
298
+ if (targetDashboardId && m.id == targetDashboardId) {
299
+ currentModel = m;
300
+ currentView = view;
301
+ }
302
+ });
303
+
304
+ this.trigger('current_change', currentModel);
305
+ currentModel.trigger('chosen');
306
+ window.history.replaceState({dashboardId: currentModel.id}, null, "/dashboards/" + currentModel.id);
307
+ this.setTitle(currentModel);
308
+ },
309
+
310
+ onPopState: function(evt) {
311
+ var self = this
312
+ , dashboardId = evt && evt.state && evt.state.dashboardId
313
+ ;
314
+
315
+ if (dashboardId) {
316
+ self.collection.each(function(m) {
317
+ if (dashboardId == m.id) {
318
+ self.trigger('current_change', m);
319
+ m.trigger('chosen');
320
+ self.setTitle(m);
321
+ }
322
+ });
323
+ }
324
+ }
325
+ });
326
+
327
+ var DashboardPane = Backbone.View.extend({
328
+ events: {
329
+ 'click .add-graph': 'onClickAddGraph'
330
+ },
331
+
332
+ initialize: function(opts) {
333
+ _.bindAll(this, 'render', 'onCurrentChange', 'onClickAddGraph', 'onAddGraph', 'periodicUpdate');
334
+
335
+ this.dashboardList = opts.dashboardListView;
336
+ this.dashboardList.bind('current_change', this.onCurrentChange);
337
+
338
+ this.model = null;
339
+
340
+ this.elGrid= this.$('#grid');
341
+ this.elAddGraph = this.$('.add-graph');
342
+
343
+ key('g', this.onClickAddGraph);
344
+
345
+ this.render();
346
+
347
+ setInterval(this.periodicUpdate, 60000);
348
+ },
349
+
350
+ onCurrentChange: function(model) {
351
+ var self = this;
352
+
353
+ // Clean up
354
+ if (self.model) {
355
+ self.model.graphs.unbind('add', self.onAddGraph);
356
+ self.elGrid.empty();
357
+ }
358
+
359
+ self.model = model;
360
+ model.graphs.each(function(m) {
361
+ self.onAddGraph(m);
362
+ })
363
+ self.render();
364
+ model.graphs.bind('add', self.onAddGraph);
365
+ },
366
+
367
+ onAddGraph: function(m) {
368
+ var graphView = new GraphView({model: m});
369
+ this.elGrid.append(graphView.render().el);
370
+ },
371
+
372
+ onClickAddGraph: function(evt) {
373
+ var self = this;
374
+ evt && evt.preventDefault();
375
+
376
+ if (this.model) {
377
+ sourceListInst.show({
378
+ callback: function(sources) {
379
+ var item = new Graph();
380
+ item.setSources(sources);
381
+ self.model.autoPlaceGraph(item);
382
+ item.save();
383
+ }
384
+ });
385
+ }
386
+ },
387
+
388
+ periodicUpdate: function() {
389
+ this.model.graphs.each(function(g) {
390
+ g.trigger("tick");
391
+ });
392
+ }
393
+ });
394
+
395
+ var graphSequence = 0
396
+ , currentGraph = null;
397
+
398
+ var GraphView = Backbone.View.extend({
399
+ tagName: 'li',
400
+ className: 'graph',
401
+
402
+ events: {
403
+ 'click .edit': 'onClickEdit',
404
+ 'click .delete': 'onClickDelete',
405
+ 'click .pick': 'onClickPick'
406
+ },
407
+
408
+ initialize: function(opts) {
409
+ _.bindAll(this, 'render', 'onDragStop', 'onClickEdit', 'onClickPick', 'onCreateModel', 'onClickDelete', 'onRemoveModel', 'onPick', 'onCancelPick', 'onDateRangeChanged', 'onTick');
410
+
411
+ this.mode = 'front';
412
+ this.model = opts.model;
413
+ this.model.bind('change:id', this.onCreateModel);
414
+ this.model.bind('remove', this.onRemoveModel);
415
+ this.model.bind('tick', this.onTick);
416
+ this.graphId = "graph" + graphSequence++;
417
+ this.kind = '';
418
+ $(document.body).bind('date_range_changed', this.onDateRangeChanged);
419
+ },
420
+
421
+ render: function() {
422
+ var el = $(this.el)
423
+ , m = this.model
424
+ , pos = m.get('position')
425
+ ;
426
+
427
+ // Position it
428
+ el.css({
429
+ top: pos.top * 30 + 'px',
430
+ left: pos.left * 30 + 'px',
431
+ width: pos.width * 30 + 'px',
432
+ height: pos.height * 30 + 'px'
433
+ });
434
+
435
+ // Construct the inner div. Only do it once
436
+ if (!this.elInner) {
437
+ this.elInner = $('<div class="graph-inner"></div>');
438
+ el.append(this.elInner);
439
+
440
+ el.draggable({grid: [30,30], containment: 'parent', cursor: 'move', stop: this.onDragStop, handle: '.graph-header'});
441
+ el.resizable({grid: 30, containment: 'parent', stop: this.onDragStop});
442
+ el.css('position', 'absolute'); // NOTE: draggable applies position: relative, which seems completely retarded.
443
+ }
444
+
445
+ // Each side is a bit different
446
+ if (this.mode === 'front') {
447
+ this.renderFront();
448
+ } else {
449
+ this.renderBack();
450
+ }
451
+
452
+ // Update the name
453
+ el.find('.graph-header span').text(m.name());
454
+
455
+ return this;
456
+ },
457
+
458
+ renderFront: function() {
459
+ var html = [
460
+ '<div class="graph-header">',
461
+ '<span></span> <a href="#" class="edit">Edit</a>',
462
+ '</div>',
463
+ '<div class="graph-body" id="' + this.graphId + '"></div>'
464
+ ];
465
+
466
+ this.elInner.html(html.join(''));
467
+ this.updateData();
468
+ },
469
+
470
+ renderBack: function() {
471
+ var html
472
+ , m = this.model
473
+ , sources = m.get('sources')
474
+ , domUl
475
+ , domItem
476
+ ;
477
+
478
+ html = [
479
+ '<div class="graph-header">',
480
+ '<span></span> <a href="#" class="edit">See Graph</a>',
481
+ '</div>',
482
+ '<div class="graph-back">',
483
+ '<div class="kind">',
484
+ '<div class="kind-label">Kind:</div>',
485
+ '<div class="kind-value">' + m.get('kind') + '</div>',
486
+ '</div>',
487
+ '<div class="graph-sources">',
488
+ '<div class="graph-sources-label">Sources:</div>',
489
+ '<ul></ul>',
490
+ '</div>',
491
+ '<a href="#" class="delete">Delete</a>',
492
+ '</div>'
493
+ ];
494
+ this.elInner.html(html.join(''));
495
+
496
+ domUl = this.elInner.find('ul');
497
+ _.each(sources, function(s) {
498
+ html = [
499
+ '<li>',
500
+ '<span class="source-item">' + s + '</span>',
501
+ '</li>'
502
+ ];
503
+
504
+ domItem = $(html.join(''));
505
+ domUl.append(domItem);
506
+ });
507
+ domUl.append('<li class="pick-item"><a href="#" class="pick">Pick Sources</a></li>');
508
+ },
509
+
510
+ updateData: function() {
511
+ var self = this
512
+ , params = {}
513
+ , range
514
+ ;
515
+
516
+ if (self.model.isNew()) {
517
+ return; // TODO: maybe render a no data thinger
518
+ }
519
+
520
+ range = DateRangePicker.getCurrentRange();
521
+
522
+ params.granularity_in_minutes = range.granularityInMinutes;
523
+ params.start_time = Math.floor(+range.startDate / 1000);
524
+ params.end_time = Math.floor(+range.endDate / 1000);
525
+
526
+ $.getJSON('/graphs/' + self.model.get('id') + '/data', params, function(data) {
527
+ var seven = null;
528
+ _.each(data, function(d) {
529
+ if (!seven) {
530
+ seven = new Seven('#' + self.graphId, {start: d.start_time * 1000, granularityInMinutes: range.granularityInMinutes, points: d.data.length, kind: self.model.get('kind')});
531
+ }
532
+ seven.graph({name: d.source, data: d.data})
533
+ });
534
+ });
535
+ },
536
+
537
+ onTick: function() {
538
+ var range = DateRangePicker.getCurrentRange();
539
+ if (+range.endDate > (+new Date() - 60000)) {
540
+ this.updateData();
541
+ }
542
+ },
543
+
544
+ onDateRangeChanged: function(evt, data) {
545
+ this.updateData();
546
+ },
547
+
548
+ onDragStop: function(evt, ui) {
549
+ var el = $(this.el)
550
+ , w = el.width()
551
+ , h = el.height()
552
+ , pos = el.position()
553
+ ;
554
+
555
+ this.model.setLocation({
556
+ top: parseInt(pos.top / 30, 10),
557
+ left: parseInt(pos.left / 30, 10),
558
+ width: parseInt(w / 30, 10),
559
+ height: parseInt(h / 30, 10)
560
+ });
561
+
562
+ this.model.save();
563
+ this.updateData();
564
+ },
565
+
566
+ onClickEdit: function(evt) {
567
+ evt && evt.preventDefault();
568
+
569
+ if (this.mode === 'front') {
570
+ this.mode = 'back';
571
+ } else {
572
+ this.mode = 'front';
573
+ }
574
+
575
+ this.render();
576
+ },
577
+
578
+ onCreateModel: function() {
579
+ this.updateData();
580
+ },
581
+
582
+ onClickPick: function(evt) {
583
+ var sources = this.model.get('sources')
584
+ , kind = this.model.get('kind')
585
+ , sourceObjs = _.map(sources, function(s) { return {kind: kind, source: s}; })
586
+ , pos = this.model.get('position')
587
+ , windowWidth = $(window).width()
588
+ , openDir = 'right'
589
+ ;
590
+
591
+ $('.graph').removeClass('highlighted').addClass('dimmed');
592
+ $(this.el).addClass('highlighted').removeClass('dimmed');
593
+
594
+ if (windowWidth / 2 < pos.left * 30) {
595
+ openDir = 'left';
596
+ }
597
+
598
+ sourceListInst.show({callback: this.onPick, sources: sourceObjs, cancel: this.onCancelPick, direction: openDir});
599
+ },
600
+
601
+ onPick: function(sources) {
602
+ $('.graph').removeClass('highlighted dimmed');
603
+ this.model.setSources(sources);
604
+ this.model.save();
605
+ this.mode = 'front';
606
+ this.render();
607
+ },
608
+
609
+ onCancelPick: function() {
610
+ $('.graph').removeClass('highlighted dimmed');
611
+ },
612
+
613
+ onClickDelete: function(evt) {
614
+ evt && evt.preventDefault();
615
+ this.model.destroy();
616
+ },
617
+
618
+ onRemoveModel: function() {
619
+ $(this.el).remove();
620
+ }
621
+ });
622
+
623
+ var sourceListInst;
624
+ var SourceList = Backbone.View.extend({
625
+ tagName: 'div',
626
+ className: "source-list",
627
+
628
+
629
+ events: {
630
+ 'click .cancel': 'onClickCancel',
631
+ 'click .ok': 'onClickOk',
632
+ 'click input:checkbox': 'onCheck'
633
+ },
634
+
635
+ initialize: function(opts) {
636
+ _.bindAll(this, 'render', 'show', 'fetch', 'onClickOk', 'onClickCancel', 'onCheck');
637
+
638
+ // Items are objects like this: {"source":"auth.inline_logged_in","kind":"counter"}
639
+ this.items = [];
640
+ this.callback = function() {};
641
+ this.cancel = function() {};
642
+ this.fetch();
643
+
644
+ $(document.body).append(this.render().hide().el);
645
+ },
646
+
647
+ render: function() {
648
+ var el = $(this.el)
649
+ , html
650
+ ;
651
+
652
+ html = [
653
+ '<div class="head">',
654
+ '<h2>Choose multiple counters or one timer:</h2>',
655
+ // '<input type="text" class="idea" />',
656
+ '<a href="#" class="cancel">X</a>',
657
+ '</div>',
658
+ '<ul>',
659
+ '</ul>',
660
+ '<div class="foot">',
661
+ '<button class="ok">Ok</button>',
662
+ '</div>'
663
+ ];
664
+
665
+ el.html(html.join(''));
666
+
667
+ var list = el.find('ul');
668
+
669
+ //
670
+ // Construct the DOM of each list item
671
+ //
672
+ _.each(this.items, function(item) {
673
+ html = [
674
+ '<li class="source-item">',
675
+ '<label>',
676
+ '<input type="checkbox" class="' + item.kind + '" />',
677
+ '<span class="source-name">' + item.kind + ": " + item.source + '</span>',
678
+ '</label>',
679
+ '</li>'
680
+ ];
681
+ var domItem = $(html.join(''));
682
+ domItem.find('input').val(JSON.stringify(item)).data('source', item);
683
+ list.append(domItem);
684
+ });
685
+
686
+ if (this.items.length == 0) {
687
+ list.append('<li class="no-data source-item">No data detected recently</li>');
688
+ }
689
+
690
+ return this;
691
+ },
692
+
693
+ show: function(opts) {
694
+ opts = opts || {};
695
+
696
+ var dir = opts.direction || 'right'
697
+ , el = $(this.el)
698
+ ;
699
+
700
+ this.callback = opts.callback || function() {};
701
+ this.cancel = opts.cancel || function() {};
702
+
703
+ this.render();
704
+ el.removeClass('right left').addClass(dir);
705
+ el.css({height: _.min([_.max([200, 100 + this.items.length * 26]), $(window).height()])});
706
+
707
+ if (opts.sources) {
708
+ var checkboxes = el.find('input:checkbox')
709
+ , sources = opts.sources
710
+ ;
711
+
712
+ _.each(checkboxes, function(e) {
713
+ var source = $(e).data('source');
714
+ if (source) {
715
+ var any = _.any(sources, function(s) { return s && source && (s.kind == source.kind) && (s.source == source.source); });
716
+ if (any) {
717
+ $(e).prop('checked', true);
718
+ }
719
+ }
720
+ });
721
+ }
722
+
723
+ el.show();
724
+ },
725
+
726
+ hide: function() {
727
+ $(this.el).hide();
728
+ return this;
729
+ },
730
+
731
+ onClickOk: function(evt) {
732
+ var el = $(this.el)
733
+ , selected = el.find('input:checked')
734
+ , vals = _.map(selected, function(item) { return JSON.parse($(item).val()); })
735
+ ;
736
+
737
+ evt.preventDefault();
738
+
739
+ this.callback(vals);
740
+ this.hide();
741
+ },
742
+
743
+ onClickCancel: function(evt) {
744
+ evt.preventDefault();
745
+ this.cancel();
746
+ return this.hide();
747
+ },
748
+
749
+ onCheck: function(evt) {
750
+ var el = $(this.el)
751
+ , target = $(evt.target)
752
+ , targetChecked = target.prop('checked')
753
+ , source = JSON.parse(target.val());
754
+ ;
755
+
756
+ if (targetChecked) {
757
+ // If you check a timer, everything else is unchecked
758
+ if (source.kind === 'timer') {
759
+ el.find('input:checkbox').prop('checked', false);
760
+ target.prop('checked', true);
761
+ }
762
+
763
+ // If you check a counter, all timers are unchecked
764
+ if (source.kind === 'counter') {
765
+ el.find('input:checkbox.timer').prop('checked', false);
766
+ }
767
+ }
768
+
769
+ },
770
+
771
+ fetch: function() {
772
+ var self = this;
773
+
774
+ $.getJSON('/sources', {}, function(data) {
775
+ self.items = data;
776
+ });
777
+ }
778
+ });
779
+
780
+ var DateRangePicker = Backbone.View.extend({
781
+ events: {
782
+ 'click .custom-date': 'onClickCustomDate',
783
+ 'click .date-reset': 'onClickReset',
784
+ 'blur .date-starting-at': 'onBlurStart',
785
+ 'click input[type=radio]': 'onClickTimeSpan'
786
+ },
787
+
788
+ initialize: function() {
789
+ _.bindAll(this, 'onClickCustomDate', 'onClickReset', 'onBlurStart', 'onClickTimeSpan');
790
+
791
+ this.isCurrent = true;
792
+ this.format = d3.time.format("%Y-%m-%d %H:%M");
793
+ DateRangePicker.instance = this;
794
+ },
795
+
796
+ triggerTimeChange: function() {
797
+ $(document.body).trigger('date_range_changed', this.getRange());
798
+ },
799
+
800
+ getRange: function() {
801
+ return {isCurrent: this.isCurrent, startDate: this.getStartDate(), endDate: this.getEndDate(), granularityInMinutes: this.getGranularityInMinutes()};
802
+ },
803
+
804
+ // Returns '2_hours', '24_hours', '7_days'
805
+ getRangeDuration: function() {
806
+ return $(this.el).find('input[type=radio][name=time_span]:checked').val();
807
+ },
808
+
809
+ getGranularityInMinutes: function() {
810
+ var r = this.getRangeDuration();
811
+ if (r == '24_hours') {
812
+ return 15;
813
+ } else if (r == '7_days') {
814
+ return 60;
815
+ } else {
816
+ return 1;
817
+ }
818
+ },
819
+
820
+ getRangeMilliseconds: function() {
821
+ var r = this.getRangeDuration();
822
+
823
+ if (r == '24_hours') {
824
+ return 24 * 60 * 60 * 1000;
825
+ } else if (r == '7_days') {
826
+ return 7 * 24 * 60 * 60 * 1000;
827
+ } else {
828
+ return 2 * 60 * 60 * 1000; // 2 hour
829
+ }
830
+ },
831
+
832
+ defaultStartDate: function() {
833
+ var r = this.getRangeDuration()
834
+ , now = +new Date()
835
+ ;
836
+
837
+ return new Date(now - this.getRangeMilliseconds());
838
+ },
839
+
840
+ // Get the start time as entered by the user, or the default.
841
+ getStartDate: function() {
842
+ var startInput = this.$('.date-starting-at')
843
+ , startVal = startInput.val().trim()
844
+ ;
845
+
846
+ if (this.isCurrent || !startVal) {
847
+ return this.defaultStartDate();
848
+ } else {
849
+ return this.format.parse(startVal) || this.defaultStartDate();
850
+ }
851
+ },
852
+
853
+ // Calculate the end time
854
+ getEndDate: function() {
855
+ return new Date(+this.getStartDate() + this.getRangeMilliseconds());
856
+ },
857
+
858
+ setCurrent: function(wantItToBeCurrent) {
859
+ var el = $(this.el)
860
+ , startInput
861
+ ;
862
+ this.isCurrent = wantItToBeCurrent;
863
+ if (this.isCurrent) {
864
+ el.addClass('current');
865
+ } else {
866
+ startInput = this.$('.date-starting-at');
867
+ el.removeClass('current');
868
+ startInput.val(this.format(this.defaultStartDate())).focus();
869
+ }
870
+ this.updateEndingAt();
871
+ },
872
+
873
+ updateEndingAt: function() {
874
+ var endingEl = this.$('.ending-at span')
875
+ if (this.isCurrent) {
876
+ endingEl.text('Now');
877
+ } else {
878
+ endingEl.text(this.format(this.getEndDate()));
879
+ }
880
+ this.triggerTimeChange();
881
+ },
882
+
883
+ onClickCustomDate: function(e) {
884
+ e.preventDefault();
885
+ this.setCurrent(!this.isCurrent);
886
+ },
887
+
888
+ onClickReset: function(e) {
889
+ e.preventDefault();
890
+ this.setCurrent(!this.isCurrent);
891
+ },
892
+
893
+ onBlurStart: function(e) {
894
+ var el = $(this.el)
895
+ , startInput = this.$('.date-starting-at')
896
+ , startVal = startInput.val().trim()
897
+ ;
898
+
899
+ if (!startVal) {
900
+ this.setCurrent(true);
901
+ } else {
902
+ this.updateEndingAt();
903
+ }
904
+ },
905
+
906
+ onClickTimeSpan: function(e) {
907
+ var el = $(this.el)
908
+ , customDate = this.$('.custom-date')
909
+ , range = this.getRangeDuration()
910
+ ;
911
+
912
+ if (range == '24_hours') {
913
+ customDate.text('24 hours ago');
914
+ } else if (range == '7_days') {
915
+ customDate.text('7 days ago');
916
+ } else {
917
+ customDate.text('2 hours ago');
918
+ }
919
+
920
+ this.updateEndingAt();
921
+ }
922
+ });
923
+
924
+ DateRangePicker.instance = null;
925
+ DateRangePicker.getCurrentRange = function() {
926
+ return DateRangePicker.instance.getRange();
927
+ };
928
+
929
+ $(function() {
930
+ var dblv = new DashboardListView({el: $('#dashboards')});
931
+
932
+ new DashboardPane({el: $('#current-dashboard'), dashboardListView: dblv});
933
+ new DateRangePicker({el: $('#date-range-picker')});
934
+
935
+ sourceListInst = new SourceList(); // TODO: refactor to use instance pattern.
936
+ });
937
+
938
+ }(jQuery));