mouth 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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));