Merge lp:~benji/juju-gui/no-foot-shooting into lp:juju-gui/experimental
- no-foot-shooting
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+143715@code.launchpad.net |
Commit message
Description of the change
Prevent destruction of the Juju GUI service
Gary Poster (gary) wrote : | # |
Gary Poster (gary) wrote : | # |
Sounds good benji. Thanks!
Gary
https:/
File app/views/
https:/
app/views/
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:/
File app/views/
https:/
app/views/
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:/
File app/views/utils.js (right):
https:/
app/views/
candidate.
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:/
> > 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:/
File lib/views/
https:/
lib/views/
On 2013/01/17 19:19:50, benji wrote:
> On 2013/01/17 17:3...
- 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
Benjamin Saller (bcsaller) wrote : | # |
LGTM, a minor suggestion or two included.
https:/
File app/views/
https:/
app/views/
Agreed, each of these changes wrt gatherRenderData is nice.
https:/
File app/views/
https:/
app/views/
menu.one(
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:/
File test/test_
https:/
test/test_
topo.events.
a simulate('click') test might be better here if you leave this in the
menu.
Preview Diff
1 | === modified file 'app/templates/overview.handlebars' | |||
2 | --- app/templates/overview.handlebars 2012-12-24 03:17:11 +0000 | |||
3 | +++ app/templates/overview.handlebars 2013-01-17 20:33:22 +0000 | |||
4 | @@ -5,7 +5,7 @@ | |||
5 | 5 | <ul> | 5 | <ul> |
6 | 6 | <li class="view-service">View</li> | 6 | <li class="view-service">View</li> |
7 | 7 | <li class="add-relation">Build Relation</li> | 7 | <li class="add-relation">Build Relation</li> |
9 | 8 | <li class="add-remove-units">Units</li> | 8 | <li class="add-remove-units disabled">Units</li> |
10 | 9 | <li class="destroy-service">Destroy</li> | 9 | <li class="destroy-service">Destroy</li> |
11 | 10 | </ul> | 10 | </ul> |
12 | 11 | </div> | 11 | </div> |
13 | 12 | 12 | ||
14 | === modified file 'app/templates/service-config.handlebars' | |||
15 | --- app/templates/service-config.handlebars 2012-10-12 18:27:36 +0000 | |||
16 | +++ app/templates/service-config.handlebars 2013-01-17 20:33:22 +0000 | |||
17 | @@ -2,12 +2,18 @@ | |||
18 | 2 | <div class="view-container"> | 2 | <div class="view-container"> |
19 | 3 | <form id="service-config" class="form-horizontal"> | 3 | <form id="service-config" class="form-horizontal"> |
20 | 4 | <legend>{{service.id}} settings</legend> | 4 | <legend>{{service.id}} settings</legend> |
26 | 5 | {{> service-config}} | 5 | {{> service-config}} |
27 | 6 | <div class="control-group"> | 6 | <div class="control-group"> |
28 | 7 | <div class="controls"> | 7 | <div class="controls"> |
29 | 8 | <button type="button" id="save-service-config">Update</button> | 8 | <button type="button" id="save-service-config">Update</button> |
30 | 9 | </div> | 9 | {{#if serviceIsJujuGUI}} |
31 | 10 | <span class="warning-message"> | ||
32 | 11 | Warning: Changing the settings on this page may adversely affect | ||
33 | 12 | your ability to use the Juju GUI. | ||
34 | 13 | </span> | ||
35 | 14 | {{/if}} | ||
36 | 10 | </div> | 15 | </div> |
37 | 16 | </div> | ||
38 | 11 | </form> | 17 | </form> |
39 | 12 | </div> | 18 | </div> |
41 | 13 | {{> service-footer}} | 19 | {{> service-footer}} |
42 | 14 | 20 | ||
43 | === modified file 'app/templates/service-footer-common-controls.partial' | |||
44 | --- app/templates/service-footer-common-controls.partial 2012-10-25 07:57:11 +0000 | |||
45 | +++ app/templates/service-footer-common-controls.partial 2013-01-17 20:33:22 +0000 | |||
46 | @@ -1,28 +1,31 @@ | |||
47 | 1 | <div class="inline service-common-controls"> | 1 | <div class="inline service-common-controls"> |
75 | 2 | {{#unless charm.is_subordinate}} | 2 | {{#unless charm.is_subordinate}} |
76 | 3 | <div class="control-unit-count"> | 3 | <div class="control-unit-count"> |
77 | 4 | <div class="inline"><span>Unit count</span></div> | 4 | <div class="inline"><span>Unit count</span></div> |
78 | 5 | <div class="inline"> | 5 | <div class="inline"> |
79 | 6 | <input type="text" id="num-service-units" value="{{service.unit_count}}"> | 6 | <input type="text" id="num-service-units" |
80 | 7 | <img class="divider" | 7 | value="{{service.unit_count}}"> |
81 | 8 | src="/juju-ui/assets/images/bottom_bar_small_div.png" /> | 8 | <img class="divider" |
82 | 9 | </div> | 9 | src="/juju-ui/assets/images/bottom_bar_small_div.png" /> |
83 | 10 | </div> | 10 | </div> |
84 | 11 | {{/unless}} | 11 | </div> |
85 | 12 | <div class="control-expose"> | 12 | {{/unless}} |
86 | 13 | <div class="inline"><span>Expose</span></div> | 13 | {{#unless serviceIsJujuGUI}} |
87 | 14 | <div class="inline"> | 14 | <div class="control-expose"> |
88 | 15 | {{#if service.exposed}} | 15 | <div class="inline"><span>Expose</span></div> |
89 | 16 | <img class="unexposeService" | 16 | <div class="inline"> |
90 | 17 | alt="Exposed" | 17 | {{#if service.exposed}} |
91 | 18 | src="/juju-ui/assets/images/slider_on.png" /> | 18 | <img class="unexposeService" |
92 | 19 | <span class="on">On</span> | 19 | alt="Exposed" |
93 | 20 | {{else}} | 20 | src="/juju-ui/assets/images/slider_on.png" /> |
94 | 21 | <img class="exposeService" | 21 | <span class="on">On</span> |
95 | 22 | alt="Not exposed" | 22 | {{else}} |
96 | 23 | src="/juju-ui/assets/images/slider_off.png" /> | 23 | <img class="exposeService" |
97 | 24 | <span class="off">Off</span> | 24 | alt="Not exposed" |
98 | 25 | {{/if}} | 25 | src="/juju-ui/assets/images/slider_off.png" /> |
99 | 26 | </div> | 26 | <span class="off">Off</span> |
100 | 27 | </div> | 27 | {{/if}} |
101 | 28 | </div> | 28 | </div> |
102 | 29 | </div> | ||
103 | 30 | </div> | ||
104 | 31 | {{/unless}} | ||
105 | 29 | 32 | ||
106 | === modified file 'app/templates/service-footer-destroy-service.partial' | |||
107 | --- app/templates/service-footer-destroy-service.partial 2012-10-22 11:57:15 +0000 | |||
108 | +++ app/templates/service-footer-destroy-service.partial 2013-01-17 20:33:22 +0000 | |||
109 | @@ -1,8 +1,7 @@ | |||
110 | 1 | <div class="destroy-control"> | ||
111 | 2 | <a id="destroy-service" data-toggle="modal" | ||
112 | 3 | href="#destroy-service-modal"> | ||
113 | 4 | <img src="/juju-ui/assets/images/destroy_icon.png" />Destroy | ||
114 | 5 | </a> | ||
115 | 6 | <img class="divider" | ||
116 | 7 | src="/juju-ui/assets/images/bottom_bar_big_div.png" /> | ||
117 | 8 | </div> | ||
118 | 9 | \ No newline at end of file | 1 | \ No newline at end of file |
119 | 2 | {{#unless serviceIsJujuGUI}} | ||
120 | 3 | <div class="destroy-control"> | ||
121 | 4 | <a id="destroy-service" data-toggle="modal" href="#destroy-service-modal" | ||
122 | 5 | ><img src="/juju-ui/assets/images/destroy_icon.png" />Destroy</a> | ||
123 | 6 | <img class="divider" src="/juju-ui/assets/images/bottom_bar_big_div.png" /> | ||
124 | 7 | </div> | ||
125 | 8 | {{/unless}} | ||
126 | 10 | 9 | ||
127 | === modified file 'app/templates/service.handlebars' | |||
128 | --- app/templates/service.handlebars 2012-11-05 23:10:01 +0000 | |||
129 | +++ app/templates/service.handlebars 2013-01-17 20:33:22 +0000 | |||
130 | @@ -14,17 +14,16 @@ | |||
131 | 14 | <div class="inline filter-label"><span>Filter</span></div> | 14 | <div class="inline filter-label"><span>Filter</span></div> |
132 | 15 | <div class="state-buttons"> | 15 | <div class="state-buttons"> |
133 | 16 | {{#states}} | 16 | {{#states}} |
141 | 17 | <div | 17 | <div class="inline {{#if active}}active{{/if}}"> |
142 | 18 | class="inline {{#if active}}active{{/if}}"> | 18 | <a href="{{link}}"> |
143 | 19 | <a href="{{link}}"> | 19 | <span class="state-title {{#if active}}active{{/if}}">{{title}} |
144 | 20 | <span class="state-title {{#if active}}active{{/if}}">{{title}} | 20 | </span> |
145 | 21 | </span> | 21 | </a> |
146 | 22 | </a> | 22 | </div> |
140 | 23 | </div> | ||
147 | 24 | {{/states}} | 23 | {{/states}} |
148 | 25 | </div> | 24 | </div> |
149 | 26 | <div class="inline control-separator"> | 25 | <div class="inline control-separator"> |
151 | 27 | <img class="divider" | 26 | <img class="divider" |
152 | 28 | src="/juju-ui/assets/images/bottom_bar_big_div.png" /> | 27 | src="/juju-ui/assets/images/bottom_bar_big_div.png" /> |
153 | 29 | </div> | 28 | </div> |
154 | 30 | </div> | 29 | </div> |
155 | 31 | 30 | ||
156 | === modified file 'app/views/service.js' | |||
157 | --- app/views/service.js 2012-11-08 16:23:16 +0000 | |||
158 | +++ app/views/service.js 2013-01-17 20:33:22 +0000 | |||
159 | @@ -164,9 +164,9 @@ | |||
160 | 164 | }, | 164 | }, |
161 | 165 | 165 | ||
162 | 166 | confirmDestroy: function(ev) { | 166 | confirmDestroy: function(ev) { |
163 | 167 | ev.preventDefault(); | ||
164 | 167 | // We wait to make the panel until now, because in the render method | 168 | // We wait to make the panel until now, because in the render method |
165 | 168 | // the container is not yet part of the document. | 169 | // the container is not yet part of the document. |
166 | 169 | ev.preventDefault(); | ||
167 | 170 | if (Y.Lang.isUndefined(this.panel)) { | 170 | if (Y.Lang.isUndefined(this.panel)) { |
168 | 171 | this.panel = views.createModalPanel( | 171 | this.panel = views.createModalPanel( |
169 | 172 | 'Are you sure you want to destroy the service? ' + | 172 | 'Are you sure you want to destroy the service? ' + |
170 | @@ -360,7 +360,7 @@ | |||
171 | 360 | }); | 360 | }); |
172 | 361 | views.serviceBase = ServiceViewBase; | 361 | views.serviceBase = ServiceViewBase; |
173 | 362 | 362 | ||
175 | 363 | var ServiceRelations = Y.Base.create( | 363 | views.service_relations = Y.Base.create( |
176 | 364 | 'ServiceRelationsView', ServiceViewBase, [views.JujuBaseView], { | 364 | 'ServiceRelationsView', ServiceViewBase, [views.JujuBaseView], { |
177 | 365 | 365 | ||
178 | 366 | template: Templates['service-relations'], | 366 | template: Templates['service-relations'], |
179 | @@ -369,17 +369,20 @@ | |||
180 | 369 | '#service-relations .btn': {click: 'confirmRemoved'} | 369 | '#service-relations .btn': {click: 'confirmRemoved'} |
181 | 370 | }, | 370 | }, |
182 | 371 | 371 | ||
184 | 372 | render: function() { | 372 | /** |
185 | 373 | * Gather up all of the data required for the template. | ||
186 | 374 | * | ||
187 | 375 | * Aside from a nice separation of concerns, this method also | ||
188 | 376 | * facilitates testing. | ||
189 | 377 | * | ||
190 | 378 | * @method gatherRenderData | ||
191 | 379 | * @return {Object} The data the template will render. | ||
192 | 380 | */ | ||
193 | 381 | gatherRenderData: function() { | ||
194 | 373 | var container = this.get('container'), | 382 | var container = this.get('container'), |
195 | 374 | getModelURL = this.get('getModelURL'), | ||
196 | 375 | service = this.get('model'), | 383 | service = this.get('model'), |
197 | 376 | db = this.get('db'), | 384 | db = this.get('db'), |
198 | 377 | querystring = this.get('querystring'); | 385 | querystring = this.get('querystring'); |
199 | 378 | if (!service || !service.get('loaded')) { | ||
200 | 379 | container.setHTML('<div class="alert">Loading...</div>'); | ||
201 | 380 | console.log('waiting on service data'); | ||
202 | 381 | return this; | ||
203 | 382 | } | ||
204 | 383 | var relation_data = utils.getRelationDataForService(db, service); | 386 | var relation_data = utils.getRelationDataForService(db, service); |
205 | 384 | Y.each(relation_data, function(rel) { | 387 | Y.each(relation_data, function(rel) { |
206 | 385 | if (rel.relation_id === querystring.rel_id) { | 388 | if (rel.relation_id === querystring.rel_id) { |
207 | @@ -389,14 +392,27 @@ | |||
208 | 389 | var charm_id = service.get('charm'), | 392 | var charm_id = service.get('charm'), |
209 | 390 | charm = db.charms.getById(charm_id), | 393 | charm = db.charms.getById(charm_id), |
210 | 391 | charm_attrs = charm ? charm.getAttrs() : undefined; | 394 | charm_attrs = charm ? charm.getAttrs() : undefined; |
218 | 392 | container.setHTML(this.template( | 395 | return { |
219 | 393 | { viewName: 'relations', | 396 | viewName: 'relations', |
220 | 394 | tabs: this.getServiceTabs('relations'), | 397 | tabs: this.getServiceTabs('relations'), |
221 | 395 | service: service.getAttrs(), | 398 | service: service.getAttrs(), |
222 | 396 | relations: relation_data, | 399 | relations: relation_data, |
223 | 397 | charm: charm_attrs, | 400 | charm: charm_attrs, |
224 | 398 | charm_id: charm_id})); | 401 | charm_id: charm_id, |
225 | 402 | serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id) | ||
226 | 403 | }; | ||
227 | 404 | }, | ||
228 | 399 | 405 | ||
229 | 406 | render: function() { | ||
230 | 407 | var container = this.get('container'); | ||
231 | 408 | var service = this.get('model'); | ||
232 | 409 | if (!service || !service.get('loaded')) { | ||
233 | 410 | container.setHTML('<div class="alert">Loading...</div>'); | ||
234 | 411 | console.log('waiting on service data'); | ||
235 | 412 | } else { | ||
236 | 413 | container.setHTML(this.template(this.gatherRenderData())); | ||
237 | 414 | } | ||
238 | 415 | return this; | ||
239 | 400 | }, | 416 | }, |
240 | 401 | 417 | ||
241 | 402 | confirmRemoved: function(ev) { | 418 | confirmRemoved: function(ev) { |
242 | @@ -474,9 +490,7 @@ | |||
243 | 474 | } | 490 | } |
244 | 475 | }); | 491 | }); |
245 | 476 | 492 | ||
249 | 477 | views.service_relations = ServiceRelations; | 493 | views.service_constraints = Y.Base.create( |
247 | 478 | |||
248 | 479 | var ServiceConstraints = Y.Base.create( | ||
250 | 480 | 'ServiceConstraintsView', ServiceViewBase, [views.JujuBaseView], { | 494 | 'ServiceConstraintsView', ServiceViewBase, [views.JujuBaseView], { |
251 | 481 | 495 | ||
252 | 482 | template: Templates['service-constraints'], | 496 | template: Templates['service-constraints'], |
253 | @@ -544,11 +558,17 @@ | |||
254 | 544 | } | 558 | } |
255 | 545 | }, | 559 | }, |
256 | 546 | 560 | ||
262 | 547 | render: function() { | 561 | /** |
263 | 548 | var container = this.get('container'), | 562 | * Gather up all of the data required for the template. |
264 | 549 | service = this.get('model'), | 563 | * |
265 | 550 | getModelURL = this.get('getModelURL'), | 564 | * Aside from a nice separation of concerns, this method also |
266 | 551 | db = this.get('db'), | 565 | * facilitates testing. |
267 | 566 | * | ||
268 | 567 | * @method gatherRenderData | ||
269 | 568 | * @return {Object} The data the template will render. | ||
270 | 569 | */ | ||
271 | 570 | gatherRenderData: function() { | ||
272 | 571 | var service = this.get('model'), | ||
273 | 552 | constraints = service.get('constraints'), | 572 | constraints = service.get('constraints'), |
274 | 553 | display_constraints = []; | 573 | display_constraints = []; |
275 | 554 | 574 | ||
276 | @@ -574,7 +594,8 @@ | |||
277 | 574 | }); | 594 | }); |
278 | 575 | 595 | ||
279 | 576 | console.log('service constraints', display_constraints); | 596 | console.log('service constraints', display_constraints); |
281 | 577 | container.setHTML(this.template({ | 597 | var charm_id = service.get('charm'); |
282 | 598 | return { | ||
283 | 578 | viewName: 'constraints', | 599 | viewName: 'constraints', |
284 | 579 | tabs: this.getServiceTabs('constraints'), | 600 | tabs: this.getServiceTabs('constraints'), |
285 | 580 | service: service.getAttrs(), | 601 | service: service.getAttrs(), |
286 | @@ -586,17 +607,20 @@ | |||
287 | 586 | }); | 607 | }); |
288 | 587 | return arr; | 608 | return arr; |
289 | 588 | })(), | 609 | })(), |
292 | 589 | charm_id: service.get('charm')} | 610 | charm_id: charm_id, |
293 | 590 | )); | 611 | serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id) |
294 | 612 | }; | ||
295 | 613 | }, | ||
296 | 591 | 614 | ||
297 | 615 | render: function() { | ||
298 | 616 | var container = this.get('container'); | ||
299 | 617 | container.setHTML(this.template(this.gatherRenderData())); | ||
300 | 592 | return this; | 618 | return this; |
301 | 593 | } | 619 | } |
302 | 594 | 620 | ||
303 | 595 | }); | 621 | }); |
304 | 596 | 622 | ||
308 | 597 | views.service_constraints = ServiceConstraints; | 623 | views.service_config = Y.Base.create( |
306 | 598 | |||
307 | 599 | var ServiceConfigView = Y.Base.create( | ||
309 | 600 | 'ServiceConfigView', ServiceViewBase, [views.JujuBaseView], { | 624 | 'ServiceConfigView', ServiceViewBase, [views.JujuBaseView], { |
310 | 601 | 625 | ||
311 | 602 | template: Templates['service-config'], | 626 | template: Templates['service-config'], |
312 | @@ -605,27 +629,27 @@ | |||
313 | 605 | '#save-service-config': {click: 'saveConfig'} | 629 | '#save-service-config': {click: 'saveConfig'} |
314 | 606 | }, | 630 | }, |
315 | 607 | 631 | ||
337 | 608 | render: function() { | 632 | /** |
338 | 609 | var container = this.get('container'), | 633 | * Gather up all of the data required for the template. |
339 | 610 | db = this.get('db'), | 634 | * |
340 | 611 | service = this.get('model'); | 635 | * Aside from a nice separation of concerns, this method also |
341 | 612 | 636 | * facilitates testing. | |
342 | 613 | if (!service || !service.get('loaded')) { | 637 | * |
343 | 614 | container.setHTML('<div class="alert">Loading...</div>'); | 638 | * @method gatherRenderData |
344 | 615 | console.log('waiting on service data'); | 639 | * @return {Object} The data the template will render. |
345 | 616 | return this; | 640 | */ |
346 | 617 | } | 641 | gatherRenderData: function() { |
347 | 618 | 642 | var container = this.get('container'); | |
348 | 619 | console.log('config', service.get('config')); | 643 | var db = this.get('db'); |
349 | 620 | 644 | var service = this.get('model'); | |
350 | 621 | // combine the charm schema and the service values for display. | 645 | var charm = db.charms.getById(service.get('charm')); |
351 | 622 | var charm = db.charms.getById(service.get('charm')), | 646 | var config = service.get('config'); |
352 | 623 | config = service.get('config'), | 647 | var getModelURL = this.get('getModelURL'); |
353 | 624 | getModelURL = this.get('getModelURL'), | 648 | var charm_config = charm.get('config'); |
354 | 625 | charm_config = charm.get('config'), | 649 | var schema = charm_config && charm_config.options; |
355 | 626 | schema = charm_config && charm_config.options, | 650 | var settings = []; |
356 | 627 | settings = [], | 651 | var charm_id = service.get('charm'); |
357 | 628 | field_def; | 652 | var field_def; |
358 | 629 | 653 | ||
359 | 630 | Y.Object.each(schema, function(field_def, field_name) { | 654 | Y.Object.each(schema, function(field_def, field_name) { |
360 | 631 | var entry = { | 655 | var entry = { |
361 | @@ -651,16 +675,25 @@ | |||
362 | 651 | settings.push(Y.mix(entry, field_def)); | 675 | settings.push(Y.mix(entry, field_def)); |
363 | 652 | }); | 676 | }); |
364 | 653 | 677 | ||
375 | 654 | console.log('render view svc config', service.getAttrs(), settings); | 678 | return { |
376 | 655 | 679 | viewName: 'config', | |
377 | 656 | container.setHTML(this.template( | 680 | tabs: this.getServiceTabs('config'), |
378 | 657 | { viewName: 'config', | 681 | service: service.getAttrs(), |
379 | 658 | tabs: this.getServiceTabs('config'), | 682 | settings: settings, |
380 | 659 | service: service.getAttrs(), | 683 | charm_id: charm_id, |
381 | 660 | settings: settings, | 684 | serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id) |
382 | 661 | charm_id: service.get('charm')} | 685 | }; |
383 | 662 | )); | 686 | }, |
384 | 663 | 687 | ||
385 | 688 | render: function() { | ||
386 | 689 | var container = this.get('container'); | ||
387 | 690 | var service = this.get('model'); | ||
388 | 691 | if (!service || !service.get('loaded')) { | ||
389 | 692 | container.setHTML('<div class="alert">Loading...</div>'); | ||
390 | 693 | console.log('waiting on service data'); | ||
391 | 694 | } else { | ||
392 | 695 | container.setHTML(this.template(this.gatherRenderData())); | ||
393 | 696 | } | ||
394 | 664 | return this; | 697 | return this; |
395 | 665 | }, | 698 | }, |
396 | 666 | 699 | ||
397 | @@ -719,8 +752,8 @@ | |||
398 | 719 | container.one('#save-service-config').set('disabled', 'disabled'); | 752 | container.one('#save-service-config').set('disabled', 'disabled'); |
399 | 720 | 753 | ||
400 | 721 | var new_values = utils.getElementsValuesMapping( | 754 | var new_values = utils.getElementsValuesMapping( |
403 | 722 | container, '.config-field'), | 755 | container, '.config-field'); |
404 | 723 | errors = utils.validate(new_values, schema); | 756 | var errors = utils.validate(new_values, schema); |
405 | 724 | 757 | ||
406 | 725 | if (Y.Object.isEmpty(errors)) { | 758 | if (Y.Object.isEmpty(errors)) { |
407 | 726 | env.set_config( | 759 | env.set_config( |
408 | @@ -767,8 +800,6 @@ | |||
409 | 767 | } | 800 | } |
410 | 768 | }); | 801 | }); |
411 | 769 | 802 | ||
412 | 770 | views.service_config = ServiceConfigView; | ||
413 | 771 | |||
414 | 772 | // Display a unit grid based on the total number of units. | 803 | // Display a unit grid based on the total number of units. |
415 | 773 | Y.Handlebars.registerHelper('show_units', function(units) { | 804 | Y.Handlebars.registerHelper('show_units', function(units) { |
416 | 774 | var template; | 805 | var template; |
417 | @@ -797,29 +828,29 @@ | |||
418 | 797 | 828 | ||
419 | 798 | template: Templates.service, | 829 | template: Templates.service, |
420 | 799 | 830 | ||
444 | 800 | render: function() { | 831 | /** |
445 | 801 | console.log('service view render'); | 832 | * Gather up all of the data required for the template. |
446 | 802 | 833 | * | |
447 | 803 | var container = this.get('container'), | 834 | * Aside from a nice separation of concerns, this method also |
448 | 804 | getModelURL = this.get('getModelURL'), | 835 | * facilitates testing. |
449 | 805 | db = this.get('db'), | 836 | * |
450 | 806 | service = this.get('model'), | 837 | * @method gatherRenderData |
451 | 807 | filter_state = this.get('querystring').state; | 838 | * @return {Object} The data the template will render. |
452 | 808 | 839 | */ | |
453 | 809 | if (!service) { | 840 | gatherRenderData: function() { |
454 | 810 | container.setHTML('<div class="alert">Loading...</div>'); | 841 | var db = this.get('db'); |
455 | 811 | console.log('waiting on service data'); | 842 | var service = this.get('model'); |
456 | 812 | return this; | 843 | var filter_state = this.get('querystring').state; |
457 | 813 | } | 844 | var units = db.units.get_units_for_service(service); |
458 | 814 | 845 | var charm_id = service.get('charm'); | |
459 | 815 | var units = db.units.get_units_for_service(service), | 846 | var charm = db.charms.getById(charm_id); |
460 | 816 | state_data = [{ | 847 | var charm_attrs = charm ? charm.getAttrs() : undefined; |
461 | 817 | title: 'All', | 848 | var state_data = [{ |
462 | 818 | link: '.', | 849 | title: 'All', |
463 | 819 | active: !filter_state, | 850 | link: '.', |
464 | 820 | count: this.filterUnits(null, units).length | 851 | active: !filter_state, |
465 | 821 | }]; | 852 | count: this.filterUnits(null, units).length |
466 | 822 | 853 | }]; | |
467 | 823 | Y.each(['Running', 'Pending', 'Error'], function(title) { | 854 | Y.each(['Running', 'Pending', 'Error'], function(title) { |
468 | 824 | var lower = title.toLowerCase(); | 855 | var lower = title.toLowerCase(); |
469 | 825 | state_data.push({ | 856 | state_data.push({ |
470 | @@ -828,18 +859,29 @@ | |||
471 | 828 | count: this.filterUnits(lower, units).length, | 859 | count: this.filterUnits(lower, units).length, |
472 | 829 | link: '?state=' + lower}); | 860 | link: '?state=' + lower}); |
473 | 830 | }, this); | 861 | }, this); |
486 | 831 | var charm_id = service.get('charm'), | 862 | return { |
487 | 832 | charm = db.charms.getById(charm_id), | 863 | viewName: 'units', |
488 | 833 | charm_attrs = charm ? charm.getAttrs() : undefined; | 864 | tabs: this.getServiceTabs('.'), |
489 | 834 | container.setHTML(this.template( | 865 | service: service.getAttrs(), |
490 | 835 | { viewName: 'units', | 866 | charm_id: charm_id, |
491 | 836 | tabs: this.getServiceTabs('.'), | 867 | charm: charm_attrs, |
492 | 837 | service: service.getAttrs(), | 868 | serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id), |
493 | 838 | charm_id: charm_id, | 869 | state: filter_state, |
494 | 839 | charm: charm_attrs, | 870 | units: this.filterUnits(filter_state, units), |
495 | 840 | state: filter_state, | 871 | states: state_data |
496 | 841 | units: this.filterUnits(filter_state, units), | 872 | }; |
497 | 842 | states: state_data})); | 873 | }, |
498 | 874 | |||
499 | 875 | render: function() { | ||
500 | 876 | console.log('service view render'); | ||
501 | 877 | var container = this.get('container'); | ||
502 | 878 | var service = this.get('model'); | ||
503 | 879 | if (!service || !service.get('loaded')) { | ||
504 | 880 | container.setHTML('<div class="alert">Loading...</div>'); | ||
505 | 881 | console.log('waiting on service data'); | ||
506 | 882 | } else { | ||
507 | 883 | container.setHTML(this.template(this.gatherRenderData())); | ||
508 | 884 | } | ||
509 | 843 | return this; | 885 | return this; |
510 | 844 | }, | 886 | }, |
511 | 845 | 887 | ||
512 | 846 | 888 | ||
513 | === modified file 'app/views/topology/service.js' | |||
514 | --- app/views/topology/service.js 2013-01-15 22:49:05 +0000 | |||
515 | +++ app/views/topology/service.js 2013-01-17 20:33:22 +0000 | |||
516 | @@ -75,6 +75,11 @@ | |||
517 | 75 | var topo = context.get('component'); | 75 | var topo = context.get('component'); |
518 | 76 | var box = topo.get('active_service'); | 76 | var box = topo.get('active_service'); |
519 | 77 | var service = topo.serviceForBox(box); | 77 | var service = topo.serviceForBox(box); |
520 | 78 | // The user is not allowed to destroy the Juju GUI service because | ||
521 | 79 | // it would break the application they are currently using. | ||
522 | 80 | if (utils.isGuiService(service)) { | ||
523 | 81 | return; | ||
524 | 82 | } | ||
525 | 78 | context.service_click_actions | 83 | context.service_click_actions |
526 | 79 | .toggleControlPanel(box, context); | 84 | .toggleControlPanel(box, context); |
527 | 80 | context.service_click_actions | 85 | context.service_click_actions |
528 | @@ -738,21 +743,31 @@ | |||
529 | 738 | */ | 743 | */ |
530 | 739 | service_click_actions: { | 744 | service_click_actions: { |
531 | 740 | /* | 745 | /* |
533 | 741 | * Default action: show or hide control panel. | 746 | * Default action: show or hide control panel (service menu). |
534 | 747 | * | ||
535 | 748 | * @method toggleControlPanel | ||
536 | 749 | * @param {Object} service The service in question (if any). | ||
537 | 750 | * @param {Object} view The current view. | ||
538 | 751 | * @param {object} context The active context. | ||
539 | 742 | */ | 752 | */ |
544 | 743 | toggleControlPanel: function(m, view, context) { | 753 | toggleControlPanel: function(service, view, context) { |
545 | 744 | var container = view.get('container'), | 754 | var menu = view.get('container').one('#service-menu'), |
546 | 745 | topo = view.get('component'), | 755 | topo = view.get('component'); |
543 | 746 | cp = container.one('#service-menu'); | ||
547 | 747 | 756 | ||
550 | 748 | if (cp.hasClass('active') || !m) { | 757 | if (menu.hasClass('active') || !service) { |
551 | 749 | cp.removeClass('active'); | 758 | menu.removeClass('active'); |
552 | 750 | topo.set('active_service', null); | 759 | topo.set('active_service', null); |
553 | 751 | topo.set('active_context', null); | 760 | topo.set('active_context', null); |
554 | 761 | // Most services can be destroyed via the GUI. | ||
555 | 762 | menu.one('.destroy-service').removeClass('disabled'); | ||
556 | 752 | } else { | 763 | } else { |
558 | 753 | topo.set('active_service', m); | 764 | topo.set('active_service', service); |
559 | 754 | topo.set('active_context', context); | 765 | topo.set('active_context', context); |
561 | 755 | cp.addClass('active'); | 766 | menu.addClass('active'); |
562 | 767 | // We do not want the user destroying the Juju GUI service. | ||
563 | 768 | if (utils.isGuiService(service)) { | ||
564 | 769 | menu.one('.destroy-service').addClass('disabled'); | ||
565 | 770 | } | ||
566 | 756 | view.updateServiceMenuLocation(); | 771 | view.updateServiceMenuLocation(); |
567 | 757 | } | 772 | } |
568 | 758 | }, | 773 | }, |
569 | 759 | 774 | ||
570 | === modified file 'app/views/utils.js' | |||
571 | --- app/views/utils.js 2013-01-15 09:43:29 +0000 | |||
572 | +++ app/views/utils.js 2013-01-17 20:33:22 +0000 | |||
573 | @@ -906,6 +906,68 @@ | |||
574 | 906 | return result; | 906 | return result; |
575 | 907 | }; | 907 | }; |
576 | 908 | 908 | ||
577 | 909 | /** | ||
578 | 910 | * Determine if a service is the Juju GUI by inspecting the charm URL. | ||
579 | 911 | * | ||
580 | 912 | * @method isGuiCharmUrl | ||
581 | 913 | * @param {String} charmUrl The service to inspect. | ||
582 | 914 | * @return {Boolean} True if the charm URL is that of the Juju GUI. | ||
583 | 915 | */ | ||
584 | 916 | utils.isGuiCharmUrl = function(charmUrl) { | ||
585 | 917 | // Regular expression to match charm URLs. Explanation generated by | ||
586 | 918 | // http://rick.measham.id.au/paste/explain.pl and then touched up a bit to | ||
587 | 919 | // fit in our maximum line width. Note that the bit about an optional | ||
588 | 920 | // newline matched by $ is a Perlism that JS does not share. | ||
589 | 921 | // | ||
590 | 922 | // NODE EXPLANATION | ||
591 | 923 | // ------------------------------------------------------------------------ | ||
592 | 924 | // ^ the beginning of the string | ||
593 | 925 | // ------------------------------------------------------------------------ | ||
594 | 926 | // (?: group, but do not capture: | ||
595 | 927 | // ------------------------------------------------------------------------ | ||
596 | 928 | // [^:]+ any character except: ':' (1 or more | ||
597 | 929 | // times (matching the most amount | ||
598 | 930 | // possible)) | ||
599 | 931 | // ------------------------------------------------------------------------ | ||
600 | 932 | // : ':' | ||
601 | 933 | // ------------------------------------------------------------------------ | ||
602 | 934 | // ) end of grouping | ||
603 | 935 | // ------------------------------------------------------------------------ | ||
604 | 936 | // (?: group, but do not capture (0 or more times | ||
605 | 937 | // (matching the most amount possible)): | ||
606 | 938 | // ------------------------------------------------------------------------ | ||
607 | 939 | // [^\/]+ any character except: '\/' (1 or more | ||
608 | 940 | // times (matching the most amount | ||
609 | 941 | // possible)) | ||
610 | 942 | // ------------------------------------------------------------------------ | ||
611 | 943 | // \/ '/' | ||
612 | 944 | // ------------------------------------------------------------------------ | ||
613 | 945 | // )* end of grouping | ||
614 | 946 | // ------------------------------------------------------------------------ | ||
615 | 947 | // juju-gui- 'juju-gui-' | ||
616 | 948 | // ------------------------------------------------------------------------ | ||
617 | 949 | // \d+ digits (0-9) (1 or more times (matching | ||
618 | 950 | // the most amount possible)) | ||
619 | 951 | // ------------------------------------------------------------------------ | ||
620 | 952 | // $ before an optional \n, and the end of the | ||
621 | 953 | // string | ||
622 | 954 | return (/^(?:[^:]+:)(?:[^\/]+\/)*juju-gui-\d+$/).test(charmUrl); | ||
623 | 955 | }; | ||
624 | 956 | |||
625 | 957 | /** | ||
626 | 958 | * Determine if a service is the Juju GUI by inspecting the charm URL. | ||
627 | 959 | * | ||
628 | 960 | * @method isGuiService | ||
629 | 961 | * @param {Object} candidate The service to inspect. | ||
630 | 962 | * @return {Boolean} True if the service is the Juju GUI. | ||
631 | 963 | */ | ||
632 | 964 | utils.isGuiService = function(candidate) { | ||
633 | 965 | // Some candidates have the charm URL as an attribute, others require a | ||
634 | 966 | // "get". | ||
635 | 967 | var charmUrl = candidate.charm || candidate.get('charm'); | ||
636 | 968 | return utils.isGuiCharmUrl(charmUrl); | ||
637 | 969 | }; | ||
638 | 970 | |||
639 | 909 | Y.Handlebars.registerHelper('unitState', function(relation_errors, | 971 | Y.Handlebars.registerHelper('unitState', function(relation_errors, |
640 | 910 | agent_state) { | 972 | agent_state) { |
641 | 911 | if ('started' === agent_state && relation_errors && | 973 | if ('started' === agent_state && relation_errors && |
642 | 912 | 974 | ||
643 | === modified file 'lib/views/stylesheet.less' | |||
644 | --- lib/views/stylesheet.less 2013-01-10 10:25:55 +0000 | |||
645 | +++ lib/views/stylesheet.less 2013-01-17 20:33:22 +0000 | |||
646 | @@ -96,8 +96,15 @@ | |||
647 | 96 | } | 96 | } |
648 | 97 | 97 | ||
649 | 98 | .view-container { | 98 | .view-container { |
652 | 99 | overflow: auto; | 99 | overflow: auto; |
653 | 100 | padding: 10px; | 100 | padding: 10px; |
654 | 101 | |||
655 | 102 | .control-group { | ||
656 | 103 | .warning-message { | ||
657 | 104 | color: red; | ||
658 | 105 | padding: 1em; | ||
659 | 106 | } | ||
660 | 107 | } | ||
661 | 101 | } | 108 | } |
662 | 102 | 109 | ||
663 | 103 | .navbar { | 110 | .navbar { |
664 | @@ -291,11 +298,6 @@ | |||
665 | 291 | } | 298 | } |
666 | 292 | &.add-remove-units { | 299 | &.add-remove-units { |
667 | 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); |
668 | 294 | color: gray; | ||
669 | 295 | cursor: default; | ||
670 | 296 | &:hover { | ||
671 | 297 | background-color: inherit; | ||
672 | 298 | } | ||
673 | 299 | } | 301 | } |
674 | 300 | &.destroy-service { | 302 | &.destroy-service { |
675 | 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); |
676 | @@ -307,6 +309,13 @@ | |||
677 | 307 | &:hover { | 309 | &:hover { |
678 | 308 | background-color: lighten(@background_color, 10%); | 310 | background-color: lighten(@background_color, 10%); |
679 | 309 | } | 311 | } |
680 | 312 | &.disabled { | ||
681 | 313 | color: gray; | ||
682 | 314 | cursor: default; | ||
683 | 315 | &:hover { | ||
684 | 316 | background-color: inherit; | ||
685 | 317 | } | ||
686 | 318 | } | ||
687 | 310 | } | 319 | } |
688 | 311 | 320 | ||
689 | 312 | } | 321 | } |
690 | 313 | 322 | ||
691 | === modified file 'test/index.html' | |||
692 | --- test/index.html 2013-01-16 09:06:43 +0000 | |||
693 | +++ test/index.html 2013-01-17 20:33:22 +0000 | |||
694 | @@ -54,6 +54,7 @@ | |||
695 | 54 | <script src="test_unit_view.js"></script> | 54 | <script src="test_unit_view.js"></script> |
696 | 55 | <script src="test_utils.js"></script> | 55 | <script src="test_utils.js"></script> |
697 | 56 | <script src="test_viewport_module.js"></script> | 56 | <script src="test_viewport_module.js"></script> |
698 | 57 | <script src="test_templates.js"></script> | ||
699 | 57 | 58 | ||
700 | 58 | 59 | ||
701 | 59 | <script> | 60 | <script> |
702 | 60 | 61 | ||
703 | === modified file 'test/test_application_notifications.js' | |||
704 | --- test/test_application_notifications.js 2013-01-15 10:56:19 +0000 | |||
705 | +++ test/test_application_notifications.js 2013-01-17 20:33:22 +0000 | |||
706 | @@ -104,6 +104,8 @@ | |||
707 | 104 | if ('unit_count' === key) { | 104 | if ('unit_count' === key) { |
708 | 105 | // We simulate a model with 2 units | 105 | // We simulate a model with 2 units |
709 | 106 | return 2; | 106 | return 2; |
710 | 107 | } else if ('loaded' === key) { | ||
711 | 108 | return true; | ||
712 | 107 | } | 109 | } |
713 | 108 | return null; | 110 | return null; |
714 | 109 | } | 111 | } |
715 | 110 | 112 | ||
716 | === modified file 'test/test_service_view.js' | |||
717 | --- test/test_service_view.js 2012-12-21 20:28:25 +0000 | |||
718 | +++ test/test_service_view.js 2013-01-17 20:33:22 +0000 | |||
719 | @@ -1,6 +1,10 @@ | |||
720 | 1 | 'use strict'; | 1 | 'use strict'; |
721 | 2 | 2 | ||
722 | 3 | (function() { | 3 | (function() { |
723 | 4 | // XXX This "describe" is a lie. There are tests here for both the service | ||
724 | 5 | // view (views.service) and the relations view (views.service_relations). | ||
725 | 6 | // This test suite needs to be broken out into two that just contain tests | ||
726 | 7 | // for one or the other. | ||
727 | 4 | describe('juju service view', function() { | 8 | describe('juju service view', function() { |
728 | 5 | var models, Y, container, service, db, conn, env, charm, ENTER, ESC, | 9 | var models, Y, container, service, db, conn, env, charm, ENTER, ESC, |
729 | 6 | makeServiceView, makeServiceRelationsView, views, unit; | 10 | makeServiceView, makeServiceRelationsView, views, unit; |
730 | @@ -670,5 +674,112 @@ | |||
731 | 670 | assert.equal('error2', filtered[1].testKey); | 674 | assert.equal('error2', filtered[1].testKey); |
732 | 671 | }); | 675 | }); |
733 | 672 | 676 | ||
736 | 673 | }); | 677 | it('tells the template if the service is the GUI (service)', function() { |
737 | 674 | }) (); | 678 | var view = makeServiceView(); |
738 | 679 | var renderData = view.gatherRenderData(); | ||
739 | 680 | assert.equal(renderData.serviceIsJujuGUI, false); | ||
740 | 681 | }); | ||
741 | 682 | |||
742 | 683 | it('tells the template if the service is the GUI (relations)', function() { | ||
743 | 684 | // This would ideally be in its own suite; see the XXX at top of this | ||
744 | 685 | // file. | ||
745 | 686 | var view = makeServiceRelationsView(); | ||
746 | 687 | var renderData = view.gatherRenderData(); | ||
747 | 688 | assert.equal(renderData.serviceIsJujuGUI, false); | ||
748 | 689 | }); | ||
749 | 690 | |||
750 | 691 | it('loading message if the service is not loaded (service)', function() { | ||
751 | 692 | var view = makeServiceView(); | ||
752 | 693 | view.get('model').set('loaded', false); | ||
753 | 694 | view.render(); | ||
754 | 695 | var html = container.getHTML(); | ||
755 | 696 | assert.match(html, /Loading\.\.\./); | ||
756 | 697 | }); | ||
757 | 698 | |||
758 | 699 | it('loading message if the service is not loaded (relations)', function() { | ||
759 | 700 | // This would ideally be in its own suite; see the XXX at top of this | ||
760 | 701 | // file. | ||
761 | 702 | var view = makeServiceRelationsView(); | ||
762 | 703 | view.get('model').set('loaded', false); | ||
763 | 704 | view.render(); | ||
764 | 705 | var html = container.getHTML(); | ||
765 | 706 | assert.match(html, /Loading\.\.\./); | ||
766 | 707 | }); | ||
767 | 708 | |||
768 | 709 | }); | ||
769 | 710 | })(); | ||
770 | 711 | |||
771 | 712 | (function() { | ||
772 | 713 | describe('Service config view (views.service_config)', function() { | ||
773 | 714 | var models, Y, container, service, db, conn, env, charm, views, view; | ||
774 | 715 | |||
775 | 716 | before(function(done) { | ||
776 | 717 | Y = YUI(GlobalConfig).use( | ||
777 | 718 | 'juju-views', 'juju-models', 'base', 'node', 'json-parse', | ||
778 | 719 | 'juju-env', 'node-event-simulate', 'juju-tests-utils', 'event-key', | ||
779 | 720 | function(Y) { | ||
780 | 721 | models = Y.namespace('juju.models'); | ||
781 | 722 | views = Y.namespace('juju.views'); | ||
782 | 723 | done(); | ||
783 | 724 | }); | ||
784 | 725 | }); | ||
785 | 726 | |||
786 | 727 | beforeEach(function(done) { | ||
787 | 728 | conn = new (Y.namespace('juju-tests.utils')).SocketStub(), | ||
788 | 729 | env = new (Y.namespace('juju')).Environment({conn: conn}); | ||
789 | 730 | env.connect(); | ||
790 | 731 | conn.open(); | ||
791 | 732 | container = Y.Node.create('<div/>').hide(); | ||
792 | 733 | Y.one('#main').append(container); | ||
793 | 734 | db = new models.Database(); | ||
794 | 735 | charm = new models.Charm({id: 'cs:precise/mysql-7', description: 'A DB'}); | ||
795 | 736 | db.charms.add([charm]); | ||
796 | 737 | service = new models.Service( | ||
797 | 738 | { id: 'mysql', | ||
798 | 739 | charm: 'cs:precise/mysql-7', | ||
799 | 740 | unit_count: db.units.size(), | ||
800 | 741 | loaded: true, | ||
801 | 742 | exposed: false}); | ||
802 | 743 | |||
803 | 744 | db.services.add([service]); | ||
804 | 745 | view = new views.service_config({ | ||
805 | 746 | db: db, | ||
806 | 747 | env: env, | ||
807 | 748 | getModelURL: function(model, intent) { | ||
808 | 749 | return model.get('name'); | ||
809 | 750 | }, | ||
810 | 751 | model: service, | ||
811 | 752 | container: container | ||
812 | 753 | }); | ||
813 | 754 | done(); | ||
814 | 755 | }); | ||
815 | 756 | |||
816 | 757 | afterEach(function(done) { | ||
817 | 758 | container.remove(true); | ||
818 | 759 | service.destroy(); | ||
819 | 760 | db.destroy(); | ||
820 | 761 | env.destroy(); | ||
821 | 762 | done(); | ||
822 | 763 | }); | ||
823 | 764 | |||
824 | 765 | it('displays a loading message if the service is not loaded', function() { | ||
825 | 766 | view.get('model').set('loaded', false); | ||
826 | 767 | view.render(); | ||
827 | 768 | var html = container.getHTML(); | ||
828 | 769 | assert.match(html, /Loading\.\.\./); | ||
829 | 770 | }); | ||
830 | 771 | |||
831 | 772 | it('displays no loading message if the service is loaded', function() { | ||
832 | 773 | view.get('model').set('loaded', true); | ||
833 | 774 | view.render(); | ||
834 | 775 | var html = container.getHTML(); | ||
835 | 776 | assert.notMatch(html, /Loading\.\.\./); | ||
836 | 777 | }); | ||
837 | 778 | |||
838 | 779 | it('informs the template if the service is the GUI', function() { | ||
839 | 780 | var renderData = view.gatherRenderData(); | ||
840 | 781 | assert.equal(renderData.serviceIsJujuGUI, false); | ||
841 | 782 | }); | ||
842 | 783 | |||
843 | 784 | }); | ||
844 | 785 | })(); | ||
845 | 675 | 786 | ||
846 | === added file 'test/test_templates.js' | |||
847 | --- test/test_templates.js 1970-01-01 00:00:00 +0000 | |||
848 | +++ test/test_templates.js 2013-01-17 20:33:22 +0000 | |||
849 | @@ -0,0 +1,128 @@ | |||
850 | 1 | 'use strict'; | ||
851 | 2 | |||
852 | 3 | (function() { | ||
853 | 4 | |||
854 | 5 | describe('Template: service-footer-destroy-service.partial', function() { | ||
855 | 6 | var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils']; | ||
856 | 7 | var Y, conn, env, partial; | ||
857 | 8 | |||
858 | 9 | before(function(done) { | ||
859 | 10 | Y = YUI(GlobalConfig).use(requires, function(Y) { | ||
860 | 11 | var partials = Y.Handlebars.partials; | ||
861 | 12 | partial = partials['service-footer-destroy-service']; | ||
862 | 13 | done(); | ||
863 | 14 | }); | ||
864 | 15 | }); | ||
865 | 16 | |||
866 | 17 | it('does not display a "Destroy" button for the Juju GUI', function() { | ||
867 | 18 | // Disallow foot-shooting. | ||
868 | 19 | var html = partial({serviceIsJujuGUI: true}); | ||
869 | 20 | assert.notMatch(html, /Destroy/); | ||
870 | 21 | }); | ||
871 | 22 | |||
872 | 23 | it('does display a "Destroy" button for other services', function() { | ||
873 | 24 | var html = partial({serviceIsJujuGUI: false}); | ||
874 | 25 | assert.match(html, /Destroy/); | ||
875 | 26 | }); | ||
876 | 27 | |||
877 | 28 | }); | ||
878 | 29 | })(); | ||
879 | 30 | |||
880 | 31 | (function() { | ||
881 | 32 | |||
882 | 33 | describe('Template: service-footer-common-controls.partial', function() { | ||
883 | 34 | var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils']; | ||
884 | 35 | var Y, conn, env, partial; | ||
885 | 36 | |||
886 | 37 | before(function(done) { | ||
887 | 38 | Y = YUI(GlobalConfig).use(requires, function(Y) { | ||
888 | 39 | var partials = Y.Handlebars.partials; | ||
889 | 40 | partial = partials['service-footer-common-controls']; | ||
890 | 41 | done(); | ||
891 | 42 | }); | ||
892 | 43 | }); | ||
893 | 44 | |||
894 | 45 | it('includes unit count UI for the Juju GUI service', function() { | ||
895 | 46 | var html = partial({serviceIsJujuGUI: true}); | ||
896 | 47 | assert.match(html, /Unit count/); | ||
897 | 48 | }); | ||
898 | 49 | |||
899 | 50 | it('does not include (un)expose for the Juju GUI service', function() { | ||
900 | 51 | var html = partial({serviceIsJujuGUI: true}); | ||
901 | 52 | assert.notMatch(html, /Expose/); | ||
902 | 53 | }); | ||
903 | 54 | |||
904 | 55 | it('includes unit count UI for non-Juju-GUI services', function() { | ||
905 | 56 | var html = partial({serviceIsJujuGUI: false}); | ||
906 | 57 | assert.match(html, /Unit count/); | ||
907 | 58 | }); | ||
908 | 59 | |||
909 | 60 | it('includes (un)expose for non-Juju-GUI services', function() { | ||
910 | 61 | var html = partial({serviceIsJujuGUI: false}); | ||
911 | 62 | assert.match(html, /Expose/); | ||
912 | 63 | }); | ||
913 | 64 | |||
914 | 65 | |||
915 | 66 | }); | ||
916 | 67 | })(); | ||
917 | 68 | |||
918 | 69 | (function() { | ||
919 | 70 | |||
920 | 71 | describe('Template: service-config.handlebars', function() { | ||
921 | 72 | var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils']; | ||
922 | 73 | var warningMessage = /Warning: Changing the settings/; | ||
923 | 74 | var Y, conn, env, template; | ||
924 | 75 | |||
925 | 76 | before(function(done) { | ||
926 | 77 | Y = YUI(GlobalConfig).use(requires, function(Y) { | ||
927 | 78 | var templates = Y.namespace('juju.views').Templates; | ||
928 | 79 | template = templates['service-config']; | ||
929 | 80 | done(); | ||
930 | 81 | }); | ||
931 | 82 | }); | ||
932 | 83 | |||
933 | 84 | it('includes a warning about reconfiguring the Juju GUI', function() { | ||
934 | 85 | // Reconfiguring the Juju GUI service could break it, causing the user to | ||
935 | 86 | // lose access to the app they are in the process of using. | ||
936 | 87 | var html = template({serviceIsJujuGUI: true}); | ||
937 | 88 | assert.match(html, warningMessage); | ||
938 | 89 | }); | ||
939 | 90 | |||
940 | 91 | it('does not warn about about reconfiguring other services', function() { | ||
941 | 92 | var html = template({serviceIsJujuGUI: false}); | ||
942 | 93 | assert.notMatch(html, warningMessage); | ||
943 | 94 | }); | ||
944 | 95 | |||
945 | 96 | }); | ||
946 | 97 | })(); | ||
947 | 98 | |||
948 | 99 | (function() { | ||
949 | 100 | |||
950 | 101 | describe('Template: service.handlebars', function() { | ||
951 | 102 | var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils']; | ||
952 | 103 | var warningMessage = 'Warning: Changing the settings'; | ||
953 | 104 | var divider = '<img class="divider"'; | ||
954 | 105 | var Y, conn, env, template; | ||
955 | 106 | |||
956 | 107 | before(function(done) { | ||
957 | 108 | Y = YUI(GlobalConfig).use(requires, function(Y) { | ||
958 | 109 | var templates = Y.namespace('juju.views').Templates; | ||
959 | 110 | template = templates.service; | ||
960 | 111 | done(); | ||
961 | 112 | }); | ||
962 | 113 | }); | ||
963 | 114 | |||
964 | 115 | it('does not show the destroy or expose UI for Juju GUI', function() { | ||
965 | 116 | var html = template({serviceIsJujuGUI: true, units: []}); | ||
966 | 117 | assert.notMatch(html, /Destroy/); | ||
967 | 118 | assert.notMatch(html, /Expose/); | ||
968 | 119 | }); | ||
969 | 120 | |||
970 | 121 | it('shows the destroy or expose UI for non-Juju-GUI services', function() { | ||
971 | 122 | var html = template({serviceIsJujuGUI: false, units: []}); | ||
972 | 123 | assert.match(html, /Destroy/); | ||
973 | 124 | assert.match(html, /Expose/); | ||
974 | 125 | }); | ||
975 | 126 | |||
976 | 127 | }); | ||
977 | 128 | })(); | ||
978 | 0 | 129 | ||
979 | === modified file 'test/test_topology.js' | |||
980 | --- test/test_topology.js 2013-01-09 17:17:46 +0000 | |||
981 | +++ test/test_topology.js 2013-01-17 20:33:22 +0000 | |||
982 | @@ -1,4 +1,3 @@ | |||
983 | 1 | |||
984 | 2 | 'use strict'; | 1 | 'use strict'; |
985 | 3 | 2 | ||
986 | 4 | describe('topology', function() { | 3 | describe('topology', function() { |
987 | @@ -94,6 +93,75 @@ | |||
988 | 94 | Y.Lang.isValue(topo.vis).should.equal(true); | 93 | Y.Lang.isValue(topo.vis).should.equal(true); |
989 | 95 | }); | 94 | }); |
990 | 96 | 95 | ||
994 | 97 | }); | 96 | it('should prevent the Juju GUI service from being destroyed', function() { |
995 | 98 | 97 | var service = { | |
996 | 99 | 98 | charm: 'cs:precise/juju-gui-7' | |
997 | 99 | }; | ||
998 | 100 | var fauxTopo = { | ||
999 | 101 | get: function() { | ||
1000 | 102 | return null; | ||
1001 | 103 | }, | ||
1002 | 104 | serviceForBox: function() { | ||
1003 | 105 | return service; | ||
1004 | 106 | } | ||
1005 | 107 | }; | ||
1006 | 108 | // The context is used to do the destroying, so if it does not have the | ||
1007 | 109 | // destroy method, an exception will be raised if the service would be | ||
1008 | 110 | // destroyed. | ||
1009 | 111 | var context = { | ||
1010 | 112 | get: function(name) { | ||
1011 | 113 | if (name === 'component') { | ||
1012 | 114 | return fauxTopo; | ||
1013 | 115 | } | ||
1014 | 116 | } | ||
1015 | 117 | }; | ||
1016 | 118 | topo.events.ServiceModule.scene['.destroy-service'].click.callback( | ||
1017 | 119 | undefined, context); | ||
1018 | 120 | }); | ||
1019 | 121 | }); | ||
1020 | 122 | |||
1021 | 123 | describe('service menu', function() { | ||
1022 | 124 | var Y, views; | ||
1023 | 125 | |||
1024 | 126 | before(function(done) { | ||
1025 | 127 | Y = YUI(GlobalConfig).use(['juju-topology'], function(Y) { | ||
1026 | 128 | views = Y.namespace('juju.views'); | ||
1027 | 129 | done(); | ||
1028 | 130 | }); | ||
1029 | 131 | }); | ||
1030 | 132 | |||
1031 | 133 | it('should disable the "Destroy" menu for the Juju GUI service', function() { | ||
1032 | 134 | var service = { | ||
1033 | 135 | charm: 'cs:precise/juju-gui-7' | ||
1034 | 136 | }; | ||
1035 | 137 | var addedClassName; | ||
1036 | 138 | var menu = { | ||
1037 | 139 | hasClass: function() { | ||
1038 | 140 | return false; | ||
1039 | 141 | }, | ||
1040 | 142 | addClass: function() {}, | ||
1041 | 143 | one: function() { | ||
1042 | 144 | return { | ||
1043 | 145 | addClass: function(className) { | ||
1044 | 146 | addedClassName = className; | ||
1045 | 147 | } | ||
1046 | 148 | }; | ||
1047 | 149 | } | ||
1048 | 150 | }; | ||
1049 | 151 | var fauxView = { | ||
1050 | 152 | get: function(name) { | ||
1051 | 153 | if (name === 'container') { | ||
1052 | 154 | return {one: function() { return menu; }}; | ||
1053 | 155 | } else if (name === 'component') { | ||
1054 | 156 | return {set: function() {}}; | ||
1055 | 157 | } | ||
1056 | 158 | }, | ||
1057 | 159 | updateServiceMenuLocation: function() {} | ||
1058 | 160 | }; | ||
1059 | 161 | var view = new views.ServiceModule(); | ||
1060 | 162 | view.service_click_actions.toggleControlPanel( | ||
1061 | 163 | service, fauxView, undefined); | ||
1062 | 164 | assert.equal(addedClassName, 'disabled'); | ||
1063 | 165 | }); | ||
1064 | 166 | |||
1065 | 167 | }); | ||
1066 | 100 | 168 | ||
1067 | === modified file 'test/test_utils.js' | |||
1068 | --- test/test_utils.js 2012-10-12 23:52:48 +0000 | |||
1069 | +++ test/test_utils.js 2013-01-17 20:33:22 +0000 | |||
1070 | @@ -339,3 +339,70 @@ | |||
1071 | 339 | 339 | ||
1072 | 340 | }); | 340 | }); |
1073 | 341 | })(); | 341 | })(); |
1074 | 342 | |||
1075 | 343 | (function() { | ||
1076 | 344 | describe('utils.isGuiService', function() { | ||
1077 | 345 | |||
1078 | 346 | var utils, Y; | ||
1079 | 347 | |||
1080 | 348 | before(function(done) { | ||
1081 | 349 | Y = YUI(GlobalConfig).use('juju-views', function(Y) { | ||
1082 | 350 | utils = Y.namespace('juju.views.utils'); | ||
1083 | 351 | done(); | ||
1084 | 352 | }); | ||
1085 | 353 | }); | ||
1086 | 354 | |||
1087 | 355 | it('should extract values from "charm" attribute', function() { | ||
1088 | 356 | var candidate = {charm: 'cs:precise/juju-gui-7'}; | ||
1089 | 357 | assert.isTrue(utils.isGuiService(candidate)); | ||
1090 | 358 | }); | ||
1091 | 359 | |||
1092 | 360 | it('should extract values from .get("charm")', function() { | ||
1093 | 361 | var candidate = { | ||
1094 | 362 | get: function(name) { | ||
1095 | 363 | if (name === 'charm') { | ||
1096 | 364 | return 'cs:precise/juju-gui-7'; | ||
1097 | 365 | } | ||
1098 | 366 | } | ||
1099 | 367 | }; | ||
1100 | 368 | assert.isTrue(utils.isGuiService(candidate)); | ||
1101 | 369 | }); | ||
1102 | 370 | |||
1103 | 371 | }); | ||
1104 | 372 | })(); | ||
1105 | 373 | |||
1106 | 374 | (function() { | ||
1107 | 375 | describe('utils.isGuiCharmUrl', function() { | ||
1108 | 376 | |||
1109 | 377 | var utils, Y; | ||
1110 | 378 | |||
1111 | 379 | before(function(done) { | ||
1112 | 380 | Y = YUI(GlobalConfig).use('juju-views', function(Y) { | ||
1113 | 381 | utils = Y.namespace('juju.views.utils'); | ||
1114 | 382 | done(); | ||
1115 | 383 | }); | ||
1116 | 384 | }); | ||
1117 | 385 | |||
1118 | 386 | it('should recognize charm store URLs', function() { | ||
1119 | 387 | assert.isTrue(utils.isGuiCharmUrl('cs:precise/juju-gui-7')); | ||
1120 | 388 | }); | ||
1121 | 389 | |||
1122 | 390 | it('should recognize unofficial charm store URLs', function() { | ||
1123 | 391 | assert.isTrue(utils.isGuiCharmUrl('cs:~foobar/precise/juju-gui-7')); | ||
1124 | 392 | }); | ||
1125 | 393 | |||
1126 | 394 | it('should ignore owners of unofficial charm store URLs', function() { | ||
1127 | 395 | assert.isFalse(utils.isGuiCharmUrl('cs:~juju-gui/precise/foobar-7')); | ||
1128 | 396 | }); | ||
1129 | 397 | |||
1130 | 398 | it('should recognize local charm URLs', function() { | ||
1131 | 399 | assert.isTrue(utils.isGuiCharmUrl('local:juju-gui-3')); | ||
1132 | 400 | }); | ||
1133 | 401 | |||
1134 | 402 | it('should not allow junk on the end of the URL', function() { | ||
1135 | 403 | assert.isFalse(utils.isGuiCharmUrl('local:juju-gui-3 ROFLCOPTR!')); | ||
1136 | 404 | }); | ||
1137 | 405 | |||
1138 | 406 | }); | ||
1139 | 407 | })(); | ||
1140 | 408 |
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 service- footer- common- controls. partial (right):
File app/templates/
https:/ /codereview. appspot. com/7129049/ diff/1/ app/templates/ service- footer- common- controls. partial# newcode1 service- footer- common- controls. partial: 1: {{#unless
app/templates/
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 service. handlebars (right):
File app/templates/
https:/ /codereview. appspot. com/7129049/ diff/1/ app/templates/ service. handlebars# newcode25 service. handlebars: 25: {{#unless serviceIsJujuGUI}}
app/templates/
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 service. js (right):
File app/views/
https:/ /codereview. appspot. com/7129049/ diff/1/ app/views/ service. js#newcode170 service. js:170: // it would break the application they are
app/views/
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 service. js:381: * Aside from a nice seperation of concerns,
app/views/
this method also
typo: separation
https:/ /codereview. appspot. com/7129049/ diff/1/ app/views/ service. js#newcode382 service. js:382: * facilitates testing.
app/views/
Interesting approach. I like it so far.
https:/ /codereview. appspot. com/7129049/ diff/1/ app/views/ topology/ service. js topology/ service. js (right):
File app/views/
https:/ /codereview. appspot. com/7129049/ diff/1/ app/views/ topology/ service. js#newcode79 topology/ service. js:79: // it would break the application they
app/views/
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 topology/ service. js:740: var menu = 'container' ).one(' #service- menu'),
app/views/
view.get(
nicer variable name, thank you :-)
https:/ /codereview. appspot. com/7129049/ diff/1/ app/views/ topology/ service. js#newcode755 topology/ service. js:755: '.destroy- service' ).addClass( 'disabled' );
app/views/
menu.one(
Per my previous comment, yep, I see you disable it visually as well.
Cool.
https:/ /codereview. appspot. com/7129049/ diff/1/ app/vie...