Merge lp:~benji/juju-gui/no-foot-shooting into lp:juju-gui/experimental

Proposed by Benji York
Status: Merged
Merged at revision: 327
Proposed branch: lp:~benji/juju-gui/no-foot-shooting
Merge into: lp:juju-gui/experimental
Diff against target: 1140 lines (+684/-172)
15 files modified
app/templates/overview.handlebars (+1/-1)
app/templates/service-config.handlebars (+12/-6)
app/templates/service-footer-common-controls.partial (+30/-27)
app/templates/service-footer-destroy-service.partial (+7/-8)
app/templates/service.handlebars (+7/-8)
app/views/service.js (+142/-100)
app/views/topology/service.js (+24/-9)
app/views/utils.js (+62/-0)
lib/views/stylesheet.less (+16/-7)
test/index.html (+1/-0)
test/test_application_notifications.js (+2/-0)
test/test_service_view.js (+113/-2)
test/test_templates.js (+128/-0)
test/test_topology.js (+72/-4)
test/test_utils.js (+67/-0)
To merge this branch: bzr merge lp:~benji/juju-gui/no-foot-shooting
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+143715@code.launchpad.net

Description of the change

Prevent destruction of the Juju GUI service

https://codereview.appspot.com/7129049/

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :
Download full text (9.1 KiB)

Good stuff, Benji, thank you.

Land with changes. What I'm bringing is usually trivial, and should be
fairly easy to address, I hope, even when I think it is important.

Gary

https://codereview.appspot.com/7129049/diff/1/app/templates/service-footer-common-controls.partial
File app/templates/service-footer-common-controls.partial (right):

https://codereview.appspot.com/7129049/diff/1/app/templates/service-footer-common-controls.partial#newcode1
app/templates/service-footer-common-controls.partial:1: {{#unless
serviceIsJujuGUI}}
My inclination is to let the GUI increase the unit count, but not be
exposed--IOW, move serviceIsJujuGUI into only be around
"control-expose," below. Given the basic approach that I think we have
now of "innocent until proven guilty" for disabling GUI service
functionality, do you have a concrete reason to exclude unit count--a
reason to say that this is a problem? Note that I am pretty sure that
unit count may not be reduced beneath one, thanks to a pre-existing
constraint in the GUI.

https://codereview.appspot.com/7129049/diff/1/app/templates/service.handlebars
File app/templates/service.handlebars (right):

https://codereview.appspot.com/7129049/diff/1/app/templates/service.handlebars#newcode25
app/templates/service.handlebars:25: {{#unless serviceIsJujuGUI}}
If you agree that we should reinstate unit count, then IIUC you would
also remove this conditional.

https://codereview.appspot.com/7129049/diff/1/app/views/service.js
File app/views/service.js (right):

https://codereview.appspot.com/7129049/diff/1/app/views/service.js#newcode170
app/views/service.js:170: // it would break the application they are
currently using.
Suggest you add another comment similar to the following:

// Note that the destroy button is not shown for the Juju GUI, so this
is a fail-safe.

https://codereview.appspot.com/7129049/diff/1/app/views/service.js#newcode381
app/views/service.js:381: * Aside from a nice seperation of concerns,
this method also
typo: separation

https://codereview.appspot.com/7129049/diff/1/app/views/service.js#newcode382
app/views/service.js:382: * facilitates testing.
Interesting approach. I like it so far.

https://codereview.appspot.com/7129049/diff/1/app/views/topology/service.js
File app/views/topology/service.js (right):

https://codereview.appspot.com/7129049/diff/1/app/views/topology/service.js#newcode79
app/views/topology/service.js:79: // it would break the application they
are currently using.
I'm not sure yet from the code, but I'm hoping that this is again a
fail-safe because the "destroy" option is not given to the user, in
which case a comment to that effect would again be my preference.

https://codereview.appspot.com/7129049/diff/1/app/views/topology/service.js#newcode740
app/views/topology/service.js:740: var menu =
view.get('container').one('#service-menu'),
nicer variable name, thank you :-)

https://codereview.appspot.com/7129049/diff/1/app/views/topology/service.js#newcode755
app/views/topology/service.js:755:
menu.one('.destroy-service').addClass('disabled');
Per my previous comment, yep, I see you disable it visually as well.
Cool.

https://codereview.appspot.com/7129049/diff/1/app/vie...

Read more...

Revision history for this message
Gary Poster (gary) wrote :
Download full text (5.7 KiB)

Sounds good benji. Thanks!

Gary

https://codereview.appspot.com/7129049/diff/1/app/views/service.js
File app/views/service.js (right):

https://codereview.appspot.com/7129049/diff/1/app/views/service.js#newcode170
app/views/service.js:170: // it would break the application they are
currently using.
On 2013/01/17 19:19:50, benji wrote:
> On 2013/01/17 17:34:06, gary.poster wrote:
> > Suggest you add another comment similar to the following:
> >
> > // Note that the destroy button is not shown for the Juju GUI, so
this is a
> > fail-safe.

> I would be inclined to remove the code altogether. I accidentally
duplicated
> this between the menu's destroy handler and the destroy button at the
bottom of
> the service view. It is needed for the menu because the menu item is
only
> disabled in as far as its visual styling is different, the click event
is still
> fired.

> I have removed the code but if you like the fail-safe, I will be glad
to put it
> back (with commentary).

Cool, that's fine. I could go either way.

https://codereview.appspot.com/7129049/diff/1/app/views/topology/service.js
File app/views/topology/service.js (right):

https://codereview.appspot.com/7129049/diff/1/app/views/topology/service.js#newcode79
app/views/topology/service.js:79: // it would break the application they
are currently using.
On 2013/01/17 19:19:50, benji wrote:
> On 2013/01/17 17:34:06, gary.poster wrote:
> > I'm not sure yet from the code, but I'm hoping that this is again a
fail-safe
> > because the "destroy" option is not given to the user, in which case
a comment
> > to that effect would again be my preference.

> Nope, this is really the code that keeps the destroy from happening.
The menu
> item is only "disabled" visually. The click event is still fired.
This is the
> pre-existing pattern in the code (for the always-dead "Units" menu
item.

Ah, OK, cool.

https://codereview.appspot.com/7129049/diff/1/app/views/utils.js
File app/views/utils.js (right):

https://codereview.appspot.com/7129049/diff/1/app/views/utils.js#newcode963
app/views/utils.js:963: var charmUrl = candidate.charm ||
candidate.get('charm');
On 2013/01/17 19:19:50, benji wrote:
> On 2013/01/17 17:34:06, gary.poster wrote:
> > In the previous branch, Nicola and I both found this to be too
fragile. See
> the
> > version in
https://codereview.appspot.com/7069068/diff/1/app/views/utils.js .
> > You don't need to use that approach, and *maybe* it is not necessary
now for
> > some reason, but it was necessary in the previous branch for tests
to pass.

> All the tests pass with this version, so I am inclined to avoid the
"sloppier"
> version. The things passed in really should have a "charm" attribute
one way or
> the other.

> That being said, I don't feel strongly about it, so one more nudge
will get me
> to use the other branch's version.

Naah, let's stick with what you have, thanks.

https://codereview.appspot.com/7129049/diff/1/lib/views/stylesheet.less
File lib/views/stylesheet.less (right):

https://codereview.appspot.com/7129049/diff/1/lib/views/stylesheet.less#newcode1507
lib/views/stylesheet.less:1507: .warning-message {
On 2013/01/17 19:19:50, benji wrote:
> On 2013/01/17 17:3...

Read more...

lp:~benji/juju-gui/no-foot-shooting updated
320. By Benji York

merge from trunk

321. By Benji York

checkpoint

322. By Benji York

more review changes

323. By Benji York

fix lint

Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM, a minor suggestion or two included.

https://codereview.appspot.com/7129049/diff/15001/app/views/service.js
File app/views/service.js (right):

https://codereview.appspot.com/7129049/diff/15001/app/views/service.js#newcode566
app/views/service.js:566: *
Agreed, each of these changes wrt gatherRenderData is nice.

https://codereview.appspot.com/7129049/diff/15001/app/views/topology/service.js
File app/views/topology/service.js (right):

https://codereview.appspot.com/7129049/diff/15001/app/views/topology/service.js#newcode769
app/views/topology/service.js:769:
menu.one('.destroy-service').addClass('disabled');
Does this actually work? .destroy-service still has the click handler
bound even if the class greys out the link or something, no?

Might we remove the link from the menu all together?

https://codereview.appspot.com/7129049/diff/15001/test/test_topology.js
File test/test_topology.js (right):

https://codereview.appspot.com/7129049/diff/15001/test/test_topology.js#newcode118
test/test_topology.js:118:
topo.events.ServiceModule.scene['.destroy-service'].click.callback(
a simulate('click') test might be better here if you leave this in the
menu.

https://codereview.appspot.com/7129049/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'app/templates/overview.handlebars'
--- app/templates/overview.handlebars 2012-12-24 03:17:11 +0000
+++ app/templates/overview.handlebars 2013-01-17 20:33:22 +0000
@@ -5,7 +5,7 @@
5 <ul>5 <ul>
6 <li class="view-service">View</li>6 <li class="view-service">View</li>
7 <li class="add-relation">Build Relation</li>7 <li class="add-relation">Build Relation</li>
8 <li class="add-remove-units">Units</li>8 <li class="add-remove-units disabled">Units</li>
9 <li class="destroy-service">Destroy</li>9 <li class="destroy-service">Destroy</li>
10 </ul>10 </ul>
11 </div>11 </div>
1212
=== modified file 'app/templates/service-config.handlebars'
--- app/templates/service-config.handlebars 2012-10-12 18:27:36 +0000
+++ app/templates/service-config.handlebars 2013-01-17 20:33:22 +0000
@@ -2,12 +2,18 @@
2<div class="view-container">2<div class="view-container">
3 <form id="service-config" class="form-horizontal">3 <form id="service-config" class="form-horizontal">
4 <legend>{{service.id}} settings</legend>4 <legend>{{service.id}} settings</legend>
5 {{> service-config}}5 {{> service-config}}
6 <div class="control-group">6 <div class="control-group">
7 <div class="controls">7 <div class="controls">
8 <button type="button" id="save-service-config">Update</button>8 <button type="button" id="save-service-config">Update</button>
9 </div>9 {{#if serviceIsJujuGUI}}
10 <span class="warning-message">
11 Warning: Changing the settings on this page may adversely affect
12 your ability to use the Juju GUI.
13 </span>
14 {{/if}}
10 </div>15 </div>
16 </div>
11 </form>17 </form>
12</div>18</div>
13{{> service-footer}} 19{{> service-footer}}
1420
=== modified file 'app/templates/service-footer-common-controls.partial'
--- app/templates/service-footer-common-controls.partial 2012-10-25 07:57:11 +0000
+++ app/templates/service-footer-common-controls.partial 2013-01-17 20:33:22 +0000
@@ -1,28 +1,31 @@
1<div class="inline service-common-controls">1<div class="inline service-common-controls">
2 {{#unless charm.is_subordinate}}2{{#unless charm.is_subordinate}}
3 <div class="control-unit-count">3 <div class="control-unit-count">
4 <div class="inline"><span>Unit count</span></div>4 <div class="inline"><span>Unit count</span></div>
5 <div class="inline">5 <div class="inline">
6 <input type="text" id="num-service-units" value="{{service.unit_count}}">6 <input type="text" id="num-service-units"
7 <img class="divider"7 value="{{service.unit_count}}">
8 src="/juju-ui/assets/images/bottom_bar_small_div.png" />8 <img class="divider"
9 </div>9 src="/juju-ui/assets/images/bottom_bar_small_div.png" />
10 </div>10 </div>
11 {{/unless}}11 </div>
12 <div class="control-expose">12{{/unless}}
13 <div class="inline"><span>Expose</span></div>13{{#unless serviceIsJujuGUI}}
14 <div class="inline">14 <div class="control-expose">
15 {{#if service.exposed}}15 <div class="inline"><span>Expose</span></div>
16 <img class="unexposeService"16 <div class="inline">
17 alt="Exposed"17 {{#if service.exposed}}
18 src="/juju-ui/assets/images/slider_on.png" />18 <img class="unexposeService"
19 <span class="on">On</span>19 alt="Exposed"
20 {{else}}20 src="/juju-ui/assets/images/slider_on.png" />
21 <img class="exposeService"21 <span class="on">On</span>
22 alt="Not exposed"22 {{else}}
23 src="/juju-ui/assets/images/slider_off.png" />23 <img class="exposeService"
24 <span class="off">Off</span>24 alt="Not exposed"
25 {{/if}}25 src="/juju-ui/assets/images/slider_off.png" />
26 </div>26 <span class="off">Off</span>
27 </div>27 {{/if}}
28</div>28 </div>
29 </div>
30 </div>
31{{/unless}}
2932
=== modified file 'app/templates/service-footer-destroy-service.partial'
--- app/templates/service-footer-destroy-service.partial 2012-10-22 11:57:15 +0000
+++ app/templates/service-footer-destroy-service.partial 2013-01-17 20:33:22 +0000
@@ -1,8 +1,7 @@
1<div class="destroy-control">
2 <a id="destroy-service" data-toggle="modal"
3 href="#destroy-service-modal">
4 <img src="/juju-ui/assets/images/destroy_icon.png" />Destroy
5 </a>
6 <img class="divider"
7 src="/juju-ui/assets/images/bottom_bar_big_div.png" />
8</div>
9\ No newline at end of file1\ No newline at end of file
2{{#unless serviceIsJujuGUI}}
3 <div class="destroy-control">
4 <a id="destroy-service" data-toggle="modal" href="#destroy-service-modal"
5 ><img src="/juju-ui/assets/images/destroy_icon.png" />Destroy</a>
6 <img class="divider" src="/juju-ui/assets/images/bottom_bar_big_div.png" />
7 </div>
8{{/unless}}
109
=== modified file 'app/templates/service.handlebars'
--- app/templates/service.handlebars 2012-11-05 23:10:01 +0000
+++ app/templates/service.handlebars 2013-01-17 20:33:22 +0000
@@ -14,17 +14,16 @@
14 <div class="inline filter-label"><span>Filter</span></div>14 <div class="inline filter-label"><span>Filter</span></div>
15 <div class="state-buttons">15 <div class="state-buttons">
16 {{#states}}16 {{#states}}
17 <div 17 <div class="inline {{#if active}}active{{/if}}">
18 class="inline {{#if active}}active{{/if}}">18 <a href="{{link}}">
19 <a href="{{link}}">19 <span class="state-title {{#if active}}active{{/if}}">{{title}}
20 <span class="state-title {{#if active}}active{{/if}}">{{title}}20 </span>
21 </span>21 </a>
22 </a>22 </div>
23 </div>
24 {{/states}}23 {{/states}}
25 </div>24 </div>
26 <div class="inline control-separator">25 <div class="inline control-separator">
27 <img class="divider" 26 <img class="divider"
28 src="/juju-ui/assets/images/bottom_bar_big_div.png" />27 src="/juju-ui/assets/images/bottom_bar_big_div.png" />
29 </div>28 </div>
30 </div>29 </div>
3130
=== modified file 'app/views/service.js'
--- app/views/service.js 2012-11-08 16:23:16 +0000
+++ app/views/service.js 2013-01-17 20:33:22 +0000
@@ -164,9 +164,9 @@
164 },164 },
165165
166 confirmDestroy: function(ev) {166 confirmDestroy: function(ev) {
167 ev.preventDefault();
167 // We wait to make the panel until now, because in the render method168 // We wait to make the panel until now, because in the render method
168 // the container is not yet part of the document.169 // the container is not yet part of the document.
169 ev.preventDefault();
170 if (Y.Lang.isUndefined(this.panel)) {170 if (Y.Lang.isUndefined(this.panel)) {
171 this.panel = views.createModalPanel(171 this.panel = views.createModalPanel(
172 'Are you sure you want to destroy the service? ' +172 'Are you sure you want to destroy the service? ' +
@@ -360,7 +360,7 @@
360 });360 });
361 views.serviceBase = ServiceViewBase;361 views.serviceBase = ServiceViewBase;
362362
363 var ServiceRelations = Y.Base.create(363 views.service_relations = Y.Base.create(
364 'ServiceRelationsView', ServiceViewBase, [views.JujuBaseView], {364 'ServiceRelationsView', ServiceViewBase, [views.JujuBaseView], {
365365
366 template: Templates['service-relations'],366 template: Templates['service-relations'],
@@ -369,17 +369,20 @@
369 '#service-relations .btn': {click: 'confirmRemoved'}369 '#service-relations .btn': {click: 'confirmRemoved'}
370 },370 },
371371
372 render: function() {372 /**
373 * Gather up all of the data required for the template.
374 *
375 * Aside from a nice separation of concerns, this method also
376 * facilitates testing.
377 *
378 * @method gatherRenderData
379 * @return {Object} The data the template will render.
380 */
381 gatherRenderData: function() {
373 var container = this.get('container'),382 var container = this.get('container'),
374 getModelURL = this.get('getModelURL'),
375 service = this.get('model'),383 service = this.get('model'),
376 db = this.get('db'),384 db = this.get('db'),
377 querystring = this.get('querystring');385 querystring = this.get('querystring');
378 if (!service || !service.get('loaded')) {
379 container.setHTML('<div class="alert">Loading...</div>');
380 console.log('waiting on service data');
381 return this;
382 }
383 var relation_data = utils.getRelationDataForService(db, service);386 var relation_data = utils.getRelationDataForService(db, service);
384 Y.each(relation_data, function(rel) {387 Y.each(relation_data, function(rel) {
385 if (rel.relation_id === querystring.rel_id) {388 if (rel.relation_id === querystring.rel_id) {
@@ -389,14 +392,27 @@
389 var charm_id = service.get('charm'),392 var charm_id = service.get('charm'),
390 charm = db.charms.getById(charm_id),393 charm = db.charms.getById(charm_id),
391 charm_attrs = charm ? charm.getAttrs() : undefined;394 charm_attrs = charm ? charm.getAttrs() : undefined;
392 container.setHTML(this.template(395 return {
393 { viewName: 'relations',396 viewName: 'relations',
394 tabs: this.getServiceTabs('relations'),397 tabs: this.getServiceTabs('relations'),
395 service: service.getAttrs(),398 service: service.getAttrs(),
396 relations: relation_data,399 relations: relation_data,
397 charm: charm_attrs,400 charm: charm_attrs,
398 charm_id: charm_id}));401 charm_id: charm_id,
402 serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id)
403 };
404 },
399405
406 render: function() {
407 var container = this.get('container');
408 var service = this.get('model');
409 if (!service || !service.get('loaded')) {
410 container.setHTML('<div class="alert">Loading...</div>');
411 console.log('waiting on service data');
412 } else {
413 container.setHTML(this.template(this.gatherRenderData()));
414 }
415 return this;
400 },416 },
401417
402 confirmRemoved: function(ev) {418 confirmRemoved: function(ev) {
@@ -474,9 +490,7 @@
474 }490 }
475 });491 });
476492
477 views.service_relations = ServiceRelations;493 views.service_constraints = Y.Base.create(
478
479 var ServiceConstraints = Y.Base.create(
480 'ServiceConstraintsView', ServiceViewBase, [views.JujuBaseView], {494 'ServiceConstraintsView', ServiceViewBase, [views.JujuBaseView], {
481495
482 template: Templates['service-constraints'],496 template: Templates['service-constraints'],
@@ -544,11 +558,17 @@
544 }558 }
545 },559 },
546560
547 render: function() {561 /**
548 var container = this.get('container'),562 * Gather up all of the data required for the template.
549 service = this.get('model'),563 *
550 getModelURL = this.get('getModelURL'),564 * Aside from a nice separation of concerns, this method also
551 db = this.get('db'),565 * facilitates testing.
566 *
567 * @method gatherRenderData
568 * @return {Object} The data the template will render.
569 */
570 gatherRenderData: function() {
571 var service = this.get('model'),
552 constraints = service.get('constraints'),572 constraints = service.get('constraints'),
553 display_constraints = [];573 display_constraints = [];
554574
@@ -574,7 +594,8 @@
574 });594 });
575595
576 console.log('service constraints', display_constraints);596 console.log('service constraints', display_constraints);
577 container.setHTML(this.template({597 var charm_id = service.get('charm');
598 return {
578 viewName: 'constraints',599 viewName: 'constraints',
579 tabs: this.getServiceTabs('constraints'),600 tabs: this.getServiceTabs('constraints'),
580 service: service.getAttrs(),601 service: service.getAttrs(),
@@ -586,17 +607,20 @@
586 });607 });
587 return arr;608 return arr;
588 })(),609 })(),
589 charm_id: service.get('charm')}610 charm_id: charm_id,
590 ));611 serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id)
612 };
613 },
591614
615 render: function() {
616 var container = this.get('container');
617 container.setHTML(this.template(this.gatherRenderData()));
592 return this;618 return this;
593 }619 }
594620
595 });621 });
596622
597 views.service_constraints = ServiceConstraints;623 views.service_config = Y.Base.create(
598
599 var ServiceConfigView = Y.Base.create(
600 'ServiceConfigView', ServiceViewBase, [views.JujuBaseView], {624 'ServiceConfigView', ServiceViewBase, [views.JujuBaseView], {
601625
602 template: Templates['service-config'],626 template: Templates['service-config'],
@@ -605,27 +629,27 @@
605 '#save-service-config': {click: 'saveConfig'}629 '#save-service-config': {click: 'saveConfig'}
606 },630 },
607631
608 render: function() {632 /**
609 var container = this.get('container'),633 * Gather up all of the data required for the template.
610 db = this.get('db'),634 *
611 service = this.get('model');635 * Aside from a nice separation of concerns, this method also
612636 * facilitates testing.
613 if (!service || !service.get('loaded')) {637 *
614 container.setHTML('<div class="alert">Loading...</div>');638 * @method gatherRenderData
615 console.log('waiting on service data');639 * @return {Object} The data the template will render.
616 return this;640 */
617 }641 gatherRenderData: function() {
618642 var container = this.get('container');
619 console.log('config', service.get('config'));643 var db = this.get('db');
620644 var service = this.get('model');
621 // combine the charm schema and the service values for display.645 var charm = db.charms.getById(service.get('charm'));
622 var charm = db.charms.getById(service.get('charm')),646 var config = service.get('config');
623 config = service.get('config'),647 var getModelURL = this.get('getModelURL');
624 getModelURL = this.get('getModelURL'),648 var charm_config = charm.get('config');
625 charm_config = charm.get('config'),649 var schema = charm_config && charm_config.options;
626 schema = charm_config && charm_config.options,650 var settings = [];
627 settings = [],651 var charm_id = service.get('charm');
628 field_def;652 var field_def;
629653
630 Y.Object.each(schema, function(field_def, field_name) {654 Y.Object.each(schema, function(field_def, field_name) {
631 var entry = {655 var entry = {
@@ -651,16 +675,25 @@
651 settings.push(Y.mix(entry, field_def));675 settings.push(Y.mix(entry, field_def));
652 });676 });
653677
654 console.log('render view svc config', service.getAttrs(), settings);678 return {
655679 viewName: 'config',
656 container.setHTML(this.template(680 tabs: this.getServiceTabs('config'),
657 { viewName: 'config',681 service: service.getAttrs(),
658 tabs: this.getServiceTabs('config'),682 settings: settings,
659 service: service.getAttrs(),683 charm_id: charm_id,
660 settings: settings,684 serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id)
661 charm_id: service.get('charm')}685 };
662 ));686 },
663687
688 render: function() {
689 var container = this.get('container');
690 var service = this.get('model');
691 if (!service || !service.get('loaded')) {
692 container.setHTML('<div class="alert">Loading...</div>');
693 console.log('waiting on service data');
694 } else {
695 container.setHTML(this.template(this.gatherRenderData()));
696 }
664 return this;697 return this;
665 },698 },
666699
@@ -719,8 +752,8 @@
719 container.one('#save-service-config').set('disabled', 'disabled');752 container.one('#save-service-config').set('disabled', 'disabled');
720753
721 var new_values = utils.getElementsValuesMapping(754 var new_values = utils.getElementsValuesMapping(
722 container, '.config-field'),755 container, '.config-field');
723 errors = utils.validate(new_values, schema);756 var errors = utils.validate(new_values, schema);
724757
725 if (Y.Object.isEmpty(errors)) {758 if (Y.Object.isEmpty(errors)) {
726 env.set_config(759 env.set_config(
@@ -767,8 +800,6 @@
767 }800 }
768 });801 });
769802
770 views.service_config = ServiceConfigView;
771
772 // Display a unit grid based on the total number of units.803 // Display a unit grid based on the total number of units.
773 Y.Handlebars.registerHelper('show_units', function(units) {804 Y.Handlebars.registerHelper('show_units', function(units) {
774 var template;805 var template;
@@ -797,29 +828,29 @@
797828
798 template: Templates.service,829 template: Templates.service,
799830
800 render: function() {831 /**
801 console.log('service view render');832 * Gather up all of the data required for the template.
802833 *
803 var container = this.get('container'),834 * Aside from a nice separation of concerns, this method also
804 getModelURL = this.get('getModelURL'),835 * facilitates testing.
805 db = this.get('db'),836 *
806 service = this.get('model'),837 * @method gatherRenderData
807 filter_state = this.get('querystring').state;838 * @return {Object} The data the template will render.
808839 */
809 if (!service) {840 gatherRenderData: function() {
810 container.setHTML('<div class="alert">Loading...</div>');841 var db = this.get('db');
811 console.log('waiting on service data');842 var service = this.get('model');
812 return this;843 var filter_state = this.get('querystring').state;
813 }844 var units = db.units.get_units_for_service(service);
814845 var charm_id = service.get('charm');
815 var units = db.units.get_units_for_service(service),846 var charm = db.charms.getById(charm_id);
816 state_data = [{847 var charm_attrs = charm ? charm.getAttrs() : undefined;
817 title: 'All',848 var state_data = [{
818 link: '.',849 title: 'All',
819 active: !filter_state,850 link: '.',
820 count: this.filterUnits(null, units).length851 active: !filter_state,
821 }];852 count: this.filterUnits(null, units).length
822853 }];
823 Y.each(['Running', 'Pending', 'Error'], function(title) {854 Y.each(['Running', 'Pending', 'Error'], function(title) {
824 var lower = title.toLowerCase();855 var lower = title.toLowerCase();
825 state_data.push({856 state_data.push({
@@ -828,18 +859,29 @@
828 count: this.filterUnits(lower, units).length,859 count: this.filterUnits(lower, units).length,
829 link: '?state=' + lower});860 link: '?state=' + lower});
830 }, this);861 }, this);
831 var charm_id = service.get('charm'),862 return {
832 charm = db.charms.getById(charm_id),863 viewName: 'units',
833 charm_attrs = charm ? charm.getAttrs() : undefined;864 tabs: this.getServiceTabs('.'),
834 container.setHTML(this.template(865 service: service.getAttrs(),
835 { viewName: 'units',866 charm_id: charm_id,
836 tabs: this.getServiceTabs('.'),867 charm: charm_attrs,
837 service: service.getAttrs(),868 serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id),
838 charm_id: charm_id,869 state: filter_state,
839 charm: charm_attrs,870 units: this.filterUnits(filter_state, units),
840 state: filter_state,871 states: state_data
841 units: this.filterUnits(filter_state, units),872 };
842 states: state_data}));873 },
874
875 render: function() {
876 console.log('service view render');
877 var container = this.get('container');
878 var service = this.get('model');
879 if (!service || !service.get('loaded')) {
880 container.setHTML('<div class="alert">Loading...</div>');
881 console.log('waiting on service data');
882 } else {
883 container.setHTML(this.template(this.gatherRenderData()));
884 }
843 return this;885 return this;
844 },886 },
845887
846888
=== modified file 'app/views/topology/service.js'
--- app/views/topology/service.js 2013-01-15 22:49:05 +0000
+++ app/views/topology/service.js 2013-01-17 20:33:22 +0000
@@ -75,6 +75,11 @@
75 var topo = context.get('component');75 var topo = context.get('component');
76 var box = topo.get('active_service');76 var box = topo.get('active_service');
77 var service = topo.serviceForBox(box);77 var service = topo.serviceForBox(box);
78 // The user is not allowed to destroy the Juju GUI service because
79 // it would break the application they are currently using.
80 if (utils.isGuiService(service)) {
81 return;
82 }
78 context.service_click_actions83 context.service_click_actions
79 .toggleControlPanel(box, context);84 .toggleControlPanel(box, context);
80 context.service_click_actions85 context.service_click_actions
@@ -738,21 +743,31 @@
738 */743 */
739 service_click_actions: {744 service_click_actions: {
740 /*745 /*
741 * Default action: show or hide control panel.746 * Default action: show or hide control panel (service menu).
747 *
748 * @method toggleControlPanel
749 * @param {Object} service The service in question (if any).
750 * @param {Object} view The current view.
751 * @param {object} context The active context.
742 */752 */
743 toggleControlPanel: function(m, view, context) {753 toggleControlPanel: function(service, view, context) {
744 var container = view.get('container'),754 var menu = view.get('container').one('#service-menu'),
745 topo = view.get('component'),755 topo = view.get('component');
746 cp = container.one('#service-menu');
747756
748 if (cp.hasClass('active') || !m) {757 if (menu.hasClass('active') || !service) {
749 cp.removeClass('active');758 menu.removeClass('active');
750 topo.set('active_service', null);759 topo.set('active_service', null);
751 topo.set('active_context', null);760 topo.set('active_context', null);
761 // Most services can be destroyed via the GUI.
762 menu.one('.destroy-service').removeClass('disabled');
752 } else {763 } else {
753 topo.set('active_service', m);764 topo.set('active_service', service);
754 topo.set('active_context', context);765 topo.set('active_context', context);
755 cp.addClass('active');766 menu.addClass('active');
767 // We do not want the user destroying the Juju GUI service.
768 if (utils.isGuiService(service)) {
769 menu.one('.destroy-service').addClass('disabled');
770 }
756 view.updateServiceMenuLocation();771 view.updateServiceMenuLocation();
757 }772 }
758 },773 },
759774
=== modified file 'app/views/utils.js'
--- app/views/utils.js 2013-01-15 09:43:29 +0000
+++ app/views/utils.js 2013-01-17 20:33:22 +0000
@@ -906,6 +906,68 @@
906 return result;906 return result;
907 };907 };
908908
909 /**
910 * Determine if a service is the Juju GUI by inspecting the charm URL.
911 *
912 * @method isGuiCharmUrl
913 * @param {String} charmUrl The service to inspect.
914 * @return {Boolean} True if the charm URL is that of the Juju GUI.
915 */
916 utils.isGuiCharmUrl = function(charmUrl) {
917 // Regular expression to match charm URLs. Explanation generated by
918 // http://rick.measham.id.au/paste/explain.pl and then touched up a bit to
919 // fit in our maximum line width. Note that the bit about an optional
920 // newline matched by $ is a Perlism that JS does not share.
921 //
922 // NODE EXPLANATION
923 // ------------------------------------------------------------------------
924 // ^ the beginning of the string
925 // ------------------------------------------------------------------------
926 // (?: group, but do not capture:
927 // ------------------------------------------------------------------------
928 // [^:]+ any character except: ':' (1 or more
929 // times (matching the most amount
930 // possible))
931 // ------------------------------------------------------------------------
932 // : ':'
933 // ------------------------------------------------------------------------
934 // ) end of grouping
935 // ------------------------------------------------------------------------
936 // (?: group, but do not capture (0 or more times
937 // (matching the most amount possible)):
938 // ------------------------------------------------------------------------
939 // [^\/]+ any character except: '\/' (1 or more
940 // times (matching the most amount
941 // possible))
942 // ------------------------------------------------------------------------
943 // \/ '/'
944 // ------------------------------------------------------------------------
945 // )* end of grouping
946 // ------------------------------------------------------------------------
947 // juju-gui- 'juju-gui-'
948 // ------------------------------------------------------------------------
949 // \d+ digits (0-9) (1 or more times (matching
950 // the most amount possible))
951 // ------------------------------------------------------------------------
952 // $ before an optional \n, and the end of the
953 // string
954 return (/^(?:[^:]+:)(?:[^\/]+\/)*juju-gui-\d+$/).test(charmUrl);
955 };
956
957 /**
958 * Determine if a service is the Juju GUI by inspecting the charm URL.
959 *
960 * @method isGuiService
961 * @param {Object} candidate The service to inspect.
962 * @return {Boolean} True if the service is the Juju GUI.
963 */
964 utils.isGuiService = function(candidate) {
965 // Some candidates have the charm URL as an attribute, others require a
966 // "get".
967 var charmUrl = candidate.charm || candidate.get('charm');
968 return utils.isGuiCharmUrl(charmUrl);
969 };
970
909 Y.Handlebars.registerHelper('unitState', function(relation_errors,971 Y.Handlebars.registerHelper('unitState', function(relation_errors,
910 agent_state) {972 agent_state) {
911 if ('started' === agent_state && relation_errors &&973 if ('started' === agent_state && relation_errors &&
912974
=== modified file 'lib/views/stylesheet.less'
--- lib/views/stylesheet.less 2013-01-10 10:25:55 +0000
+++ lib/views/stylesheet.less 2013-01-17 20:33:22 +0000
@@ -96,8 +96,15 @@
96}96}
9797
98.view-container {98.view-container {
99 overflow: auto;99 overflow: auto;
100 padding: 10px;100 padding: 10px;
101
102 .control-group {
103 .warning-message {
104 color: red;
105 padding: 1em;
106 }
107 }
101}108}
102109
103.navbar {110.navbar {
@@ -291,11 +298,6 @@
291 }298 }
292 &.add-remove-units {299 &.add-remove-units {
293 background-image: url(/juju-ui/assets/images/icons/icon_noshadow_relation.png);300 background-image: url(/juju-ui/assets/images/icons/icon_noshadow_relation.png);
294 color: gray;
295 cursor: default;
296 &:hover {
297 background-color: inherit;
298 }
299 }301 }
300 &.destroy-service {302 &.destroy-service {
301 background-image: url(/juju-ui/assets/images/icons/icon_noshadow_destroy.png);303 background-image: url(/juju-ui/assets/images/icons/icon_noshadow_destroy.png);
@@ -307,6 +309,13 @@
307 &:hover {309 &:hover {
308 background-color: lighten(@background_color, 10%);310 background-color: lighten(@background_color, 10%);
309 }311 }
312 &.disabled {
313 color: gray;
314 cursor: default;
315 &:hover {
316 background-color: inherit;
317 }
318 }
310 }319 }
311320
312 }321 }
313322
=== modified file 'test/index.html'
--- test/index.html 2013-01-16 09:06:43 +0000
+++ test/index.html 2013-01-17 20:33:22 +0000
@@ -54,6 +54,7 @@
54 <script src="test_unit_view.js"></script>54 <script src="test_unit_view.js"></script>
55 <script src="test_utils.js"></script>55 <script src="test_utils.js"></script>
56 <script src="test_viewport_module.js"></script>56 <script src="test_viewport_module.js"></script>
57 <script src="test_templates.js"></script>
5758
5859
59 <script>60 <script>
6061
=== modified file 'test/test_application_notifications.js'
--- test/test_application_notifications.js 2013-01-15 10:56:19 +0000
+++ test/test_application_notifications.js 2013-01-17 20:33:22 +0000
@@ -104,6 +104,8 @@
104 if ('unit_count' === key) {104 if ('unit_count' === key) {
105 // We simulate a model with 2 units105 // We simulate a model with 2 units
106 return 2;106 return 2;
107 } else if ('loaded' === key) {
108 return true;
107 }109 }
108 return null;110 return null;
109 }111 }
110112
=== modified file 'test/test_service_view.js'
--- test/test_service_view.js 2012-12-21 20:28:25 +0000
+++ test/test_service_view.js 2013-01-17 20:33:22 +0000
@@ -1,6 +1,10 @@
1'use strict';1'use strict';
22
3(function() {3(function() {
4 // XXX This "describe" is a lie. There are tests here for both the service
5 // view (views.service) and the relations view (views.service_relations).
6 // This test suite needs to be broken out into two that just contain tests
7 // for one or the other.
4 describe('juju service view', function() {8 describe('juju service view', function() {
5 var models, Y, container, service, db, conn, env, charm, ENTER, ESC,9 var models, Y, container, service, db, conn, env, charm, ENTER, ESC,
6 makeServiceView, makeServiceRelationsView, views, unit;10 makeServiceView, makeServiceRelationsView, views, unit;
@@ -670,5 +674,112 @@
670 assert.equal('error2', filtered[1].testKey);674 assert.equal('error2', filtered[1].testKey);
671 });675 });
672676
673 });677 it('tells the template if the service is the GUI (service)', function() {
674}) ();678 var view = makeServiceView();
679 var renderData = view.gatherRenderData();
680 assert.equal(renderData.serviceIsJujuGUI, false);
681 });
682
683 it('tells the template if the service is the GUI (relations)', function() {
684 // This would ideally be in its own suite; see the XXX at top of this
685 // file.
686 var view = makeServiceRelationsView();
687 var renderData = view.gatherRenderData();
688 assert.equal(renderData.serviceIsJujuGUI, false);
689 });
690
691 it('loading message if the service is not loaded (service)', function() {
692 var view = makeServiceView();
693 view.get('model').set('loaded', false);
694 view.render();
695 var html = container.getHTML();
696 assert.match(html, /Loading\.\.\./);
697 });
698
699 it('loading message if the service is not loaded (relations)', function() {
700 // This would ideally be in its own suite; see the XXX at top of this
701 // file.
702 var view = makeServiceRelationsView();
703 view.get('model').set('loaded', false);
704 view.render();
705 var html = container.getHTML();
706 assert.match(html, /Loading\.\.\./);
707 });
708
709 });
710})();
711
712(function() {
713 describe('Service config view (views.service_config)', function() {
714 var models, Y, container, service, db, conn, env, charm, views, view;
715
716 before(function(done) {
717 Y = YUI(GlobalConfig).use(
718 'juju-views', 'juju-models', 'base', 'node', 'json-parse',
719 'juju-env', 'node-event-simulate', 'juju-tests-utils', 'event-key',
720 function(Y) {
721 models = Y.namespace('juju.models');
722 views = Y.namespace('juju.views');
723 done();
724 });
725 });
726
727 beforeEach(function(done) {
728 conn = new (Y.namespace('juju-tests.utils')).SocketStub(),
729 env = new (Y.namespace('juju')).Environment({conn: conn});
730 env.connect();
731 conn.open();
732 container = Y.Node.create('<div/>').hide();
733 Y.one('#main').append(container);
734 db = new models.Database();
735 charm = new models.Charm({id: 'cs:precise/mysql-7', description: 'A DB'});
736 db.charms.add([charm]);
737 service = new models.Service(
738 { id: 'mysql',
739 charm: 'cs:precise/mysql-7',
740 unit_count: db.units.size(),
741 loaded: true,
742 exposed: false});
743
744 db.services.add([service]);
745 view = new views.service_config({
746 db: db,
747 env: env,
748 getModelURL: function(model, intent) {
749 return model.get('name');
750 },
751 model: service,
752 container: container
753 });
754 done();
755 });
756
757 afterEach(function(done) {
758 container.remove(true);
759 service.destroy();
760 db.destroy();
761 env.destroy();
762 done();
763 });
764
765 it('displays a loading message if the service is not loaded', function() {
766 view.get('model').set('loaded', false);
767 view.render();
768 var html = container.getHTML();
769 assert.match(html, /Loading\.\.\./);
770 });
771
772 it('displays no loading message if the service is loaded', function() {
773 view.get('model').set('loaded', true);
774 view.render();
775 var html = container.getHTML();
776 assert.notMatch(html, /Loading\.\.\./);
777 });
778
779 it('informs the template if the service is the GUI', function() {
780 var renderData = view.gatherRenderData();
781 assert.equal(renderData.serviceIsJujuGUI, false);
782 });
783
784 });
785})();
675786
=== added file 'test/test_templates.js'
--- test/test_templates.js 1970-01-01 00:00:00 +0000
+++ test/test_templates.js 2013-01-17 20:33:22 +0000
@@ -0,0 +1,128 @@
1'use strict';
2
3(function() {
4
5 describe('Template: service-footer-destroy-service.partial', function() {
6 var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils'];
7 var Y, conn, env, partial;
8
9 before(function(done) {
10 Y = YUI(GlobalConfig).use(requires, function(Y) {
11 var partials = Y.Handlebars.partials;
12 partial = partials['service-footer-destroy-service'];
13 done();
14 });
15 });
16
17 it('does not display a "Destroy" button for the Juju GUI', function() {
18 // Disallow foot-shooting.
19 var html = partial({serviceIsJujuGUI: true});
20 assert.notMatch(html, /Destroy/);
21 });
22
23 it('does display a "Destroy" button for other services', function() {
24 var html = partial({serviceIsJujuGUI: false});
25 assert.match(html, /Destroy/);
26 });
27
28 });
29})();
30
31(function() {
32
33 describe('Template: service-footer-common-controls.partial', function() {
34 var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils'];
35 var Y, conn, env, partial;
36
37 before(function(done) {
38 Y = YUI(GlobalConfig).use(requires, function(Y) {
39 var partials = Y.Handlebars.partials;
40 partial = partials['service-footer-common-controls'];
41 done();
42 });
43 });
44
45 it('includes unit count UI for the Juju GUI service', function() {
46 var html = partial({serviceIsJujuGUI: true});
47 assert.match(html, /Unit count/);
48 });
49
50 it('does not include (un)expose for the Juju GUI service', function() {
51 var html = partial({serviceIsJujuGUI: true});
52 assert.notMatch(html, /Expose/);
53 });
54
55 it('includes unit count UI for non-Juju-GUI services', function() {
56 var html = partial({serviceIsJujuGUI: false});
57 assert.match(html, /Unit count/);
58 });
59
60 it('includes (un)expose for non-Juju-GUI services', function() {
61 var html = partial({serviceIsJujuGUI: false});
62 assert.match(html, /Expose/);
63 });
64
65
66 });
67})();
68
69(function() {
70
71 describe('Template: service-config.handlebars', function() {
72 var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils'];
73 var warningMessage = /Warning: Changing the settings/;
74 var Y, conn, env, template;
75
76 before(function(done) {
77 Y = YUI(GlobalConfig).use(requires, function(Y) {
78 var templates = Y.namespace('juju.views').Templates;
79 template = templates['service-config'];
80 done();
81 });
82 });
83
84 it('includes a warning about reconfiguring the Juju GUI', function() {
85 // Reconfiguring the Juju GUI service could break it, causing the user to
86 // lose access to the app they are in the process of using.
87 var html = template({serviceIsJujuGUI: true});
88 assert.match(html, warningMessage);
89 });
90
91 it('does not warn about about reconfiguring other services', function() {
92 var html = template({serviceIsJujuGUI: false});
93 assert.notMatch(html, warningMessage);
94 });
95
96 });
97})();
98
99(function() {
100
101 describe('Template: service.handlebars', function() {
102 var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils'];
103 var warningMessage = 'Warning: Changing the settings';
104 var divider = '<img class="divider"';
105 var Y, conn, env, template;
106
107 before(function(done) {
108 Y = YUI(GlobalConfig).use(requires, function(Y) {
109 var templates = Y.namespace('juju.views').Templates;
110 template = templates.service;
111 done();
112 });
113 });
114
115 it('does not show the destroy or expose UI for Juju GUI', function() {
116 var html = template({serviceIsJujuGUI: true, units: []});
117 assert.notMatch(html, /Destroy/);
118 assert.notMatch(html, /Expose/);
119 });
120
121 it('shows the destroy or expose UI for non-Juju-GUI services', function() {
122 var html = template({serviceIsJujuGUI: false, units: []});
123 assert.match(html, /Destroy/);
124 assert.match(html, /Expose/);
125 });
126
127 });
128})();
0129
=== modified file 'test/test_topology.js'
--- test/test_topology.js 2013-01-09 17:17:46 +0000
+++ test/test_topology.js 2013-01-17 20:33:22 +0000
@@ -1,4 +1,3 @@
1
2'use strict';1'use strict';
32
4describe('topology', function() {3describe('topology', function() {
@@ -94,6 +93,75 @@
94 Y.Lang.isValue(topo.vis).should.equal(true);93 Y.Lang.isValue(topo.vis).should.equal(true);
95 });94 });
9695
97});96 it('should prevent the Juju GUI service from being destroyed', function() {
9897 var service = {
9998 charm: 'cs:precise/juju-gui-7'
99 };
100 var fauxTopo = {
101 get: function() {
102 return null;
103 },
104 serviceForBox: function() {
105 return service;
106 }
107 };
108 // The context is used to do the destroying, so if it does not have the
109 // destroy method, an exception will be raised if the service would be
110 // destroyed.
111 var context = {
112 get: function(name) {
113 if (name === 'component') {
114 return fauxTopo;
115 }
116 }
117 };
118 topo.events.ServiceModule.scene['.destroy-service'].click.callback(
119 undefined, context);
120 });
121});
122
123describe('service menu', function() {
124 var Y, views;
125
126 before(function(done) {
127 Y = YUI(GlobalConfig).use(['juju-topology'], function(Y) {
128 views = Y.namespace('juju.views');
129 done();
130 });
131 });
132
133 it('should disable the "Destroy" menu for the Juju GUI service', function() {
134 var service = {
135 charm: 'cs:precise/juju-gui-7'
136 };
137 var addedClassName;
138 var menu = {
139 hasClass: function() {
140 return false;
141 },
142 addClass: function() {},
143 one: function() {
144 return {
145 addClass: function(className) {
146 addedClassName = className;
147 }
148 };
149 }
150 };
151 var fauxView = {
152 get: function(name) {
153 if (name === 'container') {
154 return {one: function() { return menu; }};
155 } else if (name === 'component') {
156 return {set: function() {}};
157 }
158 },
159 updateServiceMenuLocation: function() {}
160 };
161 var view = new views.ServiceModule();
162 view.service_click_actions.toggleControlPanel(
163 service, fauxView, undefined);
164 assert.equal(addedClassName, 'disabled');
165 });
166
167});
100168
=== modified file 'test/test_utils.js'
--- test/test_utils.js 2012-10-12 23:52:48 +0000
+++ test/test_utils.js 2013-01-17 20:33:22 +0000
@@ -339,3 +339,70 @@
339339
340 });340 });
341})();341})();
342
343(function() {
344 describe('utils.isGuiService', function() {
345
346 var utils, Y;
347
348 before(function(done) {
349 Y = YUI(GlobalConfig).use('juju-views', function(Y) {
350 utils = Y.namespace('juju.views.utils');
351 done();
352 });
353 });
354
355 it('should extract values from "charm" attribute', function() {
356 var candidate = {charm: 'cs:precise/juju-gui-7'};
357 assert.isTrue(utils.isGuiService(candidate));
358 });
359
360 it('should extract values from .get("charm")', function() {
361 var candidate = {
362 get: function(name) {
363 if (name === 'charm') {
364 return 'cs:precise/juju-gui-7';
365 }
366 }
367 };
368 assert.isTrue(utils.isGuiService(candidate));
369 });
370
371 });
372})();
373
374(function() {
375 describe('utils.isGuiCharmUrl', function() {
376
377 var utils, Y;
378
379 before(function(done) {
380 Y = YUI(GlobalConfig).use('juju-views', function(Y) {
381 utils = Y.namespace('juju.views.utils');
382 done();
383 });
384 });
385
386 it('should recognize charm store URLs', function() {
387 assert.isTrue(utils.isGuiCharmUrl('cs:precise/juju-gui-7'));
388 });
389
390 it('should recognize unofficial charm store URLs', function() {
391 assert.isTrue(utils.isGuiCharmUrl('cs:~foobar/precise/juju-gui-7'));
392 });
393
394 it('should ignore owners of unofficial charm store URLs', function() {
395 assert.isFalse(utils.isGuiCharmUrl('cs:~juju-gui/precise/foobar-7'));
396 });
397
398 it('should recognize local charm URLs', function() {
399 assert.isTrue(utils.isGuiCharmUrl('local:juju-gui-3'));
400 });
401
402 it('should not allow junk on the end of the URL', function() {
403 assert.isFalse(utils.isGuiCharmUrl('local:juju-gui-3 ROFLCOPTR!'));
404 });
405
406 });
407})();
408

Subscribers

People subscribed via source and target branches