Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(146)

Side by Side Diff: app/views/service.js

Issue 13997043: Remove DB.units list
Patch Set: Remove DB.units list Created 11 years, 6 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « app/views/landscape.js ('k') | app/views/topology/service.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 /*
2 This file is part of the Juju GUI, which lets users view and manage Juju
3 environments within a graphical interface (https://launchpad.net/juju-gui).
4 Copyright (C) 2012-2013 Canonical Ltd.
5
6 This program is free software: you can redistribute it and/or modify it under
7 the terms of the GNU Affero General Public License version 3, as published by
8 the Free Software Foundation.
9
10 This program is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12 SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
13 General Public License for more details.
14
15 You should have received a copy of the GNU Affero General Public License along
16 with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 'use strict';
20
21
22 /**
23 * Provide the service views and mixins.
24 *
25 * @module views
26 * @submodule views.services
27 */
28
29 YUI.add('juju-view-service', function(Y) {
30
31 var views = Y.namespace('juju.views'),
32 Templates = views.Templates,
33 inspector = Y.namespace('juju.views.inspector'),
34 models = Y.namespace('juju.models'),
35 plugins = Y.namespace('juju.plugins'),
36 utils = Y.namespace('juju.views.utils'),
37 viewletNS = Y.namespace('juju.viewlets');
38
39 var removeServiceMixin = {
40 // Mixin attributes
41 events: {
42 '#destroy-service': {
43 click: 'confirmDestroy'
44 }
45 },
46
47 confirmDestroy: function(ev) {
48 ev.halt();
49 // We wait to make the panel until now, because in the render method
50 // the container is not yet part of the document.
51 if (Y.Lang.isUndefined(this.panel)) {
52 this.panel = views.createModalPanel(
53 'Are you sure you want to destroy the service? ' +
54 'This cannot be undone.',
55 '#destroy-modal-panel',
56 'Destroy Service',
57 Y.bind(this.destroyService, this)
58 );
59 }
60 this.panel.show();
61 },
62
63 destroyService: function(ev) {
64 ev.preventDefault();
65 var env = this.get('env'),
66 service = this.get('model');
67 ev.target.set('disabled', true);
68 env.destroy_service(
69 service.get('id'), Y.bind(this._destroyCallback, this));
70 },
71
72 _destroyCallback: function(ev) {
73 var db = this.get('db'),
74 getModelURL = this.get('getModelURL'),
75 service = this.get('model'),
76 service_id = service.get('id');
77
78 if (ev.err) {
79 db.notifications.add(
80 new models.Notification({
81 title: 'Error destroying service',
82 message: 'Service name: ' + ev.service_name,
83 level: 'error',
84 link: getModelURL(service),
85 modelId: service
86 })
87 );
88 } else {
89 db.services.remove(service);
90 db.relations.remove(
91 db.relations.filter(
92 function(r) {
93 return Y.Array.some(r.get('endpoints'), function(ep) {
94 return ep[0] === service_id;
95 });
96 }
97 ));
98 this.panel.hide();
99 this.panel.destroy();
100 this.fire('navigateTo', {url: this.get('nsRouter').url({gui: '/'})});
101 db.fire('update');
102 }
103 }
104 };
105
106
107 /**
108 * @class ServiceViewBase
109 */
110 var ServiceViewBase = Y.Base.create('ServiceViewBase', Y.View,
111 [views.JujuBaseView], {
112
113 initializer: function() {
114 Y.mix(this, inspector.exposeButtonMixin,
115 undefined, undefined, undefined, true);
116 Y.mix(this, inspector.manageUnitsMixin,
117 undefined, undefined, undefined, true);
118 Y.mix(this, removeServiceMixin, undefined, undefined, undefined,
119 true);
120
121 // Bind visualization resizing on window resize.
122 Y.on('windowresize', Y.bind(function() {
123 this.fitToWindow();
124 }, this));
125 },
126
127 getServiceTabs: function(href) {
128 var db = this.get('db'),
129 service = this.get('model'),
130 getModelURL = this.get('getModelURL'),
131 charmId = service.get('charm'),
132 charm = db.charms.getById(charmId),
133 charmUrl = (charm ? getModelURL(charm) : '#');
134
135 var tabs = [{
136 href: getModelURL(service),
137 title: 'Units',
138 active: false
139 }, {
140 href: getModelURL(service, 'relations'),
141 title: 'Relations',
142 active: false
143 }, {
144 href: getModelURL(service, 'config'),
145 title: 'Settings',
146 active: false
147 }, {
148 href: charmUrl,
149 title: 'Charm',
150 active: false
151 }, {
152 href: getModelURL(service, 'constraints'),
153 title: 'Constraints',
154 active: false
155 }];
156
157 Y.each(tabs, function(value) {
158 if (value.href === href) {
159 value.active = true;
160 }
161 });
162
163 return tabs;
164 },
165
166 /**
167 Fit to window. Must be called after the container
168 has been added to the DOM.
169
170 @method containerAttached
171 */
172 containerAttached: function() {
173 this.fitToWindow();
174 },
175
176 fitToWindow: function() {
177 function getHeight(node) {
178 if (!node) {
179 return 0;
180 }
181 return node.get('clientHeight');
182 }
183 var container = this.get('container'),
184 viewContainer = container.one('.view-container');
185 if (viewContainer) {
186 Y.fire('beforePageSizeRecalculation');
187 var navbarHeight = getHeight(Y.one('.navbar')),
188 windowHeight = container.get('winHeight'),
189 headerHeight = getHeight(container.one(
190 '.service-header-partial')),
191 footerHeight = getHeight(container.one('.bottom-navbar')),
192 size = (Math.max(windowHeight, 600) - navbarHeight -
193 headerHeight - footerHeight - 19);
194 viewContainer.set('offsetHeight', size);
195 Y.fire('afterPageSizeRecalculation');
196 }
197 },
198
199 /**
200 Reject callback for the model promise which creates an error
201 notification and then redirects the user to the evironment view
202
203 @method noServiceAvailable
204 */
205 noServiceAvailable: function() {
206 this.get('db').notifications.add(
207 new Y.juju.models.Notification({
208 title: 'Service is not available',
209 message: 'The service you are trying to view does not exist',
210 level: 'error'
211 })
212 );
213
214 this.fire('navigateTo', {
215 url: this.get('nsRouter').url({gui: '/'})
216 });
217 },
218
219 /**
220 Shared rendering method to render the loading service data view
221
222 @method renderLoading
223 */
224 renderLoading: function() {
225 var container = this.get('container');
226 container.setHTML(
227 '<div class="alert">Loading service details...</div>');
228 console.log('waiting on service data');
229 },
230
231 /**
232 Shared rendering method to render the service data view
233
234 @method renderData
235 */
236 renderData: function() {
237 var container = this.get('container');
238 var service = this.get('model');
239 var db = this.get('db');
240 var env = db.environment.get('annotations');
241 container.setHTML(this.template(this.gatherRenderData()));
242 // to be able to use this same method for all service views
243 if (container.one('.landscape-controls')) {
244 Y.juju.views.utils.updateLandscapeBottomBar(this.get('landscape'),
245 env, service, container);
246 }
247 },
248
249 /**
250 Shared render method to be used in service detail views
251
252 @method render
253 @return {Object} view instance.
254 */
255 render: function() {
256 var model = this.get('model');
257 if (!model) {
258 this.renderLoading();
259 } else {
260 this.renderData();
261 }
262 return this;
263 }
264
265 });
266 views.serviceBase = ServiceViewBase;
267
268 /**
269 * @class ServiceRelationsView
270 */
271 views.service_relations = Y.Base.create(
272 'ServiceRelationsView', ServiceViewBase, [
273 views.JujuBaseView], {
274
275 template: Templates['service-relations'],
276
277 events: {
278 '#service-relations .btn': {click: 'confirmRemoved'}
279 },
280
281 /**
282 * Gather up all of the data required for the template.
283 *
284 * Aside from a nice separation of concerns, this method also
285 * facilitates testing.
286 *
287 * @method gatherRenderData
288 * @return {Object} The data the template will render.
289 */
290 gatherRenderData: function() {
291 var service = this.get('model'),
292 db = this.get('db'),
293 querystring = this.get('querystring');
294 var relation_data = utils.getRelationDataForService(db, service);
295 Y.each(relation_data, function(rel) {
296 if (rel.elementId === querystring.rel_id) {
297 rel.highlight = true;
298 }
299 });
300 var charm_id = service.get('charm'),
301 charm = db.charms.getById(charm_id),
302 charm_attrs = charm ? charm.getAttrs() : undefined;
303 return {
304 viewName: 'relations',
305 tabs: this.getServiceTabs('relations'),
306 service: service.getAttrs(),
307 landscape: this.get('landscape'),
308 serviceModel: service,
309 relations: relation_data,
310 charm: charm_attrs,
311 charm_id: charm_id,
312 serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id),
313 serviceRemoteUri: this.get('nsRouter').url({ gui: '/service/'})
314 };
315 },
316
317 confirmRemoved: function(ev) {
318 // We wait to make the panel until now, because in the render method
319 // the container is not yet part of the document.
320 ev.preventDefault();
321 if (Y.Lang.isUndefined(this.remove_panel)) {
322 this.remove_panel = views.createModalPanel(
323 'Are you sure you want to remove this service relation? ' +
324 'This action cannot be undone, though you can ' +
325 'recreate it later.',
326 '#remove-modal-panel');
327 }
328 // We set the buttons separately every time because we want to bind
329 // the target, which can vary. Since the page is redrawn after a
330 // relation is removed, this is technically unnecessary in this
331 // particular case, but a good pattern to get into.
332 views.setModalButtons(
333 this.remove_panel,
334 'Remove Service Relation',
335 Y.bind(this.doRemoveRelation, this, ev.target));
336 this.remove_panel.show();
337 },
338
339 doRemoveRelation: function(button, ev) {
340 ev.preventDefault();
341 var rel_id = button.get('value'),
342 db = this.get('db'),
343 env = this.get('env'),
344 service = this.get('model'),
345 relation = db.relations.getById(rel_id),
346 endpoints = relation.get('endpoints'),
347 endpoint_a = endpoints[0],
348 endpoint_b;
349
350 if (endpoints.length === 1) {
351 // For a peer relationship, both endpoints are the same.
352 endpoint_b = endpoint_a;
353 } else {
354 endpoint_b = endpoints[1];
355 }
356
357 ev.target.set('disabled', true);
358
359 env.remove_relation(
360 endpoint_a,
361 endpoint_b,
362 Y.bind(this._removeRelationCallback, this,
363 relation, button, ev.target));
364 },
365
366 _removeRelationCallback: function(relation, rm_button,
367 confirm_button, ev) {
368 var db = this.get('db'),
369 getModelURL = this.get('getModelURL'),
370 service = this.get('model');
371 views.highlightRow(rm_button.ancestor('tr'), ev.err);
372 if (ev.err) {
373 db.notifications.add(
374 new models.Notification({
375 title: 'Error deleting relation',
376 message: 'Relation ' + ev.endpoint_a + ' to ' + ev.endpoint_b,
377 level: 'error',
378 link: getModelURL(service) + 'relations?rel_id=' +
379 rm_button.get('id'),
380 modelId: relation
381 })
382 );
383 } else {
384 db.relations.remove(relation);
385 db.fire('update');
386 }
387 confirm_button.set('disabled', false);
388 this.remove_panel.hide();
389 }
390 });
391
392 /**
393 * @class ServiceConstraintsView
394 */
395 views.service_constraints = Y.Base.create(
396 'ServiceConstraintsView', ServiceViewBase, [
397 views.JujuBaseView], {
398
399 template: Templates['service-constraints'],
400
401 events: {
402 '#save-service-constraints': {click: 'updateConstraints'}
403 },
404
405 updateConstraints: function() {
406 var service = this.get('model'),
407 container = this.get('container'),
408 env = this.get('env');
409 var constraints = utils.getElementsValuesMapping(
410 container, '.constraint-field');
411
412 // Disable the "Update" button while the RPC call is outstanding.
413 container.one('#save-service-constraints')
414 .set('disabled', 'disabled');
415 env.set_constraints(service.get('id'),
416 constraints,
417 Y.bind(this._setConstraintsCallback, this, container)
418 );
419 },
420
421 _setConstraintsCallback: function(container, ev) {
422 var service = this.get('model'),
423 env = this.get('env'),
424 getModelURL = this.get('getModelURL'),
425 db = this.get('db');
426
427 if (ev.err) {
428 db.notifications.add(
429 new models.Notification({
430 title: 'Error setting service constraints',
431 message: 'Service name: ' + ev.service_name,
432 level: 'error',
433 link: getModelURL(service) + 'constraints',
434 modelId: service
435 })
436 );
437 container.one('#save-service-constraints')
438 .removeAttribute('disabled');
439
440 } else {
441 // The usual result of a successful request is a page refresh.
442 // Therefore, we need to set this delay in order to show the
443 // "success" message after the page page refresh.
444 setTimeout(function() {
445 utils.showSuccessMessage(container, 'Constraints updated');
446 }, 1000);
447 }
448 },
449
450 /**
451 * Gather up all of the data required for the template.
452 *
453 * Aside from a nice separation of concerns, this method also
454 * facilitates testing.
455 *
456 * @method gatherRenderData
457 * @return {Object} The data the template will render.
458 */
459 gatherRenderData: function() {
460 var service = this.get('model'),
461 env = this.get('env'),
462 constraints = service.get('constraints'),
463 display_constraints = [];
464
465 //these are read-only values
466 var readOnlyConstraints = {
467 'provider-type': constraints['provider-type'],
468 'ubuntu-series': constraints['ubuntu-series']
469 };
470
471 Y.Object.each(constraints, function(value, name) {
472 if (!(name in readOnlyConstraints)) {
473 display_constraints.push({
474 name: name,
475 value: value});
476 }
477 });
478
479 Y.Array.each(env.genericConstraints, function(gkey) {
480 if (!(gkey in constraints)) {
481 display_constraints.push({name: gkey, value: ''});
482 }
483 });
484
485 console.log('service constraints', display_constraints);
486 var charm_id = service.get('charm');
487 return {
488 viewName: 'constraints',
489 tabs: this.getServiceTabs('constraints'),
490 service: service.getAttrs(),
491 landscape: this.get('landscape'),
492 serviceModel: service,
493 constraints: display_constraints,
494 readOnlyConstraints: (function() {
495 var arr = [];
496 Y.Object.each(readOnlyConstraints, function(name, value) {
497 arr.push({name: name, value: value});
498 });
499 return arr;
500 })(),
501 charm_id: charm_id,
502 serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id)
503 };
504 }
505
506 });
507
508 /**
509 * @class ServiceConfigView
510 */
511 views.service_config = Y.Base.create(
512 'ServiceConfigView', ServiceViewBase, [
513 views.JujuBaseView], {
514
515 template: Templates['service-config'],
516
517 events: {
518 '#save-service-config': {click: 'saveConfig'}
519 },
520
521 /**
522 * Gather up all of the data required for the template.
523 *
524 * Aside from a nice separation of concerns, this method also
525 * facilitates testing.
526 *
527 * @method gatherRenderData
528 * @return {Object} The data the template will render.
529 */
530 gatherRenderData: function() {
531 var db = this.get('db');
532 var service = this.get('model');
533 var charm = db.charms.getById(service.get('charm'));
534 var config = service.get('config');
535 var schema = charm.get('options');
536 var charm_id = service.get('charm');
537
538 var settings = utils.extractServiceSettings(schema, config);
539
540 return {
541 viewName: 'config',
542 tabs: this.getServiceTabs('config'),
543 service: service.getAttrs(),
544 settings: settings,
545 charm_id: charm_id,
546 landscape: this.get('landscape'),
547 serviceModel: service,
548 serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id)
549 };
550 },
551
552 /**
553 Attach the plugins. Must be called after the container
554 has been added to the DOM.
555
556 @method containerAttached
557 */
558 containerAttached: function() {
559 this.constructor.superclass.containerAttached.call(this);
560 var container = this.get('container');
561 container.all('textarea.config-field').plug(plugins.ResizingTextarea,
562 { max_height: 200,
563 min_height: 18,
564 single_line: 18});
565 },
566
567 showErrors: function(errors) {
568 var container = this.get('container');
569 container.one('#save-service-config').removeAttribute('disabled');
570
571
572 // Remove old error messages
573 container.all('.help-inline').each(function(node) {
574 node.remove();
575 });
576
577 // Remove remove the "error" class from the "div"
578 // that previously had "help-inline" tags
579 container.all('.error').each(function(node) {
580 node.removeClass('error');
581 });
582
583 var firstErrorKey = null;
584 Y.Object.each(errors, function(value, key) {
585 var errorTag = Y.Node.create('<span/>')
586 .set('id', 'error-' + key)
587 .addClass('help-inline');
588
589 var field = container.one('#input-' + key);
590 // Add the "error" class to the wrapping "control-group" div
591 field.get('parentNode').get('parentNode').addClass('error');
592
593 errorTag.appendTo(field.get('parentNode'));
594
595 errorTag.setHTML(value);
596 if (!firstErrorKey) {
597 firstErrorKey = key;
598 }
599 });
600
601 if (firstErrorKey) {
602 var field = container.one('#input-' + firstErrorKey);
603 field.focus();
604 }
605 },
606
607 saveConfig: function() {
608 var env = this.get('env'),
609 db = this.get('db'),
610 getModelURL = this.get('getModelURL'),
611 service = this.get('model'),
612 charm_url = service.get('charm'),
613 charm = db.charms.getById(charm_url),
614 schema = charm.get('options'),
615 container = this.get('container');
616
617 // Disable the "Update" button while the RPC call is outstanding.
618 container.one('#save-service-config').set('disabled', 'disabled');
619
620 var new_values = utils.getElementsValuesMapping(
621 container, '.config-field');
622 var errors = utils.validate(new_values, schema);
623
624 if (Y.Object.isEmpty(errors)) {
625 env.set_config(
626 service.get('id'),
627 new_values,
628 null,
629 service.get('config'),
630 Y.bind(this._setConfigCallback, this, container)
631 );
632
633 } else {
634 this.showErrors(errors);
635 }
636 },
637
638 _setConfigCallback: function(container, ev) {
639 var service = this.get('model'),
640 env = this.get('env'),
641 getModelURL = this.get('getModelURL'),
642 db = this.get('db');
643
644 if (ev.err) {
645 db.notifications.add(
646 new models.Notification({
647 title: 'Error setting service config',
648 message: 'Service name: ' + ev.service_name,
649 level: 'error',
650 link: getModelURL(service) + 'config',
651 modelId: service
652 })
653 );
654 container.one('#save-service-config')
655 .removeAttribute('disabled');
656
657 } else {
658 // The usual result of a successful request is a page refresh.
659 // Therefore, we need to set this delay in order to show the
660 // "success" message after the page page refresh.
661 setTimeout(function() {
662 utils.showSuccessMessage(container, 'Settings updated');
663 }, 1000);
664 }
665 }
666 });
667
668 // Display a unit grid based on the total number of units.
669 Y.Handlebars.registerHelper('show_units', function(units) {
670 var template;
671 var numUnits = units.length;
672 // TODO: different visualization based on the viewport size.
673 if (numUnits <= 25) {
674 template = Templates.show_units_large;
675 } else if (numUnits <= 50) {
676 template = Templates.show_units_medium;
677 } else if (numUnits <= 250) {
678 template = Templates.show_units_small;
679 } else {
680 template = Templates.show_units_tiny;
681 }
682 return template({units: units});
683 });
684
685 // Translate the given state to the matching style.
686 Y.Handlebars.registerHelper('state_to_style', function(state) {
687 // Using a closure to avoid the second argument to be passed through.
688 return utils.stateToStyle(state);
689 });
690
691 /**
692 * @class ServiceView
693 */
694 var ServiceView = Y.Base.create('ServiceView', ServiceViewBase, [
695 views.JujuBaseView], {
696
697 template: Templates.service,
698
699 /**
700 * Gather up all of the data required for the template.
701 *
702 * Aside from a nice separation of concerns, this method also
703 * facilitates testing.
704 *
705 * @method gatherRenderData
706 * @return {Object} The data the template will render.
707 */
708 gatherRenderData: function() {
709 var db = this.get('db');
710 var service = this.get('model');
711 var filter_state = this.get('querystring').state;
712 var units = db.units.get_units_for_service(service);
713 var charm_id = service.get('charm');
714 var charm = db.charms.getById(charm_id);
715 var charm_attrs = charm ? charm.getAttrs() : undefined;
716 var state_data = [{
717 title: 'All',
718 link: '.',
719 active: !filter_state,
720 count: this.filterUnits(null, units).length
721 }];
722 Y.each(['Running', 'Pending', 'Error'], function(title) {
723 var lower = title.toLowerCase();
724 state_data.push({
725 title: title,
726 active: lower === filter_state,
727 count: this.filterUnits(lower, units).length,
728 link: '?state=' + lower});
729 }, this);
730 return {
731 viewName: 'units',
732 landscape: this.get('landscape'),
733 serviceModel: service,
734 tabs: this.getServiceTabs('.'),
735 service: service.getAttrs(),
736 charm_id: charm_id,
737 charm: charm_attrs,
738 serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id),
739 state: filter_state,
740 units: this.filterUnits(filter_state, units),
741 states: state_data
742 };
743 },
744
745 filterUnits: function(filter_state, units) {
746 // If filtering was requested, do it.
747 if (filter_state) {
748 // Build a matcher that will identify units of the requested state.
749 var matcher = function(unit) {
750 // Is this unit's (simplified) state the one we are looking for?
751 return utils.simplifyState(unit) === filter_state;
752 };
753 return Y.Array.filter(units, matcher);
754 } else { // Otherwise just return all the units we were given.
755 return units;
756 }
757 },
758
759 events: {
760 'div.unit': {click: function(ev) {
761 var id = ev.currentTarget.get('id');
762 console.log('Unit clicked', id);
763 this.fire('navigateTo', {
764 url: this.get('nsRouter').url({
765 gui: '/unit/' + id.replace('/', '-') + '/'
766 })
767 });
768 }}
769 }
770 }, {
771 ATTRS: {
772 /**
773 Applications router utility methods
774
775 @attribute nsRouter
776 */
777 nsRouter: {}
778 }
779 });
780
781 views.service = ServiceView;
782
783 }, '0.1.0', {
784 requires: [
785 'base-build',
786 'd3-statusbar',
787 'event-key',
788 'event-resize',
789 'handlebars',
790 'juju-databinding',
791 'juju-models',
792 'juju-view-container',
793 'juju-view-inspector',
794 'juju-view-utils',
795 'juju-templates',
796 'node',
797 'panel',
798 'transition',
799 'view',
800 'event-resize'
801 ]
802 });
OLDNEW
« no previous file with comments | « app/views/landscape.js ('k') | app/views/topology/service.js » ('j') | no next file with comments »

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b