Merge lp:~bcsaller/juju-gui/renderBundle2 into lp:~bcsaller/juju-gui/charmFind
- renderBundle2
- Merge into charmFind
Proposed by
Benjamin Saller
Status: | Needs review |
---|---|
Proposed branch: | lp:~bcsaller/juju-gui/renderBundle2 |
Merge into: | lp:~bcsaller/juju-gui/charmFind |
Diff against target: |
5534 lines (+2257/-990) 76 files modified
app/app.js (+68/-4) app/assets/javascripts/d3-components.js (+12/-1) app/config-debug.js (+1/-1) app/config-prod.js (+1/-1) app/index.html (+3/-3) app/models/charm.js (+1/-1) app/models/model-controller.js (+30/-6) app/models/models.js (+19/-0) app/modules-debug.js (+3/-0) app/store/charm.js (+25/-0) app/store/env/env.js (+2/-2) app/store/env/fakebackend.js (+7/-1) app/store/env/go.js (+23/-5) app/store/env/python.js (+6/-1) app/store/env/sandbox.js (+4/-1) app/subapps/browser/browser.js (+51/-19) app/subapps/browser/templates/editorial.handlebars (+0/-1) app/subapps/browser/views/editorial.js (+0/-31) app/subapps/browser/views/minimized.js (+15/-8) app/subapps/browser/views/view.js (+7/-53) app/templates/category-icons.partial (+0/-57) app/templates/ghost-config-viewlet.handlebars (+12/-0) app/templates/inspector-header.handlebars (+3/-1) app/templates/service-constraints-viewlet.partial (+2/-2) app/templates/service-overview-constraints.handlebars (+24/-19) app/views/charm-panel.js (+1/-1) app/views/charm.js (+3/-0) app/views/environment.js (+2/-2) app/views/ghost-inspector.js (+17/-4) app/views/inspector.js (+31/-8) app/views/topology/bundle.js (+368/-0) app/views/topology/relation.js (+3/-0) app/views/topology/service.js (+424/-416) app/views/topology/topology.js (+15/-12) app/views/utils.js (+5/-4) app/views/viewlets/inspector-header.js (+5/-18) app/views/viewlets/service-constraints.js (+1/-0) app/views/viewlets/service-ghost.js (+17/-10) app/widgets/viewmode-controls.js (+87/-0) docs/d3-component-framework.rst (+5/-0) lib/views/browser/charm-full.less (+0/-1) lib/views/browser/charm-token.less (+0/-9) lib/views/juju-inspector.less (+15/-7) test/data/wp-deployer.yaml (+41/-0) test/index.html (+2/-1) test/test_app.js (+26/-13) test/test_browser_app.js (+101/-1) test/test_browser_editorial.js (+0/-20) test/test_bundle_module.js (+98/-0) test/test_charm_panel.js (+2/-1) test/test_charm_store.js (+30/-1) test/test_charm_view.js (+11/-12) test/test_endpoints.js (+14/-9) test/test_env.js (+2/-2) test/test_env_go.js (+28/-8) test/test_env_python.js (+13/-2) test/test_fakebackend.js (+27/-1) test/test_ghost_inspector.js (+74/-8) test/test_inspector_constraints.js (+12/-15) test/test_inspector_overview.js (+73/-25) test/test_inspector_settings.js (+2/-1) test/test_login.js (+19/-18) test/test_model.js (+43/-51) test/test_model_controller.js (+31/-3) test/test_notifications.js (+2/-2) test/test_sandbox_go.js (+67/-29) test/test_sandbox_python.js (+41/-3) test/test_service_config_view.js (+5/-4) test/test_service_view.js (+18/-24) test/test_startup.js.bottom (+6/-2) test/test_topology.js (+37/-0) test/test_unit_view.js (+21/-20) test/test_viewlet_manager.js (+1/-1) test/test_viewmode_controls_widget.js (+82/-0) test/test_websocket_logging.js (+10/-2) undocumented (+0/-1) |
To merge this branch: | bzr merge lp:~bcsaller/juju-gui/renderBundle2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Benjamin Saller | Pending | ||
Review via email:
|
Commit message
Description of the change
Bundle Topology
Provide a reusable view of topologies with a much more
limited and non-interactive service module. The tests show
how this works but don't wire the view into any current
UI which comes later.
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
diff is totally broken, I'll try to repair this.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
- 980. By Benjamin Saller
-
merge trunk
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
After repeated attempts this is still not getting a proper diff for me.
bzr diff -r ancestor:
will show it though.
Sorry and thanks.
- 981. By Benjamin Saller
-
more tests
- 982. By Benjamin Saller
-
missing file
- 983. By Benjamin Saller
-
lint, docs, various cleanups
Unmerged revisions
- 983. By Benjamin Saller
-
lint, docs, various cleanups
- 982. By Benjamin Saller
-
missing file
- 981. By Benjamin Saller
-
more tests
- 980. By Benjamin Saller
-
merge trunk
- 979. By Benjamin Saller
-
clean up diff, spotchecks
- 978. By Benjamin Saller
-
lint+
- 977. By Benjamin Saller
-
touchups
- 976. By Benjamin Saller
-
merge trunk
- 975. By Benjamin Saller
-
wip
- 974. By Benjamin Saller
-
wip
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'app/app.js' |
2 | --- app/app.js 2013-08-12 19:24:55 +0000 |
3 | +++ app/app.js 2013-08-30 19:48:17 +0000 |
4 | @@ -39,7 +39,8 @@ |
5 | var juju = Y.namespace('juju'), |
6 | models = Y.namespace('juju.models'), |
7 | views = Y.namespace('juju.views'), |
8 | - utils = views.utils; |
9 | + utils = views.utils, |
10 | + widgets = Y.namespace('juju.widgets'); |
11 | |
12 | /** |
13 | * The main app class. |
14 | @@ -398,7 +399,8 @@ |
15 | |
16 | // Set up a new modelController instance. |
17 | this.modelController = new juju.ModelController({ |
18 | - db: this.db |
19 | + db: this.db, |
20 | + store: this.get('store') |
21 | }); |
22 | |
23 | // Update the on-screen environment name provided in the configuration, |
24 | @@ -1082,7 +1084,6 @@ |
25 | this.db.environment.set('defaultSeries', evt.newVal); |
26 | }, |
27 | |
28 | - |
29 | /** |
30 | Display the Environment Name. |
31 | |
32 | @@ -1126,10 +1127,71 @@ |
33 | // route on root namespaced paths and this check will no longer |
34 | // be needed |
35 | this.renderEnvironment = false; |
36 | + |
37 | + // XXX bug:1217383 |
38 | + // We're hiding the subapp from view, but people want to be able to |
39 | + // click on the viewmode controls. We handle that here as a temp |
40 | + // hack until the old :gui: views are gone and we've moved to the |
41 | + // serviceInspector. Then the browser will always be around and can |
42 | + // handle this widget for us. This is horrible and we know it. When |
43 | + // the idea of 'hidden' is removed with the old views this hack will |
44 | + // go away with it. |
45 | + if (!this._controlEvents || this._controlEvents.length === 0) { |
46 | + this._controls = new widgets.ViewmodeControls({ |
47 | + currentViewmode: subapps.charmbrowser._viewState.viewmode |
48 | + }); |
49 | + this._controls.render(); |
50 | + this._controlEvents = []; |
51 | + this._controlEvents.push( |
52 | + this._controls.on( |
53 | + this._controls.EVT_FULLSCREEN, |
54 | + function(ev) { |
55 | + // Navigate away from anything in :gui: and to the |
56 | + // /fullscreen in :charmbrowser: |
57 | + this._controls._updateActiveNav('fullscreen'); |
58 | + this.navigate(this.nsRouter.url({ |
59 | + gui: '/', |
60 | + charmbrowser: '/fullscreen' |
61 | + }), { overrideAllNamespaces: true }); |
62 | + |
63 | + }, this |
64 | + ) |
65 | + ); |
66 | + this._controlEvents.push( |
67 | + this._controls.on( |
68 | + this._controls.EVT_SIDEBAR, |
69 | + function(ev) { |
70 | + // Navigate away from anything in :gui: and to the |
71 | + // /sidebar in :charmbrowser: |
72 | + this._controls._updateActiveNav('sidebar'); |
73 | + this.navigate(this.nsRouter.url({ |
74 | + gui: '/', |
75 | + charmbrowser: '/sidebar' |
76 | + }), { overrideAllNamespaces: true }); |
77 | + }, this |
78 | + ) |
79 | + ); |
80 | + } |
81 | + |
82 | } else { |
83 | charmbrowser.hidden = false; |
84 | this.renderEnvironment = true; |
85 | + |
86 | + // XXX bug:1217383 |
87 | + // Destroy the controls widget we might have had around for a bit. |
88 | + if (this._controlEvents) { |
89 | + this._controlEvents.forEach(function(ev) { |
90 | + ev.detach(); |
91 | + }); |
92 | + // reset the list to no events. |
93 | + this._controlEvents = []; |
94 | + } |
95 | + |
96 | + if (this._controls) { |
97 | + this._controls.destroy(); |
98 | + } |
99 | } |
100 | + |
101 | charmbrowser.updateVisible(); |
102 | } |
103 | |
104 | @@ -1444,6 +1506,8 @@ |
105 | 'model-controller', |
106 | 'FileSaver', |
107 | 'juju-inspector-widget', |
108 | - 'juju-ghost-inspector' |
109 | + 'juju-ghost-inspector', |
110 | + 'juju-view-bundle', |
111 | + 'viewmode-controls' |
112 | ] |
113 | }); |
114 | |
115 | === modified file 'app/assets/javascripts/d3-components.js' |
116 | --- app/assets/javascripts/d3-components.js 2013-08-08 22:36:30 +0000 |
117 | +++ app/assets/javascripts/d3-components.js 2013-08-30 19:48:17 +0000 |
118 | @@ -132,6 +132,7 @@ |
119 | |
120 | modEvents = module.events; |
121 | this.events[module.name] = modEvents; |
122 | + |
123 | this.bind(module.name); |
124 | module.componentBound(); |
125 | |
126 | @@ -286,6 +287,7 @@ |
127 | * @method bind |
128 | **/ |
129 | bind: function(moduleName) { |
130 | + if (this.get('interactive') === false) { return; } |
131 | var eventSet = this.events, |
132 | filtered = {}; |
133 | |
134 | @@ -319,6 +321,7 @@ |
135 | owns = Y.Object.owns, |
136 | module; |
137 | |
138 | + if (this.get('interactive') === false) { return; } |
139 | if (!modEvents || !modEvents.d3) { |
140 | return; |
141 | } |
142 | @@ -520,7 +523,15 @@ |
143 | } |
144 | }, { |
145 | ATTRS: { |
146 | - container: {} |
147 | + container: {}, |
148 | + /** |
149 | + Boolean indicating if bind should be allowed to actually |
150 | + bind events for the component. |
151 | + |
152 | + @property {Boolean} interactive |
153 | + @default true |
154 | + */ |
155 | + interactive: {value: true} |
156 | } |
157 | |
158 | }); |
159 | |
160 | === modified file 'app/config-debug.js' |
161 | --- app/config-debug.js 2013-08-14 14:34:19 +0000 |
162 | +++ app/config-debug.js 2013-08-30 19:48:17 +0000 |
163 | @@ -40,7 +40,7 @@ |
164 | socket_port: 8081, |
165 | user: 'admin', |
166 | password: 'admin', |
167 | - apiBackend: 'python', // Value can be 'python' or 'go'. |
168 | + apiBackend: 'go', // Value can be 'python' or 'go'. |
169 | sandbox: true, |
170 | // When in sandbox mode should we create events to simulate a live env. |
171 | // You can also use the :flags:/simulateEvents feature flag. |
172 | |
173 | === modified file 'app/config-prod.js' |
174 | --- app/config-prod.js 2013-07-31 13:20:37 +0000 |
175 | +++ app/config-prod.js 2013-08-30 19:48:17 +0000 |
176 | @@ -40,7 +40,7 @@ |
177 | socket_port: 8081, |
178 | user: undefined, |
179 | password: undefined, |
180 | - apiBackend: 'python', // Value can be 'python' or 'go'. |
181 | + apiBackend: 'go', // Value can be 'python' or 'go'. |
182 | sandbox: false, |
183 | // When in sandbox mode should we create events to simulate a live env. |
184 | // You can also use the :flags:/simulateEvents feature flag. |
185 | |
186 | === modified file 'app/index.html' |
187 | --- app/index.html 2013-08-01 23:47:57 +0000 |
188 | +++ app/index.html 2013-08-30 19:48:17 +0000 |
189 | @@ -82,7 +82,7 @@ |
190 | <div class="header"> |
191 | <div class="error"> |
192 | <span><i class="sprite alert_icon2"></i> |
193 | - <h4>Your browser is not fully supported</h4></span> |
194 | + <h4>Your browser is not supported</h4></span> |
195 | </div> |
196 | </div> |
197 | <p> |
198 | @@ -213,8 +213,8 @@ |
199 | }; |
200 | |
201 | isBrowserSupported = function(agent) { |
202 | - // At the moment Chrome and Firefox are supported. |
203 | - return (/Chrome|Firefox/.test(agent)); |
204 | + // Latest Chrome, Firefox, IE10 are supported |
205 | + return (/Chrome|Firefox|MSIE\ 10/.test(agent)); |
206 | }; |
207 | |
208 | displayBrowserWarning = function() { |
209 | |
210 | === modified file 'app/models/charm.js' |
211 | --- app/models/charm.js 2013-08-21 16:02:10 +0000 |
212 | +++ app/models/charm.js 2013-08-30 19:48:17 +0000 |
213 | @@ -30,7 +30,7 @@ |
214 | var RECENT_DAYS = 30; |
215 | |
216 | var models = Y.namespace('juju.models'); |
217 | - var charmIdRe = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+)$/; |
218 | + var charmIdRe = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+|HEAD)$/; |
219 | var idElements = ['scheme', 'owner', 'series', 'package_name', 'revision']; |
220 | var simpleCharmIdRe = /^(?:(\w+):)?(?!:~)(\w+)$/; |
221 | var simpleIdElements = ['scheme', 'package_name']; |
222 | |
223 | === modified file 'app/models/model-controller.js' |
224 | --- app/models/model-controller.js 2013-05-17 14:51:05 +0000 |
225 | +++ app/models/model-controller.js 2013-08-30 19:48:17 +0000 |
226 | @@ -82,7 +82,7 @@ |
227 | if (charm && charm.loaded) { |
228 | resolve(charm); |
229 | } else { |
230 | - charm = db.charms.add({id: charmId}).load(env, |
231 | + charm = db.charms.add({url: charmId}).load(env, |
232 | // If views are bound to the charm model, firing "update" is |
233 | // unnecessary, and potentially even mildly harmful. |
234 | function(err, data) { |
235 | @@ -144,6 +144,7 @@ |
236 | getServiceWithCharm: function(serviceId) { |
237 | var db = this.get('db'), |
238 | env = this.get('env'), |
239 | + store = this.get('store'), |
240 | mController = this; |
241 | |
242 | return this._getPromise( |
243 | @@ -151,7 +152,21 @@ |
244 | function(resolve, reject) { |
245 | mController.getService(serviceId).then(function(service) { |
246 | mController.getCharm(service.get('charm')).then(function(charm) { |
247 | - resolve({service: service, charm: charm}); |
248 | + // Check if a newer charm is available for this service so that |
249 | + // we can offer it as an upgrade. |
250 | + // XXX Makyo Aug. 20 - Remove feature flag when upgradecharm |
251 | + // feature lands. |
252 | + if (charm.get('scheme') === 'cs' && |
253 | + window.flags.upgradeCharm) { |
254 | + store.promiseUpgradeAvailability(charm, db.charms) |
255 | + .then(function(latestId) { |
256 | + service.set('upgrade_available', !!latestId); |
257 | + service.set('upgrade_to', latestId); |
258 | + resolve({service: service, charm: charm}); |
259 | + }, reject); |
260 | + } else { |
261 | + resolve({service: service, charm: charm}); |
262 | + } |
263 | }, reject); |
264 | }, reject); |
265 | }); |
266 | @@ -160,6 +175,15 @@ |
267 | }, { |
268 | ATTRS: { |
269 | /** |
270 | + Reference to the client db. |
271 | + |
272 | + @attribute db |
273 | + @type {Y.Base} |
274 | + @default undefined |
275 | + */ |
276 | + db: {}, |
277 | + |
278 | + /** |
279 | Reference to the client env. |
280 | |
281 | @attribute env |
282 | @@ -169,13 +193,13 @@ |
283 | env: {}, |
284 | |
285 | /** |
286 | - Reference to the client db. |
287 | + Reference to the client charm store. |
288 | |
289 | - @attribute db |
290 | - @type {Y.Base} |
291 | + @attribute store |
292 | + @type {Charmworld2} |
293 | @default undefined |
294 | */ |
295 | - db: {} |
296 | + store: {} |
297 | } |
298 | }); |
299 | |
300 | |
301 | === modified file 'app/models/models.js' |
302 | --- app/models/models.js 2013-08-21 16:02:10 +0000 |
303 | +++ app/models/models.js 2013-08-30 19:48:17 +0000 |
304 | @@ -196,6 +196,25 @@ |
305 | value: ALIVE |
306 | }, |
307 | unit_count: {}, |
308 | + |
309 | + /** |
310 | + Whether or not an upgrade is available. |
311 | + |
312 | + @attribute upgrade_available |
313 | + @type {boolean} |
314 | + @default false |
315 | + */ |
316 | + upgrade_available: { |
317 | + value: false |
318 | + }, |
319 | + |
320 | + /** |
321 | + The latest charm URL that the service can be upgraded to. |
322 | + |
323 | + @attribute upgrade_to |
324 | + @type {string} |
325 | + */ |
326 | + upgrade_to: {}, |
327 | aggregated_status: {} |
328 | } |
329 | }); |
330 | |
331 | === modified file 'app/modules-debug.js' |
332 | --- app/modules-debug.js 2013-08-19 15:53:41 +0000 |
333 | +++ app/modules-debug.js 2013-08-30 19:48:17 +0000 |
334 | @@ -226,6 +226,9 @@ |
335 | fullpath: '/juju-ui/views/topology/topology.js' |
336 | }, |
337 | |
338 | + 'juju-view-bundle': { |
339 | + fullpath: '/juju-ui/views/topology/bundle.js' |
340 | + }, |
341 | 'juju-view-utils': { |
342 | fullpath: '/juju-ui/views/utils.js' |
343 | }, |
344 | |
345 | === modified file 'app/store/charm.js' |
346 | --- app/store/charm.js 2013-08-21 16:02:10 +0000 |
347 | +++ app/store/charm.js 2013-08-30 19:48:17 +0000 |
348 | @@ -212,6 +212,31 @@ |
349 | }, |
350 | |
351 | /** |
352 | + Promises to return the latest charm ID for a given charm if a newer one |
353 | + exists; this also caches the newer charm if one is available. |
354 | + |
355 | + @method promiseUpgradeAvailability |
356 | + @param {Charm} charm an existing charm potentially in need of an upgrade. |
357 | + @param {ModelList} cache a local cache of browser charms. |
358 | + @return {Promise} with an id or undefined. |
359 | + */ |
360 | + promiseUpgradeAvailability: function(charm, cache) { |
361 | + // Get the charm's store ID, then replace the version number |
362 | + // with '-HEAD' to retrieve the latest version of the charm. |
363 | + var storeId = charm.get('storeId').replace(/-\d+$/, '-HEAD'); |
364 | + return this.promiseCharm(storeId, cache) |
365 | + .then(function(latest) { |
366 | + var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10), |
367 | + currentVersion = parseInt(charm.get('revision'), 10); |
368 | + if (latestVersion > currentVersion) { |
369 | + return latest.charm.id; |
370 | + } |
371 | + }, function(e) { |
372 | + throw e; |
373 | + }); |
374 | + }, |
375 | + |
376 | + /** |
377 | * Api call to search charms |
378 | * |
379 | * @method search |
380 | |
381 | === modified file 'app/store/env/env.js' |
382 | --- app/store/env/env.js 2013-05-17 14:51:05 +0000 |
383 | +++ app/store/env/env.js 2013-08-30 19:48:17 +0000 |
384 | @@ -26,8 +26,8 @@ |
385 | |
386 | YUI.add('juju-env', function(Y) { |
387 | |
388 | - // Default to the Python environment. |
389 | - var DEFAULT_BACKEND = 'python'; |
390 | + // Default to the Go environment. |
391 | + var DEFAULT_BACKEND = 'go'; |
392 | |
393 | /** |
394 | * Create and return a store environment suitable for connecting to the |
395 | |
396 | === modified file 'app/store/env/fakebackend.js' |
397 | --- app/store/env/fakebackend.js 2013-08-13 16:22:03 +0000 |
398 | +++ app/store/env/fakebackend.js 2013-08-30 19:48:17 +0000 |
399 | @@ -368,11 +368,17 @@ |
400 | throw e; |
401 | } |
402 | } |
403 | + |
404 | + var constraints = {}; |
405 | + if (options.constraints) { |
406 | + constraints = options.constraints; |
407 | + } |
408 | + |
409 | var service = this.db.services.add({ |
410 | id: options.name, |
411 | name: options.name, |
412 | charm: charm.get('id'), |
413 | - constraints: {}, |
414 | + constraints: constraints, |
415 | exposed: false, |
416 | subordinate: charm.get('is_subordinate'), |
417 | config: options.config |
418 | |
419 | === modified file 'app/store/env/go.js' |
420 | --- app/store/env/go.js 2013-08-13 18:25:46 +0000 |
421 | +++ app/store/env/go.js 2013-08-30 19:48:17 +0000 |
422 | @@ -119,6 +119,8 @@ |
423 | |
424 | Y.extend(GoEnvironment, environments.BaseEnvironment, { |
425 | |
426 | + genericConstraints: ['cpu-power', 'cpu-cores', 'mem', 'arch'], |
427 | + |
428 | /** |
429 | * Go environment constructor. |
430 | * |
431 | @@ -273,7 +275,11 @@ |
432 | */ |
433 | login: function() { |
434 | // If the user is already authenticated there is nothing to do. |
435 | - if (this.userIsAuthenticated || this.pendingLoginResponse) { |
436 | + if (this.userIsAuthenticated) { |
437 | + this.fire('login', {data: {result: true}}); |
438 | + return; |
439 | + } |
440 | + if (this.pendingLoginResponse) { |
441 | return; |
442 | } |
443 | var credentials = this.getCredentials(); |
444 | @@ -337,17 +343,31 @@ |
445 | configuration options. Only one of `config` and `config_raw` should be |
446 | provided, though `config_raw` takes precedence if it is given. |
447 | @param {Integer} num_units The number of units to be deployed. |
448 | + @param {Object} constraints The machine constraints to use in the |
449 | + object format key: value. |
450 | @param {Function} callback A callable that must be called once the |
451 | operation is performed. |
452 | @return {undefined} Sends a message to the server only. |
453 | */ |
454 | deploy: function(charm_url, service_name, config, config_raw, num_units, |
455 | - callback) { |
456 | + constraints, callback) { |
457 | var intermediateCallback = null; |
458 | if (callback) { |
459 | intermediateCallback = Y.bind(this.handleDeploy, this, |
460 | callback, service_name, charm_url); |
461 | } |
462 | + |
463 | + if (constraints) { |
464 | + // If the constraints is a function (this arg position used to be a |
465 | + // callback) then log it out to the console to fix it. |
466 | + if (typeof constraints === 'function') { |
467 | + console.error('Constraints need to be an object not a function'); |
468 | + console.warn(constraints); |
469 | + } |
470 | + } else { |
471 | + constraints = {}; |
472 | + } |
473 | + |
474 | this._send_rpc( |
475 | { Type: 'Client', |
476 | Request: 'ServiceDeploy', |
477 | @@ -355,6 +375,7 @@ |
478 | ServiceName: service_name, |
479 | Config: stringifyObjectValues(config), |
480 | ConfigYAML: config_raw, |
481 | + Constraints: constraints, |
482 | CharmUrl: charm_url, |
483 | NumUnits: num_units |
484 | } |
485 | @@ -868,9 +889,6 @@ |
486 | }, intermediateCallback); |
487 | }, |
488 | |
489 | - // The constraints that the backend understands. Used to generate forms. |
490 | - genericConstraints: ['cpu-power', 'cpu-cores', 'mem', 'arch'], |
491 | - |
492 | /** |
493 | Change the constraints of the given service. |
494 | |
495 | |
496 | === modified file 'app/store/env/python.js' |
497 | --- app/store/env/python.js 2013-06-24 14:56:21 +0000 |
498 | +++ app/store/env/python.js 2013-08-30 19:48:17 +0000 |
499 | @@ -255,13 +255,18 @@ |
500 | * @return {undefined} Sends a message to the server only. |
501 | */ |
502 | deploy: function(charm_url, service_name, config, config_raw, num_units, |
503 | - callback) { |
504 | + constraints, callback) { |
505 | + if (!constraints) { |
506 | + constraints = {}; |
507 | + } |
508 | + |
509 | this._send_rpc( |
510 | { op: 'deploy', |
511 | service_name: service_name, |
512 | config: config, |
513 | config_raw: config_raw, |
514 | charm_url: charm_url, |
515 | + constraints: constraints, |
516 | num_units: num_units}, |
517 | callback, true); |
518 | }, |
519 | |
520 | === modified file 'app/store/env/sandbox.js' |
521 | --- app/store/env/sandbox.js 2013-08-14 19:15:01 +0000 |
522 | +++ app/store/env/sandbox.js 2013-08-30 19:48:17 +0000 |
523 | @@ -482,6 +482,7 @@ |
524 | name: data.service_name, |
525 | config: data.config, |
526 | configYAML: data.config_raw, |
527 | + constraints: data.constraints, |
528 | unitCount: data.num_units |
529 | }); |
530 | }, |
531 | @@ -784,7 +785,6 @@ |
532 | @return {undefined} Nothing. |
533 | */ |
534 | receive: function(data) { |
535 | - console.log('client message', data); |
536 | if (this.connected) { |
537 | var client = this.get('client'); |
538 | this['handle' + data.Type + data.Request](data, |
539 | @@ -995,10 +995,12 @@ |
540 | var callback = Y.bind(function(result) { |
541 | this._basicReceive(data, client, result); |
542 | }, this); |
543 | + |
544 | state.deploy(data.Params.CharmUrl, callback, { |
545 | name: data.Params.ServiceName, |
546 | config: data.Params.Config, |
547 | configYAML: data.Params.ConfigYAML, |
548 | + constraints: data.Params.Constraints, |
549 | unitCount: data.Params.NumUnits |
550 | }); |
551 | }, |
552 | @@ -1300,6 +1302,7 @@ |
553 | 'base', |
554 | 'js-yaml', |
555 | 'json-parse', |
556 | + 'juju-env-go', |
557 | 'timers' |
558 | ] |
559 | }); |
560 | |
561 | === modified file 'app/subapps/browser/browser.js' |
562 | --- app/subapps/browser/browser.js 2013-08-15 15:40:05 +0000 |
563 | +++ app/subapps/browser/browser.js 2013-08-30 19:48:17 +0000 |
564 | @@ -51,8 +51,34 @@ |
565 | _cleanOldViews: function(newViewMode) { |
566 | if (this._hasStateChanged('viewmode') && this._oldState.viewmode) { |
567 | var viewAttr = '_' + this._oldState.viewmode; |
568 | - this[viewAttr].destroy(); |
569 | - delete this[viewAttr]; |
570 | + if (this[viewAttr]) { |
571 | + this[viewAttr].destroy(); |
572 | + delete this[viewAttr]; |
573 | + } |
574 | + } |
575 | + }, |
576 | + |
577 | + /** |
578 | + * Destroy and remove any lingering views. |
579 | + * |
580 | + * Make sure they don't linger and hold UX bound events on us when they |
581 | + * should be gone. |
582 | + * |
583 | + * @method _clearViews |
584 | + * |
585 | + */ |
586 | + _clearViews: function() { |
587 | + if (this._sidebar) { |
588 | + this._sidebar.destroy(); |
589 | + delete this._sidebar; |
590 | + } |
591 | + if (this._minimized) { |
592 | + this._minimized.destroy(); |
593 | + delete this._minimized; |
594 | + } |
595 | + if (this._fullscreen) { |
596 | + this._fullscreen.destroy(); |
597 | + delete this._fullscreen; |
598 | } |
599 | }, |
600 | |
601 | @@ -88,7 +114,6 @@ |
602 | */ |
603 | _getStateUrl: function(change) { |
604 | var urlParts = []; |
605 | - this._oldState = this._viewState; |
606 | |
607 | // If there are changes to the filters, we need to update our filter |
608 | // object first, and then generate a new query string for the state to |
609 | @@ -485,6 +510,7 @@ |
610 | viewmode: null |
611 | }; |
612 | this._viewState = Y.merge(this._oldState, {}); |
613 | + this._clearViews(); |
614 | }, |
615 | |
616 | /** |
617 | @@ -576,17 +602,6 @@ |
618 | // Add any sidebar charms to the running cache. |
619 | this._cache = Y.merge(this._cache, ev.cache); |
620 | }, this); |
621 | - this._editorial.on(this._editorial.EV_CATEGORY_LINK_CLICKED, |
622 | - function(ev) { |
623 | - var change = { |
624 | - search: true, |
625 | - filter: { |
626 | - categories: [ev.category] |
627 | - } |
628 | - }; |
629 | - this.fire('viewNavigate', {change: change}); |
630 | - }); |
631 | - |
632 | this._editorial.render(this._cache.interesting); |
633 | this._editorial.addTarget(this); |
634 | }, |
635 | @@ -644,7 +659,12 @@ |
636 | // If we've switched to viewmode fullscreen, we need to render it. |
637 | // We know the viewmode is already fullscreen because we're in this |
638 | // function. |
639 | - if (this._hasStateChanged('viewmode')) { |
640 | + var forceFullscreen = false; |
641 | + if (!this._fullscreen) { |
642 | + forceFullscreen = true; |
643 | + } |
644 | + |
645 | + if (this._hasStateChanged('viewmode') || forceFullscreen) { |
646 | var extraCfg = {}; |
647 | if (this._viewState.search || this._viewState.charmID) { |
648 | extraCfg.withHome = true; |
649 | @@ -730,8 +750,14 @@ |
650 | @param {function} next callable for the next route in the chain. |
651 | */ |
652 | sidebar: function(req, res, next) { |
653 | + // If we've gone from no _sidebar to having one, then force editorial to |
654 | + // render. |
655 | + var forceSidebar = false; |
656 | + if (!this._sidebar) { |
657 | + forceSidebar = true; |
658 | + } |
659 | // If we've switched to viewmode sidebar, we need to render it. |
660 | - if (this._hasStateChanged('viewmode')) { |
661 | + if (this._hasStateChanged('viewmode') || forceSidebar) { |
662 | this._sidebar = new views.Sidebar( |
663 | this._getViewCfg({ |
664 | container: this.get('container') |
665 | @@ -759,9 +785,7 @@ |
666 | } |
667 | |
668 | this.renderSearchResults(req, res, next); |
669 | - } |
670 | - |
671 | - if (this._shouldShowEditorial()) { |
672 | + } else if (this._shouldShowEditorial() || forceSidebar) { |
673 | // Showing editorial implies that other sidebar content is destroyed. |
674 | if (this._search) { |
675 | this._search.destroy(); |
676 | @@ -770,6 +794,7 @@ |
677 | this.renderEditorial(req, res, next); |
678 | } |
679 | |
680 | + |
681 | // If we've changed the charmID or the viewmode has changed and we have |
682 | // a charmID, render charmDetails. |
683 | if (this._shouldShowCharm()) { |
684 | @@ -863,6 +888,8 @@ |
685 | this[viewmode](req, res, next); |
686 | } |
687 | } else { |
688 | + // Update the app state even though we're not showing anything. |
689 | + this._saveState(); |
690 | // Let the next route go on. |
691 | next(); |
692 | } |
693 | @@ -920,6 +947,8 @@ |
694 | if (!this.hidden) { |
695 | this[viewmode](req, res, next); |
696 | } else { |
697 | + // Update the app state even though we're not showing anything. |
698 | + this._saveState(); |
699 | // Let the next route go on. |
700 | next(); |
701 | } |
702 | @@ -965,6 +994,8 @@ |
703 | if (!this.hidden) { |
704 | this[req.params.viewmode](req, res, next); |
705 | } else { |
706 | + // Update the app state even though we're not showing anything. |
707 | + this._saveState(); |
708 | // Let the next route go on. |
709 | next(); |
710 | } |
711 | @@ -992,6 +1023,7 @@ |
712 | if (this.hidden) { |
713 | browser.hide(); |
714 | minview.hide(); |
715 | + this._clearViews(); |
716 | } else { |
717 | if (this._viewState.viewmode === 'minimized') { |
718 | minview.show(); |
719 | |
720 | === modified file 'app/subapps/browser/templates/editorial.handlebars' |
721 | --- app/subapps/browser/templates/editorial.handlebars 2013-07-17 18:27:38 +0000 |
722 | +++ app/subapps/browser/templates/editorial.handlebars 2013-08-30 19:48:17 +0000 |
723 | @@ -21,7 +21,6 @@ |
724 | <div class="featured"></div> |
725 | <div class="popular"></div> |
726 | <div class="new"></div> |
727 | - {{> category-icons }} |
728 | |
729 | {{#if isFullscreen}} |
730 | </div> |
731 | |
732 | === modified file 'app/subapps/browser/views/editorial.js' |
733 | --- app/subapps/browser/views/editorial.js 2013-07-12 16:46:15 +0000 |
734 | +++ app/subapps/browser/views/editorial.js 2013-08-30 19:48:17 +0000 |
735 | @@ -41,7 +41,6 @@ |
736 | */ |
737 | ns.EditorialView = Y.Base.create('browser-view-sidebar', ns.CharmResults, [], |
738 | { |
739 | - EV_CATEGORY_LINK_CLICKED: 'category-link-clicked', |
740 | template: views.Templates.editorial, |
741 | |
742 | // How many of each charm container do we show by default. |
743 | @@ -69,33 +68,6 @@ |
744 | }, |
745 | |
746 | /** |
747 | - Binds clicks on the category links in the editorial view and fires |
748 | - that information to any listeners. |
749 | - |
750 | - @private |
751 | - @method _bindCategoryLinks |
752 | - */ |
753 | - _bindCategoryLinks: function() { |
754 | - var categories = Y.one('#category-links'); |
755 | - if (categories) { |
756 | - categories.delegate('click', function(ev) { |
757 | - // A link has been clicked, we need to kill the navigation |
758 | - // event. |
759 | - ev.halt(); |
760 | - var category = ev.currentTarget.getData('link'); |
761 | - var change = { |
762 | - search: true, |
763 | - filter: { |
764 | - categories: [category], |
765 | - replace: true |
766 | - } |
767 | - }; |
768 | - this.fire('viewNavigate', {change: change}); |
769 | - }, 'a', this); |
770 | - } |
771 | - }, |
772 | - |
773 | - /** |
774 | Renders the editorial, "interesting" data to the view. |
775 | |
776 | @private |
777 | @@ -195,9 +167,6 @@ |
778 | cache.charms.add(popularCharms); |
779 | cache.charms.add(featuredCharms); |
780 | this.fire(this.EV_CACHE_UPDATED, {cache: cache}); |
781 | - |
782 | - // Bind the category links, which now exist |
783 | - this._bindCategoryLinks(); |
784 | }, |
785 | |
786 | /** |
787 | |
788 | === modified file 'app/subapps/browser/views/minimized.js' |
789 | --- app/subapps/browser/views/minimized.js 2013-08-02 15:34:18 +0000 |
790 | +++ app/subapps/browser/views/minimized.js 2013-08-30 19:48:17 +0000 |
791 | @@ -29,7 +29,6 @@ |
792 | YUI.add('subapp-browser-minimized', function(Y) { |
793 | var ns = Y.namespace('juju.browser.views'), |
794 | views = Y.namespace('juju.views'); |
795 | - |
796 | /** |
797 | * The minimized state view. |
798 | * |
799 | @@ -37,7 +36,9 @@ |
800 | * @extends {Y.View} |
801 | * |
802 | */ |
803 | - ns.MinimizedView = Y.Base.create('browser-view-minimized', Y.View, [], { |
804 | + ns.MinimizedView = Y.Base.create('browser-view-minimized', Y.View, [ |
805 | + Y.juju.widgets.ViewmodeControlsViewExtension |
806 | + ], { |
807 | template: views.Templates.minimized, |
808 | |
809 | events: { |
810 | @@ -47,15 +48,13 @@ |
811 | }, |
812 | |
813 | /** |
814 | - * Toggle the visibility of the browser. Bound to nav controls in the |
815 | - * view, however this will be expanded to be controlled from the new |
816 | - * constant nav menu outside of the view once it's completed. |
817 | + * Toggle the visibility of the browser. |
818 | * |
819 | - * @method _toggle_sidebar |
820 | + * @method _toggleMinimized |
821 | * @param {Event} ev event to trigger the toggle. |
822 | * |
823 | */ |
824 | - _toggleViewState: function(ev) { |
825 | + _toggleMinimized: function(ev) { |
826 | ev.halt(); |
827 | |
828 | this.get('container').hide(); |
829 | @@ -88,6 +87,13 @@ |
830 | var tpl = this.template(), |
831 | tplNode = Y.Node.create(tpl); |
832 | this.get('container').setHTML(tplNode); |
833 | + // Make sure the controls starts out setting the correct active state |
834 | + // based on the current viewmode for our View. |
835 | + this.controls = new Y.juju.widgets.ViewmodeControls({ |
836 | + currentViewmode: this.get('oldViewMode') |
837 | + }); |
838 | + this.controls.render(); |
839 | + this._bindViewmodeControls(this.controls); |
840 | } |
841 | |
842 | }, { |
843 | @@ -116,6 +122,7 @@ |
844 | 'base', |
845 | 'juju-templates', |
846 | 'juju-views', |
847 | - 'view' |
848 | + 'view', |
849 | + 'viewmode-controls' |
850 | ] |
851 | }); |
852 | |
853 | === modified file 'app/subapps/browser/views/view.js' |
854 | --- app/subapps/browser/views/view.js 2013-08-08 15:17:01 +0000 |
855 | +++ app/subapps/browser/views/view.js 2013-08-30 19:48:17 +0000 |
856 | @@ -40,7 +40,8 @@ |
857 | * |
858 | */ |
859 | ns.MainView = Y.Base.create('browser-view-mainview', Y.View, [ |
860 | - Y.Event.EventTracker |
861 | + Y.Event.EventTracker, |
862 | + Y.juju.widgets.ViewmodeControlsViewExtension |
863 | ], { |
864 | |
865 | /** |
866 | @@ -85,20 +86,6 @@ |
867 | */ |
868 | _bindSearchWidgetEvents: function() { |
869 | var container = this.get('container'); |
870 | - this.addEvent( |
871 | - this.controls.on( |
872 | - this.controls.EVT_TOGGLE_VIEWABLE, this._toggleBrowser, this) |
873 | - ); |
874 | - |
875 | - this.addEvent( |
876 | - this.controls.on( |
877 | - this.controls.EVT_FULLSCREEN, this._goFullscreen, this) |
878 | - ); |
879 | - this.addEvent( |
880 | - this.controls.on( |
881 | - this.controls.EVT_SIDEBAR, this._goSidebar, this) |
882 | - ); |
883 | - |
884 | if (this.search) { |
885 | this.addEvent( |
886 | this.search.on( |
887 | @@ -106,7 +93,6 @@ |
888 | ); |
889 | } |
890 | |
891 | - |
892 | if (this.search) { |
893 | this.addEvent( |
894 | this.search.on( |
895 | @@ -137,6 +123,7 @@ |
896 | } |
897 | }, this); |
898 | } |
899 | + this._bindViewmodeControls(this.controls); |
900 | }, |
901 | |
902 | /** |
903 | @@ -233,11 +220,13 @@ |
904 | view, however this will be expanded to be controlled from the new |
905 | constant nav menu outside of the view once it's completed. |
906 | |
907 | - @method _toggle_sidebar |
908 | + This is called by the ViewmodeControlsViewExtension. |
909 | + |
910 | + @method _toggleMinimized |
911 | @param {Event} ev event to trigger the toggle. |
912 | |
913 | */ |
914 | - _toggleBrowser: function(ev) { |
915 | + _toggleMinimized: function(ev) { |
916 | ev.halt(); |
917 | |
918 | this.fire('viewNavigate', { |
919 | @@ -248,44 +237,9 @@ |
920 | }, |
921 | |
922 | /** |
923 | - Upon clicking the browser icon make sure we re-route to the |
924 | - new form of the UX. |
925 | - |
926 | - @method _goFullscreen |
927 | - @param {Event} ev the click event handler on the button. |
928 | - |
929 | - */ |
930 | - _goFullscreen: function(ev) { |
931 | - ev.halt(); |
932 | - this.fire('viewNavigate', { |
933 | - change: { |
934 | - viewmode: 'fullscreen' |
935 | - } |
936 | - }); |
937 | - }, |
938 | - |
939 | - /** |
940 | - Upon clicking the build icon make sure we re-route to the |
941 | - new form of the UX. |
942 | - |
943 | - @method _goSidebar |
944 | - @param {Event} ev the click event handler on the button. |
945 | - |
946 | - */ |
947 | - _goSidebar: function(ev) { |
948 | - ev.halt(); |
949 | - this.fire('viewNavigate', { |
950 | - change: { |
951 | - viewmode: 'sidebar' |
952 | - } |
953 | - }); |
954 | - }, |
955 | - |
956 | - /** |
957 | * Destroy this view and clear from the dom world. |
958 | * |
959 | * @method destructor |
960 | - * |
961 | */ |
962 | destructor: function() { |
963 | // Clean up any details view we might have hanging around. |
964 | |
965 | === removed file 'app/templates/category-icons.partial' |
966 | --- app/templates/category-icons.partial 2013-07-10 08:10:08 +0000 |
967 | +++ app/templates/category-icons.partial 1970-01-01 00:00:00 +0000 |
968 | @@ -1,57 +0,0 @@ |
969 | -<div id="category-links" class="categories"> |
970 | - {{#unless isFullscreen}} |
971 | - <h3 class="section-title"> |
972 | - Browse by category |
973 | - </h3> |
974 | - {{/unless}} |
975 | - <ul> |
976 | - <li> |
977 | - <a data-link="databases" href=""> |
978 | - <img |
979 | - src="/juju-ui/assets/images/non-sprites/category_icons/category-database.svg" |
980 | - alt="Databases" /> |
981 | - <span>Databases</span> |
982 | - </a> |
983 | - </li> |
984 | - <li> |
985 | - <a data-link="file-servers" href=""> |
986 | - <img |
987 | - src="/juju-ui/assets/images/non-sprites/category_icons/category-file-server.svg" |
988 | - alt="File Servers" /> |
989 | - <span>File Servers</span> |
990 | - </a> |
991 | - </li> |
992 | - <li> |
993 | - <a data-link="applications" href=""> |
994 | - <img |
995 | - src="/juju-ui/assets/images/non-sprites/category_icons/category-application.svg" |
996 | - alt="Applications" /> |
997 | - <span>Applications</span> |
998 | - </a> |
999 | - </li> |
1000 | - <li> |
1001 | - <a data-link="cache-proxy" href=""> |
1002 | - <img |
1003 | - src="/juju-ui/assets/images/non-sprites/category_icons/category-cache-proxy.svg" |
1004 | - alt="Cache/Proxy" /> |
1005 | - <span>Cache/Proxy</span> |
1006 | - </a> |
1007 | - </li> |
1008 | - <li> |
1009 | - <a data-link="app-servers" href=""> |
1010 | - <img |
1011 | - src="/juju-ui/assets/images/non-sprites/category_icons/category-app-server.svg" |
1012 | - alt="App Servers" /> |
1013 | - <span>App Servers</span> |
1014 | - </a> |
1015 | - </li> |
1016 | - <li> |
1017 | - <a data-link="misc" href=""> |
1018 | - <img |
1019 | - src="/juju-ui/assets/images/non-sprites/category_icons/category-misc.svg" |
1020 | - alt="Miscellaneous" /> |
1021 | - <span>Miscellaneous</span> |
1022 | - </a> |
1023 | - </li> |
1024 | - </ul> |
1025 | -</div> |
1026 | |
1027 | === modified file 'app/templates/ghost-config-viewlet.handlebars' |
1028 | --- app/templates/ghost-config-viewlet.handlebars 2013-08-08 00:06:48 +0000 |
1029 | +++ app/templates/ghost-config-viewlet.handlebars 2013-08-30 19:48:17 +0000 |
1030 | @@ -8,6 +8,17 @@ |
1031 | </div> |
1032 | </div> |
1033 | {{/unless}} |
1034 | + |
1035 | + {{#if constraints}} |
1036 | + <!-- Service Constraints added in --> |
1037 | + <div class="ghost-config-wrapper service-constraints"> |
1038 | + <div class="ghost-config-header">Constraints</div> |
1039 | + <div class="ghost-config-content use-defaults"> |
1040 | + {{> service-constraints-viewlet}} |
1041 | + </div> |
1042 | + </div> |
1043 | + {{/if}} |
1044 | + |
1045 | {{#if settings}} |
1046 | <!-- Service configuration form --> |
1047 | <div class="ghost-config-wrapper service-configuration"> |
1048 | @@ -35,4 +46,5 @@ |
1049 | </div> |
1050 | </div> |
1051 | {{/if}} |
1052 | + |
1053 | </div> |
1054 | |
1055 | === modified file 'app/templates/inspector-header.handlebars' |
1056 | --- app/templates/inspector-header.handlebars 2013-07-25 19:15:42 +0000 |
1057 | +++ app/templates/inspector-header.handlebars 2013-08-30 19:48:17 +0000 |
1058 | @@ -1,6 +1,8 @@ |
1059 | <header> |
1060 | <div class="service-charm"> |
1061 | - <div class="charm-icon" data-bind="icon"></div> |
1062 | + <div class="icon"> |
1063 | + <img src="{{icon}}" alt="{{name}} icon" class="icon"> |
1064 | + </div> |
1065 | <div class="details-wrapper"> |
1066 | {{#if ghost}} |
1067 | <input type="text" class="service-name config-field" name="service-name" value="{{package_name}}"><br /> |
1068 | |
1069 | === renamed file 'app/templates/viewlet-manager.handlebars' => 'app/templates/service-config-wrapper.handlebars' |
1070 | === modified file 'app/templates/service-constraints-viewlet.partial' |
1071 | --- app/templates/service-constraints-viewlet.partial 2013-08-05 02:08:41 +0000 |
1072 | +++ app/templates/service-constraints-viewlet.partial 2013-08-30 19:48:17 +0000 |
1073 | @@ -1,6 +1,6 @@ |
1074 | {{#constraints}} |
1075 | - <div class="control-group settings-wrapper"> |
1076 | - <div class="control-label" for="{{name}}">{{title}}</div> |
1077 | + <div class="settings-wrapper"> |
1078 | + <label for="{{name}}">{{title}}</label> |
1079 | <div> |
1080 | <input class="constraint-field" type="text" name="{{name}}" |
1081 | value="{{value}}" data-bind="constraints.{{name}}" /> |
1082 | |
1083 | === modified file 'app/templates/service-overview-constraints.handlebars' |
1084 | --- app/templates/service-overview-constraints.handlebars 2013-08-20 15:27:44 +0000 |
1085 | +++ app/templates/service-overview-constraints.handlebars 2013-08-30 19:48:17 +0000 |
1086 | @@ -1,25 +1,30 @@ |
1087 | <span>Scale up with the following constraints?</span> |
1088 | -<span class="constraint-details"> |
1089 | - {{#if cpu}} |
1090 | - {{cpu}}Ghz |
1091 | - {{else}} |
1092 | - Default CPU |
1093 | - {{/if}} |
1094 | - |
1095 | - {{#if mem}} |
1096 | - {{mem}}GB |
1097 | - {{else}} |
1098 | - Default Mem |
1099 | - {{/if}} |
1100 | - |
1101 | - {{#if arch}} |
1102 | - {{arch}} |
1103 | - {{else}} |
1104 | - Default Arch |
1105 | - {{/if}} |
1106 | +<span class="constraint-details hide-on-edit"> |
1107 | + {{#srvConstraints}} |
1108 | + {{#if cpu}} |
1109 | + {{cpu}}Ghz |
1110 | + {{else}} |
1111 | + Default CPU |
1112 | + {{/if}} |
1113 | + |
1114 | + {{#if mem}} |
1115 | + {{mem}}GB |
1116 | + {{else}} |
1117 | + Default Mem |
1118 | + {{/if}} |
1119 | + |
1120 | + {{#if arch}} |
1121 | + {{arch}} |
1122 | + {{else}} |
1123 | + Default Arch |
1124 | + {{/if}} |
1125 | + {{/srvConstraints}} |
1126 | </span> |
1127 | <div class="edit-constraints-wrapper"> |
1128 | - <a class="edit-constraints">Edit</a> |
1129 | + <a class="edit-constraints hide-on-edit">Edit</a> |
1130 | + <div class="editable-constraints" style="display: none;"> |
1131 | + {{> service-constraints-viewlet}} |
1132 | + </div> |
1133 | </div> |
1134 | <div class="overview-constraints"></div> |
1135 | <div class="inspector-buttons"> |
1136 | |
1137 | === modified file 'app/views/charm-panel.js' |
1138 | --- app/views/charm-panel.js 2013-08-14 15:52:29 +0000 |
1139 | +++ app/views/charm-panel.js 2013-08-30 19:48:17 +0000 |
1140 | @@ -632,7 +632,7 @@ |
1141 | } |
1142 | numUnits = charm.get('is_subordinate') ? 0 : parseInt(numUnits, 10); |
1143 | env.deploy(url, serviceName, config, this.configFileContent, |
1144 | - numUnits, function(ev) { |
1145 | + numUnits, null, function(ev) { |
1146 | if (ev.err) { |
1147 | console.log(url + ' deployment failed', ev.err); |
1148 | db.notifications.add( |
1149 | |
1150 | === modified file 'app/views/charm.js' |
1151 | --- app/views/charm.js 2013-08-09 20:52:35 +0000 |
1152 | +++ app/views/charm.js 2013-08-30 19:48:17 +0000 |
1153 | @@ -153,6 +153,9 @@ |
1154 | charmId, |
1155 | serviceName, |
1156 | config, |
1157 | + null, |
1158 | + null, |
1159 | + null, |
1160 | Y.bind(this._deployCallback, this) |
1161 | ); |
1162 | }, |
1163 | |
1164 | === modified file 'app/views/environment.js' |
1165 | --- app/views/environment.js 2013-08-21 15:47:36 +0000 |
1166 | +++ app/views/environment.js 2013-08-30 19:48:17 +0000 |
1167 | @@ -194,7 +194,7 @@ |
1168 | }, |
1169 | '.cancel-num-units': { click: '_closeUnitConfirm'}, |
1170 | '.confirm-num-units': { click: '_confirmUnitChange'}, |
1171 | - 'a.edit-constraints': { click: '_editUnitConstraints'}, |
1172 | + 'a.edit-constraints': { click: '_showEditUnitConstraints'}, |
1173 | // Settings viewlet. |
1174 | 'input.expose-toggle': { click: 'toggleExpose' }, |
1175 | '.config-file .fakebutton': { click: 'handleFileClick'}, |
1176 | @@ -216,7 +216,7 @@ |
1177 | 'unitDetails', |
1178 | 'inspectorHeader' |
1179 | ], |
1180 | - template: Y.juju.views.Templates['viewlet-manager'] |
1181 | + template: Y.juju.views.Templates['service-config-wrapper'] |
1182 | }, |
1183 | configGhost: { |
1184 | // controller will show the first one in this array by default |
1185 | |
1186 | === modified file 'app/views/ghost-inspector.js' |
1187 | --- app/views/ghost-inspector.js 2013-08-14 15:31:50 +0000 |
1188 | +++ app/views/ghost-inspector.js 2013-08-30 19:48:17 +0000 |
1189 | @@ -121,13 +121,22 @@ |
1190 | container, '.service-config .config-field'); |
1191 | } |
1192 | |
1193 | + // Deploy needs constraints in simple key:value object. |
1194 | + var constraints = utils.getElementsValuesMapping( |
1195 | + container, '.constraint-field'); |
1196 | + |
1197 | options.env.deploy( |
1198 | model.get('id'), |
1199 | serviceName, |
1200 | config, |
1201 | this.viewletManager.configFileContent, |
1202 | numUnits, |
1203 | - Y.bind(this._deployCallbackHandler, this, serviceName, config)); |
1204 | + constraints, |
1205 | + Y.bind(this._deployCallbackHandler, |
1206 | + this, |
1207 | + serviceName, |
1208 | + config, |
1209 | + constraints)); |
1210 | }, |
1211 | |
1212 | /** |
1213 | @@ -225,10 +234,10 @@ |
1214 | |
1215 | @method _deployCallbackHandler |
1216 | @param {String} serviceName The service name. |
1217 | - @param {Object} config The configuration oject of the service. |
1218 | + @param {Object} config The configuration object of the service. |
1219 | @param {Y.EventFacade} e The event facade from the deploy event. |
1220 | */ |
1221 | - _deployCallbackHandler: function(serviceName, config, e) { |
1222 | + _deployCallbackHandler: function(serviceName, config, constraints, e) { |
1223 | var options = this.options, |
1224 | db = options.db, |
1225 | ghostService = options.ghostService; |
1226 | @@ -283,7 +292,8 @@ |
1227 | id: serviceName, |
1228 | pending: false, |
1229 | loading: false, |
1230 | - config: config |
1231 | + config: config, |
1232 | + constraints: constraints |
1233 | }); |
1234 | |
1235 | this.closeInspector(); |
1236 | @@ -291,4 +301,7 @@ |
1237 | |
1238 | }; |
1239 | |
1240 | +}, '0.1.0', { |
1241 | + requires: [ |
1242 | + ] |
1243 | }); |
1244 | |
1245 | === modified file 'app/views/inspector.js' |
1246 | --- app/views/inspector.js 2013-08-16 20:29:39 +0000 |
1247 | +++ app/views/inspector.js 2013-08-30 19:48:17 +0000 |
1248 | @@ -123,10 +123,14 @@ |
1249 | */ |
1250 | _confirmUnitConstraints: function(requestedUnitCount) { |
1251 | var container = this.viewletManager.viewlets.overview.container, |
1252 | + genericConstraints = this.options.env.genericConstraints, |
1253 | confirm = container.one('.unit-constraints-confirm'), |
1254 | - constraints = this.model.get('constraints') || {}; |
1255 | + srvConstraints = this.model.get('constraints') || {}; |
1256 | |
1257 | - confirm.setHTML(Templates['service-overview-constraints'](constraints)); |
1258 | + confirm.setHTML(Templates['service-overview-constraints']({ |
1259 | + srvConstraints: srvConstraints, |
1260 | + constraints: utils.getConstraints(srvConstraints, genericConstraints) |
1261 | + })); |
1262 | confirm.removeClass('closed'); |
1263 | }, |
1264 | |
1265 | @@ -145,7 +149,10 @@ |
1266 | this.resetUnits(); |
1267 | } |
1268 | |
1269 | + // editing class added if the user clicked 'edit' |
1270 | + confirm.removeClass('editing'); |
1271 | confirm.addClass('closed'); |
1272 | + this.overviewConstraintsEdit = false; |
1273 | }, |
1274 | |
1275 | /** |
1276 | @@ -156,8 +163,19 @@ |
1277 | */ |
1278 | _confirmUnitChange: function(e) { |
1279 | e.halt(); |
1280 | - var container = this.viewletManager.viewlets.overview.container; |
1281 | - this._modifyUnits(container.one('input.num-units-control').get('value')); |
1282 | + var container = this.viewletManager.viewlets.overview.container, |
1283 | + unitCount = container.one('input.num-units-control').get('value'), |
1284 | + service = this.model; |
1285 | + |
1286 | + // If the user chose to edit the constraints |
1287 | + if (this.overviewConstraintsEdit) { |
1288 | + var constraints = utils.getElementsValuesMapping( |
1289 | + container, '.constraint-field'); |
1290 | + var cb = Y.bind(this._modifyUnits, this, unitCount); |
1291 | + this.options.env.set_constraints(service.get('id'), constraints, cb); |
1292 | + } else { |
1293 | + this._modifyUnits(unitCount); |
1294 | + } |
1295 | this._closeUnitConfirm(); |
1296 | }, |
1297 | |
1298 | @@ -165,11 +183,15 @@ |
1299 | Shows the unit constraints when the user wants to edit them |
1300 | while increasing the total number of units |
1301 | |
1302 | - @method _editUnitConstraints |
1303 | + @method _showEditUnitConstraints |
1304 | */ |
1305 | - _editUnitConstraints: function() { |
1306 | - // show constraints viewlet on overview page to allow the user to |
1307 | - // edit them without changing viewlets. |
1308 | + _showEditUnitConstraints: function(e) { |
1309 | + e.halt(); |
1310 | + var container = this.viewletManager.viewlets.overview.container; |
1311 | + container.all('.hide-on-edit').hide(); |
1312 | + container.one('.editable-constraints').show(); |
1313 | + container.one('.unit-constraints-confirm').addClass('editing'); |
1314 | + this.overviewConstraintsEdit = true; |
1315 | }, |
1316 | |
1317 | _modifyUnits: function(requested_unit_count) { |
1318 | @@ -181,6 +203,7 @@ |
1319 | container = this.get('container'); |
1320 | env = this.get('env'); |
1321 | } |
1322 | + |
1323 | var service = this.model || this.get('model'); |
1324 | var unit_count = service.get('unit_count'); |
1325 | var field = container.one('.num-units-control'); |
1326 | |
1327 | === added file 'app/views/topology/bundle.js' |
1328 | --- app/views/topology/bundle.js 1970-01-01 00:00:00 +0000 |
1329 | +++ app/views/topology/bundle.js 2013-08-30 19:48:17 +0000 |
1330 | @@ -0,0 +1,368 @@ |
1331 | +/* |
1332 | +This file is part of the Juju GUI, which lets users view and manage Juju |
1333 | +environments within a graphical interface (https://launchpad.net/juju-gui). |
1334 | +Copyright (C) 2012-2013 Canonical Ltd. |
1335 | + |
1336 | +This program is free software: you can redistribute it and/or modify it under |
1337 | +the terms of the GNU Affero General Public License version 3, as published by |
1338 | +the Free Software Foundation. |
1339 | + |
1340 | +This program is distributed in the hope that it will be useful, but WITHOUT |
1341 | +ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
1342 | +SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero |
1343 | +General Public License for more details. |
1344 | + |
1345 | +You should have received a copy of the GNU Affero General Public License along |
1346 | +with this program. If not, see <http://www.gnu.org/licenses/>. |
1347 | +*/ |
1348 | + |
1349 | +'use strict'; |
1350 | + |
1351 | +/** |
1352 | + * Provide the BundleTopology class. |
1353 | + * |
1354 | + * @module views |
1355 | + * @submodule views.BundleTopology |
1356 | + */ |
1357 | + |
1358 | +YUI.add('juju-view-bundle', function(Y) { |
1359 | + |
1360 | + var juju = Y.namespace('juju'), |
1361 | + views = Y.namespace('juju.views'), |
1362 | + utils = Y.namespace('juju.views.utils'), |
1363 | + models = Y.namespace('juju.models'), |
1364 | + d3ns = Y.namespace('d3'), |
1365 | + topoUtils = Y.namespace('juju.topology.utils'); |
1366 | + |
1367 | + /** |
1368 | + Manage service rendering and events. |
1369 | + |
1370 | + @class BundleModule |
1371 | + */ |
1372 | + |
1373 | + var BundleModule = Y.Base.create('BundleModule', d3ns.Module, |
1374 | + [views.ServiceModuleCommon], { |
1375 | + |
1376 | + /** |
1377 | + Attempt to reuse as much of the existing graph and view models |
1378 | + as possible to re-render the graph. |
1379 | + |
1380 | + @method update |
1381 | + */ |
1382 | + update: function() { |
1383 | + var self = this, |
1384 | + topo = this.get('component'), |
1385 | + width = topo.get('width'), |
1386 | + height = topo.get('height'); |
1387 | + |
1388 | + // Process any changed data. |
1389 | + this.updateData(); |
1390 | + |
1391 | + // Generate a node for each service, draw it as a rect with |
1392 | + // labels for service and charm. |
1393 | + var node = this.node; |
1394 | + |
1395 | + // enter |
1396 | + node |
1397 | + .enter().append('g') |
1398 | + .attr({ |
1399 | + 'class': function(d) { |
1400 | + return (d.subordinate ? 'subordinate ' : '') + |
1401 | + (d.pending ? 'pending ' : '') + 'service'; |
1402 | + }, |
1403 | + 'transform': function(d) { return d.translateStr;}}) |
1404 | + .call(self.createServiceNode, self); |
1405 | + |
1406 | + // Update all nodes. |
1407 | + self.updateServiceNodes(node); |
1408 | + }, |
1409 | + |
1410 | + /** |
1411 | + Fill a service node with empty structures that will be filled out |
1412 | + in the update stage. |
1413 | + |
1414 | + @param {object} node the node to construct. |
1415 | + @param {object} self reference to the view instance. |
1416 | + @return {null} side effects only. |
1417 | + @method createServiceNode |
1418 | + */ |
1419 | + createServiceNode: function(node, self) { |
1420 | + node.append('image') |
1421 | + .classed('service-icon', true) |
1422 | + .attr({ |
1423 | + 'xlink:href': function(d) { |
1424 | + return d.icon; |
1425 | + }, |
1426 | + width: 96, |
1427 | + height: 96 |
1428 | + }); |
1429 | + node.append('text').append('tspan') |
1430 | + .attr('class', 'name') |
1431 | + .text(function(d) {return d.displayName; }); |
1432 | + }, |
1433 | + |
1434 | + /** |
1435 | + Fill the empty structures within a service node such that they |
1436 | + match the db. |
1437 | + |
1438 | + @param {object} node the collection of nodes to update. |
1439 | + @return {null} side effects only. |
1440 | + @method updateServiceNodes |
1441 | + */ |
1442 | + updateServiceNodes: function(node) { |
1443 | + if (node.empty()) { |
1444 | + return; |
1445 | + } |
1446 | + var self = this, |
1447 | + topo = this.get('component'), |
1448 | + landscape = topo.get('landscape'); |
1449 | + |
1450 | + // Apply Position Annotations |
1451 | + // This is done after the services_boxes |
1452 | + // binding as the event handler will |
1453 | + // use that index. |
1454 | + node.each(function(d) { |
1455 | + var service = d.model, |
1456 | + annotations = service.get('annotations'), |
1457 | + x, y; |
1458 | + |
1459 | + if (!annotations) { |
1460 | + return; |
1461 | + } |
1462 | + |
1463 | + // If there are x/y annotations on the service model and they are |
1464 | + // different from the node's current x/y coordinates, update the |
1465 | + // node, as the annotations may have been set in another session. |
1466 | + x = annotations['gui-x']; |
1467 | + y = annotations['gui-y']; |
1468 | + if (!d || |
1469 | + (x !== undefined && x !== d.x) || |
1470 | + (y !== undefined && y !== d.y)) { |
1471 | + d.x = x; |
1472 | + d.y = y; |
1473 | + d3.select(this).attr({ |
1474 | + x: x, |
1475 | + y: y, |
1476 | + transform: d.translateStr}); |
1477 | + |
1478 | + }}); |
1479 | + |
1480 | + // Mark subordinates as such. This is needed for when a new service |
1481 | + // is created. |
1482 | + node.filter(function(d) { |
1483 | + return d.subordinate; |
1484 | + }).classed('subordinate', true); |
1485 | + |
1486 | + // Size the node for drawing. |
1487 | + node.attr({ |
1488 | + 'width': function(box) { box.w = 96; return box.w;}, |
1489 | + 'height': function(box) { box.h = 96; return box.h;} |
1490 | + }); |
1491 | + |
1492 | + // Draw a subordinate relation indicator. |
1493 | + var subRelationIndicator = node.filter(function(d) { |
1494 | + return d.subordinate && |
1495 | + d3.select(this) |
1496 | + .select('.sub-rel-block').empty(); |
1497 | + }) |
1498 | + .append('g') |
1499 | + .attr('class', 'sub-rel-block') |
1500 | + .attr('transform', function(d) { |
1501 | + // Position the block so that the relation indicator will |
1502 | + // appear at the right connector. |
1503 | + return 'translate(' + [d.w, d.h / 2 - 26] + ')'; |
1504 | + }); |
1505 | + |
1506 | + subRelationIndicator.append('image') |
1507 | + .attr({'xlink:href': '/juju-ui/assets/svgs/sub_relation.svg', |
1508 | + 'width': 87, |
1509 | + 'height': 47}); |
1510 | + subRelationIndicator.append('text').append('tspan') |
1511 | + .attr({'class': 'sub-rel-count', |
1512 | + 'x': 64, |
1513 | + 'y': 47 * 0.8}); |
1514 | + |
1515 | + // The following are sizes in pixels of the SVG assets used to |
1516 | + // render a service, and are used to in calculating the vertical |
1517 | + // positioning of text down along the service block. |
1518 | + var service_height = 224, |
1519 | + name_size = 22, |
1520 | + charm_label_size = 16, |
1521 | + name_padding = 26, |
1522 | + charm_label_padding = 150; |
1523 | + |
1524 | + node.select('.name') |
1525 | + .attr({'style': function(d) { |
1526 | + // Programmatically size the font. |
1527 | + // Number derived from service assets: |
1528 | + // font-size 22px when asset is 224px. |
1529 | + return 'font-size:' + d.h * |
1530 | + (name_size / service_height) + 'px'; |
1531 | + }, |
1532 | + 'x': function(d) { return d.w / 2; }, |
1533 | + 'y': function(d) { |
1534 | + // Number derived from service assets: |
1535 | + // padding-top 26px when asset is 224px. |
1536 | + return d.h * (name_padding / service_height) + d.h * |
1537 | + (name_size / service_height) / 2; |
1538 | + } |
1539 | + }); |
1540 | + |
1541 | + // Show whether or not the service is exposed using an indicator. |
1542 | + var exposed = node.filter(function(d) { |
1543 | + return d.exposed; |
1544 | + }); |
1545 | + exposed.each(function(d) { |
1546 | + var existing = Y.one(this).one('.exposed-indicator'); |
1547 | + if (!existing) { |
1548 | + existing = d3.select(this).append('image') |
1549 | + .attr({'class': 'exposed-indicator on', |
1550 | + 'xlink:href': '/juju-ui/assets/svgs/exposed.svg', |
1551 | + 'width': 32, |
1552 | + 'height': 32 |
1553 | + }) |
1554 | + .append('title') |
1555 | + .text(function(d) { |
1556 | + return d.exposed ? 'Exposed' : ''; |
1557 | + }); |
1558 | + } |
1559 | + existing = d3.select(this).select('.exposed-indicator') |
1560 | + .attr({ |
1561 | + 'x': 145, |
1562 | + 'y': 79 |
1563 | + }); |
1564 | + }); |
1565 | + }, |
1566 | + |
1567 | + |
1568 | + /** |
1569 | + Pans the environment view to the center all the services on the canvas. |
1570 | + |
1571 | + @method panToCenter |
1572 | + @param {object} evt The event fired. |
1573 | + @return {undefined} Side effects only. |
1574 | + */ |
1575 | + panToCenter: function(evt) { |
1576 | + var topo = this.get('component'); |
1577 | + var vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes); |
1578 | + this.findAndSetCentroid(vertices); |
1579 | + }, |
1580 | + |
1581 | + /** |
1582 | + Given a set of vertices, find the centroid and pan to that location. |
1583 | + |
1584 | + @method findAndSetCentroid |
1585 | + @param {array} vertices A list of vertices in the form [x, y]. |
1586 | + @return {undefined} Side effects only. |
1587 | + */ |
1588 | + findAndSetCentroid: function(vertices) { |
1589 | + var topo = this.get('component'); |
1590 | + var centroid = topoUtils.centroid(vertices); |
1591 | + /* The centroid is set on the topology object due to the fact that it |
1592 | + is used as a sigil to tell whether or not to pan to the point |
1593 | + after the first delta. */ |
1594 | + topo.centroid = centroid; |
1595 | + topo.fire('panToPoint', {point: topo.centroid}); |
1596 | + } |
1597 | + }, { |
1598 | + ATTRS: { |
1599 | + /** |
1600 | + @property {d3ns.Component} component |
1601 | + */ |
1602 | + component: {} |
1603 | + } |
1604 | + }); |
1605 | + views.BundleModule = BundleModule; |
1606 | + |
1607 | + /** |
1608 | + Display a Bundle using an internal topology. |
1609 | + |
1610 | + @class BundleTopology |
1611 | + */ |
1612 | + function BundleTopology(options) { |
1613 | + // Options and Init |
1614 | + var self = this; |
1615 | + options = options || {}; |
1616 | + this.options = options; |
1617 | + this._cleanups = []; |
1618 | + this.db = options.db; |
1619 | + if (!this.db) { |
1620 | + this.db = new models.Database(); |
1621 | + this._cleanups.push(this.db.destroy); |
1622 | + } |
1623 | + this.store = options.store; |
1624 | + if (!this.store) { |
1625 | + this.store = new juju.Charmworld2({}); |
1626 | + this._cleanups.push(this.store.destroy); |
1627 | + } |
1628 | + this.container = options.container; |
1629 | + if (!this.container) { |
1630 | + this.container = Y.Node.create('<div>'); |
1631 | + this.container.addClass('topology-canvas'); |
1632 | + this._cleanups.push(function() { |
1633 | + self.container.remove(true); |
1634 | + }); |
1635 | + } |
1636 | + |
1637 | + var topo = this.topology = new views.Topology(); |
1638 | + topo.setAttrs(Y.mix(options, { |
1639 | + interactive: false, |
1640 | + container: this.container, |
1641 | + db: this.db, |
1642 | + store: this.store |
1643 | + }, true)); |
1644 | + |
1645 | + // Service view doesn't support Level Of Detail views. |
1646 | + // BundleModule provides an icon centric view |
1647 | + // of services till service module can support this directly. |
1648 | + topo.addModule(views.BundleModule); |
1649 | + topo.addModule(views.RelationModule); |
1650 | + topo.addModule(views.PanZoomModule); |
1651 | + } |
1652 | + |
1653 | + BundleTopology.prototype.centerViewport = function(scale) { |
1654 | + this.topology.modules.PanZoomModule._fire_zoom(scale); |
1655 | + // Pan to the centroid of it all after the zoom |
1656 | + this.panToCenter(); |
1657 | + return this; |
1658 | + }; |
1659 | + |
1660 | + /** |
1661 | + Pans the canvas to the center all the services. |
1662 | + |
1663 | + @method panToCenter |
1664 | + @param {object} evt The event fired. |
1665 | + @return {undefined} Side effects only. |
1666 | + */ |
1667 | + BundleTopology.prototype.panToCenter = function() { |
1668 | + var topo = this.topology; |
1669 | + var vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes); |
1670 | + var centroid = topoUtils.centroid(vertices); |
1671 | + this.topology.modules.PanZoomModule.panToPoint({point: centroid}); |
1672 | + }; |
1673 | + |
1674 | + |
1675 | + BundleTopology.prototype.render = function() { |
1676 | + this.topology.render(); |
1677 | + this.centerViewport(0.66); |
1678 | + return this; |
1679 | + }; |
1680 | + |
1681 | + BundleTopology.prototype.destroy = function() { |
1682 | + this._cleanups.forEach(function(cleanupFunc) { |
1683 | + cleanupFunc(); |
1684 | + }); |
1685 | + }; |
1686 | + |
1687 | + views.BundleTopology = BundleTopology; |
1688 | + |
1689 | +}, '0.1.0', { |
1690 | + requires: [ |
1691 | + 'd3', |
1692 | + 'd3-components', |
1693 | + 'juju-charm-store', |
1694 | + 'juju-models', |
1695 | + 'juju-topology', |
1696 | + 'juju-view-utils' |
1697 | + ] |
1698 | +}); |
1699 | |
1700 | === modified file 'app/views/topology/relation.js' |
1701 | --- app/views/topology/relation.js 2013-07-03 17:35:21 +0000 |
1702 | +++ app/views/topology/relation.js 2013-08-30 19:48:17 +0000 |
1703 | @@ -178,6 +178,9 @@ |
1704 | var db = topo.get('db'); |
1705 | var self = this; |
1706 | var relations = db.relations.toArray(); |
1707 | + if (!relations || relations.length === 0) { |
1708 | + return; |
1709 | + } |
1710 | this.relations = this.decorateRelations(relations); |
1711 | this.updateLinks(); |
1712 | this.updateSubordinateRelationsCount(); |
1713 | |
1714 | === modified file 'app/views/topology/service.js' |
1715 | --- app/views/topology/service.js 2013-08-21 16:02:10 +0000 |
1716 | +++ app/views/topology/service.js 2013-08-30 19:48:17 +0000 |
1717 | @@ -33,35 +33,303 @@ |
1718 | d3ns = Y.namespace('d3'), |
1719 | Templates = views.Templates; |
1720 | |
1721 | - /** |
1722 | - * Manage service rendering and events. |
1723 | - * |
1724 | - * ## Emitted events: |
1725 | - * |
1726 | - * - *clearState:* clear all possible states that the environment view can be |
1727 | - * in as it pertains to actions (building a relation, viewing |
1728 | - * a service menu, etc.) |
1729 | - * - *snapToService:* fired when mousing over a service, causing the pending |
1730 | - * relation dragline to snap to the service rather than |
1731 | - * following the mouse. |
1732 | - * - *snapOutOfService:* fired when mousing out of a service, causing the |
1733 | - * pending relation line to follow the mouse again. |
1734 | - * - *addRelationDrag:* |
1735 | - * - *addRelationDragStart:* |
1736 | - * - *addRelationDragEnd:* fired when creating a relation through the long- |
1737 | - * click process, when moving the cursor over the environment, and when |
1738 | - * dropping the endpoint on a valid service. |
1739 | - * - *cancelRelationBuild:* fired when dropping a pending relation line |
1740 | - * started through the long-click method somewhere other than a valid |
1741 | - * service. |
1742 | - * - *serviceMoved:* fired when a service block is dragged so that relation |
1743 | - * endpoints can follow it. |
1744 | - * - *navigateTo:* fired when clicking the "View Service" menu item or when |
1745 | - * double-clicking a service. |
1746 | - * |
1747 | - * @class ServiceModule |
1748 | + var ServiceModuleCommon = function() {}; |
1749 | + /** |
1750 | + Sync view models with current db.models. |
1751 | + |
1752 | + @method updateData |
1753 | + */ |
1754 | + ServiceModuleCommon.prototype.updateData = function() { |
1755 | + //model data |
1756 | + var topo = this.get('component'); |
1757 | + var vis = topo.vis; |
1758 | + var db = topo.get('db'); |
1759 | + var store = topo.get('store'); |
1760 | + |
1761 | + var visibleServices = db.services.visible(); |
1762 | + views.toBoundingBoxes(this, visibleServices, topo.service_boxes, store); |
1763 | + // Break a reference cycle that results in uncollectable objects leaking. |
1764 | + visibleServices.reset(); |
1765 | + |
1766 | + // Nodes are mapped by modelId tuples. |
1767 | + this.node = vis.selectAll('.service') |
1768 | + .data(Y.Object.values(topo.service_boxes), |
1769 | + function(d) {return d.modelId;}); |
1770 | + }; |
1771 | + |
1772 | + /** |
1773 | + Fill the empty structures within a service node such that they |
1774 | + match the db. |
1775 | + |
1776 | + @param {object} node the collection of nodes to update. |
1777 | + @return {null} side effects only. |
1778 | + @method updateServiceNodes |
1779 | + */ |
1780 | + ServiceModuleCommon.prototype.updateServiceNodes = function(node) { |
1781 | + if (node.empty()) { |
1782 | + return; |
1783 | + } |
1784 | + var self = this, |
1785 | + topo = this.get('component'), |
1786 | + landscape = topo.get('landscape'), |
1787 | + service_scale = this.service_scale, |
1788 | + service_scale_width = this.service_scale_width, |
1789 | + service_scale_height = this.service_scale_height; |
1790 | + |
1791 | + // Apply Position Annotations |
1792 | + // This is done after the services_boxes |
1793 | + // binding as the event handler will |
1794 | + // use that index. |
1795 | + node.each(function(d) { |
1796 | + var service = d.model, |
1797 | + annotations = service.get('annotations'), |
1798 | + x, y; |
1799 | + |
1800 | + // If there are no annotations or the service is being dragged |
1801 | + if (!annotations || service.inDrag === views.DRAG_ACTIVE) { |
1802 | + return; |
1803 | + } |
1804 | + |
1805 | + // If there are x/y annotations on the service model and they are |
1806 | + // different from the node's current x/y coordinates, update the |
1807 | + // node, as the annotations may have been set in another session. |
1808 | + x = annotations['gui-x']; |
1809 | + y = annotations['gui-y']; |
1810 | + if (!d || |
1811 | + (x !== undefined && x !== d.x) || |
1812 | + (y !== undefined && y !== d.y)) { |
1813 | + // Delete gui-x and gui-y from annotations as we use the values. |
1814 | + // This is to prevent deltas coming in on a service while it is |
1815 | + // being dragged from resetting its position during the drag. |
1816 | + |
1817 | + delete annotations['gui-x']; |
1818 | + delete annotations['gui-y']; |
1819 | + // Only update position if we're not already in a drag state (the |
1820 | + // current drag supercedes any previous annotations). |
1821 | + if (!d.inDrag) { |
1822 | + self.drag.call(this, d, self, {x: x, y: y}, |
1823 | + self.get('useTransitions')); |
1824 | + } |
1825 | + }}); |
1826 | + |
1827 | + // Mark subordinates as such. This is needed for when a new service |
1828 | + // is created. |
1829 | + node.filter(function(d) { |
1830 | + return d.subordinate; |
1831 | + }) |
1832 | + .classed('subordinate', true); |
1833 | + |
1834 | + // Size the node for drawing. |
1835 | + node.attr({ |
1836 | + 'width': function(box) { box.w = 190; return box.w;}, |
1837 | + 'height': function(box) { box.h = 190; return box.h;} |
1838 | + }); |
1839 | + |
1840 | + node.select('.service-block-image').each(function(d) { |
1841 | + var curr_node = d3.select(this); |
1842 | + var curr_href = curr_node.attr('xlink:href'); |
1843 | + var new_href = d.subordinate ? |
1844 | + '/juju-ui/assets/svgs/sub_module.svg' : |
1845 | + '/juju-ui/assets/svgs/service_module.svg'; |
1846 | + |
1847 | + // Only set 'xlink:href' if not already set to the new value, |
1848 | + // thus avoiding redundant requests to the server. #1182135 |
1849 | + if (curr_href !== new_href) { |
1850 | + curr_node.attr({'xlink:href': new_href}); |
1851 | + } |
1852 | + curr_node.attr({ |
1853 | + 'width': d.w, |
1854 | + 'height': d.h |
1855 | + }); |
1856 | + }); |
1857 | + |
1858 | + // Draw a subordinate relation indicator. |
1859 | + var subRelationIndicator = node.filter(function(d) { |
1860 | + return d.subordinate && |
1861 | + d3.select(this) |
1862 | + .select('.sub-rel-block').empty(); |
1863 | + }) |
1864 | + .append('g') |
1865 | + .attr('class', 'sub-rel-block') |
1866 | + .attr('transform', function(d) { |
1867 | + // Position the block so that the relation indicator will |
1868 | + // appear at the right connector. |
1869 | + return 'translate(' + [d.w, d.h / 2 - 26] + ')'; |
1870 | + }); |
1871 | + |
1872 | + subRelationIndicator.append('image') |
1873 | + .attr({'xlink:href': '/juju-ui/assets/svgs/sub_relation.svg', |
1874 | + 'width': 87, |
1875 | + 'height': 47}); |
1876 | + subRelationIndicator.append('text').append('tspan') |
1877 | + .attr({'class': 'sub-rel-count', |
1878 | + 'x': 64, |
1879 | + 'y': 47 * 0.8}); |
1880 | + |
1881 | + // Landscape badge |
1882 | + if (landscape) { |
1883 | + node.each(function(d) { |
1884 | + var landscapeAsset; |
1885 | + var securityBadge = landscape.getLandscapeBadge( |
1886 | + d.model, 'security', 'round'); |
1887 | + var rebootBadge = landscape.getLandscapeBadge( |
1888 | + d.model, 'reboot', 'round'); |
1889 | + |
1890 | + if (securityBadge && rebootBadge) { |
1891 | + landscapeAsset = |
1892 | + '/juju-ui/assets/images/landscape_restart_round.png'; |
1893 | + } else if (securityBadge) { |
1894 | + landscapeAsset = |
1895 | + '/juju-ui/assets/images/landscape_security_round.png'; |
1896 | + } else if (rebootBadge) { |
1897 | + landscapeAsset = |
1898 | + '/juju-ui/assets/images/landscape_restart_round.png'; |
1899 | + } |
1900 | + if (landscapeAsset === undefined) { |
1901 | + // Remove any existing badge. |
1902 | + d3.select(this).select('.landscape-badge').remove(); |
1903 | + } else { |
1904 | + var existing = Y.one(this).one('.landscape-badge'), |
1905 | + curr_href, target; |
1906 | + |
1907 | + if (!existing) { |
1908 | + existing = d3.select(this).append('image'); |
1909 | + existing.attr({ |
1910 | + 'class': 'landscape-badge', |
1911 | + 'width': 32, |
1912 | + 'height': 32 |
1913 | + }); |
1914 | + } |
1915 | + existing = d3.select(this).select('.landscape-badge'); |
1916 | + existing.attr({ |
1917 | + 'x': 13, |
1918 | + 'y': 79 |
1919 | + }); |
1920 | + |
1921 | + // Only set 'xlink:href' if not already set to the new value, |
1922 | + // thus avoiding redundant requests to the server. #1182135 |
1923 | + curr_href = existing.attr('xlink:href'); |
1924 | + if (curr_href !== landscapeAsset) { |
1925 | + existing.attr({'xlink:href': landscapeAsset}); |
1926 | + } |
1927 | + } |
1928 | + }); |
1929 | + } |
1930 | + // The following are sizes in pixels of the SVG assets used to |
1931 | + // render a service, and are used to in calculating the vertical |
1932 | + // positioning of text down along the service block. |
1933 | + var service_height = 224, |
1934 | + name_size = 22, |
1935 | + charm_label_size = 16, |
1936 | + name_padding = 26, |
1937 | + charm_label_padding = 150; |
1938 | + |
1939 | + node.select('.name') |
1940 | + .attr({'style': function(d) { |
1941 | + // Programmatically size the font. |
1942 | + // Number derived from service assets: |
1943 | + // font-size 22px when asset is 224px. |
1944 | + return 'font-size:' + d.h * |
1945 | + (name_size / service_height) + 'px'; |
1946 | + }, |
1947 | + 'x': function(d) { return d.w / 2; }, |
1948 | + 'y': function(d) { |
1949 | + // Number derived from service assets: |
1950 | + // padding-top 26px when asset is 224px. |
1951 | + return d.h * (name_padding / service_height) + d.h * |
1952 | + (name_size / service_height) / 2; |
1953 | + } |
1954 | + }); |
1955 | + node.select('.charm-label') |
1956 | + .attr({'style': function(d) { |
1957 | + // Programmatically size the font. |
1958 | + // Number derived from service assets: |
1959 | + // font-size 16px when asset is 224px. |
1960 | + return 'font-size:' + d.h * |
1961 | + (charm_label_size / service_height) + 'px'; |
1962 | + }, |
1963 | + 'x': function(d) { return d.w / 2;}, |
1964 | + 'y': function(d) { |
1965 | + // Number derived from service assets: |
1966 | + // padding-top: 118px when asset is 224px. |
1967 | + return d.h * (charm_label_padding / service_height) - d.h * |
1968 | + (charm_label_size / service_height) / 2; |
1969 | + } |
1970 | + }); |
1971 | + |
1972 | + // Show whether or not the service is exposed using an indicator. |
1973 | + var exposed = node.filter(function(d) { |
1974 | + return d.exposed; |
1975 | + }); |
1976 | + exposed.each(function(d) { |
1977 | + var existing = Y.one(this).one('.exposed-indicator'); |
1978 | + if (!existing) { |
1979 | + existing = d3.select(this).append('image') |
1980 | + .attr({'class': 'exposed-indicator on', |
1981 | + 'xlink:href': '/juju-ui/assets/svgs/exposed.svg', |
1982 | + 'width': 32, |
1983 | + 'height': 32 |
1984 | + }) |
1985 | + .append('title') |
1986 | + .text(function(d) { |
1987 | + return d.exposed ? 'Exposed' : ''; |
1988 | + }); |
1989 | + } |
1990 | + existing = d3.select(this).select('.exposed-indicator') |
1991 | + .attr({ |
1992 | + 'x': 145, |
1993 | + 'y': 79 |
1994 | + }); |
1995 | + }); |
1996 | + |
1997 | + // Remove exposed indicator from nodes that are no longer exposed. |
1998 | + node.filter(function(d) { |
1999 | + return !d.exposed && |
2000 | + !d3.select(this) |
2001 | + .select('.exposed-indicator').empty(); |
2002 | + }).select('.exposed-indicator').remove(); |
2003 | + |
2004 | + // Adds the relative health in the form of a percentage bar. |
2005 | + node.each(function(d) { |
2006 | + var status_graph = d3.select(this).select('.statusbar'); |
2007 | + var status_bar = status_graph.property('status_bar'); |
2008 | + if (status_bar && !d.subordinate) { |
2009 | + status_bar.update(d.aggregated_status); |
2010 | + } |
2011 | + }); |
2012 | + }; |
2013 | + views.ServiceModuleCommon = ServiceModuleCommon; |
2014 | + |
2015 | + /** |
2016 | + Manage service rendering and events. |
2017 | + |
2018 | + ## Emitted events: |
2019 | + |
2020 | + - *clearState:* clear all possible states that the environment view can be |
2021 | + in as it pertains to actions (building a relation, viewing |
2022 | + a service menu, etc.) |
2023 | + - *snapToService:* fired when mousing over a service, causing the pending |
2024 | + relation dragline to snap to the service rather than |
2025 | + following the mouse. |
2026 | + - *snapOutOfService:* fired when mousing out of a service, causing the |
2027 | + pending relation line to follow the mouse again. |
2028 | + - *addRelationDrag:* |
2029 | + - *addRelationDragStart:* |
2030 | + - *addRelationDragEnd:* fired when creating a relation through the long- |
2031 | + click process, when moving the cursor over the environment, and when |
2032 | + dropping the endpoint on a valid service. |
2033 | + - *cancelRelationBuild:* fired when dropping a pending relation line |
2034 | + started through the long-click method somewhere other than a valid |
2035 | + service. |
2036 | + - *serviceMoved:* fired when a service block is dragged so that relation |
2037 | + endpoints can follow it. |
2038 | + - *navigateTo:* fired when clicking the "View Service" menu item or when |
2039 | + double-clicking a service. |
2040 | + |
2041 | + @class ServiceModule |
2042 | */ |
2043 | - var ServiceModule = Y.Base.create('ServiceModule', d3ns.Module, [], { |
2044 | + var ServiceModule = Y.Base.create('ServiceModule', d3ns.Module, [ |
2045 | + ServiceModuleCommon], { |
2046 | events: { |
2047 | scene: { |
2048 | '.service': { |
2049 | @@ -192,14 +460,15 @@ |
2050 | */ |
2051 | _attachDragEvents: function() { |
2052 | var container = this.get('container'), |
2053 | - ZP = '.zoom-plane', |
2054 | - EC = 'i.sprite.empty_canvas'; |
2055 | + ZP = '.zoom-plane', |
2056 | + EC = 'i.sprite.empty_canvas'; |
2057 | |
2058 | container.delegate('drop', this.canvasDropHandler, ZP, this); |
2059 | container.delegate('dragenter', this._ignore, ZP, this); |
2060 | container.delegate('dragover', this._ignore, ZP, this); |
2061 | |
2062 | - // allows the user to drop the charm on the 'drop here' help text in IE10. |
2063 | + // allows the user to drop the charm on the 'drop here' help text in |
2064 | + // IE10. |
2065 | container.delegate('drop', this.canvasDropHandler, EC, this); |
2066 | container.delegate('dragenter', this._ignore, EC, this); |
2067 | container.delegate('dragover', this._ignore, EC, this); |
2068 | @@ -225,7 +494,7 @@ |
2069 | */ |
2070 | attachTouchstartEvents: function(data, node) { |
2071 | var topo = this.get('component'), |
2072 | - yuiNode = Y.Node(node); |
2073 | + yuiNode = Y.Node(node); |
2074 | |
2075 | // Do not attach the event to the ghost nodes |
2076 | if (!d3.select(node).classed('pending')) { |
2077 | @@ -244,9 +513,9 @@ |
2078 | // To execute the serviceClick method under the same context as |
2079 | // click we call it under the touch target context |
2080 | var node = e.currentTarget.getDOMNode(), |
2081 | - box = d3.select(node).datum(); |
2082 | - // If we're dragging with two fingers, ignore this as a tap and let drag |
2083 | - // take over. |
2084 | + box = d3.select(node).datum(); |
2085 | + // If we're dragging with two fingers, ignore this as a tap and let |
2086 | + // drag take over. |
2087 | if (e.touches.length > 1) { |
2088 | box.tapped = false; |
2089 | return; |
2090 | @@ -284,9 +553,9 @@ |
2091 | return; |
2092 | } |
2093 | } else { |
2094 | - // Touch events will also fire a click event about 300ms later. If this |
2095 | - // event isn't ignored, the service menu will disappear 300ms after it |
2096 | - // appears, so set a flag to ignore that event. |
2097 | + // Touch events will also fire a click event about 300ms later. If |
2098 | + // this event isn't ignored, the service menu will disappear 300ms |
2099 | + // after it appears, so set a flag to ignore that event. |
2100 | box.ignoreNextClick = true; |
2101 | } |
2102 | |
2103 | @@ -298,8 +567,8 @@ |
2104 | // If the service box is pending, ensure that the charm panel is |
2105 | // visible, but don't do anything else. |
2106 | if (box.pending && !window.flags.serviceInspector) { |
2107 | - // Prevent the clickoutside event from firing and immediately closing |
2108 | - // the panel. |
2109 | + // Prevent the clickoutside event from firing and immediately |
2110 | + // closing the panel. |
2111 | d3.event.halt(); |
2112 | // Ensure service menus are closed. |
2113 | topo.fire('clearState'); |
2114 | @@ -327,10 +596,11 @@ |
2115 | return; |
2116 | } |
2117 | // Just show the service on double-click. |
2118 | - var topo = self.get('component'), |
2119 | - service = box.model; |
2120 | - // The browser sends a click event right before the dblclick one, and it |
2121 | - // opens the service menu: close it before moving to the service details. |
2122 | + var topo = self.get('component'); |
2123 | + var service = box.model; |
2124 | + // The browser sends a click event right before the dblclick one, and |
2125 | + // it opens the service menu: close it before moving to the service |
2126 | + // details. |
2127 | self.hideServiceMenu(); |
2128 | self.show_service(service); |
2129 | }, |
2130 | @@ -440,7 +710,7 @@ |
2131 | reader.onload = function(e) { |
2132 | // Import each into the environment |
2133 | db.importDeployer(jsyaml.safeLoad(e.target.result), |
2134 | - store, {useGhost: false}) |
2135 | + store, {useGhost: false}) |
2136 | .then(function() { |
2137 | notifications.add({ |
2138 | title: 'Imported Environment', |
2139 | @@ -468,14 +738,15 @@ |
2140 | // required to position the service in the proper y position. |
2141 | var dropXY = [evt.clientX, (evt.clientY - 71)]; |
2142 | |
2143 | - // Take the x,y offset (translation) of the topology view into account. |
2144 | + // Take the x,y offset (translation) of the topology view into |
2145 | + // account. |
2146 | Y.Array.each(dropXY, function(_, index) { |
2147 | ghostAttributes.coordinates[index] = |
2148 | (dropXY[index] - translation[index]) / scale; |
2149 | }); |
2150 | if (dragData.dataType === 'charm-token-drag-and-drop') { |
2151 | - // The charm data was JSON encoded because the dataTransfer mechanism |
2152 | - // only allows for string values. |
2153 | + // The charm data was JSON encoded because the dataTransfer |
2154 | + // mechanism only allows for string values. |
2155 | var charmData = Y.JSON.parse(dragData.charmData); |
2156 | // Add the icon url to the ghost attributes for the ghost icon |
2157 | ghostAttributes.icon = dragData.iconSrc; |
2158 | @@ -494,7 +765,7 @@ |
2159 | */ |
2160 | clearStateHandler: function() { |
2161 | var container = this.get('container'), |
2162 | - topo = this.get('component'); |
2163 | + topo = this.get('component'); |
2164 | container.all('.environment-menu.active').removeClass('active'); |
2165 | this.hideServiceMenu(); |
2166 | }, |
2167 | @@ -568,7 +839,7 @@ |
2168 | context.longClickTimer = Y.later(750, this, function(d, e) { |
2169 | // Provide some leeway for accidental dragging. |
2170 | if ((Math.abs(box.x - box.oldX) + Math.abs(box.y - box.oldY)) / |
2171 | - 2 > 5) { |
2172 | + 2 > 5) { |
2173 | return; |
2174 | } |
2175 | |
2176 | @@ -598,29 +869,6 @@ |
2177 | context.longClickTimer.cancel(); |
2178 | } |
2179 | }, |
2180 | - /* |
2181 | - * Sync view models with current db.models. |
2182 | - * |
2183 | - * @method updateData |
2184 | - */ |
2185 | - updateData: function() { |
2186 | - //model data |
2187 | - var topo = this.get('component'); |
2188 | - var vis = topo.vis; |
2189 | - var db = topo.get('db'); |
2190 | - var store = topo.get('store'); |
2191 | - |
2192 | - var visibleServices = db.services.visible(); |
2193 | - views.toBoundingBoxes(this, visibleServices, topo.service_boxes, store); |
2194 | - // Break a reference cycle that results in uncollectable objects leaking. |
2195 | - visibleServices.reset(); |
2196 | - |
2197 | - // Nodes are mapped by modelId tuples. |
2198 | - this.node = vis.selectAll('.service') |
2199 | - .data(Y.Object.values(topo.service_boxes), |
2200 | - function(d) {return d.modelId;}); |
2201 | - }, |
2202 | - |
2203 | /** |
2204 | * Handle drag events for a service. |
2205 | * |
2206 | @@ -650,17 +898,17 @@ |
2207 | } |
2208 | else { |
2209 | |
2210 | - // If the service hasn't been dragged (in the case of long-click to add |
2211 | - // relation, or a double-fired event) or the old and new coordinates |
2212 | - // are the same, exit. |
2213 | + // If the service hasn't been dragged (in the case of long-click to |
2214 | + // add relation, or a double-fired event) or the old and new |
2215 | + // coordinates are the same, exit. |
2216 | if (!box.inDrag || |
2217 | - (box.oldX === box.x && |
2218 | - box.oldY === box.y)) { |
2219 | + (box.oldX === box.x && |
2220 | + box.oldY === box.y)) { |
2221 | return; |
2222 | } |
2223 | |
2224 | - // If the service is still pending, persist x/y coordinates in order |
2225 | - // to set them as annotations when the service is created. |
2226 | + // If the service is still pending, persist x/y coordinates in |
2227 | + // order to set them as annotations when the service is created. |
2228 | if (box.pending) { |
2229 | box.model.set('hasBeenPositioned', true); |
2230 | box.model.set('x', box.x); |
2231 | @@ -681,21 +929,21 @@ |
2232 | }, |
2233 | |
2234 | /** |
2235 | - * Specialized drag event handler |
2236 | - * when called as an event handler it |
2237 | - * Allows optional extra param, pos |
2238 | - * which when used overrides the mouse |
2239 | - * handling. This method can then be |
2240 | - * though of as 'drag to position'. |
2241 | - * |
2242 | - * @method drag |
2243 | - * @param {Box} d viewModel BoundingBox. |
2244 | - * @param {ServiceModule} self ServiceModule. |
2245 | - * @param {Object} pos (optional) containing x/y numbers. |
2246 | - * @param {Boolean} includeTransition (optional) Use transition to drag. |
2247 | - * |
2248 | - * [At the time of this writing useTransition works in practice but |
2249 | - * introduces a timing issue in the tests.] |
2250 | + Specialized drag event handler |
2251 | + when called as an event handler it |
2252 | + Allows optional extra param, pos |
2253 | + which when used overrides the mouse |
2254 | + handling. This method can then be |
2255 | + though of as 'drag to position'. |
2256 | + |
2257 | + @method drag |
2258 | + @param {Box} d viewModel BoundingBox. |
2259 | + @param {ServiceModule} self ServiceModule. |
2260 | + @param {Object} pos (optional) containing x/y numbers. |
2261 | + @param {Boolean} includeTransition (optional) Use transition to drag. |
2262 | + |
2263 | + [At the time of this writing useTransition works in practice but |
2264 | + introduces a timing issue in the tests.] |
2265 | */ |
2266 | drag: function(box, self, pos, includeTransition) { |
2267 | if (box.tapped) { |
2268 | @@ -711,9 +959,9 @@ |
2269 | if (self.longClickTimer) { |
2270 | self.longClickTimer.cancel(); |
2271 | } |
2272 | - // Translate the service (and, potentially, menu). |
2273 | - // If a position was provided, update the box's coordinates and the |
2274 | - // selection's bound data. |
2275 | + // Translate the service (and, potentially, menu). If a position was |
2276 | + // provided, update the box's coordinates and the selection's bound |
2277 | + // data. |
2278 | if (pos) { |
2279 | box.x = pos.x; |
2280 | box.y = pos.y; |
2281 | @@ -726,8 +974,8 @@ |
2282 | |
2283 | if (includeTransition) { |
2284 | selection = selection.transition() |
2285 | - .duration(500) |
2286 | - .ease('elastic'); |
2287 | + .duration(500) |
2288 | + .ease('elastic'); |
2289 | } |
2290 | |
2291 | selection.attr('transform', function(d, i) { |
2292 | @@ -739,7 +987,7 @@ |
2293 | |
2294 | // Remove any active menus. |
2295 | self.get('container').all('.environment-menu.active') |
2296 | - .removeClass('active'); |
2297 | + .removeClass('active'); |
2298 | if (box.inDrag === views.DRAG_START) { |
2299 | self.hideServiceMenu(); |
2300 | box.inDrag = views.DRAG_ACTIVE; |
2301 | @@ -749,12 +997,12 @@ |
2302 | topo.fire('serviceMoved', { service: box }); |
2303 | }, |
2304 | |
2305 | - /* |
2306 | - * Attempt to reuse as much of the existing graph and view models |
2307 | - * as possible to re-render the graph. |
2308 | - * |
2309 | - * @method update |
2310 | - */ |
2311 | + /** |
2312 | + Attempt to reuse as much of the existing graph and view models as |
2313 | + possible to re-render the graph. |
2314 | + |
2315 | + @method update |
2316 | + */ |
2317 | update: function() { |
2318 | var self = this, |
2319 | topo = this.get('component'), |
2320 | @@ -776,18 +1024,18 @@ |
2321 | |
2322 | if (!this.tree) { |
2323 | this.tree = d3.layout.unscaledPack() |
2324 | - .size([width, height]) |
2325 | - .value(function(d) { |
2326 | - return Math.max(d.unit_count, 1); |
2327 | - }) |
2328 | - .padding(300); |
2329 | + .size([width, height]) |
2330 | + .value(function(d) { |
2331 | + return Math.max(d.unit_count, 1); |
2332 | + }) |
2333 | + .padding(300); |
2334 | } |
2335 | |
2336 | if (!this.dragBehavior) { |
2337 | this.dragBehavior = d3.behavior.drag() |
2338 | - .on('dragstart', function(d) { self.dragstart.call(this, d, self);}) |
2339 | - .on('drag', function(d) { self.drag.call(this, d, self);}) |
2340 | - .on('dragend', function(d) { self.dragend.call(this, d, self);}); |
2341 | + .on('dragstart', function(d) { self.dragstart.call(this, d, self);}) |
2342 | + .on('drag', function(d) { self.drag.call(this, d, self);}) |
2343 | + .on('dragend', function(d) { self.dragend.call(this, d, self);}); |
2344 | } |
2345 | |
2346 | //Process any changed data. |
2347 | @@ -798,16 +1046,15 @@ |
2348 | var node = this.node; |
2349 | |
2350 | // Rerun the pack layout. |
2351 | - // Pack doesn't honor existing positions and will |
2352 | - // re-layout the entire graph. As a short term work |
2353 | - // around we layout only new nodes. This has the side |
2354 | - // effect that service blocks can overlap and will |
2355 | - // be fixed later. |
2356 | + // Pack doesn't honor existing positions and will re-layout the |
2357 | + // entire graph. As a short term work around we layout only new |
2358 | + // nodes. This has the side effect that service blocks can overlap |
2359 | + // and will be fixed later. |
2360 | var vertices; |
2361 | var new_services = Y.Object.values(topo.service_boxes) |
2362 | - .filter(function(boundingBox) { |
2363 | - return !Y.Lang.isNumber(boundingBox.x); |
2364 | - }); |
2365 | + .filter(function(boundingBox) { |
2366 | + return !Y.Lang.isNumber(boundingBox.x); |
2367 | + }); |
2368 | if (new_services.length > 0) { |
2369 | // If the there is only one new service and it's pending (as in, it was |
2370 | // added via the charm panel as a ghost), position it intelligently and |
2371 | @@ -816,16 +1063,19 @@ |
2372 | // in the case of opening an unannotated environment for the first |
2373 | // time). |
2374 | var pendingServicePlaced = false; |
2375 | - if (new_services.length === 1 && new_services[0].model.get('pending')) { |
2376 | + if (new_services.length === 1 && |
2377 | + new_services[0].model.get('pending')) { |
2378 | pendingServicePlaced = true; |
2379 | // Get a coordinate outside the cluster of existing services. |
2380 | var coords = topo.servicePointOutside(); |
2381 | - // Set the coordinates on both the box model and the service model. |
2382 | + // Set the coordinates on both the box model and the service |
2383 | + // model. |
2384 | new_services[0].x = coords[0]; |
2385 | new_services[0].y = coords[1]; |
2386 | new_services[0].model.set('x', coords[0]); |
2387 | new_services[0].model.set('y', coords[1]); |
2388 | - // This ensures that the x/y coordinates will be saved as annotations. |
2389 | + // This ensures that the x/y coordinates will be saved as |
2390 | + // annotations. |
2391 | new_services[0].model.set('hasBeenPositioned', true); |
2392 | // Set the centroid to the new service's position |
2393 | topo.centroid = coords; |
2394 | @@ -833,8 +1083,8 @@ |
2395 | } else { |
2396 | this.tree.nodes({children: new_services}); |
2397 | } |
2398 | - // Update annotations settings position on backend |
2399 | - // (but only do this if there is no existing annotations). |
2400 | + // Update annotations settings position on backend (but only do |
2401 | + // this if there is no existing annotations). |
2402 | if (!pendingServicePlaced) { |
2403 | vertices = []; |
2404 | } |
2405 | @@ -858,7 +1108,8 @@ |
2406 | }); |
2407 | } |
2408 | if (!topo.centroid || vertices) { |
2409 | - // Find the centroid of our hull of services and inform the topology. |
2410 | + // Find the centroid of our hull of services and inform the |
2411 | + // topology. |
2412 | if (!vertices) { |
2413 | vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes); |
2414 | } |
2415 | @@ -866,9 +1117,9 @@ |
2416 | } |
2417 | // enter |
2418 | node |
2419 | - .enter().append('g') |
2420 | - .attr({ |
2421 | - 'pointer-events': 'all', // IE doesn't drag properly without this. |
2422 | + .enter().append('g') |
2423 | + .attr({ |
2424 | + 'pointer-events': 'all', // IE needs this. |
2425 | 'class': function(d) { |
2426 | return (d.subordinate ? 'subordinate ' : '') + |
2427 | (d.pending ? 'pending ' : '') + 'service'; |
2428 | @@ -882,18 +1133,18 @@ |
2429 | |
2430 | // Remove old nodes. |
2431 | node.exit() |
2432 | - .each(function(d) { |
2433 | + .each(function(d) { |
2434 | delete topo.service_boxes[d.id]; |
2435 | }) |
2436 | - .remove(); |
2437 | + .remove(); |
2438 | }, |
2439 | |
2440 | /** |
2441 | - Pans the environment view to the center all the services on the canvas. |
2442 | + Pans the environment view to the center all the services on the canvas. |
2443 | |
2444 | - @method panToCenter |
2445 | - @param {object} evt The event fired. |
2446 | - @return {undefined} Side effects only. |
2447 | + @method panToCenter |
2448 | + @param {object} evt The event fired. |
2449 | + @return {undefined} Side effects only. |
2450 | */ |
2451 | panToCenter: function(evt) { |
2452 | var topo = this.get('component'); |
2453 | @@ -902,15 +1153,15 @@ |
2454 | }, |
2455 | |
2456 | /** |
2457 | - Given a set of vertices, find the centroid and pan to that location. |
2458 | + Given a set of vertices, find the centroid and pan to that location. |
2459 | |
2460 | - @method findAndSetCentroid |
2461 | - @param {array} vertices A list of vertices in the form [x, y]. |
2462 | - @return {undefined} Side effects only. |
2463 | + @method findAndSetCentroid |
2464 | + @param {array} vertices A list of vertices in the form [x, y]. |
2465 | + @return {undefined} Side effects only. |
2466 | */ |
2467 | findAndSetCentroid: function(vertices) { |
2468 | var topo = this.get('component'), |
2469 | - centroid = topoUtils.centroid(vertices); |
2470 | + centroid = topoUtils.centroid(vertices); |
2471 | // The centroid is set on the topology object due to the fact that it is |
2472 | // used as a sigil to tell whether or not to pan to the point after the |
2473 | // first delta. |
2474 | @@ -950,13 +1201,13 @@ |
2475 | node.append('image') |
2476 | .classed('service-icon', true) |
2477 | .attr({ |
2478 | - 'xlink:href': function(d) { |
2479 | - return d.icon; |
2480 | - }, |
2481 | - width: 96, |
2482 | - height: 96, |
2483 | - transform: 'translate(47, 50)' |
2484 | - }); |
2485 | + 'xlink:href': function(d) { |
2486 | + return d.icon; |
2487 | + }, |
2488 | + width: 96, |
2489 | + height: 96, |
2490 | + transform: 'translate(47, 50)' |
2491 | + }); |
2492 | node.append('text').append('tspan') |
2493 | .attr('class', 'name') |
2494 | .text(function(d) {return d.displayName; }); |
2495 | @@ -985,249 +1236,6 @@ |
2496 | }); |
2497 | }, |
2498 | |
2499 | - /** |
2500 | - * Fill the empty structures within a service node such that they |
2501 | - * match the db. |
2502 | - * |
2503 | - * @param {object} node the collection of nodes to update. |
2504 | - * @return {null} side effects only. |
2505 | - * @method updateServiceNodes |
2506 | - */ |
2507 | - updateServiceNodes: function(node) { |
2508 | - if (node.empty()) { |
2509 | - return; |
2510 | - } |
2511 | - var self = this, |
2512 | - topo = this.get('component'), |
2513 | - landscape = topo.get('landscape'), |
2514 | - service_scale = this.service_scale, |
2515 | - service_scale_width = this.service_scale_width, |
2516 | - service_scale_height = this.service_scale_height; |
2517 | - |
2518 | - // Apply Position Annotations |
2519 | - // This is done after the services_boxes |
2520 | - // binding as the event handler will |
2521 | - // use that index. |
2522 | - node.each(function(d) { |
2523 | - var service = d.model, |
2524 | - annotations = service.get('annotations'), |
2525 | - x, y; |
2526 | - |
2527 | - // If there are no annotations or the service is being dragged |
2528 | - if (!annotations || service.inDrag === views.DRAG_ACTIVE) { |
2529 | - return; |
2530 | - } |
2531 | - |
2532 | - // If there are x/y annotations on the service model and they are |
2533 | - // different from the node's current x/y coordinates, update the |
2534 | - // node, as the annotations may have been set in another session. |
2535 | - x = annotations['gui-x']; |
2536 | - y = annotations['gui-y']; |
2537 | - if (!d || |
2538 | - (x !== undefined && x !== d.x) || |
2539 | - (y !== undefined && y !== d.y)) { |
2540 | - // Delete gui-x and gui-y from annotations as we use the values. |
2541 | - // This is to prevent deltas coming in on a service while it is |
2542 | - // being dragged from resetting its position during the drag. |
2543 | - |
2544 | - delete annotations['gui-x']; |
2545 | - delete annotations['gui-y']; |
2546 | - // Only update position if we're not already in a drag state (the |
2547 | - // current drag supercedes any previous annotations). |
2548 | - if (!d.inDrag) { |
2549 | - self.drag.call(this, d, self, {x: x, y: y}, |
2550 | - self.get('useTransitions')); |
2551 | - } |
2552 | - }}); |
2553 | - |
2554 | - // Mark subordinates as such. This is needed for when a new service |
2555 | - // is created. |
2556 | - node.filter(function(d) { |
2557 | - return d.subordinate; |
2558 | - }) |
2559 | - .classed('subordinate', true); |
2560 | - |
2561 | - // Size the node for drawing. |
2562 | - node.attr({ |
2563 | - 'width': function(box) { box.w = 190; return box.w;}, |
2564 | - 'height': function(box) { box.h = 190; return box.h;} |
2565 | - }); |
2566 | - |
2567 | - node.select('.service-block-image').each(function(d) { |
2568 | - var curr_node = d3.select(this); |
2569 | - var curr_href = curr_node.attr('xlink:href'); |
2570 | - var new_href = d.subordinate ? |
2571 | - '/juju-ui/assets/svgs/sub_module.svg' : |
2572 | - '/juju-ui/assets/svgs/service_module.svg'; |
2573 | - |
2574 | - // Only set 'xlink:href' if not already set to the new value, |
2575 | - // thus avoiding redundant requests to the server. #1182135 |
2576 | - if (curr_href !== new_href) { |
2577 | - curr_node.attr({'xlink:href': new_href}); |
2578 | - } |
2579 | - curr_node.attr({ |
2580 | - 'width': d.w, |
2581 | - 'height': d.h |
2582 | - }); |
2583 | - }); |
2584 | - |
2585 | - // Draw a subordinate relation indicator. |
2586 | - var subRelationIndicator = node.filter(function(d) { |
2587 | - return d.subordinate && |
2588 | - d3.select(this) |
2589 | - .select('.sub-rel-block').empty(); |
2590 | - }) |
2591 | - .append('g') |
2592 | - .attr('class', 'sub-rel-block') |
2593 | - .attr('transform', function(d) { |
2594 | - // Position the block so that the relation indicator will |
2595 | - // appear at the right connector. |
2596 | - return 'translate(' + [d.w, d.h / 2 - 26] + ')'; |
2597 | - }); |
2598 | - |
2599 | - subRelationIndicator.append('image') |
2600 | - .attr({'xlink:href': '/juju-ui/assets/svgs/sub_relation.svg', |
2601 | - 'width': 87, |
2602 | - 'height': 47}); |
2603 | - subRelationIndicator.append('text').append('tspan') |
2604 | - .attr({'class': 'sub-rel-count', |
2605 | - 'x': 64, |
2606 | - 'y': 47 * 0.8}); |
2607 | - |
2608 | - // Landscape badge |
2609 | - if (landscape) { |
2610 | - node.each(function(d) { |
2611 | - var landscapeAsset; |
2612 | - var securityBadge = landscape.getLandscapeBadge( |
2613 | - d.model, 'security', 'round'); |
2614 | - var rebootBadge = landscape.getLandscapeBadge( |
2615 | - d.model, 'reboot', 'round'); |
2616 | - |
2617 | - if (securityBadge && rebootBadge) { |
2618 | - landscapeAsset = |
2619 | - '/juju-ui/assets/images/landscape_restart_round.png'; |
2620 | - } else if (securityBadge) { |
2621 | - landscapeAsset = |
2622 | - '/juju-ui/assets/images/landscape_security_round.png'; |
2623 | - } else if (rebootBadge) { |
2624 | - landscapeAsset = |
2625 | - '/juju-ui/assets/images/landscape_restart_round.png'; |
2626 | - } |
2627 | - if (landscapeAsset === undefined) { |
2628 | - // Remove any existing badge. |
2629 | - d3.select(this).select('.landscape-badge').remove(); |
2630 | - } else { |
2631 | - var existing = Y.one(this).one('.landscape-badge'), |
2632 | - curr_href, target; |
2633 | - |
2634 | - if (!existing) { |
2635 | - existing = d3.select(this).append('image'); |
2636 | - existing.attr({ |
2637 | - 'class': 'landscape-badge', |
2638 | - 'width': 32, |
2639 | - 'height': 32 |
2640 | - }); |
2641 | - } |
2642 | - existing = d3.select(this).select('.landscape-badge'); |
2643 | - existing.attr({ |
2644 | - 'x': 13, |
2645 | - 'y': 79 |
2646 | - }); |
2647 | - |
2648 | - // Only set 'xlink:href' if not already set to the new value, |
2649 | - // thus avoiding redundant requests to the server. #1182135 |
2650 | - curr_href = existing.attr('xlink:href'); |
2651 | - if (curr_href !== landscapeAsset) { |
2652 | - existing.attr({'xlink:href': landscapeAsset}); |
2653 | - } |
2654 | - } |
2655 | - }); |
2656 | - } |
2657 | - // The following are sizes in pixels of the SVG assets used to |
2658 | - // render a service, and are used to in calculating the vertical |
2659 | - // positioning of text down along the service block. |
2660 | - var service_height = 224, |
2661 | - name_size = 22, |
2662 | - charm_label_size = 16, |
2663 | - name_padding = 26, |
2664 | - charm_label_padding = 150; |
2665 | - |
2666 | - node.select('.name') |
2667 | - .attr({'style': function(d) { |
2668 | - // Programmatically size the font. |
2669 | - // Number derived from service assets: |
2670 | - // font-size 22px when asset is 224px. |
2671 | - return 'font-size:' + d.h * |
2672 | - (name_size / service_height) + 'px'; |
2673 | - }, |
2674 | - 'x': function(d) { return d.w / 2; }, |
2675 | - 'y': function(d) { |
2676 | - // Number derived from service assets: |
2677 | - // padding-top 26px when asset is 224px. |
2678 | - return d.h * (name_padding / service_height) + d.h * |
2679 | - (name_size / service_height) / 2; |
2680 | - } |
2681 | - }); |
2682 | - node.select('.charm-label') |
2683 | - .attr({'style': function(d) { |
2684 | - // Programmatically size the font. |
2685 | - // Number derived from service assets: |
2686 | - // font-size 16px when asset is 224px. |
2687 | - return 'font-size:' + d.h * |
2688 | - (charm_label_size / service_height) + 'px'; |
2689 | - }, |
2690 | - 'x': function(d) { return d.w / 2;}, |
2691 | - 'y': function(d) { |
2692 | - // Number derived from service assets: |
2693 | - // padding-top: 118px when asset is 224px. |
2694 | - return d.h * (charm_label_padding / service_height) - d.h * |
2695 | - (charm_label_size / service_height) / 2; |
2696 | - } |
2697 | - }); |
2698 | - |
2699 | - // Show whether or not the service is exposed using an indicator. |
2700 | - var exposed = node.filter(function(d) { |
2701 | - return d.exposed; |
2702 | - }); |
2703 | - exposed.each(function(d) { |
2704 | - var existing = Y.one(this).one('.exposed-indicator'); |
2705 | - if (!existing) { |
2706 | - existing = d3.select(this).append('image') |
2707 | - .attr({'class': 'exposed-indicator on', |
2708 | - 'xlink:href': '/juju-ui/assets/svgs/exposed.svg', |
2709 | - 'width': 32, |
2710 | - 'height': 32 |
2711 | - }) |
2712 | - .append('title') |
2713 | - .text(function(d) { |
2714 | - return d.exposed ? 'Exposed' : ''; |
2715 | - }); |
2716 | - } |
2717 | - existing = d3.select(this).select('.exposed-indicator') |
2718 | - .attr({ |
2719 | - 'x': 145, |
2720 | - 'y': 79 |
2721 | - }); |
2722 | - }); |
2723 | - |
2724 | - // Remove exposed indicator from nodes that are no longer exposed. |
2725 | - node.filter(function(d) { |
2726 | - return !d.exposed && |
2727 | - !d3.select(this) |
2728 | - .select('.exposed-indicator').empty(); |
2729 | - }).select('.exposed-indicator').remove(); |
2730 | - |
2731 | - // Adds the relative health in the form of a percentage bar. |
2732 | - node.each(function(d) { |
2733 | - var status_graph = d3.select(this).select('.statusbar'); |
2734 | - var status_bar = status_graph.property('status_bar'); |
2735 | - if (status_bar && !d.subordinate) { |
2736 | - status_bar.update(d.aggregated_status); |
2737 | - } |
2738 | - }); |
2739 | - }, |
2740 | - |
2741 | - |
2742 | /* |
2743 | * Show/hide/fade selection. |
2744 | */ |
2745 | @@ -1245,7 +1253,7 @@ |
2746 | |
2747 | fade: function(evt) { |
2748 | var selection = evt.selection, |
2749 | - alpha = evt.alpha; |
2750 | + alpha = evt.alpha; |
2751 | selection.transition() |
2752 | .duration(400) |
2753 | .attr('opacity', alpha !== undefined && alpha || '0.2'); |
2754 | @@ -1267,20 +1275,20 @@ |
2755 | |
2756 | updateServiceMenuLocation: function() { |
2757 | var topo = this.get('component'), |
2758 | - container = this.get('container'), |
2759 | - cp = container.one('.environment-menu.active'), |
2760 | - service = topo.get('active_service'), |
2761 | - tr = topo.get('translate'), |
2762 | - z = topo.get('scale'); |
2763 | + container = this.get('container'), |
2764 | + cp = container.one('.environment-menu.active'), |
2765 | + service = topo.get('active_service'), |
2766 | + tr = topo.get('translate'), |
2767 | + z = topo.get('scale'); |
2768 | |
2769 | if (service && cp) { |
2770 | var cpRect = cp.getDOMNode().getClientRects()[0], |
2771 | - cpWidth = cpRect.width, |
2772 | - serviceCenter = service.relativeCenter, |
2773 | - menuLeft = (service.x * z + tr[0] + serviceCenter[0] * z < |
2774 | + cpWidth = cpRect.width, |
2775 | + serviceCenter = service.relativeCenter, |
2776 | + menuLeft = (service.x * z + tr[0] + serviceCenter[0] * z < |
2777 | topo.get('width') / 2), |
2778 | - cpHeight = cpRect.height, |
2779 | - arrowWidth = 16; // Hard coded for now for simplicity. |
2780 | + cpHeight = cpRect.height, |
2781 | + arrowWidth = 16; // Hard coded for now for simplicity. |
2782 | |
2783 | if (menuLeft) { |
2784 | cp.removeClass('left') |
2785 | @@ -1298,14 +1306,14 @@ |
2786 | // right, and vice versa. |
2787 | cp.setStyles({ |
2788 | 'top': ( |
2789 | - service.y * z + tr[1] + |
2790 | - (serviceCenter[1] * z) - (cpHeight / 2)), |
2791 | + service.y * z + tr[1] + |
2792 | + (serviceCenter[1] * z) - (cpHeight / 2)), |
2793 | 'left': ( |
2794 | service.x * z + |
2795 | - (menuLeft ? |
2796 | - service.w * z + arrowWidth : |
2797 | - -(cpWidth) - arrowWidth) + |
2798 | - tr[0]) |
2799 | + (menuLeft ? |
2800 | + service.w * z + arrowWidth : |
2801 | + -(cpWidth) - arrowWidth) + |
2802 | + tr[0]) |
2803 | }); |
2804 | } |
2805 | }, |
2806 | @@ -1453,7 +1461,7 @@ |
2807 | // Show dialog. |
2808 | this.set('destroy_dialog', views.createModalPanel( |
2809 | 'Are you sure you want to destroy the service? ' + |
2810 | - 'This cannot be undone.', |
2811 | + 'This cannot be undone.', |
2812 | '#destroy-modal-panel', |
2813 | 'Destroy Service', |
2814 | Y.bind(function(ev) { |
2815 | @@ -1471,16 +1479,16 @@ |
2816 | */ |
2817 | destroyService: function(btn) { |
2818 | var env = this.get('component').get('env'), |
2819 | - service = this.get('destroy_service'); |
2820 | + service = this.get('destroy_service'); |
2821 | env.destroy_service(service.get('id'), |
2822 | - Y.bind(this._destroyCallback, this, |
2823 | - service, btn)); |
2824 | + Y.bind(this._destroyCallback, this, |
2825 | + service, btn)); |
2826 | }, |
2827 | |
2828 | _destroyCallback: function(service, btn, ev) { |
2829 | var getModelURL = this.get('component').get('getModelURL'), |
2830 | - topo = this.get('component'), |
2831 | - db = topo.get('db'); |
2832 | + topo = this.get('component'), |
2833 | + db = topo.get('db'); |
2834 | if (ev.err) { |
2835 | db.notifications.add( |
2836 | new models.Notification({ |
2837 | |
2838 | === modified file 'app/views/topology/topology.js' |
2839 | --- app/views/topology/topology.js 2013-05-17 14:51:05 +0000 |
2840 | +++ app/views/topology/topology.js 2013-08-30 19:48:17 +0000 |
2841 | @@ -49,17 +49,15 @@ |
2842 | */ |
2843 | var Topology = Y.Base.create('Topology', d3ns.Component, [], { |
2844 | initializer: function(options) { |
2845 | - Topology.superclass.constructor.apply(this, arguments); |
2846 | - this.options = Y.mix(options || { |
2847 | + this.options = Y.mix(options || {}, { |
2848 | minZoom: 0.25, |
2849 | maxZoom: 2, |
2850 | minSlider: 25, |
2851 | maxSlider: 200 |
2852 | }); |
2853 | - |
2854 | + Topology.superclass.constructor.apply(this, arguments); |
2855 | // Build a service.id -> BoundingBox map for services. |
2856 | this.service_boxes = {}; |
2857 | - |
2858 | this._subscriptions = []; |
2859 | }, |
2860 | |
2861 | @@ -111,11 +109,16 @@ |
2862 | this.computeScales(); |
2863 | |
2864 | // Set up the visualization with a pack layout. |
2865 | - svg = d3.select(container.getDOMNode()) |
2866 | - .selectAll('.topology-canvas') |
2867 | - .append('svg:svg') |
2868 | - .attr('width', width) |
2869 | - .attr('height', height); |
2870 | + var canvas = d3.select(container.getDOMNode()); |
2871 | + var base = canvas.select('.topology-canvas'); |
2872 | + if (base.empty()) { |
2873 | + base = canvas.append('div') |
2874 | + .classed('topology-canvas', true); |
2875 | + } |
2876 | + |
2877 | + svg = base.append('svg:svg') |
2878 | + .attr('width', width) |
2879 | + .attr('height', height); |
2880 | this.svg = svg; |
2881 | |
2882 | this.zoomPlane = svg.append('rect') |
2883 | @@ -209,9 +212,9 @@ |
2884 | getter: function() {return this.get('size')[1];} |
2885 | }, |
2886 | /* |
2887 | - * Scale and translate are managed by an external module |
2888 | - * (PanZoom in this case). If that module isn't |
2889 | - * loaded nothing will modify these values. |
2890 | + Scale and translate are managed by an external module |
2891 | + (PanZoom in this case). If that module isn't |
2892 | + loaded nothing will modify these values. |
2893 | */ |
2894 | scale: { |
2895 | getter: function() {return this.zoom.scale();}, |
2896 | |
2897 | === modified file 'app/views/utils.js' |
2898 | --- app/views/utils.js 2013-08-19 20:51:17 +0000 |
2899 | +++ app/views/utils.js 2013-08-30 19:48:17 +0000 |
2900 | @@ -564,11 +564,11 @@ |
2901 | @property constraintDescriptions |
2902 | */ |
2903 | utils.constraintDescriptions = { |
2904 | - arch: {title: 'Architecture'}, |
2905 | - cpu: {title: 'CPU', unit: 'GHz'}, |
2906 | + 'arch': {title: 'Architecture'}, |
2907 | + 'cpu': {title: 'CPU', unit: 'GHz'}, |
2908 | 'cpu-cores': {title: 'CPU Cores'}, |
2909 | 'cpu-power': {title: 'CPU Power', unit: 'GHz'}, |
2910 | - mem: {title: 'Memory', unit: 'GB'} |
2911 | + 'mem': {title: 'Memory', unit: 'GB'} |
2912 | }; |
2913 | |
2914 | /** |
2915 | @@ -1457,10 +1457,11 @@ |
2916 | debugger; |
2917 | /*jshint debug:false */ |
2918 | }); |
2919 | + |
2920 | /* |
2921 | * Extension for views to provide an apiFailure method. |
2922 | * |
2923 | - * @class apiFailure |
2924 | + * @class apiFailingView |
2925 | */ |
2926 | utils.apiFailingView = function() { |
2927 | this._initAPIFailingView(); |
2928 | |
2929 | === modified file 'app/views/viewlets/inspector-header.js' |
2930 | --- app/views/viewlets/inspector-header.js 2013-08-12 15:13:17 +0000 |
2931 | +++ app/views/viewlets/inspector-header.js 2013-08-30 19:48:17 +0000 |
2932 | @@ -29,35 +29,22 @@ |
2933 | name: 'inspectorHeader', |
2934 | template: templates['inspector-header'], |
2935 | slot: 'header', |
2936 | - bindings: { |
2937 | - icon: { |
2938 | - 'update': function(node, value) { |
2939 | - // XXX: Icon is only present on services that pass through |
2940 | - // the Ghost phase of the GUI. Once we have better integration |
2941 | - // with the charm browser API services handling of icon |
2942 | - // can be improved. |
2943 | - var icon = node.one('img'); |
2944 | - if (icon === null && value) { |
2945 | - node.append('<img>'); |
2946 | - icon = node.one('img'); |
2947 | - } |
2948 | - if (value) { |
2949 | - icon.set('src', value); |
2950 | - } |
2951 | - } |
2952 | - } |
2953 | - }, |
2954 | 'render': function(model, viewContainerAttrs) { |
2955 | this.container = Y.Node.create(this.templateWrapper); |
2956 | var pojoModel = model.getAttrs(); |
2957 | if (pojoModel.scheme) { |
2958 | pojoModel.ghost = true; |
2959 | } |
2960 | + // If this is a service, the id is the service.charm. |
2961 | if (pojoModel.charm) { |
2962 | pojoModel.charmUrl = pojoModel.charm; |
2963 | } else { |
2964 | + // If this is a charm model, just use the id. |
2965 | pojoModel.charmUrl = pojoModel.id; |
2966 | } |
2967 | + // Manually add the icon url for the charm since we don't have access to |
2968 | + // the browser handlebars helper at this location. |
2969 | + pojoModel.icon = viewContainerAttrs.store.iconpath(pojoModel.charmUrl); |
2970 | this.container.setHTML(this.template(pojoModel)); |
2971 | } |
2972 | }; |
2973 | |
2974 | === modified file 'app/views/viewlets/service-constraints.js' |
2975 | --- app/views/viewlets/service-constraints.js 2013-08-19 16:58:14 +0000 |
2976 | +++ app/views/viewlets/service-constraints.js 2013-08-30 19:48:17 +0000 |
2977 | @@ -58,6 +58,7 @@ |
2978 | } |
2979 | |
2980 | }; |
2981 | + |
2982 | }, '0.0.1', { |
2983 | requires: [ |
2984 | 'node', |
2985 | |
2986 | === modified file 'app/views/viewlets/service-ghost.js' |
2987 | --- app/views/viewlets/service-ghost.js 2013-08-08 17:30:58 +0000 |
2988 | +++ app/views/viewlets/service-ghost.js 2013-08-30 19:48:17 +0000 |
2989 | @@ -43,27 +43,34 @@ |
2990 | } |
2991 | } |
2992 | }, |
2993 | - 'render': function(model) { |
2994 | + 'render': function(model, viewletMgrAttrs) { |
2995 | this.container = Y.Node.create(this.templateWrapper); |
2996 | |
2997 | // This is to allow for data binding on the ghost settings |
2998 | // while using a shared template across both inspectors |
2999 | - var options = model.getAttrs(); |
3000 | + var templateOptions = model.getAttrs(); |
3001 | |
3002 | // XXX - Jeff |
3003 | // not sure this should be done like this |
3004 | // but this will allow us to use the old template. |
3005 | - options.settings = utils.extractServiceSettings(options.options); |
3006 | + templateOptions.settings = utils.extractServiceSettings( |
3007 | + templateOptions.options); |
3008 | + |
3009 | + templateOptions.constraints = utils.getConstraints( |
3010 | + // no current constraints in play. |
3011 | + {}, |
3012 | + viewletMgrAttrs.env.genericConstraints); |
3013 | |
3014 | // Signalling to the shared templates that this is the ghost view. |
3015 | - options.ghost = true; |
3016 | - this.container.setHTML(this.template(options)); |
3017 | + templateOptions.ghost = true; |
3018 | + this.container.setHTML(this.template(templateOptions)); |
3019 | |
3020 | - this.container.all('textarea.config-field') |
3021 | - .plug(plugins.ResizingTextarea, |
3022 | - { max_height: 200, |
3023 | - min_height: 18, |
3024 | - single_line: 18}); |
3025 | + var ResizingTextarea = plugins.ResizingTextArea; |
3026 | + this.container.all('textarea.config-field').plug(ResizingTextarea, { |
3027 | + max_height: 200, |
3028 | + min_height: 18, |
3029 | + single_line: 18 |
3030 | + }); |
3031 | } |
3032 | }; |
3033 | |
3034 | |
3035 | === modified file 'app/widgets/viewmode-controls.js' |
3036 | --- app/widgets/viewmode-controls.js 2013-07-15 13:17:42 +0000 |
3037 | +++ app/widgets/viewmode-controls.js 2013-08-30 19:48:17 +0000 |
3038 | @@ -142,6 +142,16 @@ |
3039 | ); |
3040 | |
3041 | this._updateActiveNav(this.get('currentViewmode')); |
3042 | + this.on('destroy', function(ev) { |
3043 | + // We don't actually want the widget to remove any DOM. Just run our |
3044 | + // unbinding of events for us. |
3045 | + ev.halt(); |
3046 | + this._events.forEach(function(e) { |
3047 | + e.detach(); |
3048 | + }); |
3049 | + this._events = []; |
3050 | + }, this); |
3051 | + |
3052 | }, |
3053 | |
3054 | /** |
3055 | @@ -189,6 +199,83 @@ |
3056 | } |
3057 | }); |
3058 | |
3059 | + /** |
3060 | + * Extension for views to provide viewmode controls. |
3061 | + * |
3062 | + * @class viewmodeControllingView |
3063 | + */ |
3064 | + ns.ViewmodeControlsViewExtension = function() {}; |
3065 | + ns.ViewmodeControlsViewExtension.prototype = { |
3066 | + /** |
3067 | + * Binds the viewmode controls on the page to the viewmode change events. |
3068 | + * |
3069 | + * @method _bindViewmodeControls |
3070 | + * @param {Y.Widget} controls The viewmode control widget. |
3071 | + */ |
3072 | + _bindViewmodeControls: function(controls) { |
3073 | + this._fullscreen = controls.on( |
3074 | + controls.EVT_FULLSCREEN, this._goFullscreen, this); |
3075 | + this._sidebar = controls.on( |
3076 | + controls.EVT_SIDEBAR, this._goSidebar, this); |
3077 | + this._minimized = controls.on( |
3078 | + controls.EVT_TOGGLE_VIEWABLE, this._toggleMinimized, this); |
3079 | + this._destroyMe = this.on('destroy', function() { |
3080 | + // Unbind the View events listening for events from the widget. |
3081 | + this._fullscreen.detach(); |
3082 | + this._sidebar.detach(); |
3083 | + this._minimized.detach(); |
3084 | + // Including this event. |
3085 | + this._destroyMe.detach(); |
3086 | + // Finally, make sure we run destroy on the widget itself to unbind |
3087 | + // it's own events. |
3088 | + controls.destroy(); |
3089 | + }); |
3090 | + }, |
3091 | + |
3092 | + /** |
3093 | + Upon clicking the browser icon make sure we re-route to the |
3094 | + new form of the UX. |
3095 | + |
3096 | + @method _goFullscreen |
3097 | + @param {Event} ev the click event handler on the button. |
3098 | + |
3099 | + */ |
3100 | + _goFullscreen: function(ev) { |
3101 | + ev.halt(); |
3102 | + this.fire('viewNavigate', { |
3103 | + change: { |
3104 | + viewmode: 'fullscreen' |
3105 | + } |
3106 | + }); |
3107 | + }, |
3108 | + |
3109 | + /** |
3110 | + Upon clicking the build icon make sure we re-route to the |
3111 | + new form of the UX. |
3112 | + |
3113 | + @method _goSidebar |
3114 | + @param {Event} ev the click event handler on the button. |
3115 | + |
3116 | + */ |
3117 | + _goSidebar: function(ev) { |
3118 | + ev.halt(); |
3119 | + this.fire('viewNavigate', { |
3120 | + change: { |
3121 | + viewmode: 'sidebar' |
3122 | + } |
3123 | + }); |
3124 | + }, |
3125 | + |
3126 | + /** |
3127 | + * Place holder to toggle the minimized view; in minimized this should show |
3128 | + * sidebar, in sidebar this should show minimized. |
3129 | + * @method _toggleMinimized |
3130 | + * @param {Event} ev event to trigger the toggle. |
3131 | + * |
3132 | + */ |
3133 | + _toggleMinimized: function(ev) {} |
3134 | + }; |
3135 | + |
3136 | }, '0.1.0', { |
3137 | requires: [ |
3138 | 'base', |
3139 | |
3140 | === modified file 'docs/d3-component-framework.rst' |
3141 | --- docs/d3-component-framework.rst 2013-01-15 14:39:44 +0000 |
3142 | +++ docs/d3-component-framework.rst 2013-08-30 19:48:17 +0000 |
3143 | @@ -117,6 +117,11 @@ |
3144 | module declaration, but a module writer must understand them all to properly use |
3145 | the framework. |
3146 | |
3147 | +When a Component is created it can be in either an interactive or |
3148 | +non-interactive state. This is controlled through a Boolean 'interactive' |
3149 | +attribute which defaults to true. When false events will not be bound and this |
3150 | +section can be skipped. |
3151 | + |
3152 | When modules are added, three sets of declarative events are bound. This is |
3153 | done by including in the module an events object with the following (each |
3154 | optional) sections:: |
3155 | |
3156 | === modified file 'lib/views/browser/charm-full.less' |
3157 | --- lib/views/browser/charm-full.less 2013-08-13 00:35:01 +0000 |
3158 | +++ lib/views/browser/charm-full.less 2013-08-30 19:48:17 +0000 |
3159 | @@ -86,7 +86,6 @@ |
3160 | float: left; |
3161 | } |
3162 | .charm-icon, |
3163 | - .category-icon, |
3164 | img.icon { |
3165 | width: 120px; |
3166 | height: 120px; |
3167 | |
3168 | === modified file 'lib/views/browser/charm-token.less' |
3169 | --- lib/views/browser/charm-token.less 2013-08-09 14:28:06 +0000 |
3170 | +++ lib/views/browser/charm-token.less 2013-08-30 19:48:17 +0000 |
3171 | @@ -13,15 +13,12 @@ |
3172 | min-width: 200px; |
3173 | |
3174 | .charm-icon, |
3175 | - .category-icon, |
3176 | .icon { |
3177 | margin-right: 12px; |
3178 | position: relative; |
3179 | } |
3180 | .charm-icon, |
3181 | .charm-icon img, |
3182 | - .category-icon, |
3183 | - .category-icon img, |
3184 | .icon, |
3185 | .icon img { |
3186 | width: 48px; |
3187 | @@ -39,15 +36,12 @@ |
3188 | } |
3189 | &.small { |
3190 | .charm-icon, |
3191 | - .category-icon, |
3192 | .icon { |
3193 | margin-right: 10px; |
3194 | position: relative; |
3195 | } |
3196 | .charm-icon, |
3197 | .charm-icon img, |
3198 | - .category-icon, |
3199 | - .category-icon img, |
3200 | .icon, |
3201 | .icon img { |
3202 | width: 50px; |
3203 | @@ -67,15 +61,12 @@ |
3204 | } |
3205 | &.large { |
3206 | .charm-icon, |
3207 | - .category-icon, |
3208 | .icon { |
3209 | margin-right: 16px; |
3210 | position: relative; |
3211 | } |
3212 | .charm-icon, |
3213 | .charm-icon img, |
3214 | - .category-icon, |
3215 | - .category-icon img, |
3216 | .icon, |
3217 | .icon img { |
3218 | width: 96px; |
3219 | |
3220 | === modified file 'lib/views/juju-inspector.less' |
3221 | --- lib/views/juju-inspector.less 2013-08-21 03:20:46 +0000 |
3222 | +++ lib/views/juju-inspector.less 2013-08-30 19:48:17 +0000 |
3223 | @@ -41,7 +41,7 @@ |
3224 | position: absolute; |
3225 | top: 20px; |
3226 | bottom: @navbar-bottom-height + 20px; |
3227 | - right: @inspector-width + 35px; |
3228 | + right: @inspector-width + 71px; |
3229 | overflow-y: auto; |
3230 | overflow-x: hidden; |
3231 | width: @bws-panel-width; |
3232 | @@ -115,7 +115,7 @@ |
3233 | .create-border-radius(@border-radius); |
3234 | position: absolute; |
3235 | top: 20px; |
3236 | - right: 35px; |
3237 | + right: 70px; |
3238 | width: @inspector-width; |
3239 | min-height: 250px; |
3240 | border: 0; |
3241 | @@ -248,6 +248,9 @@ |
3242 | padding-left: 10px; |
3243 | cursor: pointer; |
3244 | } |
3245 | + .editable-constraints { |
3246 | + padding-left: 10px; |
3247 | + } |
3248 | .unit-constraints-confirm { |
3249 | overflow: hidden; |
3250 | height: 130px; |
3251 | @@ -272,6 +275,9 @@ |
3252 | .unit-constraints-confirm.closed { |
3253 | height: 0; |
3254 | } |
3255 | + .unit-constraints-confirm.editing { |
3256 | + height: 300px; |
3257 | + } |
3258 | } |
3259 | |
3260 | .settings-wrapper { |
3261 | @@ -290,6 +296,9 @@ |
3262 | textarea { |
3263 | width: 100%; |
3264 | } |
3265 | + input[type=text] { |
3266 | + width: 150px; |
3267 | + } |
3268 | .settings-description { |
3269 | font-size: 12px; |
3270 | |
3271 | @@ -389,7 +398,7 @@ |
3272 | } |
3273 | |
3274 | /* Resetting panel styles. Can be removed after old inspector styles are |
3275 | - removed. */ |
3276 | + removed. (ServiceInspector) */ |
3277 | .charm-panel-configure { |
3278 | background: transparent; |
3279 | float: none; |
3280 | @@ -468,11 +477,11 @@ |
3281 | input.service-name { |
3282 | width: 125px; |
3283 | } |
3284 | - .charm-icon { |
3285 | + .icon { |
3286 | width: 64px; |
3287 | height: 64px; |
3288 | margin-right: 1em; |
3289 | - background: transparent url(/juju-ui/assets/images/charm_64.png) left top no-repeat; |
3290 | + |
3291 | img { |
3292 | /* bootstrap sets this to middle */ |
3293 | vertical-align: baseline; |
3294 | @@ -582,8 +591,7 @@ |
3295 | font-size: 18px; |
3296 | padding-left: 0; |
3297 | } |
3298 | - .settings-wrapper, |
3299 | - .control-group { |
3300 | + .settings-wrapper { |
3301 | position: relative; |
3302 | padding: 0 10px; |
3303 | |
3304 | |
3305 | === added file 'test/data/wp-deployer.yaml' |
3306 | --- test/data/wp-deployer.yaml 1970-01-01 00:00:00 +0000 |
3307 | +++ test/data/wp-deployer.yaml 2013-08-30 19:48:17 +0000 |
3308 | @@ -0,0 +1,41 @@ |
3309 | +envExport: |
3310 | + series: precise |
3311 | + services: |
3312 | + mysql: |
3313 | + charm: "cs:precise/mysql-27" |
3314 | + num_units: 1 |
3315 | + options: |
3316 | + "binlog-format": MIXED |
3317 | + "block-size": "5" |
3318 | + "dataset-size": "80%" |
3319 | + flavor: distro |
3320 | + 'gui-x': 50 |
3321 | + 'gui-y': 50 |
3322 | + "ha-bindiface": eth0 |
3323 | + "ha-mcastport": "5411" |
3324 | + "max-connections": "-1" |
3325 | + "preferred-storage-engine": InnoDB |
3326 | + "query-cache-size": "-1" |
3327 | + "query-cache-type": "OFF" |
3328 | + "rbd-name": mysql1 |
3329 | + "tuning-level": safest |
3330 | + vip: "" |
3331 | + vip_cidr: "24" |
3332 | + vip_iface: eth0 |
3333 | + annotations: |
3334 | + "gui-x": 115 |
3335 | + "gui-y": 89 |
3336 | + wordpress: |
3337 | + charm: "cs:precise/wordpress-16" |
3338 | + num_units: 1 |
3339 | + options: |
3340 | + debug: "no" |
3341 | + engine: nginx |
3342 | + tuning: single |
3343 | + "wp-content": "" |
3344 | + annotations: |
3345 | + "gui-x": 510 |
3346 | + "gui-y": 184 |
3347 | + relations: |
3348 | + - - "wordpress:db" |
3349 | + - "mysql:db" |
3350 | |
3351 | === modified file 'test/index.html' |
3352 | --- test/index.html 2013-08-06 04:35:06 +0000 |
3353 | +++ test/index.html 2013-08-30 19:48:17 +0000 |
3354 | @@ -32,7 +32,7 @@ |
3355 | should = chai.should(); |
3356 | mocha.reporter('html'); |
3357 | mocha.ui('bdd'); |
3358 | - mocha.setup({ignoreLeaks: false, timeout: 10000}) |
3359 | + mocha.setup({ignoreLeaks: false, timeout: 10000}); |
3360 | </script> |
3361 | |
3362 | <!-- Load up YUI base, app modules, and test utils --> |
3363 | @@ -56,6 +56,7 @@ |
3364 | <script src="test_browser_models.js"></script> |
3365 | <script src="test_browser_search_view.js"></script> |
3366 | <script src="test_browser_search_widget.js"></script> |
3367 | + <script src="test_bundle_module.js"></script> |
3368 | <script src="test_charm_configuration.js"></script> |
3369 | <script src="test_charm_container.js"></script> |
3370 | <script src="test_charm_panel.js"></script> |
3371 | |
3372 | === modified file 'test/test_app.js' |
3373 | --- test/test_app.js 2013-07-31 14:30:47 +0000 |
3374 | +++ test/test_app.js 2013-08-30 19:48:17 +0000 |
3375 | @@ -160,7 +160,7 @@ |
3376 | |
3377 | it('should be able to route objects to internal URLs', function() { |
3378 | constructAppInstance({ |
3379 | - env: juju.newEnvironment({ conn: new utils.SocketStub() }) |
3380 | + env: juju.newEnvironment({conn: new utils.SocketStub()}, 'python') |
3381 | }); |
3382 | // Take handles to database objects and ensure we can route to the view |
3383 | // needed to show them. |
3384 | @@ -241,6 +241,14 @@ |
3385 | }) |
3386 | }); |
3387 | |
3388 | + // XXX bug:1217383 |
3389 | + // Force an app._controlEvents so that we don't try to bind viewmode |
3390 | + // controls. |
3391 | + var fakeEv = { |
3392 | + detach: function() {} |
3393 | + }; |
3394 | + app._controlEvents = [fakeEv, fakeEv]; |
3395 | + |
3396 | var checkUrls = [{ |
3397 | url: ':gui:/service/memcached/', |
3398 | hidden: true |
3399 | @@ -329,6 +337,12 @@ |
3400 | return app; |
3401 | }; |
3402 | |
3403 | + // Ensure the given message is a login request. |
3404 | + var assertIsLogin = function(message) { |
3405 | + assert.equal('Admin', message.Type); |
3406 | + assert.equal('Login', message.Request); |
3407 | + }; |
3408 | + |
3409 | it('avoids trying to login if the env is not connected', function(done) { |
3410 | var app = makeApp(false); // Create a disconnected app. |
3411 | app.after('ready', function() { |
3412 | @@ -341,7 +355,7 @@ |
3413 | var app = makeApp(true); // Create a connected app. |
3414 | app.after('ready', function() { |
3415 | assert.equal(1, conn.messages.length); |
3416 | - assert.equal('login', conn.last_message().op); |
3417 | + assertIsLogin(conn.last_message()); |
3418 | done(); |
3419 | }); |
3420 | }); |
3421 | @@ -360,21 +374,22 @@ |
3422 | it('displays the login view if credentials are not valid', function(done) { |
3423 | var app = makeApp(true); // Create a connected app. |
3424 | app.after('ready', function() { |
3425 | - // Mimic a login failed response. |
3426 | - conn.msg({op: 'login', result: false}); |
3427 | + app.env.login(); |
3428 | + // Mimic a login failed response assuming login is the first request. |
3429 | + conn.msg({RequestId: 1, Error: 'Invalid user or password'}); |
3430 | assert.equal(1, conn.messages.length); |
3431 | - assert.equal('login', conn.last_message().op); |
3432 | + assertIsLogin(conn.last_message()); |
3433 | assert.equal(LOGIN_VIEW_NAME, app.get('activeView').name); |
3434 | done(); |
3435 | }); |
3436 | }); |
3437 | |
3438 | - it('login method hanlder is called after successful login', function(done) { |
3439 | + it('login method handler is called after successful login', function(done) { |
3440 | var oldOnLogin = Y.juju.App.onLogin; |
3441 | Y.juju.App.prototype.onLogin = function(e) { |
3442 | assert.equal(conn.messages.length, 1); |
3443 | - assert.equal(conn.last_message().op, 'login'); |
3444 | - assert.equal(e.data.result, true); |
3445 | + assertIsLogin(conn.last_message()); |
3446 | + assert.isTrue(e.data.result, true); |
3447 | Y.juju.App.onLogin = oldOnLogin; |
3448 | done(); |
3449 | }; |
3450 | @@ -391,7 +406,7 @@ |
3451 | app.after('ready', function() { |
3452 | env.connect(); |
3453 | assert.equal(1, conn.messages.length); |
3454 | - assert.equal('login', conn.last_message().op); |
3455 | + assertIsLogin(conn.last_message()); |
3456 | done(); |
3457 | }); |
3458 | }); |
3459 | @@ -403,10 +418,8 @@ |
3460 | // Disconnect and reconnect the WebSocket. |
3461 | conn.transient_close(); |
3462 | conn.open(); |
3463 | - assert.equal(2, conn.messages.length); |
3464 | - Y.each(conn.messages, function(message) { |
3465 | - assert.equal('login', message.op); |
3466 | - }); |
3467 | + assert.equal(1, conn.messages.length); |
3468 | + assertIsLogin(conn.last_message()); |
3469 | done(); |
3470 | }); |
3471 | }); |
3472 | |
3473 | === modified file 'test/test_browser_app.js' |
3474 | --- test/test_browser_app.js 2013-08-19 17:15:16 +0000 |
3475 | +++ test/test_browser_app.js 2013-08-30 19:48:17 +0000 |
3476 | @@ -181,6 +181,54 @@ |
3477 | })(); |
3478 | |
3479 | (function() { |
3480 | + describe('browser minimzed view', function() { |
3481 | + var Y, browser, container, view, views, Minimized; |
3482 | + |
3483 | + before(function(done) { |
3484 | + Y = YUI(GlobalConfig).use( |
3485 | + 'juju-browser', |
3486 | + 'juju-models', |
3487 | + 'juju-views', |
3488 | + 'juju-tests-utils', |
3489 | + 'subapp-browser-minimized', |
3490 | + function(Y) { |
3491 | + browser = Y.namespace('juju.browser'); |
3492 | + views = Y.namespace('juju.browser.views'); |
3493 | + Minimized = views.MinimizedView; |
3494 | + done(); |
3495 | + }); |
3496 | + }); |
3497 | + |
3498 | + beforeEach(function() { |
3499 | + container = Y.namespace('juju-tests.utils').makeContainer('container'); |
3500 | + addBrowserContainer(Y, container); |
3501 | + // Mock out a dummy location for the Store used in view instances. |
3502 | + window.juju_config = { |
3503 | + charmworldURL: 'http://localhost' |
3504 | + }; |
3505 | + }); |
3506 | + |
3507 | + afterEach(function() { |
3508 | + view.destroy(); |
3509 | + Y.one('#subapp-browser').remove(true); |
3510 | + delete window.juju_config; |
3511 | + container.remove(true); |
3512 | + }); |
3513 | + |
3514 | + it('toggles to sidebar', function(done) { |
3515 | + var container = Y.one('#subapp-browser'); |
3516 | + view = new Minimized(); |
3517 | + view.on('viewNavigate', function(ev) { |
3518 | + assert(ev.change.viewmode === 'sidebar'); |
3519 | + done(); |
3520 | + }); |
3521 | + view.render(container); |
3522 | + view.controls._toggleViewable({halt: function() {}}); |
3523 | + }); |
3524 | + }); |
3525 | + })(); |
3526 | + |
3527 | + (function() { |
3528 | describe('browser sidebar view', function() { |
3529 | var Y, browser, container, view, views, Sidebar; |
3530 | |
3531 | @@ -332,13 +380,14 @@ |
3532 | |
3533 | (function() { |
3534 | describe('browser app', function() { |
3535 | - var Y, app, browser, Charmworld2, next; |
3536 | + var Y, app, browser, Charmworld2, container, next; |
3537 | |
3538 | before(function(done) { |
3539 | Y = YUI(GlobalConfig).use( |
3540 | 'app-subapp-extension', |
3541 | 'juju-browser', |
3542 | 'juju-charm-store', |
3543 | + 'juju-tests-utils', |
3544 | 'juju-views', |
3545 | 'subapp-browser', function(Y) { |
3546 | browser = Y.namespace('juju.subapps'); |
3547 | @@ -353,12 +402,16 @@ |
3548 | window.juju_config = { |
3549 | charmworldURL: 'http://localhost' |
3550 | }; |
3551 | + container = Y.namespace('juju-tests.utils').makeContainer('container'); |
3552 | + addBrowserContainer(Y, container); |
3553 | + |
3554 | }); |
3555 | |
3556 | afterEach(function() { |
3557 | if (app) { |
3558 | app.destroy(); |
3559 | } |
3560 | + container.remove(true); |
3561 | window.juju_config = undefined; |
3562 | }); |
3563 | |
3564 | @@ -383,6 +436,28 @@ |
3565 | assert.isTrue(called); |
3566 | }); |
3567 | |
3568 | + it('resets using initState', function() { |
3569 | + app = new browser.Browser(); |
3570 | + var mockView = { |
3571 | + destroy: function() {} |
3572 | + }; |
3573 | + app._sidebar = mockView; |
3574 | + app._minimized = mockView; |
3575 | + app._fullscreen = mockView; |
3576 | + |
3577 | + // Setup some previous state to check for clearing. |
3578 | + app._oldState.viewmode = 'fullscreen'; |
3579 | + app._viewState.viewmode = 'sidebar'; |
3580 | + |
3581 | + app.initState(); |
3582 | + |
3583 | + assert.equal(app._sidebar, undefined, 'sidebar is removed'); |
3584 | + assert.equal(app._fullscreen, undefined, 'fullscreen is removed'); |
3585 | + assert.equal(app._minimized, undefined, 'minimized is removed'); |
3586 | + assert.equal(app._oldState.viewmode, null, 'old state is reset'); |
3587 | + assert.equal(app._viewState.viewmode, null, 'view state is reset'); |
3588 | + }); |
3589 | + |
3590 | it('correctly strips viewmode from the charmID', function() { |
3591 | app = new browser.Browser(); |
3592 | var paths = [ |
3593 | @@ -609,6 +684,7 @@ |
3594 | 'app-subapp-extension', |
3595 | 'juju-views', |
3596 | 'juju-browser', |
3597 | + 'juju-tests-utils', |
3598 | 'subapp-browser', function(Y) { |
3599 | browser = Y.namespace('juju.subapps'); |
3600 | |
3601 | @@ -723,6 +799,8 @@ |
3602 | it('resets filters when navigating away from search', function() { |
3603 | browser._viewState.search = true; |
3604 | browser._filter.set('text', 'foo'); |
3605 | + // Set the state before changing up. |
3606 | + browser._saveState(); |
3607 | browser._getStateUrl({search: false}); |
3608 | assert.equal('', browser._filter.get('text')); |
3609 | }); |
3610 | @@ -1143,6 +1221,18 @@ |
3611 | |
3612 | it('when hidden the browser avoids routing', function() { |
3613 | browser.hidden = true; |
3614 | + // XXX bug:1217383 |
3615 | + // We also want to verify that the old views are cleared to avoid |
3616 | + // having hidden views doing UX work for us. |
3617 | + var hitCount = 0; |
3618 | + var mockView = { |
3619 | + destroy: function() { |
3620 | + hitCount = hitCount + 1; |
3621 | + } |
3622 | + }; |
3623 | + browser._sidebar = mockView; |
3624 | + browser._minimized = mockView; |
3625 | + browser._fullscreen = mockView; |
3626 | |
3627 | var req = { |
3628 | path: '/minimized', |
3629 | @@ -1161,6 +1251,13 @@ |
3630 | |
3631 | minNode.getComputedStyle('display').should.eql('none'); |
3632 | browserNode.getComputedStyle('display').should.eql('none'); |
3633 | + |
3634 | + assert.equal(hitCount, 3); |
3635 | + |
3636 | + // The view state needs to also be sync'd and updated even though |
3637 | + // we're hidden so that we can detect changes in the app state across |
3638 | + // requests while hidden. |
3639 | + assert.equal(browser._oldState.viewmode, 'minimized'); |
3640 | }); |
3641 | |
3642 | it('knows when the search cache should be updated', function() { |
3643 | @@ -1169,16 +1266,19 @@ |
3644 | 'querystring': 'text=apache' |
3645 | }); |
3646 | assert.isTrue(browser._searchChanged()); |
3647 | + browser._saveState(); |
3648 | browser._getStateUrl({ |
3649 | 'search': true, |
3650 | 'querystring': 'text=apache' |
3651 | }); |
3652 | assert.isFalse(browser._searchChanged()); |
3653 | + browser._saveState(); |
3654 | browser._getStateUrl({ |
3655 | 'search': true, |
3656 | 'querystring': 'text=ceph' |
3657 | }); |
3658 | assert.isTrue(browser._searchChanged()); |
3659 | + browser._saveState(); |
3660 | }); |
3661 | |
3662 | it('permits a filter clear command', function() { |
3663 | |
3664 | === modified file 'test/test_browser_editorial.js' |
3665 | --- test/test_browser_editorial.js 2013-07-16 19:05:46 +0000 |
3666 | +++ test/test_browser_editorial.js 2013-08-30 19:48:17 +0000 |
3667 | @@ -140,26 +140,6 @@ |
3668 | assert(node.all('.yui3-charmtoken-hidden').size() === 14); |
3669 | }); |
3670 | |
3671 | - it('does a search when a category link is clicked', function(done) { |
3672 | - view = new EditorialView({ |
3673 | - renderTo: Y.one('.bws-content') |
3674 | - }); |
3675 | - var results = { |
3676 | - featuredCharms: [], |
3677 | - newCharms: [], |
3678 | - popularCharms: [] |
3679 | - }; |
3680 | - view.on('viewNavigate', function(ev) { |
3681 | - assert.isTrue(ev.change.search); |
3682 | - assert.equal(1, ev.change.filter.categories.length); |
3683 | - assert.equal('databases', ev.change.filter.categories[0]); |
3684 | - assert.equal(ev.change.filter.replace, true); |
3685 | - done(); |
3686 | - }); |
3687 | - view.render(results); |
3688 | - Y.one('#category-links').one('a').simulate('click'); |
3689 | - }); |
3690 | - |
3691 | it('clicking a charm navigates for fullscreen', function(done) { |
3692 | fakeStore = new Y.juju.Charmworld2({}); |
3693 | fakeStore.set('datasource', { |
3694 | |
3695 | === added file 'test/test_bundle_module.js' |
3696 | --- test/test_bundle_module.js 1970-01-01 00:00:00 +0000 |
3697 | +++ test/test_bundle_module.js 2013-08-30 19:48:17 +0000 |
3698 | @@ -0,0 +1,98 @@ |
3699 | +/* |
3700 | +This file is part of the Juju GUI, which lets users view and manage Juju |
3701 | +environments within a graphical interface (https://launchpad.net/juju-gui). |
3702 | +Copyright (C) 2012-2013 Canonical Ltd. |
3703 | + |
3704 | +This program is free software: you can redistribute it and/or modify it under |
3705 | +the terms of the GNU Affero General Public License version 3, as published by |
3706 | +the Free Software Foundation. |
3707 | + |
3708 | +This program is distributed in the hope that it will be useful, but WITHOUT |
3709 | +ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
3710 | +SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero |
3711 | +General Public License for more details. |
3712 | + |
3713 | +You should have received a copy of the GNU Affero General Public License along |
3714 | +with this program. If not, see <http://www.gnu.org/licenses/>. |
3715 | +*/ |
3716 | + |
3717 | +'use strict'; |
3718 | + |
3719 | +describe('bundle module', function() { |
3720 | + var db, juju, models, utils, views, Y, bundleModule; |
3721 | + var bundle, container, fakeStore; |
3722 | + |
3723 | + before(function(done) { |
3724 | + Y = YUI(GlobalConfig).use([ |
3725 | + 'juju-topology', |
3726 | + 'juju-view-bundle', |
3727 | + 'juju-charm-store', |
3728 | + 'juju-models', |
3729 | + 'juju-tests-utils' |
3730 | + ], |
3731 | + function(Y) { |
3732 | + juju = Y.namespace('juju'); |
3733 | + models = Y.namespace('juju.models'); |
3734 | + utils = Y.namespace('juju-tests.utils'); |
3735 | + views = Y.namespace('juju.views'); |
3736 | + done(); |
3737 | + }); |
3738 | + }); |
3739 | + |
3740 | + beforeEach(function() { |
3741 | + container = utils.makeContainer(); |
3742 | + db = new models.Database(); |
3743 | + }); |
3744 | + |
3745 | + afterEach(function() { |
3746 | + if (container) { container.remove(true); } |
3747 | + if (bundle) { bundle.destroy(); } |
3748 | + |
3749 | + }); |
3750 | + |
3751 | + function promiseBundle() { |
3752 | + db.environment.set('defaultSeries', 'precise'); |
3753 | + var fakeStore = utils.makeFakeStore(db.charms); |
3754 | + fakeStore.iconpath = function() { return 'fake.svg'; }; |
3755 | + |
3756 | + return db.importDeployer( |
3757 | + jsyaml.safeLoad(utils.loadFixture('data/wp-deployer.yaml')), |
3758 | + fakeStore, {useGhost: true, targetBundle: 'wordpress-prod'}) |
3759 | + .then(function() { |
3760 | + bundle = new views.BundleTopology({ |
3761 | + db: db, |
3762 | + container: container, |
3763 | + store: fakeStore |
3764 | + }).render(); |
3765 | + bundleModule = bundle.topology.modules.BundleModule; |
3766 | + bundleModule.set('useTransitions', false); |
3767 | + return bundle; |
3768 | + }); |
3769 | + } |
3770 | + |
3771 | + it('should create a proper service for each model', function(done) { |
3772 | + promiseBundle() |
3773 | + .then(function(bundle) { |
3774 | + // The size of the element should reflect the passed in params |
3775 | + var svg = d3.select(container.getDOMNode()).select('svg'); |
3776 | + assert.equal(svg.attr('width'), 640); |
3777 | + assert.equal(svg.attr('height'), 480); |
3778 | + |
3779 | + // We should have the two rendered services |
3780 | + assert.equal(container.all('.service').size(), 2); |
3781 | + assert.deepEqual(container.all('tspan.name').get('text'), |
3782 | + ['mysql', 'wordpress']); |
3783 | + var service = svg.select('.service'); |
3784 | + assert.equal(service.attr('width'), '96'); |
3785 | + assert.equal(service.attr('transform'), 'translate(115,89)'); |
3786 | + |
3787 | + |
3788 | + container.remove(true); |
3789 | + bundle.destroy(); |
3790 | + done(); |
3791 | + }).then(undefined, done); |
3792 | + |
3793 | + }); |
3794 | + |
3795 | +}); |
3796 | + |
3797 | |
3798 | === modified file 'test/test_charm_panel.js' |
3799 | --- test/test_charm_panel.js 2013-08-12 14:58:23 +0000 |
3800 | +++ test/test_charm_panel.js 2013-08-30 19:48:17 +0000 |
3801 | @@ -118,7 +118,8 @@ |
3802 | serviceName = 'membase'; |
3803 | // Mock the relevant environment calls. |
3804 | env = { |
3805 | - deploy: function(url, service, config, config_raw, units, callback) { |
3806 | + deploy: function(url, service, config, config_raw, units, |
3807 | + constraints, callback) { |
3808 | callback({err: false}); |
3809 | }, |
3810 | update_annotations: function(service, type, annotations, callback) { |
3811 | |
3812 | === modified file 'test/test_charm_store.js' |
3813 | --- test/test_charm_store.js 2013-08-21 16:02:10 +0000 |
3814 | +++ test/test_charm_store.js 2013-08-30 19:48:17 +0000 |
3815 | @@ -21,15 +21,17 @@ |
3816 | (function() { |
3817 | |
3818 | describe('juju Charmworld2 api', function() { |
3819 | - var Y, models, conn, env, app, container, data, juju; |
3820 | + var Y, models, conn, env, app, container, data, juju, utils; |
3821 | |
3822 | before(function(done) { |
3823 | Y = YUI(GlobalConfig).use( |
3824 | 'datasource-local', 'json-stringify', 'juju-charm-store', |
3825 | 'datasource-io', 'io', 'array-extras', 'juju-charm-models', |
3826 | + 'juju-tests-utils', |
3827 | function(Y) { |
3828 | juju = Y.namespace('juju'); |
3829 | models = Y.namespace('juju.models'); |
3830 | + utils = Y.namespace('juju-tests').utils; |
3831 | done(); |
3832 | }); |
3833 | }); |
3834 | @@ -210,6 +212,33 @@ |
3835 | assert.equal(api.normalizeCharmId('cs:precise/wordpress-10'), |
3836 | 'precise/wordpress-10'); |
3837 | }); |
3838 | + |
3839 | + it('finds upgrades for charms - upgrade available', function(done) { |
3840 | + var store = utils.makeFakeStore(); |
3841 | + var charm = new models.BrowserCharm({url: 'cs:precise/wordpress-10'}); |
3842 | + store.promiseUpgradeAvailability(charm) |
3843 | + .then(function(upgrade) { |
3844 | + assert.equal(upgrade, 'precise/wordpress-15'); |
3845 | + done(); |
3846 | + }, function(error) { |
3847 | + assert.isTrue(false, 'We should not get here'); |
3848 | + done(); |
3849 | + }); |
3850 | + }); |
3851 | + |
3852 | + it('finds upgrades for charms - no upgrade available', function(done) { |
3853 | + var store = utils.makeFakeStore(); |
3854 | + var charm = new models.BrowserCharm({url: 'cs:precise/wordpress-15'}); |
3855 | + store.promiseUpgradeAvailability(charm) |
3856 | + .then(function(upgrade) { |
3857 | + assert.isUndefined(upgrade); |
3858 | + done(); |
3859 | + }, function(error) { |
3860 | + assert.isTrue(false, 'We should not get here'); |
3861 | + done(); |
3862 | + }); |
3863 | + }); |
3864 | + |
3865 | }); |
3866 | |
3867 | })(); |
3868 | |
3869 | === modified file 'test/test_charm_view.js' |
3870 | --- test/test_charm_view.js 2013-07-26 22:05:56 +0000 |
3871 | +++ test/test_charm_view.js 2013-08-30 19:48:17 +0000 |
3872 | @@ -138,14 +138,12 @@ |
3873 | }); |
3874 | var deployInput = charmView.get('container').one('#charm-deploy'); |
3875 | deployInput.after('click', function() { |
3876 | - var msg = conn.last_message(), |
3877 | - charm = charmResults.charm, |
3878 | - expected = charm.series + '/' + charm.name; |
3879 | + var msg = conn.last_message(); |
3880 | // Ensure the websocket received the `deploy` message. |
3881 | - msg.op.should.equal('deploy'); |
3882 | - msg.charm_url.should.contain(expected); |
3883 | + assert.equal('ServiceDeploy', msg.Request); |
3884 | + assert.equal(charmResults.charm.id, msg.Params.CharmUrl); |
3885 | // A click to the deploy button redirects to the root page. |
3886 | - redirected.should.equal(true); |
3887 | + assert.isTrue(redirected); |
3888 | done(); |
3889 | }); |
3890 | deployInput.simulate('click'); |
3891 | @@ -164,8 +162,8 @@ |
3892 | // Assertions are in a callback, so set them up first. |
3893 | deployButton.after('click', function() { |
3894 | var msg = conn.last_message(); |
3895 | - assert.equal(msg.op, 'deploy'); |
3896 | - assert.equal(msg.service_name, serviceName); |
3897 | + assert.equal('ServiceDeploy', msg.Request); |
3898 | + assert.equal(serviceName, msg.Params.ServiceName); |
3899 | done(); |
3900 | }); |
3901 | var serviceNameField = container.one('#service-name'); |
3902 | @@ -188,9 +186,10 @@ |
3903 | // Assertions are in a callback, so set them up first. |
3904 | deployButton.after('click', function() { |
3905 | var msg = conn.last_message(); |
3906 | - assert.equal(msg.op, 'deploy'); |
3907 | - assert.property(msg.config, 'option0'); |
3908 | - assert.equal(msg.config.option0, option0Value); |
3909 | + assert.equal('ServiceDeploy', msg.Request); |
3910 | + var config = msg.Params.Config; |
3911 | + assert.property(config, 'option0'); |
3912 | + assert.equal(option0Value, config.option0); |
3913 | done(); |
3914 | }); |
3915 | container.one('#input-option0').set('value', option0Value); |
3916 | @@ -225,7 +224,7 @@ |
3917 | // Assertions are in a callback, so set them up first. |
3918 | deployButton.after('click', function() { |
3919 | var msg = conn.last_message(); |
3920 | - assert.equal(msg.op, 'deploy'); |
3921 | + assert.equal('ServiceDeploy', msg.Request); |
3922 | done(); |
3923 | }); |
3924 | deployButton.simulate('click'); |
3925 | |
3926 | === modified file 'test/test_endpoints.js' |
3927 | --- test/test_endpoints.js 2013-08-09 14:44:16 +0000 |
3928 | +++ test/test_endpoints.js 2013-08-30 19:48:17 +0000 |
3929 | @@ -378,6 +378,7 @@ |
3930 | |
3931 | it('should update endpoints map when non-pending services are added', |
3932 | function(done) { |
3933 | + var store = utils.makeFakeStore(); |
3934 | var service_name = 'wordpress'; |
3935 | var charm_id = 'cs:precise/wordpress-2'; |
3936 | app.db.charms.add({id: charm_id}); |
3937 | @@ -388,6 +389,7 @@ |
3938 | id: service_name, |
3939 | pending: true, |
3940 | loaded: true, |
3941 | + store: store, |
3942 | charm: charm_id}); |
3943 | |
3944 | controller.on('endpointMapAdded', function() { |
3945 | @@ -535,7 +537,13 @@ |
3946 | }); |
3947 | }); |
3948 | |
3949 | - it('should not call get_service when a pending service is added', |
3950 | + // Ensure the last message in the connection is a ServiceGet request. |
3951 | + var assertServiceGetCalled = function() { |
3952 | + assert.equal(1, conn.messages.length); |
3953 | + assert.equal('ServiceGet', conn.last_message().Request); |
3954 | + }; |
3955 | + |
3956 | + it('should not call ServiceGet when a pending service is added', |
3957 | function() { |
3958 | var charm_id = 'cs:precise/wordpress-2'; |
3959 | app.db.services.add({ |
3960 | @@ -545,7 +553,7 @@ |
3961 | assert.equal(0, conn.messages.length); |
3962 | }); |
3963 | |
3964 | - it('should call get_service when non-pending services are added', |
3965 | + it('should call ServiceGet when non-pending services are added', |
3966 | function() { |
3967 | var service_name = 'wordpress'; |
3968 | var charm_id = 'cs:precise/wordpress-2'; |
3969 | @@ -558,11 +566,10 @@ |
3970 | charm: charm_id}); |
3971 | var svc = app.db.services.getById(service_name); |
3972 | svc.set('pending', false); |
3973 | - assert.equal(1, conn.messages.length); |
3974 | - assert.equal('get_service', conn.last_message().op); |
3975 | + assertServiceGetCalled(); |
3976 | }); |
3977 | |
3978 | - it('should call get_service when a service\'s charm changes', function() { |
3979 | + it('should call ServiceGet when a service\'s charm changes', function() { |
3980 | var service_name = 'wordpress'; |
3981 | var charm_id = 'cs:precise/wordpress-2'; |
3982 | var charm = app.db.charms.add({id: charm_id}); |
3983 | @@ -572,15 +579,13 @@ |
3984 | id: service_name, |
3985 | pending: false, |
3986 | charm: charm_id}); |
3987 | - assert.equal(1, conn.messages.length); |
3988 | - assert.equal('get_service', conn.last_message().op); |
3989 | + assertServiceGetCalled(); |
3990 | var svc = app.db.services.getById(service_name); |
3991 | charm_id = 'cs:precise/wordpress-3'; |
3992 | var charm2 = app.db.charms.add({id: charm_id, loaded: true}); |
3993 | destroyMe.push(charm2); |
3994 | svc.set('charm', charm_id); |
3995 | - assert.equal(1, conn.messages.length); |
3996 | - assert.equal('get_service', conn.last_message().op); |
3997 | + assertServiceGetCalled(); |
3998 | }); |
3999 | |
4000 | }); |
4001 | |
4002 | === modified file 'test/test_env.js' |
4003 | --- test/test_env.js 2013-07-25 16:59:30 +0000 |
4004 | +++ test/test_env.js 2013-08-30 19:48:17 +0000 |
4005 | @@ -43,12 +43,12 @@ |
4006 | |
4007 | it('returns the default env if none is specified', function() { |
4008 | var env = juju.newEnvironment({}); |
4009 | - assert.equal('python-env', env.name); |
4010 | + assert.equal('go-env', env.name); |
4011 | }); |
4012 | |
4013 | it('returns the default env if an invalid one is specified', function() { |
4014 | var env = juju.newEnvironment({}, 'invalid-api-backend'); |
4015 | - assert.equal('python-env', env.name); |
4016 | + assert.equal('go-env', env.name); |
4017 | }); |
4018 | |
4019 | it('sets up the env using the provided options', function() { |
4020 | |
4021 | === modified file 'test/test_env_go.js' |
4022 | --- test/test_env_go.js 2013-06-12 18:11:05 +0000 |
4023 | +++ test/test_env_go.js 2013-08-30 19:48:17 +0000 |
4024 | @@ -55,6 +55,7 @@ |
4025 | |
4026 | }); |
4027 | |
4028 | + |
4029 | describe('Go Juju JSON replacer', function() { |
4030 | var cleanUpJSON, Y; |
4031 | |
4032 | @@ -417,6 +418,7 @@ |
4033 | Request: 'ServiceDeploy', |
4034 | Params: { |
4035 | Config: {}, |
4036 | + Constraints: {}, |
4037 | CharmUrl: 'precise/mysql' |
4038 | }, |
4039 | RequestId: 1 |
4040 | @@ -432,6 +434,7 @@ |
4041 | Params: { |
4042 | // Configuration values are sent as strings. |
4043 | Config: {debug: 'true', logo: 'example.com/mylogo.png'}, |
4044 | + Constraints: {}, |
4045 | CharmUrl: 'precise/mediawiki' |
4046 | }, |
4047 | RequestId: 1 |
4048 | @@ -450,6 +453,7 @@ |
4049 | Request: 'ServiceDeploy', |
4050 | Params: { |
4051 | Config: {}, |
4052 | + Constraints: {}, |
4053 | ConfigYAML: config_raw, |
4054 | CharmUrl: 'precise/mysql' |
4055 | }, |
4056 | @@ -460,15 +464,29 @@ |
4057 | assert.deepEqual(expected, msg); |
4058 | }); |
4059 | |
4060 | + it('successfully deploys a service with constraints', function() { |
4061 | + var constraints = { |
4062 | + 'cpu-cores': 1, |
4063 | + 'cpu-power': 0, |
4064 | + 'mem': '512M', |
4065 | + 'arch': 'i386' |
4066 | + }; |
4067 | + env.deploy('precise/mediawiki', null, null, null, 1, constraints); |
4068 | + msg = conn.last_message(); |
4069 | + assert.deepEqual(msg.Params.Constraints, constraints); |
4070 | + }); |
4071 | + |
4072 | it('successfully deploys a service storing charm data', function() { |
4073 | var charm_url; |
4074 | var err; |
4075 | var service_name; |
4076 | - env.deploy('precise/mysql', 'mysql', null, null, null, function(data) { |
4077 | - charm_url = data.charm_url; |
4078 | - err = data.err; |
4079 | - service_name = data.service_name; |
4080 | - }); |
4081 | + env.deploy( |
4082 | + 'precise/mysql', 'mysql', null, null, null, null, function(data) { |
4083 | + charm_url = data.charm_url; |
4084 | + err = data.err; |
4085 | + service_name = data.service_name; |
4086 | + } |
4087 | + ); |
4088 | // Mimic response. |
4089 | conn.msg({ |
4090 | RequestId: 1, |
4091 | @@ -481,9 +499,11 @@ |
4092 | |
4093 | it('handles failed service deploy', function() { |
4094 | var err; |
4095 | - env.deploy('precise/mysql', 'mysql', null, null, null, function(data) { |
4096 | - err = data.err; |
4097 | - }); |
4098 | + env.deploy( |
4099 | + 'precise/mysql', 'mysql', null, null, null, null, function(data) { |
4100 | + err = data.err; |
4101 | + } |
4102 | + ); |
4103 | // Mimic response. |
4104 | conn.msg({ |
4105 | RequestId: 1, |
4106 | |
4107 | === modified file 'test/test_env_python.js' |
4108 | --- test/test_env_python.js 2013-05-17 14:51:05 +0000 |
4109 | +++ test/test_env_python.js 2013-08-30 19:48:17 +0000 |
4110 | @@ -37,7 +37,7 @@ |
4111 | testUtils = Y.namespace('juju-tests.utils'); |
4112 | conn = new testUtils.SocketStub(); |
4113 | juju = Y.namespace('juju'); |
4114 | - env = juju.newEnvironment({conn: conn}); |
4115 | + env = juju.newEnvironment({conn: conn}, 'python'); |
4116 | env.connect(); |
4117 | conn.open(); |
4118 | done(); |
4119 | @@ -70,6 +70,17 @@ |
4120 | msg.config_raw.should.equal(config_raw); |
4121 | }); |
4122 | |
4123 | + it('successfully deploys a service with constraints', function() { |
4124 | + var constraints = { |
4125 | + 'cpu': 1, |
4126 | + 'mem': '512M', |
4127 | + 'arch': 'i386' |
4128 | + }; |
4129 | + env.deploy('precise/mysql', null, null, null, 1, constraints); |
4130 | + msg = conn.last_message(); |
4131 | + assert.deepEqual(msg.constraints, constraints); |
4132 | + }); |
4133 | + |
4134 | it('can add a unit', function() { |
4135 | env.add_unit('mysql', 3); |
4136 | msg = conn.last_message(); |
4137 | @@ -442,7 +453,7 @@ |
4138 | |
4139 | it('denies deploying a charm if the GUI is read-only', function() { |
4140 | assertOperationDenied( |
4141 | - 'deploy', ['cs:precise/haproxy', 'haproxy', {}, null, 3]); |
4142 | + 'deploy', ['cs:precise/haproxy', 'haproxy', {}, null, 3, null]); |
4143 | }); |
4144 | |
4145 | it('denies exposing a service if the GUI is read-only', function() { |
4146 | |
4147 | === modified file 'test/test_fakebackend.js' |
4148 | --- test/test_fakebackend.js 2013-07-30 21:00:47 +0000 |
4149 | +++ test/test_fakebackend.js 2013-08-30 19:48:17 +0000 |
4150 | @@ -134,7 +134,9 @@ |
4151 | pending: false, |
4152 | life: 'alive', |
4153 | subordinate: false, |
4154 | - unit_count: undefined |
4155 | + unit_count: undefined, |
4156 | + upgrade_available: false, |
4157 | + upgrade_to: undefined |
4158 | }); |
4159 | var units = fakebackend.db.units.get_units_for_service(service); |
4160 | assert.lengthOf(units, 1); |
4161 | @@ -143,6 +145,30 @@ |
4162 | assert.equal(units[0].service, 'wordpress'); |
4163 | }); |
4164 | |
4165 | + it('deploys a charm with constraints', function() { |
4166 | + var options = { |
4167 | + constraints: { |
4168 | + cpu: 1, |
4169 | + mem: '4G', |
4170 | + arch: 'i386' |
4171 | + } |
4172 | + }; |
4173 | + assert.isNull( |
4174 | + fakebackend.db.charms.getById('cs:precise/wordpress-15')); |
4175 | + fakebackend.deploy('cs:precise/wordpress-15', callback, options); |
4176 | + var service = fakebackend.db.services.getById('wordpress'); |
4177 | + assert.isObject( |
4178 | + service, |
4179 | + 'Null returend when a service was expected.'); |
4180 | + assert.strictEqual(service, result.service); |
4181 | + var attrs = service.getAttrs(); |
4182 | + var deployedConstraints = attrs.constraints; |
4183 | + assert.deepEqual( |
4184 | + options.constraints, |
4185 | + deployedConstraints |
4186 | + ); |
4187 | + }); |
4188 | + |
4189 | it('rejects names that duplicate an existing service', function() { |
4190 | fakebackend.deploy('cs:precise/wordpress-15', callback); |
4191 | assert.isUndefined(result.error); |
4192 | |
4193 | === modified file 'test/test_ghost_inspector.js' |
4194 | --- test/test_ghost_inspector.js 2013-08-15 00:40:51 +0000 |
4195 | +++ test/test_ghost_inspector.js 2013-08-30 19:48:17 +0000 |
4196 | @@ -70,8 +70,8 @@ |
4197 | service = db.services.ghostService(charm); |
4198 | |
4199 | var fakeStore = new Y.juju.Charmworld2({}); |
4200 | - fakeStore.iconpath = function() { |
4201 | - return 'charm icon url'; |
4202 | + fakeStore.iconpath = function(id) { |
4203 | + return '/icon/' + id; |
4204 | }; |
4205 | |
4206 | view = new jujuViews.environment({ |
4207 | @@ -118,6 +118,14 @@ |
4208 | serviceNameInput.set('value', 'foo'); |
4209 | }); |
4210 | |
4211 | + it('displays the charms icon when rendered', function() { |
4212 | + inspector = setUpInspector(); |
4213 | + var icon = container.one('.icon img'); |
4214 | + |
4215 | + // The icon url is from the fakestore we manually defined. |
4216 | + assert.equal(icon.getAttribute('src'), '/icon/cs:precise/mediawiki-8'); |
4217 | + }); |
4218 | + |
4219 | it('deploys a service with the specified unit count & config', function() { |
4220 | inspector = setUpInspector(); |
4221 | env.connect(); |
4222 | @@ -129,16 +137,74 @@ |
4223 | vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click'); |
4224 | |
4225 | var message = env.ws.last_message(); |
4226 | - assert.equal(message.num_units, numUnits); |
4227 | - assert.equal(message.op, 'deploy'); |
4228 | - assert.equal(message.service_name, 'mediawiki'); |
4229 | - assert.deepEqual(message.config, { |
4230 | + var params = message.Params; |
4231 | + var config = { |
4232 | admins: '', |
4233 | - debug: false, |
4234 | + debug: 'false', |
4235 | logo: '', |
4236 | name: 'foo', |
4237 | skin: 'vector' |
4238 | - }); |
4239 | + }; |
4240 | + assert.equal('ServiceDeploy', message.Request); |
4241 | + assert.equal('mediawiki', params.ServiceName); |
4242 | + assert.equal(numUnits, params.NumUnits); |
4243 | + assert.deepEqual(config, params.Config); |
4244 | + }); |
4245 | + |
4246 | + it('presents the contraints to the user in python env', function() { |
4247 | + // Create our own env to make sure we know which backend we're creating it |
4248 | + // against. |
4249 | + env.destroy(); |
4250 | + env = juju.newEnvironment({conn: conn}, 'python'); |
4251 | + inspector = setUpInspector(); |
4252 | + var constraintsNode = container.all('.service-constraints'); |
4253 | + assert.equal(constraintsNode.size(), 1); |
4254 | + |
4255 | + var inputNodes = container.all('.service-constraints input'); |
4256 | + assert.equal(inputNodes.size(), 3); |
4257 | + }); |
4258 | + |
4259 | + it('presents the contraints to the user in go env', function() { |
4260 | + // Create our own env to make sure we know which backend we're creating it |
4261 | + // against. |
4262 | + env.destroy(); |
4263 | + env = juju.newEnvironment({conn: conn}, 'go'); |
4264 | + inspector = setUpInspector(); |
4265 | + var constraintsNode = container.all('.service-constraints'); |
4266 | + assert.equal(constraintsNode.size(), 1); |
4267 | + |
4268 | + var inputNodes = container.all('.service-constraints input'); |
4269 | + assert.equal(inputNodes.size(), 4); |
4270 | + }); |
4271 | + |
4272 | + it('deploys with constraints in python env', function() { |
4273 | + env.destroy(); |
4274 | + env = juju.newEnvironment({conn: conn}, 'python'); |
4275 | + inspector = setUpInspector(); |
4276 | + env.connect(); |
4277 | + var vmContainer = inspector.viewletManager.get('container'); |
4278 | + |
4279 | + vmContainer.one('input[name=cpu]').set('value', 2); |
4280 | + // Called the deploy button, but the css if confirm. |
4281 | + vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click'); |
4282 | + |
4283 | + var message = env.ws.last_message(); |
4284 | + assert.equal(message.constraints.cpu, '2'); |
4285 | + }); |
4286 | + |
4287 | + it('deploys with constraints in go env', function() { |
4288 | + env.destroy(); |
4289 | + env = juju.newEnvironment({conn: conn}, 'go'); |
4290 | + inspector = setUpInspector(); |
4291 | + env.connect(); |
4292 | + var vmContainer = inspector.viewletManager.get('container'); |
4293 | + |
4294 | + vmContainer.one('input[name=cpu-power]').set('value', 2); |
4295 | + // Called the deploy button, but the css if confirm. |
4296 | + vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click'); |
4297 | + |
4298 | + var message = env.ws.last_message(); |
4299 | + assert.equal(message.Params.Constraints['cpu-power'], '2'); |
4300 | }); |
4301 | |
4302 | it('disables and resets input fields when \'use default config\' is active', |
4303 | |
4304 | === modified file 'test/test_inspector_constraints.js' |
4305 | --- test/test_inspector_constraints.js 2013-08-19 21:07:02 +0000 |
4306 | +++ test/test_inspector_constraints.js 2013-08-30 19:48:17 +0000 |
4307 | @@ -85,14 +85,15 @@ |
4308 | return inspector; |
4309 | }; |
4310 | |
4311 | - // Create a fake response from the API server. |
4312 | + // Create a fake response from the juju-core API server. |
4313 | var makeResponse = function(service, error) { |
4314 | - return { |
4315 | - err: error, |
4316 | - op: 'set_constraints', |
4317 | - request_id: 1, |
4318 | - service_name: service.get('id') |
4319 | - }; |
4320 | + var response = {RequestId: 1}; |
4321 | + if (error) { |
4322 | + response.Error = 'bad wolf'; |
4323 | + } else { |
4324 | + response.Response = {}; |
4325 | + } |
4326 | + return response; |
4327 | }; |
4328 | |
4329 | // Retrieve and return the constraints viewlet. |
4330 | @@ -131,7 +132,7 @@ |
4331 | var constraintDescriptions = viewUtils.constraintDescriptions; |
4332 | |
4333 | Y.Array.each(env.genericConstraints, function(key) { |
4334 | - var node = container.one('div[for=' + key + '].control-label'); |
4335 | + var node = container.one('label[for=' + key + ']'); |
4336 | var expectedTitle = constraintDescriptions[key].title; |
4337 | assert.strictEqual(expectedTitle, node.getHTML()); |
4338 | }); |
4339 | @@ -156,7 +157,7 @@ |
4340 | }); |
4341 | |
4342 | it('can save constraints', function() { |
4343 | - var expected = {arch: 'amd64', cpu: 'photon', mem: '1 teraflop'}; |
4344 | + var expected = {arch: 'amd64', 'cpu-power': 100, mem: 4}; |
4345 | // Change values in the form. |
4346 | Y.Object.each(expected, function(value, key) { |
4347 | var node = container.one('input[name=' + key + '].constraint-field'); |
4348 | @@ -167,14 +168,10 @@ |
4349 | saveButton.simulate('click'); |
4350 | var lastMessage = env.ws.last_message(); |
4351 | // The set_constraint API method is correctly called. |
4352 | - assert.equal('set_constraints', lastMessage.op); |
4353 | + assert.equal('SetServiceConstraints', lastMessage.Request); |
4354 | // The expected constraints are passed in the API call. |
4355 | var obtained = Object.create(null); |
4356 | - Y.Array.each(lastMessage.constraints, function(value) { |
4357 | - var pair = value.split('='); |
4358 | - obtained[pair[0]] = pair[1]; |
4359 | - }); |
4360 | - assert.deepEqual(expected, obtained); |
4361 | + assert.deepEqual(expected, lastMessage.Params.Constraints); |
4362 | }); |
4363 | |
4364 | it('handles error responses from the environment', function() { |
4365 | |
4366 | === modified file 'test/test_inspector_overview.js' |
4367 | --- test/test_inspector_overview.js 2013-08-19 15:14:33 +0000 |
4368 | +++ test/test_inspector_overview.js 2013-08-30 19:48:17 +0000 |
4369 | @@ -20,7 +20,9 @@ |
4370 | describe('Inspector Overview', function() { |
4371 | |
4372 | var view, service, db, models, utils, juju, env, conn, container, |
4373 | - inspector, Y, jujuViews, ENTER, charmConfig; |
4374 | + inspector, Y, jujuViews, ENTER, charmConfig, |
4375 | + |
4376 | + client, backendJuju, state; |
4377 | |
4378 | before(function(done) { |
4379 | var requires = ['juju-gui', 'juju-views', 'juju-tests-utils', |
4380 | @@ -60,6 +62,16 @@ |
4381 | env.destroy(); |
4382 | container.remove(true); |
4383 | window.flags = {}; |
4384 | + |
4385 | + if (client) { |
4386 | + client.destroy(); |
4387 | + } |
4388 | + if (backendJuju) { |
4389 | + backendJuju.destroy(); |
4390 | + } |
4391 | + if (state) { |
4392 | + state.destroy(); |
4393 | + } |
4394 | }); |
4395 | |
4396 | var setUpInspector = function() { |
4397 | @@ -78,8 +90,8 @@ |
4398 | ['unit', 'add', {id: 'mediawiki/2', agent_state: 'pending'}] |
4399 | ]}}); |
4400 | var fakeStore = new Y.juju.Charmworld2({}); |
4401 | - fakeStore.iconpath = function() { |
4402 | - return 'charm icon url'; |
4403 | + fakeStore.iconpath = function(id) { |
4404 | + return '/icon/' + id; |
4405 | }; |
4406 | view = new jujuViews.environment({ |
4407 | container: container, |
4408 | @@ -96,6 +108,14 @@ |
4409 | return inspector; |
4410 | }; |
4411 | |
4412 | + it('should show the proper icon based off the charm model', function() { |
4413 | + inspector = setUpInspector(); |
4414 | + var icon = container.one('.icon img'); |
4415 | + |
4416 | + // The icon url comes from the fake store and the service charm attribute. |
4417 | + assert.equal(icon.getAttribute('src'), '/icon/precise/mediawiki-4'); |
4418 | + }); |
4419 | + |
4420 | it('should start with the proper number of units shown in the text field', |
4421 | function() { |
4422 | inspector = setUpInspector(); |
4423 | @@ -110,8 +130,9 @@ |
4424 | control.set('value', 1); |
4425 | control.simulate('keydown', { keyCode: ENTER }); // Simulate Enter. |
4426 | var message = conn.last_message(); |
4427 | - message.op.should.equal('remove_units'); |
4428 | - message.unit_names.should.eql(['mediawiki/2', 'mediawiki/1']); |
4429 | + assert.equal('DestroyServiceUnits', message.Request); |
4430 | + assert.deepEqual( |
4431 | + ['mediawiki/2', 'mediawiki/1'], message.Params.UnitNames); |
4432 | }); |
4433 | |
4434 | it('should not do anything if requested is < 1', |
4435 | @@ -132,10 +153,39 @@ |
4436 | control.simulate('keydown', { keyCode: ENTER }); |
4437 | // confirm the 'please confirm constraints' dialogue |
4438 | container.one('.confirm-num-units').simulate('click'); |
4439 | - var message = conn.last_message(); |
4440 | - message.op.should.equal('add_unit'); |
4441 | - message.service_name.should.equal('mediawiki'); |
4442 | - message.num_units.should.equal(4); |
4443 | + assert.equal(container.one('.unit-constraints-confirm') |
4444 | + .one('span:first-child') |
4445 | + .getHTML(), 'Scale up with the following constraints?'); |
4446 | + var message = conn.last_message(); |
4447 | + assert.equal('AddServiceUnits', message.Request); |
4448 | + assert.equal('mediawiki', message.Params.ServiceName); |
4449 | + assert.equal(4, message.Params.NumUnits); |
4450 | + }); |
4451 | + |
4452 | + it('should set the constraints before deploying any more units', |
4453 | + function() { |
4454 | + setUpInspector(true); |
4455 | + var control = container.one('.num-units-control'); |
4456 | + control.set('value', 7); |
4457 | + control.simulate('keydown', { keyCode: ENTER }); |
4458 | + var editConstraintsButton = container.one('.edit-constraints'); |
4459 | + editConstraintsButton.simulate('click'); |
4460 | + // It should be hidden after being clicked to display the constraints |
4461 | + assert.equal(editConstraintsButton.getStyle('display'), 'none'); |
4462 | + var constraintsWrapper = container.one('.editable-constraints'); |
4463 | + assert.equal(constraintsWrapper.getStyle('display'), 'block'); |
4464 | + var constraints = {arch: 'amd64', 'cpu-cores': 4, mem: 8}; |
4465 | + Y.Object.each(constraints, function(value, key) { |
4466 | + var node = constraintsWrapper.one('input[name=' + key + ']'); |
4467 | + node.set('value', value); |
4468 | + }); |
4469 | + |
4470 | + // confirm the 'please confirm constraints' dialogue |
4471 | + container.one('.confirm-num-units').simulate('click'); |
4472 | + var message = conn.last_message(); |
4473 | + assert.equal('SetServiceConstraints', message.Request); |
4474 | + assert.equal('mediawiki', message.Params.ServiceName); |
4475 | + assert.deepEqual(constraints, message.Params.Constraints); |
4476 | }); |
4477 | |
4478 | it('generates a proper statuses object', function() { |
4479 | @@ -283,15 +333,14 @@ |
4480 | |
4481 | unit.simulate('click'); |
4482 | retryButton.simulate('click'); |
4483 | - var msg = env.ws.last_message(); |
4484 | |
4485 | - assert.deepEqual(msg, { |
4486 | - op: 'resolved', |
4487 | - unit_name: 'mediawiki/7', |
4488 | - relation_name: null, |
4489 | - retry: false, |
4490 | - request_id: 1 |
4491 | - }); |
4492 | + var expected = { |
4493 | + Params: {Retry: false, UnitName: 'mediawiki/7'}, |
4494 | + Request: 'Resolved', |
4495 | + RequestId: 1, |
4496 | + Type: 'Client' |
4497 | + }; |
4498 | + assert.deepEqual(expected, env.ws.last_message()); |
4499 | }); |
4500 | |
4501 | it('sends the retry command to the env for the selected unit', function() { |
4502 | @@ -311,15 +360,14 @@ |
4503 | |
4504 | unit.simulate('click'); |
4505 | retryButton.simulate('click'); |
4506 | - var msg = env.ws.last_message(); |
4507 | |
4508 | - assert.deepEqual(msg, { |
4509 | - op: 'resolved', |
4510 | - unit_name: 'mediawiki/7', |
4511 | - relation_name: null, |
4512 | - retry: true, |
4513 | - request_id: 1 |
4514 | - }); |
4515 | + var expected = { |
4516 | + Params: {Retry: true, UnitName: 'mediawiki/7'}, |
4517 | + Request: 'Resolved', |
4518 | + RequestId: 1, |
4519 | + Type: 'Client' |
4520 | + }; |
4521 | + assert.deepEqual(expected, env.ws.last_message()); |
4522 | }); |
4523 | |
4524 | it('generates the button display map for each unit category', function() { |
4525 | |
4526 | === modified file 'test/test_inspector_settings.js' |
4527 | --- test/test_inspector_settings.js 2013-08-15 15:37:52 +0000 |
4528 | +++ test/test_inspector_settings.js 2013-08-30 19:48:17 +0000 |
4529 | @@ -307,7 +307,8 @@ |
4530 | input.set('value', 'foo'); |
4531 | |
4532 | button.simulate('click'); |
4533 | - assert.equal(env.ws.last_message().config.admins, 'foo'); |
4534 | + var message = env.ws.last_message(); |
4535 | + assert.equal('foo', message.Params.Config.admins); |
4536 | assert.equal(button.getHTML(), 'Save Changes'); |
4537 | }); |
4538 | |
4539 | |
4540 | === modified file 'test/test_login.js' |
4541 | --- test/test_login.js 2013-05-17 14:51:05 +0000 |
4542 | +++ test/test_login.js 2013-08-30 19:48:17 +0000 |
4543 | @@ -45,26 +45,25 @@ |
4544 | }); |
4545 | |
4546 | test('the user is initially assumed to be unauthenticated', function() { |
4547 | - assert.equal(env.userIsAuthenticated, false); |
4548 | + assert.isFalse(env.userIsAuthenticated); |
4549 | }); |
4550 | |
4551 | test('successful login event marks user as authenticated', function() { |
4552 | - var evt = {data: {op: 'login', result: true}}; |
4553 | - env.handleLoginEvent(evt); |
4554 | - assert.equal(env.userIsAuthenticated, true); |
4555 | + var data = {Response: {}}; |
4556 | + env.handleLogin(data); |
4557 | + assert.isTrue(env.userIsAuthenticated); |
4558 | }); |
4559 | |
4560 | test('unsuccessful login event keeps user unauthenticated', function() { |
4561 | - var evt = {data: {op: 'login'}}; |
4562 | - env.handleLoginEvent(evt); |
4563 | - assert.equal(env.userIsAuthenticated, false); |
4564 | + var data = {Error: 'who are you?'}; |
4565 | + env.handleLogin(data); |
4566 | + assert.isFalse(env.userIsAuthenticated); |
4567 | }); |
4568 | |
4569 | test('bad credentials are removed', function() { |
4570 | - var evt = {data: {op: 'login'}}; |
4571 | - env.handleLoginEvent(evt); |
4572 | - var credentials = env.getCredentials(); |
4573 | - assert.equal(credentials, null); |
4574 | + var data = {Error: 'who are you?'}; |
4575 | + env.handleLogin(data); |
4576 | + assert.isNull(env.getCredentials()); |
4577 | }); |
4578 | |
4579 | test('credentials passed to the constructor are stored', function() { |
4580 | @@ -93,11 +92,12 @@ |
4581 | }); |
4582 | |
4583 | test('with credentials set, login() sends an RPC message', function() { |
4584 | - env.setCredentials({ user: 'user', password: 'password' }); |
4585 | + env.setCredentials({user: 'user', password: 'password'}); |
4586 | env.login(); |
4587 | - assert.equal(conn.last_message().op, 'login'); |
4588 | - assert.equal(conn.last_message().user, 'user'); |
4589 | - assert.equal(conn.last_message().password, 'password'); |
4590 | + var message = conn.last_message(); |
4591 | + assert.equal('Login', message.Request); |
4592 | + assert.equal('user', message.Params.AuthTag); |
4593 | + assert.equal('password', message.Params.Password); |
4594 | }); |
4595 | |
4596 | }); |
4597 | @@ -143,9 +143,10 @@ |
4598 | container.appendChild('<input/>').set('type', 'password').set( |
4599 | 'value', 'password'); |
4600 | loginView.login(ev); |
4601 | - assert.equal(conn.last_message().op, 'login'); |
4602 | - assert.equal(conn.last_message().user, 'user'); |
4603 | - assert.equal(conn.last_message().password, 'password'); |
4604 | + var message = conn.last_message(); |
4605 | + assert.equal('Login', message.Request); |
4606 | + assert.equal('user', message.Params.AuthTag); |
4607 | + assert.equal('password', message.Params.Password); |
4608 | }); |
4609 | |
4610 | test('the view render method adds the login form', function() { |
4611 | |
4612 | === modified file 'test/test_model.js' |
4613 | --- test/test_model.js 2013-08-21 16:02:10 +0000 |
4614 | +++ test/test_model.js 2013-08-30 19:48:17 +0000 |
4615 | @@ -522,7 +522,7 @@ |
4616 | it('must send request to juju environment for local charms', function() { |
4617 | var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load(env); |
4618 | assert(!charm.loaded); |
4619 | - conn.last_message().op.should.equal('get_charm'); |
4620 | + assert.equal('CharmInfo', conn.last_message().Request); |
4621 | }); |
4622 | |
4623 | it('must handle success from local charm request', function(done) { |
4624 | @@ -530,57 +530,47 @@ |
4625 | env, |
4626 | function(err, response) { |
4627 | assert(!err); |
4628 | - charm.get('summary').should.equal('wowza'); |
4629 | + assert.equal('wowza', charm.get('summary')); |
4630 | assert(charm.loaded); |
4631 | done(); |
4632 | }); |
4633 | - var response = conn.last_message(); |
4634 | - response.result = { summary: 'wowza' }; |
4635 | - env.dispatch_result(response); |
4636 | - // The test in the callback above should run. |
4637 | - }); |
4638 | - |
4639 | - it('parses the old charm model options location correctly', function(done) { |
4640 | - var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load( |
4641 | - env, |
4642 | - function(err, response) { |
4643 | - assert(!err); |
4644 | - // This checks to make sure the parse mechanism is working properly |
4645 | - // for both the old ane new charm browser. |
4646 | - assert.equal(charm.get('options').default_log['default'], 'global'); |
4647 | - done(); |
4648 | - }); |
4649 | - var response = conn.last_message(); |
4650 | - response.result = { |
4651 | - config: { |
4652 | - options: { |
4653 | - default_log: { |
4654 | - 'default': 'global', |
4655 | - description: 'Default log', |
4656 | - type: 'string' |
4657 | - }}}}; |
4658 | - env.dispatch_result(response); |
4659 | - }); |
4660 | - |
4661 | - it('parses the new charm model options location correctly', function(done) { |
4662 | - var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load( |
4663 | - env, |
4664 | - function(err, response) { |
4665 | - assert(!err); |
4666 | - // This checks to make sure the parse mechanism is working properly |
4667 | - // for both the old ane new charm browser. |
4668 | - assert.equal(charm.get('options').default_log['default'], 'global'); |
4669 | - done(); |
4670 | - }); |
4671 | - var response = conn.last_message(); |
4672 | - response.result = { |
4673 | - options: { |
4674 | - default_log: { |
4675 | - 'default': 'global', |
4676 | - description: 'Default log', |
4677 | - type: 'string' |
4678 | - }}}; |
4679 | - env.dispatch_result(response); |
4680 | + var response = { |
4681 | + RequestId: conn.last_message().RequestId, |
4682 | + Response: {Meta: {Summary: 'wowza'}, Config: {}} |
4683 | + }; |
4684 | + env.dispatch_result(response); |
4685 | + // The test in the callback above should run. |
4686 | + }); |
4687 | + |
4688 | + it('parses charm model options correctly', function(done) { |
4689 | + var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load( |
4690 | + env, |
4691 | + function(err, response) { |
4692 | + assert(!err); |
4693 | + // This checks to make sure the parse mechanism is working properly |
4694 | + // for both the old ane new charm browser. |
4695 | + var option = charm.get('options').default_log; |
4696 | + assert.equal('global', option['default']); |
4697 | + assert.equal('Default log', option.description); |
4698 | + done(); |
4699 | + }); |
4700 | + var response = { |
4701 | + RequestId: conn.last_message().RequestId, |
4702 | + Response: { |
4703 | + Meta: {}, |
4704 | + Config: { |
4705 | + Options: { |
4706 | + default_log: { |
4707 | + Default: 'global', |
4708 | + Description: 'Default log', |
4709 | + Type: 'string' |
4710 | + } |
4711 | + } |
4712 | + } |
4713 | + } |
4714 | + }; |
4715 | + env.dispatch_result(response); |
4716 | + // The test in the callback above should run. |
4717 | }); |
4718 | |
4719 | it('must handle failure from local charm request', function(done) { |
4720 | @@ -592,8 +582,10 @@ |
4721 | assert(!charm.loaded); |
4722 | done(); |
4723 | }); |
4724 | - var response = conn.last_message(); |
4725 | - response.err = true; |
4726 | + var response = { |
4727 | + RequestId: conn.last_message().RequestId, |
4728 | + Error: 'error' |
4729 | + }; |
4730 | env.dispatch_result(response); |
4731 | // The test in the callback above should run. |
4732 | }); |
4733 | |
4734 | === modified file 'test/test_model_controller.js' |
4735 | --- test/test_model_controller.js 2013-08-12 14:56:38 +0000 |
4736 | +++ test/test_model_controller.js 2013-08-30 19:48:17 +0000 |
4737 | @@ -20,7 +20,7 @@ |
4738 | |
4739 | describe('Model Controller Promises', function() { |
4740 | var modelController, yui, env, db, conn, environment, load, serviceError, |
4741 | - getService, cleanups, aEach; |
4742 | + getService, cleanups, aEach, utils; |
4743 | |
4744 | before(function(done) { |
4745 | YUI(GlobalConfig).use( |
4746 | @@ -31,12 +31,13 @@ |
4747 | load = Y.juju.models.BrowserCharm.prototype.load; |
4748 | getService = environments.PythonEnvironment.prototype.get_service; |
4749 | aEach = Y.Array.each; |
4750 | + utils = Y.namespace('juju-tests.utils'); |
4751 | done(); |
4752 | }); |
4753 | }); |
4754 | |
4755 | beforeEach(function() { |
4756 | - conn = new yui['juju-tests'].utils.SocketStub(); |
4757 | + conn = new utils.SocketStub(); |
4758 | environment = env = yui.juju.newEnvironment( |
4759 | {conn: conn}); |
4760 | db = new yui.juju.models.Database(); |
4761 | @@ -56,6 +57,7 @@ |
4762 | yui.Array.each(cleanups, function(cleanup) { |
4763 | cleanup(); |
4764 | }); |
4765 | + window.flags = {}; |
4766 | }); |
4767 | |
4768 | /** |
4769 | @@ -91,7 +93,7 @@ |
4770 | @static |
4771 | */ |
4772 | function clobberGetService() { |
4773 | - yui.juju.environments.PythonEnvironment.prototype.get_service = function( |
4774 | + yui.juju.environments.GoEnvironment.prototype.get_service = function( |
4775 | serviceName, callback) { |
4776 | assert(typeof serviceName, 'string'); |
4777 | // This is to test the error reject path of the getService tests |
4778 | @@ -244,4 +246,30 @@ |
4779 | done(); |
4780 | }); |
4781 | }); |
4782 | + |
4783 | + it('can check for available upgrades', function(done) { |
4784 | + clobberLoad(); |
4785 | + clobberGetService(); |
4786 | + var serviceId = 'wordpress', |
4787 | + charmId = 'cs:precise/wordpress-7'; |
4788 | + db.services.add({ |
4789 | + id: serviceId, |
4790 | + loaded: true, |
4791 | + charm: charmId |
4792 | + }); |
4793 | + window.flags.upgradeCharm = true; |
4794 | + modelController.set('store', utils.makeFakeStore()); |
4795 | + var promise = modelController.getServiceWithCharm(serviceId); |
4796 | + promise.then( |
4797 | + function(result) { |
4798 | + var service = db.services.getById(serviceId); |
4799 | + assert(service.get('upgrade_available'), true); |
4800 | + assert(service.get('upgrade_to'), 'precise/wordpress-15'); |
4801 | + done(); |
4802 | + }, |
4803 | + function() { |
4804 | + assert.fail('This should not have failed.'); |
4805 | + done(); |
4806 | + }); |
4807 | + }); |
4808 | }); |
4809 | |
4810 | === modified file 'test/test_notifications.js' |
4811 | --- test/test_notifications.js 2013-07-31 14:30:47 +0000 |
4812 | +++ test/test_notifications.js 2013-08-30 19:48:17 +0000 |
4813 | @@ -263,7 +263,7 @@ |
4814 | var container = Y.Node.create( |
4815 | '<div id="test" class="container"></div>'), |
4816 | conn = new(Y.namespace('juju-tests.utils')).SocketStub(), |
4817 | - env = juju.newEnvironment({conn: conn}); |
4818 | + env = juju.newEnvironment({conn: conn}, 'python'); |
4819 | app = new Y.juju.App({ |
4820 | env: env, |
4821 | container: container, |
4822 | @@ -307,7 +307,7 @@ |
4823 | var container = Y.Node.create( |
4824 | '<div id="test" class="container"></div>'); |
4825 | var conn = new(Y.namespace('juju-tests.utils')).SocketStub(); |
4826 | - var env = juju.newEnvironment({conn: conn}); |
4827 | + var env = juju.newEnvironment({conn: conn}, 'python'); |
4828 | env.connect(); |
4829 | app = new Y.juju.App({ |
4830 | env: env, |
4831 | |
4832 | === modified file 'test/test_sandbox_go.js' |
4833 | --- test/test_sandbox_go.js 2013-08-14 19:15:01 +0000 |
4834 | +++ test/test_sandbox_go.js 2013-08-30 19:48:17 +0000 |
4835 | @@ -277,6 +277,32 @@ |
4836 | {llama: 'pajama'}, |
4837 | null, |
4838 | 1, |
4839 | + null, |
4840 | + callback); |
4841 | + }); |
4842 | + |
4843 | + it('can deploy with constraints', function(done) { |
4844 | + var constraints = { |
4845 | + 'cpu-cores': 1, |
4846 | + 'cpu-power': 0, |
4847 | + 'mem': '512M', |
4848 | + 'arch': 'i386' |
4849 | + }; |
4850 | + |
4851 | + env.connect(); |
4852 | + // We begin logged in. See utils.makeFakeBackend. |
4853 | + var callback = function(result) { |
4854 | + var service = state.db.services.getById('kumquat'); |
4855 | + assert.deepEqual(service.get('constraints'), constraints); |
4856 | + done(); |
4857 | + }; |
4858 | + env.deploy( |
4859 | + 'cs:precise/wordpress-15', |
4860 | + 'kumquat', |
4861 | + {llama: 'pajama'}, |
4862 | + null, |
4863 | + 1, |
4864 | + constraints, |
4865 | callback); |
4866 | }); |
4867 | |
4868 | @@ -288,8 +314,8 @@ |
4869 | result.err, 'A service with this name already exists.'); |
4870 | done(); |
4871 | }; |
4872 | - env.deploy('cs:precise/wordpress-15', undefined, undefined, undefined, 1, |
4873 | - callback); |
4874 | + env.deploy('cs:precise/wordpress-15', undefined, undefined, undefined, |
4875 | + 1, null, callback); |
4876 | }); |
4877 | |
4878 | it('can destroy a service', function(done) { |
4879 | @@ -609,6 +635,7 @@ |
4880 | {llama: 'pajama'}, |
4881 | null, |
4882 | 1, |
4883 | + null, |
4884 | localCb); |
4885 | } |
4886 | |
4887 | @@ -661,6 +688,7 @@ |
4888 | {llama: 'pajama'}, |
4889 | null, |
4890 | 1, |
4891 | + null, |
4892 | localCb); |
4893 | } |
4894 | |
4895 | @@ -904,19 +932,23 @@ |
4896 | |
4897 | it('can add a relation (integration)', function(done) { |
4898 | env.connect(); |
4899 | - env.deploy('cs:precise/wordpress-15', null, null, null, 1, function() { |
4900 | - env.deploy('cs:precise/mysql-26', null, null, null, 1, function() { |
4901 | - var endpointA = ['wordpress', {name: 'db', role: 'client'}], |
4902 | - endpointB = ['mysql', {name: 'db', role: 'server'}]; |
4903 | - env.add_relation(endpointA, endpointB, function(recData) { |
4904 | - assert.equal(recData.err, undefined); |
4905 | - assert.equal(recData.endpoint_a, 'wordpress:db'); |
4906 | - assert.equal(recData.endpoint_b, 'mysql:db'); |
4907 | - assert.isObject(recData.result); |
4908 | - done(); |
4909 | - }); |
4910 | - }); |
4911 | - }); |
4912 | + env.deploy( |
4913 | + 'cs:precise/wordpress-15', null, null, null, 1, null, function() { |
4914 | + env.deploy( |
4915 | + 'cs:precise/mysql-26', null, null, null, 1, null, function() { |
4916 | + var endpointA = ['wordpress', {name: 'db', role: 'client'}], |
4917 | + endpointB = ['mysql', {name: 'db', role: 'server'}]; |
4918 | + env.add_relation(endpointA, endpointB, function(recData) { |
4919 | + assert.equal(recData.err, undefined); |
4920 | + assert.equal(recData.endpoint_a, 'wordpress:db'); |
4921 | + assert.equal(recData.endpoint_b, 'mysql:db'); |
4922 | + assert.isObject(recData.result); |
4923 | + done(); |
4924 | + }); |
4925 | + } |
4926 | + ); |
4927 | + } |
4928 | + ); |
4929 | }); |
4930 | |
4931 | it('is able to add a relation with a subordinate service', function(done) { |
4932 | @@ -1020,20 +1052,26 @@ |
4933 | |
4934 | it('can remove a relation(integration)', function(done) { |
4935 | env.connect(); |
4936 | - env.deploy('cs:precise/wordpress-15', null, null, null, 1, function() { |
4937 | - env.deploy('cs:precise/mysql-26', null, null, null, 1, function() { |
4938 | - var endpointA = ['wordpress', {name: 'db', role: 'client'}], |
4939 | - endpointB = ['mysql', {name: 'db', role: 'server'}]; |
4940 | - env.add_relation(endpointA, endpointB, function() { |
4941 | - env.remove_relation(endpointA, endpointB, function(recData) { |
4942 | - assert.equal(recData.err, undefined); |
4943 | - assert.equal(recData.endpoint_a, 'wordpress:db'); |
4944 | - assert.equal(recData.endpoint_b, 'mysql:db'); |
4945 | - done(); |
4946 | - }); |
4947 | - }); |
4948 | - }); |
4949 | - }); |
4950 | + env.deploy( |
4951 | + 'cs:precise/wordpress-15', null, null, null, 1, null, function() { |
4952 | + env.deploy( |
4953 | + 'cs:precise/mysql-26', null, null, null, 1, null, function() { |
4954 | + var endpointA = ['wordpress', {name: 'db', role: 'client'}], |
4955 | + endpointB = ['mysql', {name: 'db', role: 'server'}]; |
4956 | + env.add_relation(endpointA, endpointB, function() { |
4957 | + env.remove_relation( |
4958 | + endpointA, endpointB, function(recData) { |
4959 | + assert.equal(recData.err, undefined); |
4960 | + assert.equal(recData.endpoint_a, 'wordpress:db'); |
4961 | + assert.equal(recData.endpoint_b, 'mysql:db'); |
4962 | + done(); |
4963 | + } |
4964 | + ); |
4965 | + }); |
4966 | + } |
4967 | + ); |
4968 | + } |
4969 | + ); |
4970 | }); |
4971 | |
4972 | }); |
4973 | |
4974 | === modified file 'test/test_sandbox_python.js' |
4975 | --- test/test_sandbox_python.js 2013-07-30 21:00:47 +0000 |
4976 | +++ test/test_sandbox_python.js 2013-08-30 19:48:17 +0000 |
4977 | @@ -154,6 +154,7 @@ |
4978 | {llama: 'pajama'}, |
4979 | null, |
4980 | 1, |
4981 | + null, |
4982 | localCb); |
4983 | }); |
4984 | env.connect(); |
4985 | @@ -209,6 +210,7 @@ |
4986 | {llama: 'pajama'}, |
4987 | null, |
4988 | 1, |
4989 | + null, |
4990 | localCb); |
4991 | }); |
4992 | env.connect(); |
4993 | @@ -383,6 +385,7 @@ |
4994 | {llama: 'pajama'}, |
4995 | null, |
4996 | 1, |
4997 | + null, |
4998 | callback); |
4999 | }); |
5000 | env.connect(); |
The diff has been truncated for viewing.
Reviewers: mp+182516_ code.launchpad. net,
Message:
Please take a look.
Description:
Bundle Topology
Provide a reusable view of topologies with a much more
limited and non-interactive service module. The tests show
how this works but don't wire the view into any current
UI which comes later.
https:/ /code.launchpad .net/~bcsaller/ juju-gui/ renderBundle2/ +merge/ 182516
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/13245045/
Affected files: javascripts/ d3-components. js model-controlle r.js models. js debug.js env/env. js env/fakebackend .js env/python. js env/sandbox. js browser/ browser. js browser/ templates/ editorial. handlebars browser/ views/editorial .js browser/ views/minimized .js browser/ views/view. js category- icons.partial ghost-config- viewlet. handlebars inspector- header. handlebars service- config- wrapper. handlebars service- constraints- viewlet. partial service- overview- constraints. handlebars charm-panel. js environment. js ghost-inspector .js inspector. js topology/ bundle. js topology/ relation. js topology/ topology. js viewlets/ inspector- header. js viewlets/ service- constraints. js viewlets/ service- ghost.js viewmode- controls. js browser/ charm-full. less browser/ charm-token. less juju-inspector. less wp-deployer. yaml browser_ app.js browser_ editorial. js charm_panel. js charm_store. js charm_view. js endpoints. js env_python. js fakebackend. js ghost_inspector .js inspector_ constraints. js inspector_ overview. js inspector_ settings. js model_controlle r.js notifications. js sandbox_ go.js sandbox_ python. js service_ config_ view.js service_ view.js startup. js.bottom topology. js unit_view. js viewlet_ manager. js viewmode_ controls_ widget. js websocket_ logging. js
A [revision details]
M app/app.js
M app/assets/
M app/config-debug.js
M app/config-prod.js
M app/index.html
M app/models/charm.js
M app/models/
M app/models/
M app/modules-
M app/store/charm.js
M app/store/
M app/store/
M app/store/env/go.js
M app/store/
M app/store/
M app/subapps/
M app/subapps/
M app/subapps/
M app/subapps/
M app/subapps/
D app/templates/
M app/templates/
M app/templates/
M app/templates/
M app/templates/
M app/templates/
M app/views/
M app/views/charm.js
M app/views/
M app/views/
M app/views/
A app/views/
M app/views/
M app/views/
M app/views/utils.js
M app/views/
M app/views/
M app/views/
M app/widgets/
M lib/views/
M lib/views/
M lib/views/
A test/data/
M test/index.html
M test/test_app.js
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_env.js
M test/test_env_go.js
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_login.js
M test/test_model.js
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_
M test/test_