PageRenderTime 59ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/server/vr/server/static/js/vr.js

https://bitbucket.org/yougov/velociraptor
JavaScript | 1207 lines | 823 code | 205 blank | 179 comment | 59 complexity | 9f9482d518bc96c03830bef063bcaaef MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0
  1. // Welcome to Velociraptor's Javascript UI code. Most everything below is
  2. // built out of Backbone models and views.
  3. // This VR object is the only thing that Velociraptor puts in the global
  4. // namespace. All our other code should be nested under here.
  5. VR = {};
  6. VR.Models = {};
  7. VR.Views = {};
  8. // VR.Urls contains data and functions for getting urls to the Velociraptor
  9. // API. There are three main parts to Velociraptor's API:
  10. //
  11. // 1 - The RESTful API over the Django models. This uses the Tastypie API
  12. // framework, which provides for very consistent URLs. To get a URL to a
  13. // Tastypie-provided resource, call VR.Urls.getTasty.
  14. //
  15. // 2 - The quasi-RESTful API for procs. I say 'quasi' because I don't really
  16. // know how to express 'restart' restfully, and also because some state changes
  17. // (like proc deletion) are actually offloaded to Celery workers and handled
  18. // asyncronously. So your DELETE will return instantly but the resource will
  19. // still exist until the worker has finished.
  20. //
  21. // 3 - Server-Sent-Event streams. These let the Velociraptor web processes
  22. // push messages out to the browser when something happens in the system.
  23. VR.Urls = {
  24. // All Tastypie-provided resources are within this path.
  25. root: '/api/v1/',
  26. // Path for static resources
  27. static: '/static/',
  28. getTasty: function (resource, name, params) {
  29. // A helper for generating urls to talk to the VR Tastypie API. This
  30. // seems the least verbose way of doing this.
  31. //
  32. // 'resource' is a name of a Tastypie-provided resource. You'll most
  33. // often be passing 'apps', 'hosts', 'squads', or 'swarms' as the
  34. // resource. The full list of available resources can be seen at the api
  35. // root url.
  36. //
  37. // 'name' is the key by which instances of that resource are identified.
  38. // For hosts, it's the hostname. For many resources, it's the integer ID
  39. // used in the Django DB. Click around the JSON API to see which you
  40. // need to provide in a given circumstance.
  41. //
  42. // 'params' are optionally query params, in the form of an object
  43. var url;
  44. if (name) {
  45. // we were asked for a particular instance of a resource. Build
  46. // the whole URL.
  47. url = this.root + resource + '/' + name + '/';
  48. } else {
  49. // no instance name passed in, so assume they're asking for the
  50. // list URL.
  51. url = this.root + resource + '/';
  52. }
  53. if (params) {
  54. // Attached url encoded params
  55. url += '?' + $.param(params);
  56. }
  57. return url;
  58. },
  59. getProc: function (hostname, procname) {
  60. // unlike the tastypie resources, the procs API is normal Django views
  61. // nested inside the Tastypie API, so they have a different url
  62. // pattern.
  63. return VR.Urls.getTasty('hosts', hostname) + 'procs/' + procname + '/';
  64. },
  65. getProcLog: function(hostname, procname) {
  66. // proc logs are served as SSE streams in /api/streams/
  67. return '/api/streams/proc_log/' + hostname + '/' + procname + '/';
  68. },
  69. getProcLogView: function(hostname, procname) {
  70. return '/proclog/' + hostname + '/' + procname + '/';
  71. }
  72. // XXX: Additional routes for event streams are added to VR.Urls in
  73. // base.html, where we can pull URLs out of Django by using {% url ... %}.
  74. };
  75. // All proc objects should subscribe to messages from this object, in the form
  76. // "updateproc:<id>" and "destroyproc:<id>" and update themselves when such a
  77. // message comes in.
  78. // This saves having to drill down through collections or the DOM later if data
  79. // comes in on an API or event stream.
  80. VR.ProcMessages = {};
  81. _.extend(VR.ProcMessages, Backbone.Events);
  82. // Base model for all models that talk to the Tastypie API. This includes
  83. // hosts, squads, swarms, etc. (but not procs and events)
  84. VR.Models.Tasty = Backbone.Model.extend({
  85. url: function() {
  86. return this.attributes.resource_uri;
  87. }
  88. });
  89. VR.Models.Proc = Backbone.Model.extend({
  90. initialize: function() {
  91. this.on('change', this.updateUrl);
  92. VR.ProcMessages.on('updateproc:' + this.id, this.set, this);
  93. VR.ProcMessages.on('destroyproc:'+this.id, this.onDestroyMsg, this);
  94. _.bindAll(this);
  95. },
  96. url: function() {
  97. return VR.Urls.getProc(this.get('host'), this.get('name'));
  98. },
  99. doAction: function(action) {
  100. var proc = this;
  101. $.ajax({
  102. type: 'POST',
  103. url: proc.url(),
  104. data: JSON.stringify({'action': action}),
  105. success: function(data, sts, xhr) {
  106. proc.set(data);
  107. },
  108. dataType: 'json'
  109. });
  110. },
  111. stop: function() { this.doAction('stop'); },
  112. start: function() { this.doAction('start'); },
  113. restart: function() { this.doAction('restart'); },
  114. isRunning: function() {return this.get('statename') === 'RUNNING';},
  115. isStopped: function() {
  116. var state = this.get('statename');
  117. return state === 'STOPPED' || state === 'FATAL';
  118. },
  119. getLog: function() {
  120. // return a ProcLog model bound to this Proc model.
  121. return new VR.Models.ProcLog({proc: this});
  122. },
  123. onDestroyMsg: function(data) {
  124. this.trigger('destroy', this, this.collection);
  125. }
  126. });
  127. VR.Models.ProcLog = Backbone.Model.extend({
  128. // Should be initialized with {proc: <proc object>}
  129. initialize: function(data) {
  130. this.proc = data.proc;
  131. this.url = VR.Urls.getProcLog(this.proc.get('host'), this.proc.get('name'));
  132. this.lines = [];
  133. },
  134. connect: function() {
  135. this.eventsource = new EventSource(this.url);
  136. this.eventsource.onmessage = $.proxy(this.onmessage, this);
  137. },
  138. disconnect: function() {
  139. this.eventsource.close();
  140. },
  141. onmessage: function(msg) {
  142. this.lines.push(msg.data);
  143. this.trigger('add', msg.data);
  144. }
  145. });
  146. VR.Models.ProcList = Backbone.Collection.extend({
  147. model: VR.Models.Proc,
  148. getOrCreate: function(data) {
  149. // if proc with id is in collection, update and return it.
  150. var proc = _.find(this.models, function(proc) {
  151. return proc.id === data.id;
  152. });
  153. if (proc) {
  154. proc.set(data);
  155. return proc;
  156. }
  157. // else create, add, and return.
  158. proc = new VR.Models.Proc(data);
  159. this.add(proc);
  160. return proc;
  161. },
  162. cull: function(host, cutoff) {
  163. // given a cutoff timestamp string, look at .time on each proc in the
  164. // collection. If it's older than the cutoff, then kill it.
  165. // build a separate list of procs to remove because otherwise, we're
  166. // modifying the same list that we're iterating over, which throws things
  167. // off.
  168. var stale = _.filter(this.models, function(proc) {
  169. return proc.get('host') === host && proc.get('now') < cutoff;
  170. });
  171. _.each(stale, function(proc) {this.remove(proc);}, this);
  172. },
  173. stopAll: function() {
  174. this.each(function(proc) {
  175. proc.stop();
  176. });
  177. },
  178. startAll: function() {
  179. this.each(function(proc) {
  180. proc.start();
  181. });
  182. },
  183. restartAll: function() {
  184. this.each(function(proc) {
  185. proc.restart();
  186. });
  187. }
  188. });
  189. VR.Models.Host = VR.Models.Tasty.extend({
  190. initialize: function(data) {
  191. this.procs = new VR.Models.ProcList();
  192. // if there are procs in the data, put them in the collection.
  193. _.each(data.procs, function(el, idx, list) {
  194. var p = new VR.Models.Proc(el);
  195. this.procs.add(p);
  196. }, this);
  197. this.procs.on('add', this.onAddProc, this);
  198. this.procs.on('remove', this.onRemoveProc, this);
  199. },
  200. onProcData: function(ev, data) {
  201. // backbone will instsantiate a new Proc and handle deduplication in the collection for us,
  202. // if procs have a consistent 'id' attribute, which they do.
  203. this.procs.add(data);
  204. },
  205. onAddProc: function(proc) {
  206. this.trigger('addproc', proc);
  207. },
  208. onRemoveProc: function(proc) {
  209. this.trigger('removeproc', proc);
  210. // if there are no more procs, then remove self
  211. if (this.procs.length === 0 && this.collection) {
  212. this.collection.remove(this);
  213. }
  214. }
  215. });
  216. VR.Models.HostList = Backbone.Collection.extend({
  217. model: VR.Models.Host,
  218. onProcData: function(ev, data) {
  219. // see if we already have a host for this proc. If not, make one. Pass the proc down to that host.
  220. var hostId = [data.app_name, data.config_name, data.host].join('-');
  221. var host = this.get(hostId);
  222. if (!host) {
  223. host = new VR.Models.Host({ id: hostId, name: data.host });
  224. this.add(host);
  225. }
  226. host.onProcData(ev, data);
  227. }
  228. });
  229. VR.Models.Swarm = VR.Models.Tasty.extend({
  230. initialize: function() {
  231. this.hosts = new VR.Models.HostList();
  232. this.hosts.on('all', this.onHostsEvent, this);
  233. },
  234. getName: function() {
  235. return [this.get('app_name'),
  236. this.get('config_name'),
  237. this.get('proc_name')].join('-');
  238. },
  239. procIsMine: function(fullProcName) {
  240. // Given a full proc name like
  241. // Velociraptor-1.2.3-local-a4dfd8fa-web-5001, return True if its app,
  242. // config name, and proc name (e.g. 'web') match this swarm.
  243. var split = fullProcName.split('-');
  244. return split[0] === this.get('app_name') &&
  245. split[2] === this.get('config_name') &&
  246. split[4] === this.get('proc_name');
  247. },
  248. fetchByProcData: function(procData) {
  249. // since swarms are instantiated as a side effect of getting proc data,
  250. // we need to take that proc data and build up a Tastypie query to fetch
  251. // full swarm data from the API.
  252. var url = VR.Urls.root
  253. +'swarms/?'
  254. +'app__name='+procData.app_name
  255. +'&config_name='+procData.config_name
  256. +'&proc_name='+procData.proc_name
  257. +'&squad__hosts__name='+procData.host;
  258. // query the URL, and update swarm's attributes from data in first (and
  259. // only) result.
  260. var swarm = this;
  261. $.getJSON(url, function(data, sts, xhr) {
  262. if (data.objects && data.objects.length) {
  263. swarm.set(data.objects[0]);
  264. };
  265. });
  266. },
  267. onProcData: function(ev, data) {
  268. // called when proc data comes down from above
  269. this.hosts.onProcData(ev, data);
  270. },
  271. onHostsEvent: function(event, model, collection, options) {
  272. // all events on our hosts list should be bubbled up to be events on
  273. // the swarm itself.
  274. this.trigger.apply(this, arguments);
  275. // if all my hosts are gone, I should go too.
  276. if (event === 'remove' && this.hosts.length === 0 && this.collection) {
  277. this.collection.remove(this);
  278. }
  279. },
  280. getProcs: function() {
  281. // loop over all hosts in this.hosts and build up an array with all their procs.
  282. // return that.
  283. var procsArrayArray = _.map(this.hosts.models, function(host){return host.procs.models;});
  284. return _.flatten(procsArrayArray);
  285. },
  286. // convenience methods since we'll so often work with procs at the swarm level.
  287. // FIXME: Provide a server-side view that will restart all procs in a swarm with a
  288. // single API call, instead of doing all this looping.
  289. stopAll: function() {
  290. this.hosts.each(function(host) {
  291. host.procs.stopAll();
  292. });
  293. },
  294. startAll: function() {
  295. this.hosts.each(function(host) {
  296. host.procs.startAll();
  297. });
  298. },
  299. restartAll: function() {
  300. this.hosts.each(function(host) {
  301. host.procs.restartAll();
  302. });
  303. }
  304. });
  305. VR.Models.SwarmList = Backbone.Collection.extend({
  306. model: VR.Models.Swarm,
  307. comparator: function(swarm) {
  308. return swarm.getName();
  309. },
  310. getByProcData: function(procData) {
  311. var swarmname = [procData.app_name, procData.config_name,
  312. procData.proc_name].join('-');
  313. // if swarm with id is in collection, return it.
  314. var swarm = this.find(function(swarm) {
  315. return swarm.getName() === swarmname;
  316. });
  317. if (swarm) {
  318. return swarm;
  319. }
  320. // else create, add, and return.
  321. swarm = new VR.Models.Swarm({
  322. 'app_name': procData.app_name,
  323. 'config_name': procData.config_name,
  324. 'proc_name': procData.proc_name
  325. });
  326. // Don't ask the API for swarm data on procs that aren't in swarms.
  327. if (procData.config_name != 'UNKNOWN') {
  328. /**
  329. * Note: this is no longer being called for every proc
  330. * and only when a swarm is clicked
  331. * swarm.fetchByProcData(procData);
  332. */
  333. }
  334. swarm.on('addproc', this.onAddProc, this);
  335. this.add(swarm);
  336. return swarm;
  337. },
  338. cull: function(host, cutoff) {
  339. // call cull on each proclist on each swarm in the collection.
  340. _.each(this.models, function(swarm) {
  341. swarm.procs.cull(host, cutoff);
  342. }, this);
  343. // if there are any swarms wth no procs, remove them.
  344. var empty_swarms = _.filter(this.models, function(swarm) {
  345. return swarm.procs.models.length === 0;
  346. }, this);
  347. _.each(empty_swarms, function(swarm) {this.remove(swarm);}, this);
  348. },
  349. onProcData: function(ev, data) {
  350. // when we hear from above that new proc data has come in, send that down
  351. // the tree.
  352. var swarm = this.getByProcData(data);
  353. swarm.onProcData(ev, data);
  354. }
  355. });
  356. VR.Models.App = VR.Models.Tasty.extend({
  357. initialize: function() {
  358. this.swarms = new VR.Models.SwarmList();
  359. this.swarms.on('all', this.onSwarmsEvent, this);
  360. },
  361. onProcData: function(ev, data) {
  362. this.swarms.onProcData(ev, data);
  363. },
  364. onAddProc: function(proc) {
  365. this.trigger('addproc', proc);
  366. },
  367. onSwarmsEvent: function(event, model, collection, options) {
  368. this.trigger.apply(this, arguments);
  369. }
  370. });
  371. VR.Models.AppList = Backbone.Collection.extend({
  372. model: VR.Models.App,
  373. initialize: function() {
  374. this.on('change', this.updateUrl);
  375. VR.ProcMessages.on('all', this.onProcData, this);
  376. },
  377. comparator: function(app) {
  378. return app.id;
  379. },
  380. getOrCreate: function(name, fetchData) {
  381. // if app with id is in collection, return it.
  382. var app = _.find(this.models, function(app) {
  383. return app.get('name') === name;
  384. });
  385. if (app) {
  386. return app;
  387. }
  388. // else create, add, and return.
  389. app = new VR.Models.App({
  390. name: name,
  391. "class": "appbox",
  392. resource_uri: VR.Urls.getTasty('apps', name)
  393. });
  394. // Don't ask the API for App data on procs that aren't part of a swarm.
  395. if (fetchData) {
  396. app.fetch();
  397. }
  398. this.add(app);
  399. return app;
  400. },
  401. onProcData: function(ev, data) {
  402. // handle updates differently from removals.
  403. var parsed = ev.split(":");
  404. if (parsed[0] === 'updateproc') {
  405. var fetch = data.config_name != 'UNKNOWN';
  406. var app = this.getOrCreate(data.app_name, fetch);
  407. app.onProcData(ev, data);
  408. } else if (parsed[0] === 'destroyproc') {
  409. // TODO: here's where we should handle drilling down to remove
  410. // destroyed procs, empty swarms, and empty apps, rather than from the
  411. // outside in vrdash.js.
  412. }
  413. },
  414. cull: function(host, cutoff) {
  415. // call cull on each swarmlist on each swarm in the collection.
  416. _.each(this.models, function(app) {
  417. app.swarms.cull(host, cutoff);
  418. }, this);
  419. // if there are any apps wth no swarms, remove them.
  420. var empty_apps = _.filter(this.models, function(app) {
  421. return app.swarms.models.length === 0;
  422. }, this);
  423. _.each(empty_apps, function(app) {this.remove(app);}, this);
  424. }
  425. });
  426. VR.Views.Apps = Backbone.View.extend({
  427. initialize: function(appList, container) {
  428. this.apps = appList;
  429. this.container = container;
  430. this.apps.on('add', this.onAdd, this);
  431. _.each(this.apps.models, function(el, idx, list) {
  432. this.onAdd(el);
  433. }, this);
  434. },
  435. onAdd: function(app) {
  436. // draw the new app on the page
  437. var v = new VR.Views.App(app);
  438. v.render();
  439. var inserted = false;
  440. // loop until we see one later than us, alphabetically, and insert
  441. // there.
  442. _.each(this.container.find('.approw'), function(row) {
  443. var title = $(row).find('.apptitle').text();
  444. if (!inserted && title > app.get('name')) {
  445. $(row).before(v.el);
  446. inserted = true;
  447. }
  448. });
  449. // If still not inserted, just append to container
  450. if (!inserted) {
  451. this.container.append(v.el);
  452. }
  453. }
  454. });
  455. // VIEWS
  456. VR.Views.Proc = Backbone.View.extend({
  457. el: '<div class="procview"></div>',
  458. initialize: function(proc, template, modalTemplate) {
  459. this.proc = proc;
  460. // If you don't pass templates, we'll use the ones on VR.Templates.
  461. this.template = template || VR.Templates.Proc;
  462. this.modalTemplate = modalTemplate || VR.Templates.ProcModal;
  463. this.proc.on('change', this.render, this);
  464. this.proc.on('remove', this.onProcRemove, this);
  465. this.proc.on('destroy', this.onProcRemove, this);
  466. this.render();
  467. },
  468. render: function() {
  469. this.$el.html(this.template.goatee(this.proc.toJSON()));
  470. },
  471. events: {
  472. 'click': 'onClick'
  473. },
  474. onProcRemove: function(proc) {
  475. this.remove();
  476. },
  477. onClick: function(ev) {
  478. this.modal = new VR.Views.ProcModal(this.proc);
  479. this.modal.show();
  480. }
  481. });
  482. VR.Views.BaseModal = Backbone.View.extend({
  483. // base class for bootstrap modal views, which need to have tabindex set in
  484. // order for them to be dismissable with the Escape key.
  485. el: '<div class="modal fade" tabindex="-1"></div>'
  486. });
  487. VR.Views.ProcModal = VR.Views.BaseModal.extend({
  488. initialize: function(proc) {
  489. this.proc = proc;
  490. this.fresh = true;
  491. this.connected = false;
  492. this.proc.on('change', this.render, this);
  493. this.proc.on('destroy', this.onProcDestroy, this);
  494. this.proc.on('remove', this.onProcRemove, this);
  495. this.$el.on('show.bs.modal', $.proxy(this.onShown, this));
  496. this.$el.on('hide.bs.modal', $.proxy(this.onHidden, this));
  497. },
  498. render: function() {
  499. // this render function is careful not to do a whole repaint, because
  500. // that would mean that you'd lose your selection if you're trying to
  501. // copy/paste from the streaming log section. That'd be annoying!
  502. var data = this.proc.toJSON();
  503. data.logs_uri = VR.Urls.getProcLog(data.host, data.name);
  504. data.logs_view_uri = VR.Urls.getProcLogView(data.host, data.name);
  505. if (this.fresh) {
  506. // only do a whole render the first time. After that just update the
  507. // bits that have changed.
  508. this.$el.html(VR.Templates.ProcModal.goatee(data));
  509. this.fresh = false;
  510. }
  511. // insert/update details table.
  512. var detailsRows = VR.Templates.ProcModalRows.goatee(data);
  513. this.$el.find('.proc-details-table').html(detailsRows);
  514. // insert/update buttons
  515. var detailsButtons = VR.Templates.ProcModalButtons.goatee(data);
  516. this.$el.find('.modal-footer').html(detailsButtons);
  517. },
  518. events: {
  519. 'click .proc-start': 'onStartBtn',
  520. 'click .proc-stop': 'onStopBtn',
  521. 'click .proc-restart': 'onRestartBtn',
  522. 'click .proc-destroy': 'onDestroyBtn'
  523. },
  524. show: function() {
  525. this.render();
  526. this.$el.modal('show');
  527. this.trigger('show');
  528. },
  529. onShown: function() {
  530. // show streaming logs
  531. if(!this.connected) {
  532. this.log = this.proc.getLog();
  533. var logContainer = this.$el.find('.proc-log');
  534. logContainer.html('');
  535. this.logview = new VR.Views.ProcLog(this.log, logContainer);
  536. this.log.connect();
  537. this.connected = true;
  538. }
  539. },
  540. onHidden: function() {
  541. // stop the log stream.
  542. this.log.disconnect();
  543. this.connected = false;
  544. },
  545. onStartBtn: function(ev) {
  546. this.proc.start();
  547. },
  548. onStopBtn: function(ev) {
  549. this.proc.stop();
  550. },
  551. onRestartBtn: function(ev) {
  552. this.proc.restart();
  553. },
  554. onDestroyBtn: function(ev) {
  555. this.proc.destroy({wait: true, sync: true});
  556. },
  557. onProcDestroy: function() {
  558. this.$el.modal('hide');
  559. // Remove $el after the modal has been completely hidden
  560. this.$el.on('hidden.bs.modal', this.$el.remove);
  561. }
  562. });
  563. VR.Views.ProcLog = Backbone.View.extend({
  564. initialize: function(proclog, container, scrollContainer) {
  565. this.log = proclog;
  566. this.container = container;
  567. // optionally allow passing in a different element to be scrolled.
  568. this.scrollContainer = scrollContainer || container;
  569. this.log.on('add', this.onmessage, this);
  570. this.render();
  571. },
  572. render: function() {
  573. this.container.append(this.el);
  574. },
  575. onmessage: function(line) {
  576. var node = $('<div />');
  577. node.text(line);
  578. this.$el.append(node);
  579. // set scroll to bottom of div.
  580. var height = this.scrollContainer[0].scrollHeight;
  581. this.scrollContainer.scrollTop(height);
  582. },
  583. clear: function() {
  584. // stop listening for model changes
  585. this.log.off('add', this.onmessage, this);
  586. // clean out lines
  587. this.lines = [];
  588. }
  589. });
  590. VR.Views.Host = Backbone.View.extend({
  591. el: '<tr class="hostview"></tr>',
  592. initialize: function(host, template) {
  593. this.host = host;
  594. this.template = template || VR.Templates.Host;
  595. // when a new proc is added, make sure it gets rendered in here
  596. this.host.procs.on('add', this.renderProc, this);
  597. this.host.on('remove', this.onRemove, this);
  598. this.render();
  599. },
  600. renderProc: function(proc) {
  601. var pv = new VR.Views.Proc(proc, VR.Templates.Proc);
  602. this.grid.append(pv.el);
  603. },
  604. render: function() {
  605. this.$el.html(this.template.goatee(this.host.attributes));
  606. this.grid = this.$el.find('.procgrid');
  607. this.host.procs.each(this.renderProc, this);
  608. },
  609. onRemove: function() {
  610. this.$el.remove();
  611. }
  612. });
  613. VR.Views.ProcList = Backbone.View.extend({
  614. el: '<div class="procboxes"></div>',
  615. initialize: function(owner) {
  616. // owner is an object that may fire the 'addproc' event, which could be
  617. // an app, swarm, or host.
  618. this.owner = owner;
  619. // whenever the owner adds a proc, add a view to ourself.
  620. this.owner.on('addproc', this.addProcView, this);
  621. },
  622. addProcView: function(proc) {
  623. // create a procview.
  624. var pv = new VR.Views.Proc(proc);
  625. // insert it in my container
  626. this.$el.append(pv.el);
  627. }
  628. });
  629. VR.Views.Swarm = Backbone.View.extend({
  630. el: '<tr class="swarmbox"></tr>',
  631. initialize: function(swarm, template) {
  632. this.swarm = swarm;
  633. this.template = template || VR.Templates.Swarm;
  634. // what a new host gets added, make sure we render a hostview for it
  635. this.swarm.hosts.on('add', this.hostAdded, this);
  636. // when a new proc is added, make sure it gets rendered in here
  637. this.swarm.hosts.on('addproc', this.procAdded, this);
  638. this.swarm.on('remove', this.onRemove, this);
  639. this.procsEl = this.$el.find('.procgrid');
  640. },
  641. events: {
  642. 'click .swarmtitle': 'onClick',
  643. 'click th .expandtree': 'toggleExpanded'
  644. },
  645. onClick: function(ev) {
  646. if (!this.modal) {
  647. this.modal = new VR.Views.SwarmModal(this.swarm);
  648. }
  649. this.modal.show();
  650. },
  651. toggleExpanded: function() {
  652. this.$el.toggleClass('biggened');
  653. this.$el.find('i').toggleClass('icon-caret-right').toggleClass('icon-caret-down');
  654. },
  655. hostAdded: function(host) {
  656. var hv = new VR.Views.Host(host);
  657. this.$el.find('.hosttable').append(hv.el);
  658. },
  659. procAdded: function(proc) {
  660. var pv = new VR.Views.Proc(proc);
  661. pv.render();
  662. this.$el.children('.procgrid').append(pv.el);
  663. },
  664. render: function() {
  665. this.$el.html(this.template.goatee(this.swarm.toJSON()));
  666. },
  667. onRemove: function(model) {
  668. // filter out host removal events that have bubbled up from below
  669. if (model === this) {
  670. this.$el.remove();
  671. try {
  672. // GC on 'swarm' model
  673. model.trigger('destroy', model);
  674. } catch(e) {}
  675. }
  676. }
  677. });
  678. VR.Views.AppModal = VR.Views.BaseModal.extend({
  679. initialize: function(app, template) {
  680. this.app = app;
  681. this.template = template || VR.Templates.AppModal;
  682. },
  683. render: function() {
  684. this.modelUpdated = false;
  685. function clean_url(url) {
  686. // Remove any credentials in the form:
  687. // http://<user>:<pass>@example.com
  688. var newurl = url.replace(/\/\/.*@/, '//');
  689. // Strip .git suffix for kiln repos
  690. if (newurl.indexOf('kilnhg.com') >= 0) {
  691. newurl = newurl.replace(/.git$/, '');
  692. }
  693. return newurl;
  694. }
  695. var data = this.app.toJSON();
  696. data.repo_url_clean = clean_url(data.resolved_url);
  697. this.$el.html(this.template.goatee(data));
  698. },
  699. show: function() {
  700. this.render();
  701. this.$el.modal('show');
  702. }
  703. });
  704. VR.Views.SwarmModal = VR.Views.BaseModal.extend({
  705. initialize: function(swarm, template) {
  706. this.swarm = swarm;
  707. this.current_state = '';
  708. this.template = template || VR.Templates.SwarmModal;
  709. // FIXME: if a new proc is added to the swarm/host while the modal is open
  710. // will we see it? I think we need to listen for 'addproc' on the swarm to
  711. // catch that.
  712. this.listenTo(this.swarm, 'remove', this.remove);
  713. this.listenTo(this.swarm, 'all', this.onSwarmEvent, this);
  714. this.swarm.hosts.each(function(host) {
  715. this.listenTo(host.procs, 'all', this.updateState, this);
  716. }, this);
  717. },
  718. events: {
  719. 'click .swarm-start': 'onStartBtn',
  720. 'click .swarm-stop': 'onStopBtn',
  721. 'click .swarm-restart': 'onRestartBtn'
  722. },
  723. render: function() {
  724. this.modelUpdated = false;
  725. this.$el.html(this.template.goatee(this.swarm.toJSON()));
  726. var procs = this.swarm.getProcs();
  727. _.each(procs, function(proc) {
  728. this.procAdded(proc);
  729. }, this);
  730. },
  731. update: function() {
  732. this.$el.html(this.template.goatee(this.swarm.toJSON()));
  733. var procs = this.swarm.getProcs();
  734. _.each(procs, function(proc) {
  735. this.procAdded(proc);
  736. }, this);
  737. },
  738. show: function() {
  739. this.render();
  740. this.$el.modal('show');
  741. this.updateState();
  742. },
  743. procAdded: function(proc) {
  744. var pv = new VR.Views.Proc(proc);
  745. // unbind the model click events inside the swarm modal.
  746. pv.undelegateEvents();
  747. pv.render();
  748. this.$el.find('.procboxes').append(pv.el);
  749. this.updateState();
  750. },
  751. updateState: function() {
  752. // if there are any stopped/fatal procs, add the class to show the start
  753. // button.
  754. var procs = this.swarm.getProcs();
  755. if (_.some(procs, function(proc) {return proc.isStopped();})) {
  756. this.$el.addClass('somestopped');
  757. } else {
  758. this.$el.removeClass('somestopped');
  759. }
  760. // if there are any running procs, add the class to show the stop button
  761. if (_.some(procs, function(proc) {return proc.isRunning();})) {
  762. this.$el.addClass('somerunning');
  763. } else {
  764. this.$el.removeClass('somerunning');
  765. }
  766. if(!this.modelUpdated) this.swarm.fetchByProcData(procs[0].attributes);
  767. this.modelUpdated = true;
  768. },
  769. onRemove: function() {
  770. this.$el.remove();
  771. },
  772. onStartBtn: function(ev) {
  773. this.swarm.startAll();
  774. },
  775. onStopBtn: function(ev) {
  776. this.swarm.stopAll();
  777. },
  778. onRestartBtn: function(ev) {
  779. this.swarm.restartAll();
  780. },
  781. onSwarmEvent: function(event, model, collection, options) {
  782. //console.log(arguments);
  783. if(this.swarm.get('id')) this.update();
  784. },
  785. onProcEvent: function(event, model, collection, options) {
  786. //console.log(arguments);
  787. }
  788. });
  789. // Modal that warns about pool hijacking.
  790. VR.Views.SwarmWarningModal = Backbone.View.extend({
  791. el: '<div class="modal fade" tabindex="-1" data-backdrop="static"></div>',
  792. initialize: function(swarmInfo, proceedCallback) {
  793. this.swarmInfo = swarmInfo;
  794. this.template = VR.Templates.SwarmWarningModal;
  795. this.proceedCallback = proceedCallback || function () {return;};
  796. },
  797. events: {
  798. 'click .swarm-warning-proceed': 'onProceedBtn',
  799. 'click .swarm-warning-cancel': 'onCancelBtn'
  800. },
  801. render: function() {
  802. this.$el.html(this.template.goatee(this.swarmInfo));
  803. },
  804. show: function() {
  805. this.render();
  806. this.$el.modal('show');
  807. },
  808. onProceedBtn: function() {
  809. this.$el.modal('hide');
  810. this.proceedCallback();
  811. },
  812. onCancelBtn: function() {
  813. this.$el.modal('hide');
  814. }
  815. });
  816. VR.Views.App = Backbone.View.extend({
  817. el: '<tr class="approw"></tr>',
  818. initialize: function(app, template) {
  819. this.app = app;
  820. this.template = template || VR.Templates.App;
  821. this.app.swarms.on('add', this.swarmAdded, this);
  822. this.app.on('remove', this.onRemove, this);
  823. },
  824. swarmAdded: function(swarm) {
  825. var sv = new VR.Views.Swarm(swarm);
  826. sv.render();
  827. this.$el.find('.swarmtable').append(sv.el);
  828. },
  829. events: {
  830. 'click .titlecell .expandtree': 'toggleExpanded',
  831. 'click .apptitle': 'onClick'
  832. },
  833. onClick: function(ev) {
  834. if (!this.modal) {
  835. this.modal = new VR.Views.AppModal(this.app);
  836. }
  837. this.modal.show();
  838. },
  839. toggleExpanded: function() {
  840. this.$el.toggleClass('biggened');
  841. // only toggle this arrow, not the ones deeper inside
  842. this.$el.find('.titlecell > .expandtree > i').toggleClass('icon-caret-right').toggleClass('icon-caret-down');
  843. },
  844. render: function() {
  845. this.$el.html(this.template.goatee(this.app.toJSON()));
  846. var plv = new VR.Views.ProcList(this.app);
  847. this.$el.find('td').append(plv.el);
  848. },
  849. onRemove: function(model) {
  850. // swarm removal events are bubbled up here as well as app removal
  851. // events. Only remove self if self.app is the thing just removed
  852. if (model === this.app) {
  853. this.$el.remove();
  854. try {
  855. // GC on 'app' model
  856. model.trigger('destroy', model);
  857. } catch(e) {}
  858. }
  859. }
  860. });
  861. // Events
  862. // For displaying stuff that's going on all over the system.
  863. // Since they're just for display, the Event model itself has just data. No
  864. // behavior.
  865. VR.Models.Event = Backbone.Model.extend({});
  866. VR.Models.Events = Backbone.Collection.extend({
  867. model: VR.Models.Event,
  868. initialize: function(maxlength) {
  869. // By default, show only 100 messages. We don't want the message pane
  870. // to grow forever on the dashboard, or in memory.
  871. this.maxlength = maxlength || 100;
  872. this.on('add', this.trim, this);
  873. },
  874. trim: function() {
  875. // ensure that there are only this.maxlength items in the collection.
  876. // The rest should be discarded.
  877. while (this.models.length > this.maxlength) {
  878. var model = this.at(0);
  879. this.remove(model);
  880. // model garbage collection
  881. model.trigger('destroy', model);
  882. }
  883. }
  884. });
  885. // This view renders the clickable summary with the icon in the right hand pane
  886. // of the dashboard.
  887. VR.Views.Event = Backbone.View.extend({
  888. initialize: function(model, template, modalTemplate) {
  889. this.model = model;
  890. this.model.on('destroy', this.onDestroy, this);
  891. this.template = template || VR.Templates.Event;
  892. this.modalTemplate = modalTemplate || VR.Templates.EventModal;
  893. this.render();
  894. },
  895. render: function() {
  896. this.$el.html(this.template.goatee(this.model.attributes));
  897. },
  898. onDestroy: function() {
  899. this.$el.remove();
  900. },
  901. events: {
  902. 'click': 'onClick'
  903. },
  904. onClick: function(ev) {
  905. // When you click an Event, you should see an EventModal modal. These
  906. // are created on the fly when first requested.
  907. if (!this.modal) {
  908. this.modal = new VR.Views.EventModal(this.model, this.modalTemplate);
  909. }
  910. this.modal.show();
  911. }
  912. });
  913. // The modal that provides additional details about the event.
  914. VR.Views.EventModal = VR.Views.BaseModal.extend({
  915. initialize: function(model, template) {
  916. this.model = model;
  917. this.template = template || VR.Templates.EventModal;
  918. this.model.on('change', this.render, this);
  919. this.model.on('remove', this.onRemove, this);
  920. },
  921. render: function() {
  922. this.$el.html(this.template.goatee(this.model.attributes));
  923. },
  924. show: function() {
  925. this.render();
  926. this.$el.modal('show');
  927. },
  928. onRemove: function() {
  929. this.$el.remove();
  930. }
  931. });
  932. // The view for the pane that shows individual events inside.
  933. VR.Views.Events = Backbone.View.extend({
  934. initialize: function(collection, container, template, modalTemplate) {
  935. this.collection = collection;
  936. this.collection.on('remove', this.onRemove, this);
  937. this.collection.on('add', this.onAdd, this);
  938. // container should also be already wrapped in a jquery
  939. this.container = container;
  940. this.template = template || VR.Templates.Event;
  941. this.modalTemplate = modalTemplate || VR.Templates.EventModal;
  942. },
  943. onAdd: function(model) {
  944. // create model view and bind it to the model
  945. var mv = new VR.Views.Event(model, this.template, this.modalTemplate);
  946. this.container.prepend(mv.$el);
  947. },
  948. onRemove: function(model) {
  949. model.trigger('destroy', model);
  950. }
  951. });
  952. // Initialization of the Events system happens by calling VR.Events.init
  953. VR.Events = {
  954. init: function(container, streamUrl) {
  955. // bind stream to handler
  956. this.stream = new EventSource(streamUrl);
  957. this.stream.onmessage = $.proxy(this.onEvent, this);
  958. this.collection = new VR.Models.Events();
  959. this.listview = new VR.Views.Events(
  960. this.collection,
  961. container
  962. );
  963. },
  964. onEvent: function(e) {
  965. var data = JSON.parse(e.data);
  966. // Messages may be hidden.
  967. if (!_.contains(data.tags, 'hidden')) {
  968. data.time = new Date(data.time);
  969. data.prettytime = data.time.format("mm/dd HH:MM:ss");
  970. data.id = e.lastEventId;
  971. data.classtags = data.tags.join(' ');
  972. var evmodel = new VR.Models.Event(data);
  973. this.collection.add(evmodel);
  974. }
  975. }
  976. };
  977. // Util
  978. // Generic-use things that didn't seem to fit in a specific model.
  979. VR.Util = {
  980. isNumber: function(n){
  981. return !isNaN(parseFloat(n)) && isFinite(n);
  982. },
  983. procIsOurs: function(proc){
  984. // if we can use a "-" to split a procname into 6 parts, and the last
  985. // one is a port number, then guess that this is a proc that the
  986. // dashboard can control.
  987. var parts = proc.split('-');
  988. var len = parts.length;
  989. return len === 6 && this.isNumber(parts[len-1]);
  990. },
  991. cleanName: function(host){
  992. return host.replace(/\./g, "");
  993. },
  994. createID: function(host, proc){
  995. return host.replace(/\./g, "") + proc.replace(/\./g, "");
  996. },
  997. clearStatus: function(proc) {
  998. _.each(['RUNNING', 'STOPPED', 'FATAL', 'BACKOFF', 'STARTING'], function(el, idx, lst) {
  999. proc.removeClass('status-' + el);
  1000. });
  1001. }
  1002. }; // end Utilities