Merge lp:~bcsaller/juju-gui/renderBundle2 into lp:~bcsaller/juju-gui/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
Reviewer Review Type Date Requested Status
Benjamin Saller Pending
Review via email: mp+182516@code.launchpad.net

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.

https://codereview.appspot.com/13245045/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

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:
   A [revision details]
   M app/app.js
   M app/assets/javascripts/d3-components.js
   M app/config-debug.js
   M app/config-prod.js
   M app/index.html
   M app/models/charm.js
   M app/models/model-controller.js
   M app/models/models.js
   M app/modules-debug.js
   M app/store/charm.js
   M app/store/env/env.js
   M app/store/env/fakebackend.js
   M app/store/env/go.js
   M app/store/env/python.js
   M app/store/env/sandbox.js
   M app/subapps/browser/browser.js
   M app/subapps/browser/templates/editorial.handlebars
   M app/subapps/browser/views/editorial.js
   M app/subapps/browser/views/minimized.js
   M app/subapps/browser/views/view.js
   D app/templates/category-icons.partial
   M app/templates/ghost-config-viewlet.handlebars
   M app/templates/inspector-header.handlebars
   M app/templates/service-config-wrapper.handlebars
   M app/templates/service-constraints-viewlet.partial
   M app/templates/service-overview-constraints.handlebars
   M app/views/charm-panel.js
   M app/views/charm.js
   M app/views/environment.js
   M app/views/ghost-inspector.js
   M app/views/inspector.js
   A app/views/topology/bundle.js
   M app/views/topology/relation.js
   M app/views/topology/topology.js
   M app/views/utils.js
   M app/views/viewlets/inspector-header.js
   M app/views/viewlets/service-constraints.js
   M app/views/viewlets/service-ghost.js
   M app/widgets/viewmode-controls.js
   M lib/views/browser/charm-full.less
   M lib/views/browser/charm-token.less
   M lib/views/juju-inspector.less
   A test/data/wp-deployer.yaml
   M test/index.html
   M test/test_app.js
   M test/test_browser_app.js
   M test/test_browser_editorial.js
   M test/test_charm_panel.js
   M test/test_charm_store.js
   M test/test_charm_view.js
   M test/test_endpoints.js
   M test/test_env.js
   M test/test_env_go.js
   M test/test_env_python.js
   M test/test_fakebackend.js
   M test/test_ghost_inspector.js
   M test/test_inspector_constraints.js
   M test/test_inspector_overview.js
   M test/test_inspector_settings.js
   M test/test_login.js
   M test/test_model.js
   M test/test_model_controller.js
   M test/test_notifications.js
   M test/test_sandbox_go.js
   M test/test_sandbox_python.js
   M test/test_service_config_view.js
   M test/test_service_view.js
   M test/test_startup.js.bottom
   M test/test_topology.js
   M test/test_unit_view.js
   M test/test_viewlet_manager.js
   M test/test_viewmode_controls_widget.js
   M test/test_websocket_logging.js

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

diff is totally broken, I'll try to repair this.

https://codereview.appspot.com/13245045/

Revision history for this message
Benjamin Saller (bcsaller) wrote :
lp:~bcsaller/juju-gui/renderBundle2 updated
980. By Benjamin Saller

merge trunk

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

After repeated attempts this is still not getting a proper diff for me.

bzr diff -r ancestor:../path/to/trunk

will show it though.

Sorry and thanks.

https://codereview.appspot.com/13245045/

lp:~bcsaller/juju-gui/renderBundle2 updated
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- &nbsp;
1095- {{#if mem}}
1096- {{mem}}GB
1097- {{else}}
1098- Default Mem
1099- {{/if}}
1100- &nbsp;
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+ &nbsp;
1114+ {{#if mem}}
1115+ {{mem}}GB
1116+ {{else}}
1117+ Default Mem
1118+ {{/if}}
1119+ &nbsp;
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.

Subscribers

People subscribed via source and target branches

to all changes: