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
=== modified file 'app/app.js'
--- app/app.js 2013-08-12 19:24:55 +0000
+++ app/app.js 2013-08-30 19:48:17 +0000
@@ -39,7 +39,8 @@
39 var juju = Y.namespace('juju'),39 var juju = Y.namespace('juju'),
40 models = Y.namespace('juju.models'),40 models = Y.namespace('juju.models'),
41 views = Y.namespace('juju.views'),41 views = Y.namespace('juju.views'),
42 utils = views.utils;42 utils = views.utils,
43 widgets = Y.namespace('juju.widgets');
4344
44 /**45 /**
45 * The main app class.46 * The main app class.
@@ -398,7 +399,8 @@
398399
399 // Set up a new modelController instance.400 // Set up a new modelController instance.
400 this.modelController = new juju.ModelController({401 this.modelController = new juju.ModelController({
401 db: this.db402 db: this.db,
403 store: this.get('store')
402 });404 });
403405
404 // Update the on-screen environment name provided in the configuration,406 // Update the on-screen environment name provided in the configuration,
@@ -1082,7 +1084,6 @@
1082 this.db.environment.set('defaultSeries', evt.newVal);1084 this.db.environment.set('defaultSeries', evt.newVal);
1083 },1085 },
10841086
1085
1086 /**1087 /**
1087 Display the Environment Name.1088 Display the Environment Name.
10881089
@@ -1126,10 +1127,71 @@
1126 // route on root namespaced paths and this check will no longer1127 // route on root namespaced paths and this check will no longer
1127 // be needed1128 // be needed
1128 this.renderEnvironment = false;1129 this.renderEnvironment = false;
1130
1131 // XXX bug:1217383
1132 // We're hiding the subapp from view, but people want to be able to
1133 // click on the viewmode controls. We handle that here as a temp
1134 // hack until the old :gui: views are gone and we've moved to the
1135 // serviceInspector. Then the browser will always be around and can
1136 // handle this widget for us. This is horrible and we know it. When
1137 // the idea of 'hidden' is removed with the old views this hack will
1138 // go away with it.
1139 if (!this._controlEvents || this._controlEvents.length === 0) {
1140 this._controls = new widgets.ViewmodeControls({
1141 currentViewmode: subapps.charmbrowser._viewState.viewmode
1142 });
1143 this._controls.render();
1144 this._controlEvents = [];
1145 this._controlEvents.push(
1146 this._controls.on(
1147 this._controls.EVT_FULLSCREEN,
1148 function(ev) {
1149 // Navigate away from anything in :gui: and to the
1150 // /fullscreen in :charmbrowser:
1151 this._controls._updateActiveNav('fullscreen');
1152 this.navigate(this.nsRouter.url({
1153 gui: '/',
1154 charmbrowser: '/fullscreen'
1155 }), { overrideAllNamespaces: true });
1156
1157 }, this
1158 )
1159 );
1160 this._controlEvents.push(
1161 this._controls.on(
1162 this._controls.EVT_SIDEBAR,
1163 function(ev) {
1164 // Navigate away from anything in :gui: and to the
1165 // /sidebar in :charmbrowser:
1166 this._controls._updateActiveNav('sidebar');
1167 this.navigate(this.nsRouter.url({
1168 gui: '/',
1169 charmbrowser: '/sidebar'
1170 }), { overrideAllNamespaces: true });
1171 }, this
1172 )
1173 );
1174 }
1175
1129 } else {1176 } else {
1130 charmbrowser.hidden = false;1177 charmbrowser.hidden = false;
1131 this.renderEnvironment = true;1178 this.renderEnvironment = true;
1179
1180 // XXX bug:1217383
1181 // Destroy the controls widget we might have had around for a bit.
1182 if (this._controlEvents) {
1183 this._controlEvents.forEach(function(ev) {
1184 ev.detach();
1185 });
1186 // reset the list to no events.
1187 this._controlEvents = [];
1188 }
1189
1190 if (this._controls) {
1191 this._controls.destroy();
1192 }
1132 }1193 }
1194
1133 charmbrowser.updateVisible();1195 charmbrowser.updateVisible();
1134 }1196 }
11351197
@@ -1444,6 +1506,8 @@
1444 'model-controller',1506 'model-controller',
1445 'FileSaver',1507 'FileSaver',
1446 'juju-inspector-widget',1508 'juju-inspector-widget',
1447 'juju-ghost-inspector'1509 'juju-ghost-inspector',
1510 'juju-view-bundle',
1511 'viewmode-controls'
1448 ]1512 ]
1449});1513});
14501514
=== modified file 'app/assets/javascripts/d3-components.js'
--- app/assets/javascripts/d3-components.js 2013-08-08 22:36:30 +0000
+++ app/assets/javascripts/d3-components.js 2013-08-30 19:48:17 +0000
@@ -132,6 +132,7 @@
132132
133 modEvents = module.events;133 modEvents = module.events;
134 this.events[module.name] = modEvents;134 this.events[module.name] = modEvents;
135
135 this.bind(module.name);136 this.bind(module.name);
136 module.componentBound();137 module.componentBound();
137138
@@ -286,6 +287,7 @@
286 * @method bind287 * @method bind
287 **/288 **/
288 bind: function(moduleName) {289 bind: function(moduleName) {
290 if (this.get('interactive') === false) { return; }
289 var eventSet = this.events,291 var eventSet = this.events,
290 filtered = {};292 filtered = {};
291293
@@ -319,6 +321,7 @@
319 owns = Y.Object.owns,321 owns = Y.Object.owns,
320 module;322 module;
321323
324 if (this.get('interactive') === false) { return; }
322 if (!modEvents || !modEvents.d3) {325 if (!modEvents || !modEvents.d3) {
323 return;326 return;
324 }327 }
@@ -520,7 +523,15 @@
520 }523 }
521 }, {524 }, {
522 ATTRS: {525 ATTRS: {
523 container: {}526 container: {},
527 /**
528 Boolean indicating if bind should be allowed to actually
529 bind events for the component.
530
531 @property {Boolean} interactive
532 @default true
533 */
534 interactive: {value: true}
524 }535 }
525536
526 });537 });
527538
=== modified file 'app/config-debug.js'
--- app/config-debug.js 2013-08-14 14:34:19 +0000
+++ app/config-debug.js 2013-08-30 19:48:17 +0000
@@ -40,7 +40,7 @@
40 socket_port: 8081,40 socket_port: 8081,
41 user: 'admin',41 user: 'admin',
42 password: 'admin',42 password: 'admin',
43 apiBackend: 'python', // Value can be 'python' or 'go'.43 apiBackend: 'go', // Value can be 'python' or 'go'.
44 sandbox: true,44 sandbox: true,
45 // When in sandbox mode should we create events to simulate a live env.45 // When in sandbox mode should we create events to simulate a live env.
46 // You can also use the :flags:/simulateEvents feature flag.46 // You can also use the :flags:/simulateEvents feature flag.
4747
=== modified file 'app/config-prod.js'
--- app/config-prod.js 2013-07-31 13:20:37 +0000
+++ app/config-prod.js 2013-08-30 19:48:17 +0000
@@ -40,7 +40,7 @@
40 socket_port: 8081,40 socket_port: 8081,
41 user: undefined,41 user: undefined,
42 password: undefined,42 password: undefined,
43 apiBackend: 'python', // Value can be 'python' or 'go'.43 apiBackend: 'go', // Value can be 'python' or 'go'.
44 sandbox: false,44 sandbox: false,
45 // When in sandbox mode should we create events to simulate a live env.45 // When in sandbox mode should we create events to simulate a live env.
46 // You can also use the :flags:/simulateEvents feature flag.46 // You can also use the :flags:/simulateEvents feature flag.
4747
=== modified file 'app/index.html'
--- app/index.html 2013-08-01 23:47:57 +0000
+++ app/index.html 2013-08-30 19:48:17 +0000
@@ -82,7 +82,7 @@
82 <div class="header">82 <div class="header">
83 <div class="error">83 <div class="error">
84 <span><i class="sprite alert_icon2"></i>84 <span><i class="sprite alert_icon2"></i>
85 <h4>Your browser is not fully supported</h4></span>85 <h4>Your browser is not supported</h4></span>
86 </div>86 </div>
87 </div>87 </div>
88 <p>88 <p>
@@ -213,8 +213,8 @@
213 };213 };
214214
215 isBrowserSupported = function(agent) {215 isBrowserSupported = function(agent) {
216 // At the moment Chrome and Firefox are supported.216 // Latest Chrome, Firefox, IE10 are supported
217 return (/Chrome|Firefox/.test(agent));217 return (/Chrome|Firefox|MSIE\ 10/.test(agent));
218 };218 };
219219
220 displayBrowserWarning = function() {220 displayBrowserWarning = function() {
221221
=== modified file 'app/models/charm.js'
--- app/models/charm.js 2013-08-21 16:02:10 +0000
+++ app/models/charm.js 2013-08-30 19:48:17 +0000
@@ -30,7 +30,7 @@
30 var RECENT_DAYS = 30;30 var RECENT_DAYS = 30;
3131
32 var models = Y.namespace('juju.models');32 var models = Y.namespace('juju.models');
33 var charmIdRe = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+)$/;33 var charmIdRe = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+|HEAD)$/;
34 var idElements = ['scheme', 'owner', 'series', 'package_name', 'revision'];34 var idElements = ['scheme', 'owner', 'series', 'package_name', 'revision'];
35 var simpleCharmIdRe = /^(?:(\w+):)?(?!:~)(\w+)$/;35 var simpleCharmIdRe = /^(?:(\w+):)?(?!:~)(\w+)$/;
36 var simpleIdElements = ['scheme', 'package_name'];36 var simpleIdElements = ['scheme', 'package_name'];
3737
=== modified file 'app/models/model-controller.js'
--- app/models/model-controller.js 2013-05-17 14:51:05 +0000
+++ app/models/model-controller.js 2013-08-30 19:48:17 +0000
@@ -82,7 +82,7 @@
82 if (charm && charm.loaded) {82 if (charm && charm.loaded) {
83 resolve(charm);83 resolve(charm);
84 } else {84 } else {
85 charm = db.charms.add({id: charmId}).load(env,85 charm = db.charms.add({url: charmId}).load(env,
86 // If views are bound to the charm model, firing "update" is86 // If views are bound to the charm model, firing "update" is
87 // unnecessary, and potentially even mildly harmful.87 // unnecessary, and potentially even mildly harmful.
88 function(err, data) {88 function(err, data) {
@@ -144,6 +144,7 @@
144 getServiceWithCharm: function(serviceId) {144 getServiceWithCharm: function(serviceId) {
145 var db = this.get('db'),145 var db = this.get('db'),
146 env = this.get('env'),146 env = this.get('env'),
147 store = this.get('store'),
147 mController = this;148 mController = this;
148149
149 return this._getPromise(150 return this._getPromise(
@@ -151,7 +152,21 @@
151 function(resolve, reject) {152 function(resolve, reject) {
152 mController.getService(serviceId).then(function(service) {153 mController.getService(serviceId).then(function(service) {
153 mController.getCharm(service.get('charm')).then(function(charm) {154 mController.getCharm(service.get('charm')).then(function(charm) {
154 resolve({service: service, charm: charm});155 // Check if a newer charm is available for this service so that
156 // we can offer it as an upgrade.
157 // XXX Makyo Aug. 20 - Remove feature flag when upgradecharm
158 // feature lands.
159 if (charm.get('scheme') === 'cs' &&
160 window.flags.upgradeCharm) {
161 store.promiseUpgradeAvailability(charm, db.charms)
162 .then(function(latestId) {
163 service.set('upgrade_available', !!latestId);
164 service.set('upgrade_to', latestId);
165 resolve({service: service, charm: charm});
166 }, reject);
167 } else {
168 resolve({service: service, charm: charm});
169 }
155 }, reject);170 }, reject);
156 }, reject);171 }, reject);
157 });172 });
@@ -160,6 +175,15 @@
160 }, {175 }, {
161 ATTRS: {176 ATTRS: {
162 /**177 /**
178 Reference to the client db.
179
180 @attribute db
181 @type {Y.Base}
182 @default undefined
183 */
184 db: {},
185
186 /**
163 Reference to the client env.187 Reference to the client env.
164188
165 @attribute env189 @attribute env
@@ -169,13 +193,13 @@
169 env: {},193 env: {},
170194
171 /**195 /**
172 Reference to the client db.196 Reference to the client charm store.
173197
174 @attribute db198 @attribute store
175 @type {Y.Base}199 @type {Charmworld2}
176 @default undefined200 @default undefined
177 */201 */
178 db: {}202 store: {}
179 }203 }
180 });204 });
181205
182206
=== modified file 'app/models/models.js'
--- app/models/models.js 2013-08-21 16:02:10 +0000
+++ app/models/models.js 2013-08-30 19:48:17 +0000
@@ -196,6 +196,25 @@
196 value: ALIVE196 value: ALIVE
197 },197 },
198 unit_count: {},198 unit_count: {},
199
200 /**
201 Whether or not an upgrade is available.
202
203 @attribute upgrade_available
204 @type {boolean}
205 @default false
206 */
207 upgrade_available: {
208 value: false
209 },
210
211 /**
212 The latest charm URL that the service can be upgraded to.
213
214 @attribute upgrade_to
215 @type {string}
216 */
217 upgrade_to: {},
199 aggregated_status: {}218 aggregated_status: {}
200 }219 }
201 });220 });
202221
=== modified file 'app/modules-debug.js'
--- app/modules-debug.js 2013-08-19 15:53:41 +0000
+++ app/modules-debug.js 2013-08-30 19:48:17 +0000
@@ -226,6 +226,9 @@
226 fullpath: '/juju-ui/views/topology/topology.js'226 fullpath: '/juju-ui/views/topology/topology.js'
227 },227 },
228228
229 'juju-view-bundle': {
230 fullpath: '/juju-ui/views/topology/bundle.js'
231 },
229 'juju-view-utils': {232 'juju-view-utils': {
230 fullpath: '/juju-ui/views/utils.js'233 fullpath: '/juju-ui/views/utils.js'
231 },234 },
232235
=== modified file 'app/store/charm.js'
--- app/store/charm.js 2013-08-21 16:02:10 +0000
+++ app/store/charm.js 2013-08-30 19:48:17 +0000
@@ -212,6 +212,31 @@
212 },212 },
213213
214 /**214 /**
215 Promises to return the latest charm ID for a given charm if a newer one
216 exists; this also caches the newer charm if one is available.
217
218 @method promiseUpgradeAvailability
219 @param {Charm} charm an existing charm potentially in need of an upgrade.
220 @param {ModelList} cache a local cache of browser charms.
221 @return {Promise} with an id or undefined.
222 */
223 promiseUpgradeAvailability: function(charm, cache) {
224 // Get the charm's store ID, then replace the version number
225 // with '-HEAD' to retrieve the latest version of the charm.
226 var storeId = charm.get('storeId').replace(/-\d+$/, '-HEAD');
227 return this.promiseCharm(storeId, cache)
228 .then(function(latest) {
229 var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10),
230 currentVersion = parseInt(charm.get('revision'), 10);
231 if (latestVersion > currentVersion) {
232 return latest.charm.id;
233 }
234 }, function(e) {
235 throw e;
236 });
237 },
238
239 /**
215 * Api call to search charms240 * Api call to search charms
216 *241 *
217 * @method search242 * @method search
218243
=== modified file 'app/store/env/env.js'
--- app/store/env/env.js 2013-05-17 14:51:05 +0000
+++ app/store/env/env.js 2013-08-30 19:48:17 +0000
@@ -26,8 +26,8 @@
2626
27YUI.add('juju-env', function(Y) {27YUI.add('juju-env', function(Y) {
2828
29 // Default to the Python environment.29 // Default to the Go environment.
30 var DEFAULT_BACKEND = 'python';30 var DEFAULT_BACKEND = 'go';
3131
32 /**32 /**
33 * Create and return a store environment suitable for connecting to the33 * Create and return a store environment suitable for connecting to the
3434
=== modified file 'app/store/env/fakebackend.js'
--- app/store/env/fakebackend.js 2013-08-13 16:22:03 +0000
+++ app/store/env/fakebackend.js 2013-08-30 19:48:17 +0000
@@ -368,11 +368,17 @@
368 throw e;368 throw e;
369 }369 }
370 }370 }
371
372 var constraints = {};
373 if (options.constraints) {
374 constraints = options.constraints;
375 }
376
371 var service = this.db.services.add({377 var service = this.db.services.add({
372 id: options.name,378 id: options.name,
373 name: options.name,379 name: options.name,
374 charm: charm.get('id'),380 charm: charm.get('id'),
375 constraints: {},381 constraints: constraints,
376 exposed: false,382 exposed: false,
377 subordinate: charm.get('is_subordinate'),383 subordinate: charm.get('is_subordinate'),
378 config: options.config384 config: options.config
379385
=== modified file 'app/store/env/go.js'
--- app/store/env/go.js 2013-08-13 18:25:46 +0000
+++ app/store/env/go.js 2013-08-30 19:48:17 +0000
@@ -119,6 +119,8 @@
119119
120 Y.extend(GoEnvironment, environments.BaseEnvironment, {120 Y.extend(GoEnvironment, environments.BaseEnvironment, {
121121
122 genericConstraints: ['cpu-power', 'cpu-cores', 'mem', 'arch'],
123
122 /**124 /**
123 * Go environment constructor.125 * Go environment constructor.
124 *126 *
@@ -273,7 +275,11 @@
273 */275 */
274 login: function() {276 login: function() {
275 // If the user is already authenticated there is nothing to do.277 // If the user is already authenticated there is nothing to do.
276 if (this.userIsAuthenticated || this.pendingLoginResponse) {278 if (this.userIsAuthenticated) {
279 this.fire('login', {data: {result: true}});
280 return;
281 }
282 if (this.pendingLoginResponse) {
277 return;283 return;
278 }284 }
279 var credentials = this.getCredentials();285 var credentials = this.getCredentials();
@@ -337,17 +343,31 @@
337 configuration options. Only one of `config` and `config_raw` should be343 configuration options. Only one of `config` and `config_raw` should be
338 provided, though `config_raw` takes precedence if it is given.344 provided, though `config_raw` takes precedence if it is given.
339 @param {Integer} num_units The number of units to be deployed.345 @param {Integer} num_units The number of units to be deployed.
346 @param {Object} constraints The machine constraints to use in the
347 object format key: value.
340 @param {Function} callback A callable that must be called once the348 @param {Function} callback A callable that must be called once the
341 operation is performed.349 operation is performed.
342 @return {undefined} Sends a message to the server only.350 @return {undefined} Sends a message to the server only.
343 */351 */
344 deploy: function(charm_url, service_name, config, config_raw, num_units,352 deploy: function(charm_url, service_name, config, config_raw, num_units,
345 callback) {353 constraints, callback) {
346 var intermediateCallback = null;354 var intermediateCallback = null;
347 if (callback) {355 if (callback) {
348 intermediateCallback = Y.bind(this.handleDeploy, this,356 intermediateCallback = Y.bind(this.handleDeploy, this,
349 callback, service_name, charm_url);357 callback, service_name, charm_url);
350 }358 }
359
360 if (constraints) {
361 // If the constraints is a function (this arg position used to be a
362 // callback) then log it out to the console to fix it.
363 if (typeof constraints === 'function') {
364 console.error('Constraints need to be an object not a function');
365 console.warn(constraints);
366 }
367 } else {
368 constraints = {};
369 }
370
351 this._send_rpc(371 this._send_rpc(
352 { Type: 'Client',372 { Type: 'Client',
353 Request: 'ServiceDeploy',373 Request: 'ServiceDeploy',
@@ -355,6 +375,7 @@
355 ServiceName: service_name,375 ServiceName: service_name,
356 Config: stringifyObjectValues(config),376 Config: stringifyObjectValues(config),
357 ConfigYAML: config_raw,377 ConfigYAML: config_raw,
378 Constraints: constraints,
358 CharmUrl: charm_url,379 CharmUrl: charm_url,
359 NumUnits: num_units380 NumUnits: num_units
360 }381 }
@@ -868,9 +889,6 @@
868 }, intermediateCallback);889 }, intermediateCallback);
869 },890 },
870891
871 // The constraints that the backend understands. Used to generate forms.
872 genericConstraints: ['cpu-power', 'cpu-cores', 'mem', 'arch'],
873
874 /**892 /**
875 Change the constraints of the given service.893 Change the constraints of the given service.
876894
877895
=== modified file 'app/store/env/python.js'
--- app/store/env/python.js 2013-06-24 14:56:21 +0000
+++ app/store/env/python.js 2013-08-30 19:48:17 +0000
@@ -255,13 +255,18 @@
255 * @return {undefined} Sends a message to the server only.255 * @return {undefined} Sends a message to the server only.
256 */256 */
257 deploy: function(charm_url, service_name, config, config_raw, num_units,257 deploy: function(charm_url, service_name, config, config_raw, num_units,
258 callback) {258 constraints, callback) {
259 if (!constraints) {
260 constraints = {};
261 }
262
259 this._send_rpc(263 this._send_rpc(
260 { op: 'deploy',264 { op: 'deploy',
261 service_name: service_name,265 service_name: service_name,
262 config: config,266 config: config,
263 config_raw: config_raw,267 config_raw: config_raw,
264 charm_url: charm_url,268 charm_url: charm_url,
269 constraints: constraints,
265 num_units: num_units},270 num_units: num_units},
266 callback, true);271 callback, true);
267 },272 },
268273
=== modified file 'app/store/env/sandbox.js'
--- app/store/env/sandbox.js 2013-08-14 19:15:01 +0000
+++ app/store/env/sandbox.js 2013-08-30 19:48:17 +0000
@@ -482,6 +482,7 @@
482 name: data.service_name,482 name: data.service_name,
483 config: data.config,483 config: data.config,
484 configYAML: data.config_raw,484 configYAML: data.config_raw,
485 constraints: data.constraints,
485 unitCount: data.num_units486 unitCount: data.num_units
486 });487 });
487 },488 },
@@ -784,7 +785,6 @@
784 @return {undefined} Nothing.785 @return {undefined} Nothing.
785 */786 */
786 receive: function(data) {787 receive: function(data) {
787 console.log('client message', data);
788 if (this.connected) {788 if (this.connected) {
789 var client = this.get('client');789 var client = this.get('client');
790 this['handle' + data.Type + data.Request](data,790 this['handle' + data.Type + data.Request](data,
@@ -995,10 +995,12 @@
995 var callback = Y.bind(function(result) {995 var callback = Y.bind(function(result) {
996 this._basicReceive(data, client, result);996 this._basicReceive(data, client, result);
997 }, this);997 }, this);
998
998 state.deploy(data.Params.CharmUrl, callback, {999 state.deploy(data.Params.CharmUrl, callback, {
999 name: data.Params.ServiceName,1000 name: data.Params.ServiceName,
1000 config: data.Params.Config,1001 config: data.Params.Config,
1001 configYAML: data.Params.ConfigYAML,1002 configYAML: data.Params.ConfigYAML,
1003 constraints: data.Params.Constraints,
1002 unitCount: data.Params.NumUnits1004 unitCount: data.Params.NumUnits
1003 });1005 });
1004 },1006 },
@@ -1300,6 +1302,7 @@
1300 'base',1302 'base',
1301 'js-yaml',1303 'js-yaml',
1302 'json-parse',1304 'json-parse',
1305 'juju-env-go',
1303 'timers'1306 'timers'
1304 ]1307 ]
1305});1308});
13061309
=== modified file 'app/subapps/browser/browser.js'
--- app/subapps/browser/browser.js 2013-08-15 15:40:05 +0000
+++ app/subapps/browser/browser.js 2013-08-30 19:48:17 +0000
@@ -51,8 +51,34 @@
51 _cleanOldViews: function(newViewMode) {51 _cleanOldViews: function(newViewMode) {
52 if (this._hasStateChanged('viewmode') && this._oldState.viewmode) {52 if (this._hasStateChanged('viewmode') && this._oldState.viewmode) {
53 var viewAttr = '_' + this._oldState.viewmode;53 var viewAttr = '_' + this._oldState.viewmode;
54 this[viewAttr].destroy();54 if (this[viewAttr]) {
55 delete this[viewAttr];55 this[viewAttr].destroy();
56 delete this[viewAttr];
57 }
58 }
59 },
60
61 /**
62 * Destroy and remove any lingering views.
63 *
64 * Make sure they don't linger and hold UX bound events on us when they
65 * should be gone.
66 *
67 * @method _clearViews
68 *
69 */
70 _clearViews: function() {
71 if (this._sidebar) {
72 this._sidebar.destroy();
73 delete this._sidebar;
74 }
75 if (this._minimized) {
76 this._minimized.destroy();
77 delete this._minimized;
78 }
79 if (this._fullscreen) {
80 this._fullscreen.destroy();
81 delete this._fullscreen;
56 }82 }
57 },83 },
5884
@@ -88,7 +114,6 @@
88 */114 */
89 _getStateUrl: function(change) {115 _getStateUrl: function(change) {
90 var urlParts = [];116 var urlParts = [];
91 this._oldState = this._viewState;
92117
93 // If there are changes to the filters, we need to update our filter118 // If there are changes to the filters, we need to update our filter
94 // object first, and then generate a new query string for the state to119 // object first, and then generate a new query string for the state to
@@ -485,6 +510,7 @@
485 viewmode: null510 viewmode: null
486 };511 };
487 this._viewState = Y.merge(this._oldState, {});512 this._viewState = Y.merge(this._oldState, {});
513 this._clearViews();
488 },514 },
489515
490 /**516 /**
@@ -576,17 +602,6 @@
576 // Add any sidebar charms to the running cache.602 // Add any sidebar charms to the running cache.
577 this._cache = Y.merge(this._cache, ev.cache);603 this._cache = Y.merge(this._cache, ev.cache);
578 }, this);604 }, this);
579 this._editorial.on(this._editorial.EV_CATEGORY_LINK_CLICKED,
580 function(ev) {
581 var change = {
582 search: true,
583 filter: {
584 categories: [ev.category]
585 }
586 };
587 this.fire('viewNavigate', {change: change});
588 });
589
590 this._editorial.render(this._cache.interesting);605 this._editorial.render(this._cache.interesting);
591 this._editorial.addTarget(this);606 this._editorial.addTarget(this);
592 },607 },
@@ -644,7 +659,12 @@
644 // If we've switched to viewmode fullscreen, we need to render it.659 // If we've switched to viewmode fullscreen, we need to render it.
645 // We know the viewmode is already fullscreen because we're in this660 // We know the viewmode is already fullscreen because we're in this
646 // function.661 // function.
647 if (this._hasStateChanged('viewmode')) {662 var forceFullscreen = false;
663 if (!this._fullscreen) {
664 forceFullscreen = true;
665 }
666
667 if (this._hasStateChanged('viewmode') || forceFullscreen) {
648 var extraCfg = {};668 var extraCfg = {};
649 if (this._viewState.search || this._viewState.charmID) {669 if (this._viewState.search || this._viewState.charmID) {
650 extraCfg.withHome = true;670 extraCfg.withHome = true;
@@ -730,8 +750,14 @@
730 @param {function} next callable for the next route in the chain.750 @param {function} next callable for the next route in the chain.
731 */751 */
732 sidebar: function(req, res, next) {752 sidebar: function(req, res, next) {
753 // If we've gone from no _sidebar to having one, then force editorial to
754 // render.
755 var forceSidebar = false;
756 if (!this._sidebar) {
757 forceSidebar = true;
758 }
733 // If we've switched to viewmode sidebar, we need to render it.759 // If we've switched to viewmode sidebar, we need to render it.
734 if (this._hasStateChanged('viewmode')) {760 if (this._hasStateChanged('viewmode') || forceSidebar) {
735 this._sidebar = new views.Sidebar(761 this._sidebar = new views.Sidebar(
736 this._getViewCfg({762 this._getViewCfg({
737 container: this.get('container')763 container: this.get('container')
@@ -759,9 +785,7 @@
759 }785 }
760786
761 this.renderSearchResults(req, res, next);787 this.renderSearchResults(req, res, next);
762 }788 } else if (this._shouldShowEditorial() || forceSidebar) {
763
764 if (this._shouldShowEditorial()) {
765 // Showing editorial implies that other sidebar content is destroyed.789 // Showing editorial implies that other sidebar content is destroyed.
766 if (this._search) {790 if (this._search) {
767 this._search.destroy();791 this._search.destroy();
@@ -770,6 +794,7 @@
770 this.renderEditorial(req, res, next);794 this.renderEditorial(req, res, next);
771 }795 }
772796
797
773 // If we've changed the charmID or the viewmode has changed and we have798 // If we've changed the charmID or the viewmode has changed and we have
774 // a charmID, render charmDetails.799 // a charmID, render charmDetails.
775 if (this._shouldShowCharm()) {800 if (this._shouldShowCharm()) {
@@ -863,6 +888,8 @@
863 this[viewmode](req, res, next);888 this[viewmode](req, res, next);
864 }889 }
865 } else {890 } else {
891 // Update the app state even though we're not showing anything.
892 this._saveState();
866 // Let the next route go on.893 // Let the next route go on.
867 next();894 next();
868 }895 }
@@ -920,6 +947,8 @@
920 if (!this.hidden) {947 if (!this.hidden) {
921 this[viewmode](req, res, next);948 this[viewmode](req, res, next);
922 } else {949 } else {
950 // Update the app state even though we're not showing anything.
951 this._saveState();
923 // Let the next route go on.952 // Let the next route go on.
924 next();953 next();
925 }954 }
@@ -965,6 +994,8 @@
965 if (!this.hidden) {994 if (!this.hidden) {
966 this[req.params.viewmode](req, res, next);995 this[req.params.viewmode](req, res, next);
967 } else {996 } else {
997 // Update the app state even though we're not showing anything.
998 this._saveState();
968 // Let the next route go on.999 // Let the next route go on.
969 next();1000 next();
970 }1001 }
@@ -992,6 +1023,7 @@
992 if (this.hidden) {1023 if (this.hidden) {
993 browser.hide();1024 browser.hide();
994 minview.hide();1025 minview.hide();
1026 this._clearViews();
995 } else {1027 } else {
996 if (this._viewState.viewmode === 'minimized') {1028 if (this._viewState.viewmode === 'minimized') {
997 minview.show();1029 minview.show();
9981030
=== modified file 'app/subapps/browser/templates/editorial.handlebars'
--- app/subapps/browser/templates/editorial.handlebars 2013-07-17 18:27:38 +0000
+++ app/subapps/browser/templates/editorial.handlebars 2013-08-30 19:48:17 +0000
@@ -21,7 +21,6 @@
21 <div class="featured"></div>21 <div class="featured"></div>
22 <div class="popular"></div>22 <div class="popular"></div>
23 <div class="new"></div>23 <div class="new"></div>
24 {{> category-icons }}
2524
26 {{#if isFullscreen}}25 {{#if isFullscreen}}
27 </div>26 </div>
2827
=== modified file 'app/subapps/browser/views/editorial.js'
--- app/subapps/browser/views/editorial.js 2013-07-12 16:46:15 +0000
+++ app/subapps/browser/views/editorial.js 2013-08-30 19:48:17 +0000
@@ -41,7 +41,6 @@
41 */41 */
42 ns.EditorialView = Y.Base.create('browser-view-sidebar', ns.CharmResults, [],42 ns.EditorialView = Y.Base.create('browser-view-sidebar', ns.CharmResults, [],
43 {43 {
44 EV_CATEGORY_LINK_CLICKED: 'category-link-clicked',
45 template: views.Templates.editorial,44 template: views.Templates.editorial,
4645
47 // How many of each charm container do we show by default.46 // How many of each charm container do we show by default.
@@ -69,33 +68,6 @@
69 },68 },
7069
71 /**70 /**
72 Binds clicks on the category links in the editorial view and fires
73 that information to any listeners.
74
75 @private
76 @method _bindCategoryLinks
77 */
78 _bindCategoryLinks: function() {
79 var categories = Y.one('#category-links');
80 if (categories) {
81 categories.delegate('click', function(ev) {
82 // A link has been clicked, we need to kill the navigation
83 // event.
84 ev.halt();
85 var category = ev.currentTarget.getData('link');
86 var change = {
87 search: true,
88 filter: {
89 categories: [category],
90 replace: true
91 }
92 };
93 this.fire('viewNavigate', {change: change});
94 }, 'a', this);
95 }
96 },
97
98 /**
99 Renders the editorial, "interesting" data to the view.71 Renders the editorial, "interesting" data to the view.
10072
101 @private73 @private
@@ -195,9 +167,6 @@
195 cache.charms.add(popularCharms);167 cache.charms.add(popularCharms);
196 cache.charms.add(featuredCharms);168 cache.charms.add(featuredCharms);
197 this.fire(this.EV_CACHE_UPDATED, {cache: cache});169 this.fire(this.EV_CACHE_UPDATED, {cache: cache});
198
199 // Bind the category links, which now exist
200 this._bindCategoryLinks();
201 },170 },
202171
203 /**172 /**
204173
=== modified file 'app/subapps/browser/views/minimized.js'
--- app/subapps/browser/views/minimized.js 2013-08-02 15:34:18 +0000
+++ app/subapps/browser/views/minimized.js 2013-08-30 19:48:17 +0000
@@ -29,7 +29,6 @@
29YUI.add('subapp-browser-minimized', function(Y) {29YUI.add('subapp-browser-minimized', function(Y) {
30 var ns = Y.namespace('juju.browser.views'),30 var ns = Y.namespace('juju.browser.views'),
31 views = Y.namespace('juju.views');31 views = Y.namespace('juju.views');
32
33 /**32 /**
34 * The minimized state view.33 * The minimized state view.
35 *34 *
@@ -37,7 +36,9 @@
37 * @extends {Y.View}36 * @extends {Y.View}
38 *37 *
39 */38 */
40 ns.MinimizedView = Y.Base.create('browser-view-minimized', Y.View, [], {39 ns.MinimizedView = Y.Base.create('browser-view-minimized', Y.View, [
40 Y.juju.widgets.ViewmodeControlsViewExtension
41 ], {
41 template: views.Templates.minimized,42 template: views.Templates.minimized,
4243
43 events: {44 events: {
@@ -47,15 +48,13 @@
47 },48 },
4849
49 /**50 /**
50 * Toggle the visibility of the browser. Bound to nav controls in the51 * Toggle the visibility of the browser.
51 * view, however this will be expanded to be controlled from the new
52 * constant nav menu outside of the view once it's completed.
53 *52 *
54 * @method _toggle_sidebar53 * @method _toggleMinimized
55 * @param {Event} ev event to trigger the toggle.54 * @param {Event} ev event to trigger the toggle.
56 *55 *
57 */56 */
58 _toggleViewState: function(ev) {57 _toggleMinimized: function(ev) {
59 ev.halt();58 ev.halt();
6059
61 this.get('container').hide();60 this.get('container').hide();
@@ -88,6 +87,13 @@
88 var tpl = this.template(),87 var tpl = this.template(),
89 tplNode = Y.Node.create(tpl);88 tplNode = Y.Node.create(tpl);
90 this.get('container').setHTML(tplNode);89 this.get('container').setHTML(tplNode);
90 // Make sure the controls starts out setting the correct active state
91 // based on the current viewmode for our View.
92 this.controls = new Y.juju.widgets.ViewmodeControls({
93 currentViewmode: this.get('oldViewMode')
94 });
95 this.controls.render();
96 this._bindViewmodeControls(this.controls);
91 }97 }
9298
93 }, {99 }, {
@@ -116,6 +122,7 @@
116 'base',122 'base',
117 'juju-templates',123 'juju-templates',
118 'juju-views',124 'juju-views',
119 'view'125 'view',
126 'viewmode-controls'
120 ]127 ]
121});128});
122129
=== modified file 'app/subapps/browser/views/view.js'
--- app/subapps/browser/views/view.js 2013-08-08 15:17:01 +0000
+++ app/subapps/browser/views/view.js 2013-08-30 19:48:17 +0000
@@ -40,7 +40,8 @@
40 *40 *
41 */41 */
42 ns.MainView = Y.Base.create('browser-view-mainview', Y.View, [42 ns.MainView = Y.Base.create('browser-view-mainview', Y.View, [
43 Y.Event.EventTracker43 Y.Event.EventTracker,
44 Y.juju.widgets.ViewmodeControlsViewExtension
44 ], {45 ], {
4546
46 /**47 /**
@@ -85,20 +86,6 @@
85 */86 */
86 _bindSearchWidgetEvents: function() {87 _bindSearchWidgetEvents: function() {
87 var container = this.get('container');88 var container = this.get('container');
88 this.addEvent(
89 this.controls.on(
90 this.controls.EVT_TOGGLE_VIEWABLE, this._toggleBrowser, this)
91 );
92
93 this.addEvent(
94 this.controls.on(
95 this.controls.EVT_FULLSCREEN, this._goFullscreen, this)
96 );
97 this.addEvent(
98 this.controls.on(
99 this.controls.EVT_SIDEBAR, this._goSidebar, this)
100 );
101
102 if (this.search) {89 if (this.search) {
103 this.addEvent(90 this.addEvent(
104 this.search.on(91 this.search.on(
@@ -106,7 +93,6 @@
106 );93 );
107 }94 }
10895
109
110 if (this.search) {96 if (this.search) {
111 this.addEvent(97 this.addEvent(
112 this.search.on(98 this.search.on(
@@ -137,6 +123,7 @@
137 }123 }
138 }, this);124 }, this);
139 }125 }
126 this._bindViewmodeControls(this.controls);
140 },127 },
141128
142 /**129 /**
@@ -233,11 +220,13 @@
233 view, however this will be expanded to be controlled from the new220 view, however this will be expanded to be controlled from the new
234 constant nav menu outside of the view once it's completed.221 constant nav menu outside of the view once it's completed.
235222
236 @method _toggle_sidebar223 This is called by the ViewmodeControlsViewExtension.
224
225 @method _toggleMinimized
237 @param {Event} ev event to trigger the toggle.226 @param {Event} ev event to trigger the toggle.
238227
239 */228 */
240 _toggleBrowser: function(ev) {229 _toggleMinimized: function(ev) {
241 ev.halt();230 ev.halt();
242231
243 this.fire('viewNavigate', {232 this.fire('viewNavigate', {
@@ -248,44 +237,9 @@
248 },237 },
249238
250 /**239 /**
251 Upon clicking the browser icon make sure we re-route to the
252 new form of the UX.
253
254 @method _goFullscreen
255 @param {Event} ev the click event handler on the button.
256
257 */
258 _goFullscreen: function(ev) {
259 ev.halt();
260 this.fire('viewNavigate', {
261 change: {
262 viewmode: 'fullscreen'
263 }
264 });
265 },
266
267 /**
268 Upon clicking the build icon make sure we re-route to the
269 new form of the UX.
270
271 @method _goSidebar
272 @param {Event} ev the click event handler on the button.
273
274 */
275 _goSidebar: function(ev) {
276 ev.halt();
277 this.fire('viewNavigate', {
278 change: {
279 viewmode: 'sidebar'
280 }
281 });
282 },
283
284 /**
285 * Destroy this view and clear from the dom world.240 * Destroy this view and clear from the dom world.
286 *241 *
287 * @method destructor242 * @method destructor
288 *
289 */243 */
290 destructor: function() {244 destructor: function() {
291 // Clean up any details view we might have hanging around.245 // Clean up any details view we might have hanging around.
292246
=== removed file 'app/templates/category-icons.partial'
--- app/templates/category-icons.partial 2013-07-10 08:10:08 +0000
+++ app/templates/category-icons.partial 1970-01-01 00:00:00 +0000
@@ -1,57 +0,0 @@
1<div id="category-links" class="categories">
2 {{#unless isFullscreen}}
3 <h3 class="section-title">
4 Browse by category
5 </h3>
6 {{/unless}}
7 <ul>
8 <li>
9 <a data-link="databases" href="">
10 <img
11 src="/juju-ui/assets/images/non-sprites/category_icons/category-database.svg"
12 alt="Databases" />
13 <span>Databases</span>
14 </a>
15 </li>
16 <li>
17 <a data-link="file-servers" href="">
18 <img
19 src="/juju-ui/assets/images/non-sprites/category_icons/category-file-server.svg"
20 alt="File Servers" />
21 <span>File Servers</span>
22 </a>
23 </li>
24 <li>
25 <a data-link="applications" href="">
26 <img
27 src="/juju-ui/assets/images/non-sprites/category_icons/category-application.svg"
28 alt="Applications" />
29 <span>Applications</span>
30 </a>
31 </li>
32 <li>
33 <a data-link="cache-proxy" href="">
34 <img
35 src="/juju-ui/assets/images/non-sprites/category_icons/category-cache-proxy.svg"
36 alt="Cache/Proxy" />
37 <span>Cache/Proxy</span>
38 </a>
39 </li>
40 <li>
41 <a data-link="app-servers" href="">
42 <img
43 src="/juju-ui/assets/images/non-sprites/category_icons/category-app-server.svg"
44 alt="App Servers" />
45 <span>App Servers</span>
46 </a>
47 </li>
48 <li>
49 <a data-link="misc" href="">
50 <img
51 src="/juju-ui/assets/images/non-sprites/category_icons/category-misc.svg"
52 alt="Miscellaneous" />
53 <span>Miscellaneous</span>
54 </a>
55 </li>
56 </ul>
57</div>
580
=== modified file 'app/templates/ghost-config-viewlet.handlebars'
--- app/templates/ghost-config-viewlet.handlebars 2013-08-08 00:06:48 +0000
+++ app/templates/ghost-config-viewlet.handlebars 2013-08-30 19:48:17 +0000
@@ -8,6 +8,17 @@
8 </div>8 </div>
9 </div>9 </div>
10 {{/unless}}10 {{/unless}}
11
12 {{#if constraints}}
13 <!-- Service Constraints added in -->
14 <div class="ghost-config-wrapper service-constraints">
15 <div class="ghost-config-header">Constraints</div>
16 <div class="ghost-config-content use-defaults">
17 {{> service-constraints-viewlet}}
18 </div>
19 </div>
20 {{/if}}
21
11 {{#if settings}}22 {{#if settings}}
12 <!-- Service configuration form -->23 <!-- Service configuration form -->
13 <div class="ghost-config-wrapper service-configuration">24 <div class="ghost-config-wrapper service-configuration">
@@ -35,4 +46,5 @@
35 </div>46 </div>
36 </div>47 </div>
37 {{/if}}48 {{/if}}
49
38</div>50</div>
3951
=== modified file 'app/templates/inspector-header.handlebars'
--- app/templates/inspector-header.handlebars 2013-07-25 19:15:42 +0000
+++ app/templates/inspector-header.handlebars 2013-08-30 19:48:17 +0000
@@ -1,6 +1,8 @@
1<header>1<header>
2 <div class="service-charm">2 <div class="service-charm">
3 <div class="charm-icon" data-bind="icon"></div>3 <div class="icon">
4 <img src="{{icon}}" alt="{{name}} icon" class="icon">
5 </div>
4 <div class="details-wrapper">6 <div class="details-wrapper">
5 {{#if ghost}}7 {{#if ghost}}
6 <input type="text" class="service-name config-field" name="service-name" value="{{package_name}}"><br />8 <input type="text" class="service-name config-field" name="service-name" value="{{package_name}}"><br />
79
=== renamed file 'app/templates/viewlet-manager.handlebars' => 'app/templates/service-config-wrapper.handlebars'
=== modified file 'app/templates/service-constraints-viewlet.partial'
--- app/templates/service-constraints-viewlet.partial 2013-08-05 02:08:41 +0000
+++ app/templates/service-constraints-viewlet.partial 2013-08-30 19:48:17 +0000
@@ -1,6 +1,6 @@
1{{#constraints}}1{{#constraints}}
2 <div class="control-group settings-wrapper">2 <div class="settings-wrapper">
3 <div class="control-label" for="{{name}}">{{title}}</div>3 <label for="{{name}}">{{title}}</label>
4 <div>4 <div>
5 <input class="constraint-field" type="text" name="{{name}}"5 <input class="constraint-field" type="text" name="{{name}}"
6 value="{{value}}" data-bind="constraints.{{name}}" />6 value="{{value}}" data-bind="constraints.{{name}}" />
77
=== modified file 'app/templates/service-overview-constraints.handlebars'
--- app/templates/service-overview-constraints.handlebars 2013-08-20 15:27:44 +0000
+++ app/templates/service-overview-constraints.handlebars 2013-08-30 19:48:17 +0000
@@ -1,25 +1,30 @@
1<span>Scale up with the following constraints?</span>1<span>Scale up with the following constraints?</span>
2<span class="constraint-details">2<span class="constraint-details hide-on-edit">
3 {{#if cpu}}3 {{#srvConstraints}}
4 {{cpu}}Ghz4 {{#if cpu}}
5 {{else}}5 {{cpu}}Ghz
6 Default CPU6 {{else}}
7 {{/if}}7 Default CPU
8 &nbsp;8 {{/if}}
9 {{#if mem}}9 &nbsp;
10 {{mem}}GB10 {{#if mem}}
11 {{else}}11 {{mem}}GB
12 Default Mem12 {{else}}
13 {{/if}}13 Default Mem
14 &nbsp;14 {{/if}}
15 {{#if arch}}15 &nbsp;
16 {{arch}}16 {{#if arch}}
17 {{else}}17 {{arch}}
18 Default Arch18 {{else}}
19 {{/if}}19 Default Arch
20 {{/if}}
21 {{/srvConstraints}}
20</span>22</span>
21<div class="edit-constraints-wrapper">23<div class="edit-constraints-wrapper">
22 <a class="edit-constraints">Edit</a>24 <a class="edit-constraints hide-on-edit">Edit</a>
25 <div class="editable-constraints" style="display: none;">
26 {{> service-constraints-viewlet}}
27 </div>
23</div>28</div>
24<div class="overview-constraints"></div>29<div class="overview-constraints"></div>
25<div class="inspector-buttons">30<div class="inspector-buttons">
2631
=== modified file 'app/views/charm-panel.js'
--- app/views/charm-panel.js 2013-08-14 15:52:29 +0000
+++ app/views/charm-panel.js 2013-08-30 19:48:17 +0000
@@ -632,7 +632,7 @@
632 }632 }
633 numUnits = charm.get('is_subordinate') ? 0 : parseInt(numUnits, 10);633 numUnits = charm.get('is_subordinate') ? 0 : parseInt(numUnits, 10);
634 env.deploy(url, serviceName, config, this.configFileContent,634 env.deploy(url, serviceName, config, this.configFileContent,
635 numUnits, function(ev) {635 numUnits, null, function(ev) {
636 if (ev.err) {636 if (ev.err) {
637 console.log(url + ' deployment failed', ev.err);637 console.log(url + ' deployment failed', ev.err);
638 db.notifications.add(638 db.notifications.add(
639639
=== modified file 'app/views/charm.js'
--- app/views/charm.js 2013-08-09 20:52:35 +0000
+++ app/views/charm.js 2013-08-30 19:48:17 +0000
@@ -153,6 +153,9 @@
153 charmId,153 charmId,
154 serviceName,154 serviceName,
155 config,155 config,
156 null,
157 null,
158 null,
156 Y.bind(this._deployCallback, this)159 Y.bind(this._deployCallback, this)
157 );160 );
158 },161 },
159162
=== modified file 'app/views/environment.js'
--- app/views/environment.js 2013-08-21 15:47:36 +0000
+++ app/views/environment.js 2013-08-30 19:48:17 +0000
@@ -194,7 +194,7 @@
194 },194 },
195 '.cancel-num-units': { click: '_closeUnitConfirm'},195 '.cancel-num-units': { click: '_closeUnitConfirm'},
196 '.confirm-num-units': { click: '_confirmUnitChange'},196 '.confirm-num-units': { click: '_confirmUnitChange'},
197 'a.edit-constraints': { click: '_editUnitConstraints'},197 'a.edit-constraints': { click: '_showEditUnitConstraints'},
198 // Settings viewlet.198 // Settings viewlet.
199 'input.expose-toggle': { click: 'toggleExpose' },199 'input.expose-toggle': { click: 'toggleExpose' },
200 '.config-file .fakebutton': { click: 'handleFileClick'},200 '.config-file .fakebutton': { click: 'handleFileClick'},
@@ -216,7 +216,7 @@
216 'unitDetails',216 'unitDetails',
217 'inspectorHeader'217 'inspectorHeader'
218 ],218 ],
219 template: Y.juju.views.Templates['viewlet-manager']219 template: Y.juju.views.Templates['service-config-wrapper']
220 },220 },
221 configGhost: {221 configGhost: {
222 // controller will show the first one in this array by default222 // controller will show the first one in this array by default
223223
=== modified file 'app/views/ghost-inspector.js'
--- app/views/ghost-inspector.js 2013-08-14 15:31:50 +0000
+++ app/views/ghost-inspector.js 2013-08-30 19:48:17 +0000
@@ -121,13 +121,22 @@
121 container, '.service-config .config-field');121 container, '.service-config .config-field');
122 }122 }
123123
124 // Deploy needs constraints in simple key:value object.
125 var constraints = utils.getElementsValuesMapping(
126 container, '.constraint-field');
127
124 options.env.deploy(128 options.env.deploy(
125 model.get('id'),129 model.get('id'),
126 serviceName,130 serviceName,
127 config,131 config,
128 this.viewletManager.configFileContent,132 this.viewletManager.configFileContent,
129 numUnits,133 numUnits,
130 Y.bind(this._deployCallbackHandler, this, serviceName, config));134 constraints,
135 Y.bind(this._deployCallbackHandler,
136 this,
137 serviceName,
138 config,
139 constraints));
131 },140 },
132141
133 /**142 /**
@@ -225,10 +234,10 @@
225234
226 @method _deployCallbackHandler235 @method _deployCallbackHandler
227 @param {String} serviceName The service name.236 @param {String} serviceName The service name.
228 @param {Object} config The configuration oject of the service.237 @param {Object} config The configuration object of the service.
229 @param {Y.EventFacade} e The event facade from the deploy event.238 @param {Y.EventFacade} e The event facade from the deploy event.
230 */239 */
231 _deployCallbackHandler: function(serviceName, config, e) {240 _deployCallbackHandler: function(serviceName, config, constraints, e) {
232 var options = this.options,241 var options = this.options,
233 db = options.db,242 db = options.db,
234 ghostService = options.ghostService;243 ghostService = options.ghostService;
@@ -283,7 +292,8 @@
283 id: serviceName,292 id: serviceName,
284 pending: false,293 pending: false,
285 loading: false,294 loading: false,
286 config: config295 config: config,
296 constraints: constraints
287 });297 });
288298
289 this.closeInspector();299 this.closeInspector();
@@ -291,4 +301,7 @@
291301
292 };302 };
293303
304}, '0.1.0', {
305 requires: [
306 ]
294});307});
295308
=== modified file 'app/views/inspector.js'
--- app/views/inspector.js 2013-08-16 20:29:39 +0000
+++ app/views/inspector.js 2013-08-30 19:48:17 +0000
@@ -123,10 +123,14 @@
123 */123 */
124 _confirmUnitConstraints: function(requestedUnitCount) {124 _confirmUnitConstraints: function(requestedUnitCount) {
125 var container = this.viewletManager.viewlets.overview.container,125 var container = this.viewletManager.viewlets.overview.container,
126 genericConstraints = this.options.env.genericConstraints,
126 confirm = container.one('.unit-constraints-confirm'),127 confirm = container.one('.unit-constraints-confirm'),
127 constraints = this.model.get('constraints') || {};128 srvConstraints = this.model.get('constraints') || {};
128129
129 confirm.setHTML(Templates['service-overview-constraints'](constraints));130 confirm.setHTML(Templates['service-overview-constraints']({
131 srvConstraints: srvConstraints,
132 constraints: utils.getConstraints(srvConstraints, genericConstraints)
133 }));
130 confirm.removeClass('closed');134 confirm.removeClass('closed');
131 },135 },
132136
@@ -145,7 +149,10 @@
145 this.resetUnits();149 this.resetUnits();
146 }150 }
147151
152 // editing class added if the user clicked 'edit'
153 confirm.removeClass('editing');
148 confirm.addClass('closed');154 confirm.addClass('closed');
155 this.overviewConstraintsEdit = false;
149 },156 },
150157
151 /**158 /**
@@ -156,8 +163,19 @@
156 */163 */
157 _confirmUnitChange: function(e) {164 _confirmUnitChange: function(e) {
158 e.halt();165 e.halt();
159 var container = this.viewletManager.viewlets.overview.container;166 var container = this.viewletManager.viewlets.overview.container,
160 this._modifyUnits(container.one('input.num-units-control').get('value'));167 unitCount = container.one('input.num-units-control').get('value'),
168 service = this.model;
169
170 // If the user chose to edit the constraints
171 if (this.overviewConstraintsEdit) {
172 var constraints = utils.getElementsValuesMapping(
173 container, '.constraint-field');
174 var cb = Y.bind(this._modifyUnits, this, unitCount);
175 this.options.env.set_constraints(service.get('id'), constraints, cb);
176 } else {
177 this._modifyUnits(unitCount);
178 }
161 this._closeUnitConfirm();179 this._closeUnitConfirm();
162 },180 },
163181
@@ -165,11 +183,15 @@
165 Shows the unit constraints when the user wants to edit them183 Shows the unit constraints when the user wants to edit them
166 while increasing the total number of units184 while increasing the total number of units
167185
168 @method _editUnitConstraints186 @method _showEditUnitConstraints
169 */187 */
170 _editUnitConstraints: function() {188 _showEditUnitConstraints: function(e) {
171 // show constraints viewlet on overview page to allow the user to189 e.halt();
172 // edit them without changing viewlets.190 var container = this.viewletManager.viewlets.overview.container;
191 container.all('.hide-on-edit').hide();
192 container.one('.editable-constraints').show();
193 container.one('.unit-constraints-confirm').addClass('editing');
194 this.overviewConstraintsEdit = true;
173 },195 },
174196
175 _modifyUnits: function(requested_unit_count) {197 _modifyUnits: function(requested_unit_count) {
@@ -181,6 +203,7 @@
181 container = this.get('container');203 container = this.get('container');
182 env = this.get('env');204 env = this.get('env');
183 }205 }
206
184 var service = this.model || this.get('model');207 var service = this.model || this.get('model');
185 var unit_count = service.get('unit_count');208 var unit_count = service.get('unit_count');
186 var field = container.one('.num-units-control');209 var field = container.one('.num-units-control');
187210
=== added file 'app/views/topology/bundle.js'
--- app/views/topology/bundle.js 1970-01-01 00:00:00 +0000
+++ app/views/topology/bundle.js 2013-08-30 19:48:17 +0000
@@ -0,0 +1,368 @@
1/*
2This file is part of the Juju GUI, which lets users view and manage Juju
3environments within a graphical interface (https://launchpad.net/juju-gui).
4Copyright (C) 2012-2013 Canonical Ltd.
5
6This program is free software: you can redistribute it and/or modify it under
7the terms of the GNU Affero General Public License version 3, as published by
8the Free Software Foundation.
9
10This program is distributed in the hope that it will be useful, but WITHOUT
11ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
13General Public License for more details.
14
15You should have received a copy of the GNU Affero General Public License along
16with this program. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19'use strict';
20
21/**
22 * Provide the BundleTopology class.
23 *
24 * @module views
25 * @submodule views.BundleTopology
26 */
27
28YUI.add('juju-view-bundle', function(Y) {
29
30 var juju = Y.namespace('juju'),
31 views = Y.namespace('juju.views'),
32 utils = Y.namespace('juju.views.utils'),
33 models = Y.namespace('juju.models'),
34 d3ns = Y.namespace('d3'),
35 topoUtils = Y.namespace('juju.topology.utils');
36
37 /**
38 Manage service rendering and events.
39
40 @class BundleModule
41 */
42
43 var BundleModule = Y.Base.create('BundleModule', d3ns.Module,
44 [views.ServiceModuleCommon], {
45
46 /**
47 Attempt to reuse as much of the existing graph and view models
48 as possible to re-render the graph.
49
50 @method update
51 */
52 update: function() {
53 var self = this,
54 topo = this.get('component'),
55 width = topo.get('width'),
56 height = topo.get('height');
57
58 // Process any changed data.
59 this.updateData();
60
61 // Generate a node for each service, draw it as a rect with
62 // labels for service and charm.
63 var node = this.node;
64
65 // enter
66 node
67 .enter().append('g')
68 .attr({
69 'class': function(d) {
70 return (d.subordinate ? 'subordinate ' : '') +
71 (d.pending ? 'pending ' : '') + 'service';
72 },
73 'transform': function(d) { return d.translateStr;}})
74 .call(self.createServiceNode, self);
75
76 // Update all nodes.
77 self.updateServiceNodes(node);
78 },
79
80 /**
81 Fill a service node with empty structures that will be filled out
82 in the update stage.
83
84 @param {object} node the node to construct.
85 @param {object} self reference to the view instance.
86 @return {null} side effects only.
87 @method createServiceNode
88 */
89 createServiceNode: function(node, self) {
90 node.append('image')
91 .classed('service-icon', true)
92 .attr({
93 'xlink:href': function(d) {
94 return d.icon;
95 },
96 width: 96,
97 height: 96
98 });
99 node.append('text').append('tspan')
100 .attr('class', 'name')
101 .text(function(d) {return d.displayName; });
102 },
103
104 /**
105 Fill the empty structures within a service node such that they
106 match the db.
107
108 @param {object} node the collection of nodes to update.
109 @return {null} side effects only.
110 @method updateServiceNodes
111 */
112 updateServiceNodes: function(node) {
113 if (node.empty()) {
114 return;
115 }
116 var self = this,
117 topo = this.get('component'),
118 landscape = topo.get('landscape');
119
120 // Apply Position Annotations
121 // This is done after the services_boxes
122 // binding as the event handler will
123 // use that index.
124 node.each(function(d) {
125 var service = d.model,
126 annotations = service.get('annotations'),
127 x, y;
128
129 if (!annotations) {
130 return;
131 }
132
133 // If there are x/y annotations on the service model and they are
134 // different from the node's current x/y coordinates, update the
135 // node, as the annotations may have been set in another session.
136 x = annotations['gui-x'];
137 y = annotations['gui-y'];
138 if (!d ||
139 (x !== undefined && x !== d.x) ||
140 (y !== undefined && y !== d.y)) {
141 d.x = x;
142 d.y = y;
143 d3.select(this).attr({
144 x: x,
145 y: y,
146 transform: d.translateStr});
147
148 }});
149
150 // Mark subordinates as such. This is needed for when a new service
151 // is created.
152 node.filter(function(d) {
153 return d.subordinate;
154 }).classed('subordinate', true);
155
156 // Size the node for drawing.
157 node.attr({
158 'width': function(box) { box.w = 96; return box.w;},
159 'height': function(box) { box.h = 96; return box.h;}
160 });
161
162 // Draw a subordinate relation indicator.
163 var subRelationIndicator = node.filter(function(d) {
164 return d.subordinate &&
165 d3.select(this)
166 .select('.sub-rel-block').empty();
167 })
168 .append('g')
169 .attr('class', 'sub-rel-block')
170 .attr('transform', function(d) {
171 // Position the block so that the relation indicator will
172 // appear at the right connector.
173 return 'translate(' + [d.w, d.h / 2 - 26] + ')';
174 });
175
176 subRelationIndicator.append('image')
177 .attr({'xlink:href': '/juju-ui/assets/svgs/sub_relation.svg',
178 'width': 87,
179 'height': 47});
180 subRelationIndicator.append('text').append('tspan')
181 .attr({'class': 'sub-rel-count',
182 'x': 64,
183 'y': 47 * 0.8});
184
185 // The following are sizes in pixels of the SVG assets used to
186 // render a service, and are used to in calculating the vertical
187 // positioning of text down along the service block.
188 var service_height = 224,
189 name_size = 22,
190 charm_label_size = 16,
191 name_padding = 26,
192 charm_label_padding = 150;
193
194 node.select('.name')
195 .attr({'style': function(d) {
196 // Programmatically size the font.
197 // Number derived from service assets:
198 // font-size 22px when asset is 224px.
199 return 'font-size:' + d.h *
200 (name_size / service_height) + 'px';
201 },
202 'x': function(d) { return d.w / 2; },
203 'y': function(d) {
204 // Number derived from service assets:
205 // padding-top 26px when asset is 224px.
206 return d.h * (name_padding / service_height) + d.h *
207 (name_size / service_height) / 2;
208 }
209 });
210
211 // Show whether or not the service is exposed using an indicator.
212 var exposed = node.filter(function(d) {
213 return d.exposed;
214 });
215 exposed.each(function(d) {
216 var existing = Y.one(this).one('.exposed-indicator');
217 if (!existing) {
218 existing = d3.select(this).append('image')
219 .attr({'class': 'exposed-indicator on',
220 'xlink:href': '/juju-ui/assets/svgs/exposed.svg',
221 'width': 32,
222 'height': 32
223 })
224 .append('title')
225 .text(function(d) {
226 return d.exposed ? 'Exposed' : '';
227 });
228 }
229 existing = d3.select(this).select('.exposed-indicator')
230 .attr({
231 'x': 145,
232 'y': 79
233 });
234 });
235 },
236
237
238 /**
239 Pans the environment view to the center all the services on the canvas.
240
241 @method panToCenter
242 @param {object} evt The event fired.
243 @return {undefined} Side effects only.
244 */
245 panToCenter: function(evt) {
246 var topo = this.get('component');
247 var vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes);
248 this.findAndSetCentroid(vertices);
249 },
250
251 /**
252 Given a set of vertices, find the centroid and pan to that location.
253
254 @method findAndSetCentroid
255 @param {array} vertices A list of vertices in the form [x, y].
256 @return {undefined} Side effects only.
257 */
258 findAndSetCentroid: function(vertices) {
259 var topo = this.get('component');
260 var centroid = topoUtils.centroid(vertices);
261 /* The centroid is set on the topology object due to the fact that it
262 is used as a sigil to tell whether or not to pan to the point
263 after the first delta. */
264 topo.centroid = centroid;
265 topo.fire('panToPoint', {point: topo.centroid});
266 }
267 }, {
268 ATTRS: {
269 /**
270 @property {d3ns.Component} component
271 */
272 component: {}
273 }
274 });
275 views.BundleModule = BundleModule;
276
277 /**
278 Display a Bundle using an internal topology.
279
280 @class BundleTopology
281 */
282 function BundleTopology(options) {
283 // Options and Init
284 var self = this;
285 options = options || {};
286 this.options = options;
287 this._cleanups = [];
288 this.db = options.db;
289 if (!this.db) {
290 this.db = new models.Database();
291 this._cleanups.push(this.db.destroy);
292 }
293 this.store = options.store;
294 if (!this.store) {
295 this.store = new juju.Charmworld2({});
296 this._cleanups.push(this.store.destroy);
297 }
298 this.container = options.container;
299 if (!this.container) {
300 this.container = Y.Node.create('<div>');
301 this.container.addClass('topology-canvas');
302 this._cleanups.push(function() {
303 self.container.remove(true);
304 });
305 }
306
307 var topo = this.topology = new views.Topology();
308 topo.setAttrs(Y.mix(options, {
309 interactive: false,
310 container: this.container,
311 db: this.db,
312 store: this.store
313 }, true));
314
315 // Service view doesn't support Level Of Detail views.
316 // BundleModule provides an icon centric view
317 // of services till service module can support this directly.
318 topo.addModule(views.BundleModule);
319 topo.addModule(views.RelationModule);
320 topo.addModule(views.PanZoomModule);
321 }
322
323 BundleTopology.prototype.centerViewport = function(scale) {
324 this.topology.modules.PanZoomModule._fire_zoom(scale);
325 // Pan to the centroid of it all after the zoom
326 this.panToCenter();
327 return this;
328 };
329
330 /**
331 Pans the canvas to the center all the services.
332
333 @method panToCenter
334 @param {object} evt The event fired.
335 @return {undefined} Side effects only.
336 */
337 BundleTopology.prototype.panToCenter = function() {
338 var topo = this.topology;
339 var vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes);
340 var centroid = topoUtils.centroid(vertices);
341 this.topology.modules.PanZoomModule.panToPoint({point: centroid});
342 };
343
344
345 BundleTopology.prototype.render = function() {
346 this.topology.render();
347 this.centerViewport(0.66);
348 return this;
349 };
350
351 BundleTopology.prototype.destroy = function() {
352 this._cleanups.forEach(function(cleanupFunc) {
353 cleanupFunc();
354 });
355 };
356
357 views.BundleTopology = BundleTopology;
358
359}, '0.1.0', {
360 requires: [
361 'd3',
362 'd3-components',
363 'juju-charm-store',
364 'juju-models',
365 'juju-topology',
366 'juju-view-utils'
367 ]
368});
0369
=== modified file 'app/views/topology/relation.js'
--- app/views/topology/relation.js 2013-07-03 17:35:21 +0000
+++ app/views/topology/relation.js 2013-08-30 19:48:17 +0000
@@ -178,6 +178,9 @@
178 var db = topo.get('db');178 var db = topo.get('db');
179 var self = this;179 var self = this;
180 var relations = db.relations.toArray();180 var relations = db.relations.toArray();
181 if (!relations || relations.length === 0) {
182 return;
183 }
181 this.relations = this.decorateRelations(relations);184 this.relations = this.decorateRelations(relations);
182 this.updateLinks();185 this.updateLinks();
183 this.updateSubordinateRelationsCount();186 this.updateSubordinateRelationsCount();
184187
=== modified file 'app/views/topology/service.js'
--- app/views/topology/service.js 2013-08-21 16:02:10 +0000
+++ app/views/topology/service.js 2013-08-30 19:48:17 +0000
@@ -33,35 +33,303 @@
33 d3ns = Y.namespace('d3'),33 d3ns = Y.namespace('d3'),
34 Templates = views.Templates;34 Templates = views.Templates;
3535
36 /**36 var ServiceModuleCommon = function() {};
37 * Manage service rendering and events.37 /**
38 *38 Sync view models with current db.models.
39 * ## Emitted events:39
40 *40 @method updateData
41 * - *clearState:* clear all possible states that the environment view can be41 */
42 * in as it pertains to actions (building a relation, viewing42 ServiceModuleCommon.prototype.updateData = function() {
43 * a service menu, etc.)43 //model data
44 * - *snapToService:* fired when mousing over a service, causing the pending44 var topo = this.get('component');
45 * relation dragline to snap to the service rather than45 var vis = topo.vis;
46 * following the mouse.46 var db = topo.get('db');
47 * - *snapOutOfService:* fired when mousing out of a service, causing the47 var store = topo.get('store');
48 * pending relation line to follow the mouse again.48
49 * - *addRelationDrag:*49 var visibleServices = db.services.visible();
50 * - *addRelationDragStart:*50 views.toBoundingBoxes(this, visibleServices, topo.service_boxes, store);
51 * - *addRelationDragEnd:* fired when creating a relation through the long-51 // Break a reference cycle that results in uncollectable objects leaking.
52 * click process, when moving the cursor over the environment, and when52 visibleServices.reset();
53 * dropping the endpoint on a valid service.53
54 * - *cancelRelationBuild:* fired when dropping a pending relation line54 // Nodes are mapped by modelId tuples.
55 * started through the long-click method somewhere other than a valid55 this.node = vis.selectAll('.service')
56 * service.56 .data(Y.Object.values(topo.service_boxes),
57 * - *serviceMoved:* fired when a service block is dragged so that relation57 function(d) {return d.modelId;});
58 * endpoints can follow it.58 };
59 * - *navigateTo:* fired when clicking the "View Service" menu item or when59
60 * double-clicking a service.60 /**
61 *61 Fill the empty structures within a service node such that they
62 * @class ServiceModule62 match the db.
63
64 @param {object} node the collection of nodes to update.
65 @return {null} side effects only.
66 @method updateServiceNodes
67 */
68 ServiceModuleCommon.prototype.updateServiceNodes = function(node) {
69 if (node.empty()) {
70 return;
71 }
72 var self = this,
73 topo = this.get('component'),
74 landscape = topo.get('landscape'),
75 service_scale = this.service_scale,
76 service_scale_width = this.service_scale_width,
77 service_scale_height = this.service_scale_height;
78
79 // Apply Position Annotations
80 // This is done after the services_boxes
81 // binding as the event handler will
82 // use that index.
83 node.each(function(d) {
84 var service = d.model,
85 annotations = service.get('annotations'),
86 x, y;
87
88 // If there are no annotations or the service is being dragged
89 if (!annotations || service.inDrag === views.DRAG_ACTIVE) {
90 return;
91 }
92
93 // If there are x/y annotations on the service model and they are
94 // different from the node's current x/y coordinates, update the
95 // node, as the annotations may have been set in another session.
96 x = annotations['gui-x'];
97 y = annotations['gui-y'];
98 if (!d ||
99 (x !== undefined && x !== d.x) ||
100 (y !== undefined && y !== d.y)) {
101 // Delete gui-x and gui-y from annotations as we use the values.
102 // This is to prevent deltas coming in on a service while it is
103 // being dragged from resetting its position during the drag.
104
105 delete annotations['gui-x'];
106 delete annotations['gui-y'];
107 // Only update position if we're not already in a drag state (the
108 // current drag supercedes any previous annotations).
109 if (!d.inDrag) {
110 self.drag.call(this, d, self, {x: x, y: y},
111 self.get('useTransitions'));
112 }
113 }});
114
115 // Mark subordinates as such. This is needed for when a new service
116 // is created.
117 node.filter(function(d) {
118 return d.subordinate;
119 })
120 .classed('subordinate', true);
121
122 // Size the node for drawing.
123 node.attr({
124 'width': function(box) { box.w = 190; return box.w;},
125 'height': function(box) { box.h = 190; return box.h;}
126 });
127
128 node.select('.service-block-image').each(function(d) {
129 var curr_node = d3.select(this);
130 var curr_href = curr_node.attr('xlink:href');
131 var new_href = d.subordinate ?
132 '/juju-ui/assets/svgs/sub_module.svg' :
133 '/juju-ui/assets/svgs/service_module.svg';
134
135 // Only set 'xlink:href' if not already set to the new value,
136 // thus avoiding redundant requests to the server. #1182135
137 if (curr_href !== new_href) {
138 curr_node.attr({'xlink:href': new_href});
139 }
140 curr_node.attr({
141 'width': d.w,
142 'height': d.h
143 });
144 });
145
146 // Draw a subordinate relation indicator.
147 var subRelationIndicator = node.filter(function(d) {
148 return d.subordinate &&
149 d3.select(this)
150 .select('.sub-rel-block').empty();
151 })
152 .append('g')
153 .attr('class', 'sub-rel-block')
154 .attr('transform', function(d) {
155 // Position the block so that the relation indicator will
156 // appear at the right connector.
157 return 'translate(' + [d.w, d.h / 2 - 26] + ')';
158 });
159
160 subRelationIndicator.append('image')
161 .attr({'xlink:href': '/juju-ui/assets/svgs/sub_relation.svg',
162 'width': 87,
163 'height': 47});
164 subRelationIndicator.append('text').append('tspan')
165 .attr({'class': 'sub-rel-count',
166 'x': 64,
167 'y': 47 * 0.8});
168
169 // Landscape badge
170 if (landscape) {
171 node.each(function(d) {
172 var landscapeAsset;
173 var securityBadge = landscape.getLandscapeBadge(
174 d.model, 'security', 'round');
175 var rebootBadge = landscape.getLandscapeBadge(
176 d.model, 'reboot', 'round');
177
178 if (securityBadge && rebootBadge) {
179 landscapeAsset =
180 '/juju-ui/assets/images/landscape_restart_round.png';
181 } else if (securityBadge) {
182 landscapeAsset =
183 '/juju-ui/assets/images/landscape_security_round.png';
184 } else if (rebootBadge) {
185 landscapeAsset =
186 '/juju-ui/assets/images/landscape_restart_round.png';
187 }
188 if (landscapeAsset === undefined) {
189 // Remove any existing badge.
190 d3.select(this).select('.landscape-badge').remove();
191 } else {
192 var existing = Y.one(this).one('.landscape-badge'),
193 curr_href, target;
194
195 if (!existing) {
196 existing = d3.select(this).append('image');
197 existing.attr({
198 'class': 'landscape-badge',
199 'width': 32,
200 'height': 32
201 });
202 }
203 existing = d3.select(this).select('.landscape-badge');
204 existing.attr({
205 'x': 13,
206 'y': 79
207 });
208
209 // Only set 'xlink:href' if not already set to the new value,
210 // thus avoiding redundant requests to the server. #1182135
211 curr_href = existing.attr('xlink:href');
212 if (curr_href !== landscapeAsset) {
213 existing.attr({'xlink:href': landscapeAsset});
214 }
215 }
216 });
217 }
218 // The following are sizes in pixels of the SVG assets used to
219 // render a service, and are used to in calculating the vertical
220 // positioning of text down along the service block.
221 var service_height = 224,
222 name_size = 22,
223 charm_label_size = 16,
224 name_padding = 26,
225 charm_label_padding = 150;
226
227 node.select('.name')
228 .attr({'style': function(d) {
229 // Programmatically size the font.
230 // Number derived from service assets:
231 // font-size 22px when asset is 224px.
232 return 'font-size:' + d.h *
233 (name_size / service_height) + 'px';
234 },
235 'x': function(d) { return d.w / 2; },
236 'y': function(d) {
237 // Number derived from service assets:
238 // padding-top 26px when asset is 224px.
239 return d.h * (name_padding / service_height) + d.h *
240 (name_size / service_height) / 2;
241 }
242 });
243 node.select('.charm-label')
244 .attr({'style': function(d) {
245 // Programmatically size the font.
246 // Number derived from service assets:
247 // font-size 16px when asset is 224px.
248 return 'font-size:' + d.h *
249 (charm_label_size / service_height) + 'px';
250 },
251 'x': function(d) { return d.w / 2;},
252 'y': function(d) {
253 // Number derived from service assets:
254 // padding-top: 118px when asset is 224px.
255 return d.h * (charm_label_padding / service_height) - d.h *
256 (charm_label_size / service_height) / 2;
257 }
258 });
259
260 // Show whether or not the service is exposed using an indicator.
261 var exposed = node.filter(function(d) {
262 return d.exposed;
263 });
264 exposed.each(function(d) {
265 var existing = Y.one(this).one('.exposed-indicator');
266 if (!existing) {
267 existing = d3.select(this).append('image')
268 .attr({'class': 'exposed-indicator on',
269 'xlink:href': '/juju-ui/assets/svgs/exposed.svg',
270 'width': 32,
271 'height': 32
272 })
273 .append('title')
274 .text(function(d) {
275 return d.exposed ? 'Exposed' : '';
276 });
277 }
278 existing = d3.select(this).select('.exposed-indicator')
279 .attr({
280 'x': 145,
281 'y': 79
282 });
283 });
284
285 // Remove exposed indicator from nodes that are no longer exposed.
286 node.filter(function(d) {
287 return !d.exposed &&
288 !d3.select(this)
289 .select('.exposed-indicator').empty();
290 }).select('.exposed-indicator').remove();
291
292 // Adds the relative health in the form of a percentage bar.
293 node.each(function(d) {
294 var status_graph = d3.select(this).select('.statusbar');
295 var status_bar = status_graph.property('status_bar');
296 if (status_bar && !d.subordinate) {
297 status_bar.update(d.aggregated_status);
298 }
299 });
300 };
301 views.ServiceModuleCommon = ServiceModuleCommon;
302
303 /**
304 Manage service rendering and events.
305
306 ## Emitted events:
307
308 - *clearState:* clear all possible states that the environment view can be
309 in as it pertains to actions (building a relation, viewing
310 a service menu, etc.)
311 - *snapToService:* fired when mousing over a service, causing the pending
312 relation dragline to snap to the service rather than
313 following the mouse.
314 - *snapOutOfService:* fired when mousing out of a service, causing the
315 pending relation line to follow the mouse again.
316 - *addRelationDrag:*
317 - *addRelationDragStart:*
318 - *addRelationDragEnd:* fired when creating a relation through the long-
319 click process, when moving the cursor over the environment, and when
320 dropping the endpoint on a valid service.
321 - *cancelRelationBuild:* fired when dropping a pending relation line
322 started through the long-click method somewhere other than a valid
323 service.
324 - *serviceMoved:* fired when a service block is dragged so that relation
325 endpoints can follow it.
326 - *navigateTo:* fired when clicking the "View Service" menu item or when
327 double-clicking a service.
328
329 @class ServiceModule
63 */330 */
64 var ServiceModule = Y.Base.create('ServiceModule', d3ns.Module, [], {331 var ServiceModule = Y.Base.create('ServiceModule', d3ns.Module, [
332 ServiceModuleCommon], {
65 events: {333 events: {
66 scene: {334 scene: {
67 '.service': {335 '.service': {
@@ -192,14 +460,15 @@
192 */460 */
193 _attachDragEvents: function() {461 _attachDragEvents: function() {
194 var container = this.get('container'),462 var container = this.get('container'),
195 ZP = '.zoom-plane',463 ZP = '.zoom-plane',
196 EC = 'i.sprite.empty_canvas';464 EC = 'i.sprite.empty_canvas';
197465
198 container.delegate('drop', this.canvasDropHandler, ZP, this);466 container.delegate('drop', this.canvasDropHandler, ZP, this);
199 container.delegate('dragenter', this._ignore, ZP, this);467 container.delegate('dragenter', this._ignore, ZP, this);
200 container.delegate('dragover', this._ignore, ZP, this);468 container.delegate('dragover', this._ignore, ZP, this);
201469
202 // allows the user to drop the charm on the 'drop here' help text in IE10.470 // allows the user to drop the charm on the 'drop here' help text in
471 // IE10.
203 container.delegate('drop', this.canvasDropHandler, EC, this);472 container.delegate('drop', this.canvasDropHandler, EC, this);
204 container.delegate('dragenter', this._ignore, EC, this);473 container.delegate('dragenter', this._ignore, EC, this);
205 container.delegate('dragover', this._ignore, EC, this);474 container.delegate('dragover', this._ignore, EC, this);
@@ -225,7 +494,7 @@
225 */494 */
226 attachTouchstartEvents: function(data, node) {495 attachTouchstartEvents: function(data, node) {
227 var topo = this.get('component'),496 var topo = this.get('component'),
228 yuiNode = Y.Node(node);497 yuiNode = Y.Node(node);
229498
230 // Do not attach the event to the ghost nodes499 // Do not attach the event to the ghost nodes
231 if (!d3.select(node).classed('pending')) {500 if (!d3.select(node).classed('pending')) {
@@ -244,9 +513,9 @@
244 // To execute the serviceClick method under the same context as513 // To execute the serviceClick method under the same context as
245 // click we call it under the touch target context514 // click we call it under the touch target context
246 var node = e.currentTarget.getDOMNode(),515 var node = e.currentTarget.getDOMNode(),
247 box = d3.select(node).datum();516 box = d3.select(node).datum();
248 // If we're dragging with two fingers, ignore this as a tap and let drag517 // If we're dragging with two fingers, ignore this as a tap and let
249 // take over.518 // drag take over.
250 if (e.touches.length > 1) {519 if (e.touches.length > 1) {
251 box.tapped = false;520 box.tapped = false;
252 return;521 return;
@@ -284,9 +553,9 @@
284 return;553 return;
285 }554 }
286 } else {555 } else {
287 // Touch events will also fire a click event about 300ms later. If this556 // Touch events will also fire a click event about 300ms later. If
288 // event isn't ignored, the service menu will disappear 300ms after it557 // this event isn't ignored, the service menu will disappear 300ms
289 // appears, so set a flag to ignore that event.558 // after it appears, so set a flag to ignore that event.
290 box.ignoreNextClick = true;559 box.ignoreNextClick = true;
291 }560 }
292561
@@ -298,8 +567,8 @@
298 // If the service box is pending, ensure that the charm panel is567 // If the service box is pending, ensure that the charm panel is
299 // visible, but don't do anything else.568 // visible, but don't do anything else.
300 if (box.pending && !window.flags.serviceInspector) {569 if (box.pending && !window.flags.serviceInspector) {
301 // Prevent the clickoutside event from firing and immediately closing570 // Prevent the clickoutside event from firing and immediately
302 // the panel.571 // closing the panel.
303 d3.event.halt();572 d3.event.halt();
304 // Ensure service menus are closed.573 // Ensure service menus are closed.
305 topo.fire('clearState');574 topo.fire('clearState');
@@ -327,10 +596,11 @@
327 return;596 return;
328 }597 }
329 // Just show the service on double-click.598 // Just show the service on double-click.
330 var topo = self.get('component'),599 var topo = self.get('component');
331 service = box.model;600 var service = box.model;
332 // The browser sends a click event right before the dblclick one, and it601 // The browser sends a click event right before the dblclick one, and
333 // opens the service menu: close it before moving to the service details.602 // it opens the service menu: close it before moving to the service
603 // details.
334 self.hideServiceMenu();604 self.hideServiceMenu();
335 self.show_service(service);605 self.show_service(service);
336 },606 },
@@ -440,7 +710,7 @@
440 reader.onload = function(e) {710 reader.onload = function(e) {
441 // Import each into the environment711 // Import each into the environment
442 db.importDeployer(jsyaml.safeLoad(e.target.result),712 db.importDeployer(jsyaml.safeLoad(e.target.result),
443 store, {useGhost: false})713 store, {useGhost: false})
444 .then(function() {714 .then(function() {
445 notifications.add({715 notifications.add({
446 title: 'Imported Environment',716 title: 'Imported Environment',
@@ -468,14 +738,15 @@
468 // required to position the service in the proper y position.738 // required to position the service in the proper y position.
469 var dropXY = [evt.clientX, (evt.clientY - 71)];739 var dropXY = [evt.clientX, (evt.clientY - 71)];
470740
471 // Take the x,y offset (translation) of the topology view into account.741 // Take the x,y offset (translation) of the topology view into
742 // account.
472 Y.Array.each(dropXY, function(_, index) {743 Y.Array.each(dropXY, function(_, index) {
473 ghostAttributes.coordinates[index] =744 ghostAttributes.coordinates[index] =
474 (dropXY[index] - translation[index]) / scale;745 (dropXY[index] - translation[index]) / scale;
475 });746 });
476 if (dragData.dataType === 'charm-token-drag-and-drop') {747 if (dragData.dataType === 'charm-token-drag-and-drop') {
477 // The charm data was JSON encoded because the dataTransfer mechanism748 // The charm data was JSON encoded because the dataTransfer
478 // only allows for string values.749 // mechanism only allows for string values.
479 var charmData = Y.JSON.parse(dragData.charmData);750 var charmData = Y.JSON.parse(dragData.charmData);
480 // Add the icon url to the ghost attributes for the ghost icon751 // Add the icon url to the ghost attributes for the ghost icon
481 ghostAttributes.icon = dragData.iconSrc;752 ghostAttributes.icon = dragData.iconSrc;
@@ -494,7 +765,7 @@
494 */765 */
495 clearStateHandler: function() {766 clearStateHandler: function() {
496 var container = this.get('container'),767 var container = this.get('container'),
497 topo = this.get('component');768 topo = this.get('component');
498 container.all('.environment-menu.active').removeClass('active');769 container.all('.environment-menu.active').removeClass('active');
499 this.hideServiceMenu();770 this.hideServiceMenu();
500 },771 },
@@ -568,7 +839,7 @@
568 context.longClickTimer = Y.later(750, this, function(d, e) {839 context.longClickTimer = Y.later(750, this, function(d, e) {
569 // Provide some leeway for accidental dragging.840 // Provide some leeway for accidental dragging.
570 if ((Math.abs(box.x - box.oldX) + Math.abs(box.y - box.oldY)) /841 if ((Math.abs(box.x - box.oldX) + Math.abs(box.y - box.oldY)) /
571 2 > 5) {842 2 > 5) {
572 return;843 return;
573 }844 }
574845
@@ -598,29 +869,6 @@
598 context.longClickTimer.cancel();869 context.longClickTimer.cancel();
599 }870 }
600 },871 },
601 /*
602 * Sync view models with current db.models.
603 *
604 * @method updateData
605 */
606 updateData: function() {
607 //model data
608 var topo = this.get('component');
609 var vis = topo.vis;
610 var db = topo.get('db');
611 var store = topo.get('store');
612
613 var visibleServices = db.services.visible();
614 views.toBoundingBoxes(this, visibleServices, topo.service_boxes, store);
615 // Break a reference cycle that results in uncollectable objects leaking.
616 visibleServices.reset();
617
618 // Nodes are mapped by modelId tuples.
619 this.node = vis.selectAll('.service')
620 .data(Y.Object.values(topo.service_boxes),
621 function(d) {return d.modelId;});
622 },
623
624 /**872 /**
625 * Handle drag events for a service.873 * Handle drag events for a service.
626 *874 *
@@ -650,17 +898,17 @@
650 }898 }
651 else {899 else {
652900
653 // If the service hasn't been dragged (in the case of long-click to add901 // If the service hasn't been dragged (in the case of long-click to
654 // relation, or a double-fired event) or the old and new coordinates902 // add relation, or a double-fired event) or the old and new
655 // are the same, exit.903 // coordinates are the same, exit.
656 if (!box.inDrag ||904 if (!box.inDrag ||
657 (box.oldX === box.x &&905 (box.oldX === box.x &&
658 box.oldY === box.y)) {906 box.oldY === box.y)) {
659 return;907 return;
660 }908 }
661909
662 // If the service is still pending, persist x/y coordinates in order910 // If the service is still pending, persist x/y coordinates in
663 // to set them as annotations when the service is created.911 // order to set them as annotations when the service is created.
664 if (box.pending) {912 if (box.pending) {
665 box.model.set('hasBeenPositioned', true);913 box.model.set('hasBeenPositioned', true);
666 box.model.set('x', box.x);914 box.model.set('x', box.x);
@@ -681,21 +929,21 @@
681 },929 },
682930
683 /**931 /**
684 * Specialized drag event handler932 Specialized drag event handler
685 * when called as an event handler it933 when called as an event handler it
686 * Allows optional extra param, pos934 Allows optional extra param, pos
687 * which when used overrides the mouse935 which when used overrides the mouse
688 * handling. This method can then be936 handling. This method can then be
689 * though of as 'drag to position'.937 though of as 'drag to position'.
690 *938
691 * @method drag939 @method drag
692 * @param {Box} d viewModel BoundingBox.940 @param {Box} d viewModel BoundingBox.
693 * @param {ServiceModule} self ServiceModule.941 @param {ServiceModule} self ServiceModule.
694 * @param {Object} pos (optional) containing x/y numbers.942 @param {Object} pos (optional) containing x/y numbers.
695 * @param {Boolean} includeTransition (optional) Use transition to drag.943 @param {Boolean} includeTransition (optional) Use transition to drag.
696 *944
697 * [At the time of this writing useTransition works in practice but945 [At the time of this writing useTransition works in practice but
698 * introduces a timing issue in the tests.]946 introduces a timing issue in the tests.]
699 */947 */
700 drag: function(box, self, pos, includeTransition) {948 drag: function(box, self, pos, includeTransition) {
701 if (box.tapped) {949 if (box.tapped) {
@@ -711,9 +959,9 @@
711 if (self.longClickTimer) {959 if (self.longClickTimer) {
712 self.longClickTimer.cancel();960 self.longClickTimer.cancel();
713 }961 }
714 // Translate the service (and, potentially, menu).962 // Translate the service (and, potentially, menu). If a position was
715 // If a position was provided, update the box's coordinates and the963 // provided, update the box's coordinates and the selection's bound
716 // selection's bound data.964 // data.
717 if (pos) {965 if (pos) {
718 box.x = pos.x;966 box.x = pos.x;
719 box.y = pos.y;967 box.y = pos.y;
@@ -726,8 +974,8 @@
726974
727 if (includeTransition) {975 if (includeTransition) {
728 selection = selection.transition()976 selection = selection.transition()
729 .duration(500)977 .duration(500)
730 .ease('elastic');978 .ease('elastic');
731 }979 }
732980
733 selection.attr('transform', function(d, i) {981 selection.attr('transform', function(d, i) {
@@ -739,7 +987,7 @@
739987
740 // Remove any active menus.988 // Remove any active menus.
741 self.get('container').all('.environment-menu.active')989 self.get('container').all('.environment-menu.active')
742 .removeClass('active');990 .removeClass('active');
743 if (box.inDrag === views.DRAG_START) {991 if (box.inDrag === views.DRAG_START) {
744 self.hideServiceMenu();992 self.hideServiceMenu();
745 box.inDrag = views.DRAG_ACTIVE;993 box.inDrag = views.DRAG_ACTIVE;
@@ -749,12 +997,12 @@
749 topo.fire('serviceMoved', { service: box });997 topo.fire('serviceMoved', { service: box });
750 },998 },
751999
752 /*1000 /**
753 * Attempt to reuse as much of the existing graph and view models1001 Attempt to reuse as much of the existing graph and view models as
754 * as possible to re-render the graph.1002 possible to re-render the graph.
755 *1003
756 * @method update1004 @method update
757 */1005 */
758 update: function() {1006 update: function() {
759 var self = this,1007 var self = this,
760 topo = this.get('component'),1008 topo = this.get('component'),
@@ -776,18 +1024,18 @@
7761024
777 if (!this.tree) {1025 if (!this.tree) {
778 this.tree = d3.layout.unscaledPack()1026 this.tree = d3.layout.unscaledPack()
779 .size([width, height])1027 .size([width, height])
780 .value(function(d) {1028 .value(function(d) {
781 return Math.max(d.unit_count, 1);1029 return Math.max(d.unit_count, 1);
782 })1030 })
783 .padding(300);1031 .padding(300);
784 }1032 }
7851033
786 if (!this.dragBehavior) {1034 if (!this.dragBehavior) {
787 this.dragBehavior = d3.behavior.drag()1035 this.dragBehavior = d3.behavior.drag()
788 .on('dragstart', function(d) { self.dragstart.call(this, d, self);})1036 .on('dragstart', function(d) { self.dragstart.call(this, d, self);})
789 .on('drag', function(d) { self.drag.call(this, d, self);})1037 .on('drag', function(d) { self.drag.call(this, d, self);})
790 .on('dragend', function(d) { self.dragend.call(this, d, self);});1038 .on('dragend', function(d) { self.dragend.call(this, d, self);});
791 }1039 }
7921040
793 //Process any changed data.1041 //Process any changed data.
@@ -798,16 +1046,15 @@
798 var node = this.node;1046 var node = this.node;
7991047
800 // Rerun the pack layout.1048 // Rerun the pack layout.
801 // Pack doesn't honor existing positions and will1049 // Pack doesn't honor existing positions and will re-layout the
802 // re-layout the entire graph. As a short term work1050 // entire graph. As a short term work around we layout only new
803 // around we layout only new nodes. This has the side1051 // nodes. This has the side effect that service blocks can overlap
804 // effect that service blocks can overlap and will1052 // and will be fixed later.
805 // be fixed later.
806 var vertices;1053 var vertices;
807 var new_services = Y.Object.values(topo.service_boxes)1054 var new_services = Y.Object.values(topo.service_boxes)
808 .filter(function(boundingBox) {1055 .filter(function(boundingBox) {
809 return !Y.Lang.isNumber(boundingBox.x);1056 return !Y.Lang.isNumber(boundingBox.x);
810 });1057 });
811 if (new_services.length > 0) {1058 if (new_services.length > 0) {
812 // If the there is only one new service and it's pending (as in, it was1059 // If the there is only one new service and it's pending (as in, it was
813 // added via the charm panel as a ghost), position it intelligently and1060 // added via the charm panel as a ghost), position it intelligently and
@@ -816,16 +1063,19 @@
816 // in the case of opening an unannotated environment for the first1063 // in the case of opening an unannotated environment for the first
817 // time).1064 // time).
818 var pendingServicePlaced = false;1065 var pendingServicePlaced = false;
819 if (new_services.length === 1 && new_services[0].model.get('pending')) {1066 if (new_services.length === 1 &&
1067 new_services[0].model.get('pending')) {
820 pendingServicePlaced = true;1068 pendingServicePlaced = true;
821 // Get a coordinate outside the cluster of existing services.1069 // Get a coordinate outside the cluster of existing services.
822 var coords = topo.servicePointOutside();1070 var coords = topo.servicePointOutside();
823 // Set the coordinates on both the box model and the service model.1071 // Set the coordinates on both the box model and the service
1072 // model.
824 new_services[0].x = coords[0];1073 new_services[0].x = coords[0];
825 new_services[0].y = coords[1];1074 new_services[0].y = coords[1];
826 new_services[0].model.set('x', coords[0]);1075 new_services[0].model.set('x', coords[0]);
827 new_services[0].model.set('y', coords[1]);1076 new_services[0].model.set('y', coords[1]);
828 // This ensures that the x/y coordinates will be saved as annotations.1077 // This ensures that the x/y coordinates will be saved as
1078 // annotations.
829 new_services[0].model.set('hasBeenPositioned', true);1079 new_services[0].model.set('hasBeenPositioned', true);
830 // Set the centroid to the new service's position1080 // Set the centroid to the new service's position
831 topo.centroid = coords;1081 topo.centroid = coords;
@@ -833,8 +1083,8 @@
833 } else {1083 } else {
834 this.tree.nodes({children: new_services});1084 this.tree.nodes({children: new_services});
835 }1085 }
836 // Update annotations settings position on backend1086 // Update annotations settings position on backend (but only do
837 // (but only do this if there is no existing annotations).1087 // this if there is no existing annotations).
838 if (!pendingServicePlaced) {1088 if (!pendingServicePlaced) {
839 vertices = [];1089 vertices = [];
840 }1090 }
@@ -858,7 +1108,8 @@
858 });1108 });
859 }1109 }
860 if (!topo.centroid || vertices) {1110 if (!topo.centroid || vertices) {
861 // Find the centroid of our hull of services and inform the topology.1111 // Find the centroid of our hull of services and inform the
1112 // topology.
862 if (!vertices) {1113 if (!vertices) {
863 vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes);1114 vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes);
864 }1115 }
@@ -866,9 +1117,9 @@
866 }1117 }
867 // enter1118 // enter
868 node1119 node
869 .enter().append('g')1120 .enter().append('g')
870 .attr({1121 .attr({
871 'pointer-events': 'all', // IE doesn't drag properly without this.1122 'pointer-events': 'all', // IE needs this.
872 'class': function(d) {1123 'class': function(d) {
873 return (d.subordinate ? 'subordinate ' : '') +1124 return (d.subordinate ? 'subordinate ' : '') +
874 (d.pending ? 'pending ' : '') + 'service';1125 (d.pending ? 'pending ' : '') + 'service';
@@ -882,18 +1133,18 @@
8821133
883 // Remove old nodes.1134 // Remove old nodes.
884 node.exit()1135 node.exit()
885 .each(function(d) {1136 .each(function(d) {
886 delete topo.service_boxes[d.id];1137 delete topo.service_boxes[d.id];
887 })1138 })
888 .remove();1139 .remove();
889 },1140 },
8901141
891 /**1142 /**
892 Pans the environment view to the center all the services on the canvas.1143 Pans the environment view to the center all the services on the canvas.
8931144
894 @method panToCenter1145 @method panToCenter
895 @param {object} evt The event fired.1146 @param {object} evt The event fired.
896 @return {undefined} Side effects only.1147 @return {undefined} Side effects only.
897 */1148 */
898 panToCenter: function(evt) {1149 panToCenter: function(evt) {
899 var topo = this.get('component');1150 var topo = this.get('component');
@@ -902,15 +1153,15 @@
902 },1153 },
9031154
904 /**1155 /**
905 Given a set of vertices, find the centroid and pan to that location.1156 Given a set of vertices, find the centroid and pan to that location.
9061157
907 @method findAndSetCentroid1158 @method findAndSetCentroid
908 @param {array} vertices A list of vertices in the form [x, y].1159 @param {array} vertices A list of vertices in the form [x, y].
909 @return {undefined} Side effects only.1160 @return {undefined} Side effects only.
910 */1161 */
911 findAndSetCentroid: function(vertices) {1162 findAndSetCentroid: function(vertices) {
912 var topo = this.get('component'),1163 var topo = this.get('component'),
913 centroid = topoUtils.centroid(vertices);1164 centroid = topoUtils.centroid(vertices);
914 // The centroid is set on the topology object due to the fact that it is1165 // The centroid is set on the topology object due to the fact that it is
915 // used as a sigil to tell whether or not to pan to the point after the1166 // used as a sigil to tell whether or not to pan to the point after the
916 // first delta.1167 // first delta.
@@ -950,13 +1201,13 @@
950 node.append('image')1201 node.append('image')
951 .classed('service-icon', true)1202 .classed('service-icon', true)
952 .attr({1203 .attr({
953 'xlink:href': function(d) {1204 'xlink:href': function(d) {
954 return d.icon;1205 return d.icon;
955 },1206 },
956 width: 96,1207 width: 96,
957 height: 96,1208 height: 96,
958 transform: 'translate(47, 50)'1209 transform: 'translate(47, 50)'
959 });1210 });
960 node.append('text').append('tspan')1211 node.append('text').append('tspan')
961 .attr('class', 'name')1212 .attr('class', 'name')
962 .text(function(d) {return d.displayName; });1213 .text(function(d) {return d.displayName; });
@@ -985,249 +1236,6 @@
985 });1236 });
986 },1237 },
9871238
988 /**
989 * Fill the empty structures within a service node such that they
990 * match the db.
991 *
992 * @param {object} node the collection of nodes to update.
993 * @return {null} side effects only.
994 * @method updateServiceNodes
995 */
996 updateServiceNodes: function(node) {
997 if (node.empty()) {
998 return;
999 }
1000 var self = this,
1001 topo = this.get('component'),
1002 landscape = topo.get('landscape'),
1003 service_scale = this.service_scale,
1004 service_scale_width = this.service_scale_width,
1005 service_scale_height = this.service_scale_height;
1006
1007 // Apply Position Annotations
1008 // This is done after the services_boxes
1009 // binding as the event handler will
1010 // use that index.
1011 node.each(function(d) {
1012 var service = d.model,
1013 annotations = service.get('annotations'),
1014 x, y;
1015
1016 // If there are no annotations or the service is being dragged
1017 if (!annotations || service.inDrag === views.DRAG_ACTIVE) {
1018 return;
1019 }
1020
1021 // If there are x/y annotations on the service model and they are
1022 // different from the node's current x/y coordinates, update the
1023 // node, as the annotations may have been set in another session.
1024 x = annotations['gui-x'];
1025 y = annotations['gui-y'];
1026 if (!d ||
1027 (x !== undefined && x !== d.x) ||
1028 (y !== undefined && y !== d.y)) {
1029 // Delete gui-x and gui-y from annotations as we use the values.
1030 // This is to prevent deltas coming in on a service while it is
1031 // being dragged from resetting its position during the drag.
1032
1033 delete annotations['gui-x'];
1034 delete annotations['gui-y'];
1035 // Only update position if we're not already in a drag state (the
1036 // current drag supercedes any previous annotations).
1037 if (!d.inDrag) {
1038 self.drag.call(this, d, self, {x: x, y: y},
1039 self.get('useTransitions'));
1040 }
1041 }});
1042
1043 // Mark subordinates as such. This is needed for when a new service
1044 // is created.
1045 node.filter(function(d) {
1046 return d.subordinate;
1047 })
1048 .classed('subordinate', true);
1049
1050 // Size the node for drawing.
1051 node.attr({
1052 'width': function(box) { box.w = 190; return box.w;},
1053 'height': function(box) { box.h = 190; return box.h;}
1054 });
1055
1056 node.select('.service-block-image').each(function(d) {
1057 var curr_node = d3.select(this);
1058 var curr_href = curr_node.attr('xlink:href');
1059 var new_href = d.subordinate ?
1060 '/juju-ui/assets/svgs/sub_module.svg' :
1061 '/juju-ui/assets/svgs/service_module.svg';
1062
1063 // Only set 'xlink:href' if not already set to the new value,
1064 // thus avoiding redundant requests to the server. #1182135
1065 if (curr_href !== new_href) {
1066 curr_node.attr({'xlink:href': new_href});
1067 }
1068 curr_node.attr({
1069 'width': d.w,
1070 'height': d.h
1071 });
1072 });
1073
1074 // Draw a subordinate relation indicator.
1075 var subRelationIndicator = node.filter(function(d) {
1076 return d.subordinate &&
1077 d3.select(this)
1078 .select('.sub-rel-block').empty();
1079 })
1080 .append('g')
1081 .attr('class', 'sub-rel-block')
1082 .attr('transform', function(d) {
1083 // Position the block so that the relation indicator will
1084 // appear at the right connector.
1085 return 'translate(' + [d.w, d.h / 2 - 26] + ')';
1086 });
1087
1088 subRelationIndicator.append('image')
1089 .attr({'xlink:href': '/juju-ui/assets/svgs/sub_relation.svg',
1090 'width': 87,
1091 'height': 47});
1092 subRelationIndicator.append('text').append('tspan')
1093 .attr({'class': 'sub-rel-count',
1094 'x': 64,
1095 'y': 47 * 0.8});
1096
1097 // Landscape badge
1098 if (landscape) {
1099 node.each(function(d) {
1100 var landscapeAsset;
1101 var securityBadge = landscape.getLandscapeBadge(
1102 d.model, 'security', 'round');
1103 var rebootBadge = landscape.getLandscapeBadge(
1104 d.model, 'reboot', 'round');
1105
1106 if (securityBadge && rebootBadge) {
1107 landscapeAsset =
1108 '/juju-ui/assets/images/landscape_restart_round.png';
1109 } else if (securityBadge) {
1110 landscapeAsset =
1111 '/juju-ui/assets/images/landscape_security_round.png';
1112 } else if (rebootBadge) {
1113 landscapeAsset =
1114 '/juju-ui/assets/images/landscape_restart_round.png';
1115 }
1116 if (landscapeAsset === undefined) {
1117 // Remove any existing badge.
1118 d3.select(this).select('.landscape-badge').remove();
1119 } else {
1120 var existing = Y.one(this).one('.landscape-badge'),
1121 curr_href, target;
1122
1123 if (!existing) {
1124 existing = d3.select(this).append('image');
1125 existing.attr({
1126 'class': 'landscape-badge',
1127 'width': 32,
1128 'height': 32
1129 });
1130 }
1131 existing = d3.select(this).select('.landscape-badge');
1132 existing.attr({
1133 'x': 13,
1134 'y': 79
1135 });
1136
1137 // Only set 'xlink:href' if not already set to the new value,
1138 // thus avoiding redundant requests to the server. #1182135
1139 curr_href = existing.attr('xlink:href');
1140 if (curr_href !== landscapeAsset) {
1141 existing.attr({'xlink:href': landscapeAsset});
1142 }
1143 }
1144 });
1145 }
1146 // The following are sizes in pixels of the SVG assets used to
1147 // render a service, and are used to in calculating the vertical
1148 // positioning of text down along the service block.
1149 var service_height = 224,
1150 name_size = 22,
1151 charm_label_size = 16,
1152 name_padding = 26,
1153 charm_label_padding = 150;
1154
1155 node.select('.name')
1156 .attr({'style': function(d) {
1157 // Programmatically size the font.
1158 // Number derived from service assets:
1159 // font-size 22px when asset is 224px.
1160 return 'font-size:' + d.h *
1161 (name_size / service_height) + 'px';
1162 },
1163 'x': function(d) { return d.w / 2; },
1164 'y': function(d) {
1165 // Number derived from service assets:
1166 // padding-top 26px when asset is 224px.
1167 return d.h * (name_padding / service_height) + d.h *
1168 (name_size / service_height) / 2;
1169 }
1170 });
1171 node.select('.charm-label')
1172 .attr({'style': function(d) {
1173 // Programmatically size the font.
1174 // Number derived from service assets:
1175 // font-size 16px when asset is 224px.
1176 return 'font-size:' + d.h *
1177 (charm_label_size / service_height) + 'px';
1178 },
1179 'x': function(d) { return d.w / 2;},
1180 'y': function(d) {
1181 // Number derived from service assets:
1182 // padding-top: 118px when asset is 224px.
1183 return d.h * (charm_label_padding / service_height) - d.h *
1184 (charm_label_size / service_height) / 2;
1185 }
1186 });
1187
1188 // Show whether or not the service is exposed using an indicator.
1189 var exposed = node.filter(function(d) {
1190 return d.exposed;
1191 });
1192 exposed.each(function(d) {
1193 var existing = Y.one(this).one('.exposed-indicator');
1194 if (!existing) {
1195 existing = d3.select(this).append('image')
1196 .attr({'class': 'exposed-indicator on',
1197 'xlink:href': '/juju-ui/assets/svgs/exposed.svg',
1198 'width': 32,
1199 'height': 32
1200 })
1201 .append('title')
1202 .text(function(d) {
1203 return d.exposed ? 'Exposed' : '';
1204 });
1205 }
1206 existing = d3.select(this).select('.exposed-indicator')
1207 .attr({
1208 'x': 145,
1209 'y': 79
1210 });
1211 });
1212
1213 // Remove exposed indicator from nodes that are no longer exposed.
1214 node.filter(function(d) {
1215 return !d.exposed &&
1216 !d3.select(this)
1217 .select('.exposed-indicator').empty();
1218 }).select('.exposed-indicator').remove();
1219
1220 // Adds the relative health in the form of a percentage bar.
1221 node.each(function(d) {
1222 var status_graph = d3.select(this).select('.statusbar');
1223 var status_bar = status_graph.property('status_bar');
1224 if (status_bar && !d.subordinate) {
1225 status_bar.update(d.aggregated_status);
1226 }
1227 });
1228 },
1229
1230
1231 /*1239 /*
1232 * Show/hide/fade selection.1240 * Show/hide/fade selection.
1233 */1241 */
@@ -1245,7 +1253,7 @@
12451253
1246 fade: function(evt) {1254 fade: function(evt) {
1247 var selection = evt.selection,1255 var selection = evt.selection,
1248 alpha = evt.alpha;1256 alpha = evt.alpha;
1249 selection.transition()1257 selection.transition()
1250 .duration(400)1258 .duration(400)
1251 .attr('opacity', alpha !== undefined && alpha || '0.2');1259 .attr('opacity', alpha !== undefined && alpha || '0.2');
@@ -1267,20 +1275,20 @@
12671275
1268 updateServiceMenuLocation: function() {1276 updateServiceMenuLocation: function() {
1269 var topo = this.get('component'),1277 var topo = this.get('component'),
1270 container = this.get('container'),1278 container = this.get('container'),
1271 cp = container.one('.environment-menu.active'),1279 cp = container.one('.environment-menu.active'),
1272 service = topo.get('active_service'),1280 service = topo.get('active_service'),
1273 tr = topo.get('translate'),1281 tr = topo.get('translate'),
1274 z = topo.get('scale');1282 z = topo.get('scale');
12751283
1276 if (service && cp) {1284 if (service && cp) {
1277 var cpRect = cp.getDOMNode().getClientRects()[0],1285 var cpRect = cp.getDOMNode().getClientRects()[0],
1278 cpWidth = cpRect.width,1286 cpWidth = cpRect.width,
1279 serviceCenter = service.relativeCenter,1287 serviceCenter = service.relativeCenter,
1280 menuLeft = (service.x * z + tr[0] + serviceCenter[0] * z <1288 menuLeft = (service.x * z + tr[0] + serviceCenter[0] * z <
1281 topo.get('width') / 2),1289 topo.get('width') / 2),
1282 cpHeight = cpRect.height,1290 cpHeight = cpRect.height,
1283 arrowWidth = 16; // Hard coded for now for simplicity.1291 arrowWidth = 16; // Hard coded for now for simplicity.
12841292
1285 if (menuLeft) {1293 if (menuLeft) {
1286 cp.removeClass('left')1294 cp.removeClass('left')
@@ -1298,14 +1306,14 @@
1298 // right, and vice versa.1306 // right, and vice versa.
1299 cp.setStyles({1307 cp.setStyles({
1300 'top': (1308 'top': (
1301 service.y * z + tr[1] +1309 service.y * z + tr[1] +
1302 (serviceCenter[1] * z) - (cpHeight / 2)),1310 (serviceCenter[1] * z) - (cpHeight / 2)),
1303 'left': (1311 'left': (
1304 service.x * z +1312 service.x * z +
1305 (menuLeft ?1313 (menuLeft ?
1306 service.w * z + arrowWidth :1314 service.w * z + arrowWidth :
1307 -(cpWidth) - arrowWidth) +1315 -(cpWidth) - arrowWidth) +
1308 tr[0])1316 tr[0])
1309 });1317 });
1310 }1318 }
1311 },1319 },
@@ -1453,7 +1461,7 @@
1453 // Show dialog.1461 // Show dialog.
1454 this.set('destroy_dialog', views.createModalPanel(1462 this.set('destroy_dialog', views.createModalPanel(
1455 'Are you sure you want to destroy the service? ' +1463 'Are you sure you want to destroy the service? ' +
1456 'This cannot be undone.',1464 'This cannot be undone.',
1457 '#destroy-modal-panel',1465 '#destroy-modal-panel',
1458 'Destroy Service',1466 'Destroy Service',
1459 Y.bind(function(ev) {1467 Y.bind(function(ev) {
@@ -1471,16 +1479,16 @@
1471 */1479 */
1472 destroyService: function(btn) {1480 destroyService: function(btn) {
1473 var env = this.get('component').get('env'),1481 var env = this.get('component').get('env'),
1474 service = this.get('destroy_service');1482 service = this.get('destroy_service');
1475 env.destroy_service(service.get('id'),1483 env.destroy_service(service.get('id'),
1476 Y.bind(this._destroyCallback, this,1484 Y.bind(this._destroyCallback, this,
1477 service, btn));1485 service, btn));
1478 },1486 },
14791487
1480 _destroyCallback: function(service, btn, ev) {1488 _destroyCallback: function(service, btn, ev) {
1481 var getModelURL = this.get('component').get('getModelURL'),1489 var getModelURL = this.get('component').get('getModelURL'),
1482 topo = this.get('component'),1490 topo = this.get('component'),
1483 db = topo.get('db');1491 db = topo.get('db');
1484 if (ev.err) {1492 if (ev.err) {
1485 db.notifications.add(1493 db.notifications.add(
1486 new models.Notification({1494 new models.Notification({
14871495
=== modified file 'app/views/topology/topology.js'
--- app/views/topology/topology.js 2013-05-17 14:51:05 +0000
+++ app/views/topology/topology.js 2013-08-30 19:48:17 +0000
@@ -49,17 +49,15 @@
49 */49 */
50 var Topology = Y.Base.create('Topology', d3ns.Component, [], {50 var Topology = Y.Base.create('Topology', d3ns.Component, [], {
51 initializer: function(options) {51 initializer: function(options) {
52 Topology.superclass.constructor.apply(this, arguments);52 this.options = Y.mix(options || {}, {
53 this.options = Y.mix(options || {
54 minZoom: 0.25,53 minZoom: 0.25,
55 maxZoom: 2,54 maxZoom: 2,
56 minSlider: 25,55 minSlider: 25,
57 maxSlider: 20056 maxSlider: 200
58 });57 });
5958 Topology.superclass.constructor.apply(this, arguments);
60 // Build a service.id -> BoundingBox map for services.59 // Build a service.id -> BoundingBox map for services.
61 this.service_boxes = {};60 this.service_boxes = {};
62
63 this._subscriptions = [];61 this._subscriptions = [];
64 },62 },
6563
@@ -111,11 +109,16 @@
111 this.computeScales();109 this.computeScales();
112110
113 // Set up the visualization with a pack layout.111 // Set up the visualization with a pack layout.
114 svg = d3.select(container.getDOMNode())112 var canvas = d3.select(container.getDOMNode());
115 .selectAll('.topology-canvas')113 var base = canvas.select('.topology-canvas');
116 .append('svg:svg')114 if (base.empty()) {
117 .attr('width', width)115 base = canvas.append('div')
118 .attr('height', height);116 .classed('topology-canvas', true);
117 }
118
119 svg = base.append('svg:svg')
120 .attr('width', width)
121 .attr('height', height);
119 this.svg = svg;122 this.svg = svg;
120123
121 this.zoomPlane = svg.append('rect')124 this.zoomPlane = svg.append('rect')
@@ -209,9 +212,9 @@
209 getter: function() {return this.get('size')[1];}212 getter: function() {return this.get('size')[1];}
210 },213 },
211 /*214 /*
212 * Scale and translate are managed by an external module215 Scale and translate are managed by an external module
213 * (PanZoom in this case). If that module isn't216 (PanZoom in this case). If that module isn't
214 * loaded nothing will modify these values.217 loaded nothing will modify these values.
215 */218 */
216 scale: {219 scale: {
217 getter: function() {return this.zoom.scale();},220 getter: function() {return this.zoom.scale();},
218221
=== modified file 'app/views/utils.js'
--- app/views/utils.js 2013-08-19 20:51:17 +0000
+++ app/views/utils.js 2013-08-30 19:48:17 +0000
@@ -564,11 +564,11 @@
564 @property constraintDescriptions564 @property constraintDescriptions
565 */565 */
566 utils.constraintDescriptions = {566 utils.constraintDescriptions = {
567 arch: {title: 'Architecture'},567 'arch': {title: 'Architecture'},
568 cpu: {title: 'CPU', unit: 'GHz'},568 'cpu': {title: 'CPU', unit: 'GHz'},
569 'cpu-cores': {title: 'CPU Cores'},569 'cpu-cores': {title: 'CPU Cores'},
570 'cpu-power': {title: 'CPU Power', unit: 'GHz'},570 'cpu-power': {title: 'CPU Power', unit: 'GHz'},
571 mem: {title: 'Memory', unit: 'GB'}571 'mem': {title: 'Memory', unit: 'GB'}
572 };572 };
573573
574 /**574 /**
@@ -1457,10 +1457,11 @@
1457 debugger;1457 debugger;
1458 /*jshint debug:false */1458 /*jshint debug:false */
1459 });1459 });
1460
1460 /*1461 /*
1461 * Extension for views to provide an apiFailure method.1462 * Extension for views to provide an apiFailure method.
1462 *1463 *
1463 * @class apiFailure1464 * @class apiFailingView
1464 */1465 */
1465 utils.apiFailingView = function() {1466 utils.apiFailingView = function() {
1466 this._initAPIFailingView();1467 this._initAPIFailingView();
14671468
=== modified file 'app/views/viewlets/inspector-header.js'
--- app/views/viewlets/inspector-header.js 2013-08-12 15:13:17 +0000
+++ app/views/viewlets/inspector-header.js 2013-08-30 19:48:17 +0000
@@ -29,35 +29,22 @@
29 name: 'inspectorHeader',29 name: 'inspectorHeader',
30 template: templates['inspector-header'],30 template: templates['inspector-header'],
31 slot: 'header',31 slot: 'header',
32 bindings: {
33 icon: {
34 'update': function(node, value) {
35 // XXX: Icon is only present on services that pass through
36 // the Ghost phase of the GUI. Once we have better integration
37 // with the charm browser API services handling of icon
38 // can be improved.
39 var icon = node.one('img');
40 if (icon === null && value) {
41 node.append('<img>');
42 icon = node.one('img');
43 }
44 if (value) {
45 icon.set('src', value);
46 }
47 }
48 }
49 },
50 'render': function(model, viewContainerAttrs) {32 'render': function(model, viewContainerAttrs) {
51 this.container = Y.Node.create(this.templateWrapper);33 this.container = Y.Node.create(this.templateWrapper);
52 var pojoModel = model.getAttrs();34 var pojoModel = model.getAttrs();
53 if (pojoModel.scheme) {35 if (pojoModel.scheme) {
54 pojoModel.ghost = true;36 pojoModel.ghost = true;
55 }37 }
38 // If this is a service, the id is the service.charm.
56 if (pojoModel.charm) {39 if (pojoModel.charm) {
57 pojoModel.charmUrl = pojoModel.charm;40 pojoModel.charmUrl = pojoModel.charm;
58 } else {41 } else {
42 // If this is a charm model, just use the id.
59 pojoModel.charmUrl = pojoModel.id;43 pojoModel.charmUrl = pojoModel.id;
60 }44 }
45 // Manually add the icon url for the charm since we don't have access to
46 // the browser handlebars helper at this location.
47 pojoModel.icon = viewContainerAttrs.store.iconpath(pojoModel.charmUrl);
61 this.container.setHTML(this.template(pojoModel));48 this.container.setHTML(this.template(pojoModel));
62 }49 }
63 };50 };
6451
=== modified file 'app/views/viewlets/service-constraints.js'
--- app/views/viewlets/service-constraints.js 2013-08-19 16:58:14 +0000
+++ app/views/viewlets/service-constraints.js 2013-08-30 19:48:17 +0000
@@ -58,6 +58,7 @@
58 }58 }
5959
60 };60 };
61
61}, '0.0.1', {62}, '0.0.1', {
62 requires: [63 requires: [
63 'node',64 'node',
6465
=== modified file 'app/views/viewlets/service-ghost.js'
--- app/views/viewlets/service-ghost.js 2013-08-08 17:30:58 +0000
+++ app/views/viewlets/service-ghost.js 2013-08-30 19:48:17 +0000
@@ -43,27 +43,34 @@
43 }43 }
44 }44 }
45 },45 },
46 'render': function(model) {46 'render': function(model, viewletMgrAttrs) {
47 this.container = Y.Node.create(this.templateWrapper);47 this.container = Y.Node.create(this.templateWrapper);
4848
49 // This is to allow for data binding on the ghost settings49 // This is to allow for data binding on the ghost settings
50 // while using a shared template across both inspectors50 // while using a shared template across both inspectors
51 var options = model.getAttrs();51 var templateOptions = model.getAttrs();
5252
53 // XXX - Jeff53 // XXX - Jeff
54 // not sure this should be done like this54 // not sure this should be done like this
55 // but this will allow us to use the old template.55 // but this will allow us to use the old template.
56 options.settings = utils.extractServiceSettings(options.options);56 templateOptions.settings = utils.extractServiceSettings(
57 templateOptions.options);
58
59 templateOptions.constraints = utils.getConstraints(
60 // no current constraints in play.
61 {},
62 viewletMgrAttrs.env.genericConstraints);
5763
58 // Signalling to the shared templates that this is the ghost view.64 // Signalling to the shared templates that this is the ghost view.
59 options.ghost = true;65 templateOptions.ghost = true;
60 this.container.setHTML(this.template(options));66 this.container.setHTML(this.template(templateOptions));
6167
62 this.container.all('textarea.config-field')68 var ResizingTextarea = plugins.ResizingTextArea;
63 .plug(plugins.ResizingTextarea,69 this.container.all('textarea.config-field').plug(ResizingTextarea, {
64 { max_height: 200,70 max_height: 200,
65 min_height: 18,71 min_height: 18,
66 single_line: 18});72 single_line: 18
73 });
67 }74 }
68 };75 };
6976
7077
=== modified file 'app/widgets/viewmode-controls.js'
--- app/widgets/viewmode-controls.js 2013-07-15 13:17:42 +0000
+++ app/widgets/viewmode-controls.js 2013-08-30 19:48:17 +0000
@@ -142,6 +142,16 @@
142 );142 );
143143
144 this._updateActiveNav(this.get('currentViewmode'));144 this._updateActiveNav(this.get('currentViewmode'));
145 this.on('destroy', function(ev) {
146 // We don't actually want the widget to remove any DOM. Just run our
147 // unbinding of events for us.
148 ev.halt();
149 this._events.forEach(function(e) {
150 e.detach();
151 });
152 this._events = [];
153 }, this);
154
145 },155 },
146156
147 /**157 /**
@@ -189,6 +199,83 @@
189 }199 }
190 });200 });
191201
202 /**
203 * Extension for views to provide viewmode controls.
204 *
205 * @class viewmodeControllingView
206 */
207 ns.ViewmodeControlsViewExtension = function() {};
208 ns.ViewmodeControlsViewExtension.prototype = {
209 /**
210 * Binds the viewmode controls on the page to the viewmode change events.
211 *
212 * @method _bindViewmodeControls
213 * @param {Y.Widget} controls The viewmode control widget.
214 */
215 _bindViewmodeControls: function(controls) {
216 this._fullscreen = controls.on(
217 controls.EVT_FULLSCREEN, this._goFullscreen, this);
218 this._sidebar = controls.on(
219 controls.EVT_SIDEBAR, this._goSidebar, this);
220 this._minimized = controls.on(
221 controls.EVT_TOGGLE_VIEWABLE, this._toggleMinimized, this);
222 this._destroyMe = this.on('destroy', function() {
223 // Unbind the View events listening for events from the widget.
224 this._fullscreen.detach();
225 this._sidebar.detach();
226 this._minimized.detach();
227 // Including this event.
228 this._destroyMe.detach();
229 // Finally, make sure we run destroy on the widget itself to unbind
230 // it's own events.
231 controls.destroy();
232 });
233 },
234
235 /**
236 Upon clicking the browser icon make sure we re-route to the
237 new form of the UX.
238
239 @method _goFullscreen
240 @param {Event} ev the click event handler on the button.
241
242 */
243 _goFullscreen: function(ev) {
244 ev.halt();
245 this.fire('viewNavigate', {
246 change: {
247 viewmode: 'fullscreen'
248 }
249 });
250 },
251
252 /**
253 Upon clicking the build icon make sure we re-route to the
254 new form of the UX.
255
256 @method _goSidebar
257 @param {Event} ev the click event handler on the button.
258
259 */
260 _goSidebar: function(ev) {
261 ev.halt();
262 this.fire('viewNavigate', {
263 change: {
264 viewmode: 'sidebar'
265 }
266 });
267 },
268
269 /**
270 * Place holder to toggle the minimized view; in minimized this should show
271 * sidebar, in sidebar this should show minimized.
272 * @method _toggleMinimized
273 * @param {Event} ev event to trigger the toggle.
274 *
275 */
276 _toggleMinimized: function(ev) {}
277 };
278
192}, '0.1.0', {279}, '0.1.0', {
193 requires: [280 requires: [
194 'base',281 'base',
195282
=== modified file 'docs/d3-component-framework.rst'
--- docs/d3-component-framework.rst 2013-01-15 14:39:44 +0000
+++ docs/d3-component-framework.rst 2013-08-30 19:48:17 +0000
@@ -117,6 +117,11 @@
117module declaration, but a module writer must understand them all to properly use117module declaration, but a module writer must understand them all to properly use
118the framework.118the framework.
119119
120When a Component is created it can be in either an interactive or
121non-interactive state. This is controlled through a Boolean 'interactive'
122attribute which defaults to true. When false events will not be bound and this
123section can be skipped.
124
120When modules are added, three sets of declarative events are bound. This is125When modules are added, three sets of declarative events are bound. This is
121done by including in the module an events object with the following (each126done by including in the module an events object with the following (each
122optional) sections::127optional) sections::
123128
=== modified file 'lib/views/browser/charm-full.less'
--- lib/views/browser/charm-full.less 2013-08-13 00:35:01 +0000
+++ lib/views/browser/charm-full.less 2013-08-30 19:48:17 +0000
@@ -86,7 +86,6 @@
86 float: left;86 float: left;
87 }87 }
88 .charm-icon,88 .charm-icon,
89 .category-icon,
90 img.icon {89 img.icon {
91 width: 120px;90 width: 120px;
92 height: 120px;91 height: 120px;
9392
=== modified file 'lib/views/browser/charm-token.less'
--- lib/views/browser/charm-token.less 2013-08-09 14:28:06 +0000
+++ lib/views/browser/charm-token.less 2013-08-30 19:48:17 +0000
@@ -13,15 +13,12 @@
13 min-width: 200px;13 min-width: 200px;
1414
15 .charm-icon,15 .charm-icon,
16 .category-icon,
17 .icon {16 .icon {
18 margin-right: 12px;17 margin-right: 12px;
19 position: relative;18 position: relative;
20 }19 }
21 .charm-icon,20 .charm-icon,
22 .charm-icon img,21 .charm-icon img,
23 .category-icon,
24 .category-icon img,
25 .icon,22 .icon,
26 .icon img {23 .icon img {
27 width: 48px;24 width: 48px;
@@ -39,15 +36,12 @@
39 }36 }
40 &.small {37 &.small {
41 .charm-icon,38 .charm-icon,
42 .category-icon,
43 .icon {39 .icon {
44 margin-right: 10px;40 margin-right: 10px;
45 position: relative;41 position: relative;
46 }42 }
47 .charm-icon,43 .charm-icon,
48 .charm-icon img,44 .charm-icon img,
49 .category-icon,
50 .category-icon img,
51 .icon,45 .icon,
52 .icon img {46 .icon img {
53 width: 50px;47 width: 50px;
@@ -67,15 +61,12 @@
67 }61 }
68 &.large {62 &.large {
69 .charm-icon,63 .charm-icon,
70 .category-icon,
71 .icon {64 .icon {
72 margin-right: 16px;65 margin-right: 16px;
73 position: relative;66 position: relative;
74 }67 }
75 .charm-icon,68 .charm-icon,
76 .charm-icon img,69 .charm-icon img,
77 .category-icon,
78 .category-icon img,
79 .icon,70 .icon,
80 .icon img {71 .icon img {
81 width: 96px;72 width: 96px;
8273
=== modified file 'lib/views/juju-inspector.less'
--- lib/views/juju-inspector.less 2013-08-21 03:20:46 +0000
+++ lib/views/juju-inspector.less 2013-08-30 19:48:17 +0000
@@ -41,7 +41,7 @@
41 position: absolute;41 position: absolute;
42 top: 20px;42 top: 20px;
43 bottom: @navbar-bottom-height + 20px;43 bottom: @navbar-bottom-height + 20px;
44 right: @inspector-width + 35px;44 right: @inspector-width + 71px;
45 overflow-y: auto;45 overflow-y: auto;
46 overflow-x: hidden;46 overflow-x: hidden;
47 width: @bws-panel-width;47 width: @bws-panel-width;
@@ -115,7 +115,7 @@
115 .create-border-radius(@border-radius);115 .create-border-radius(@border-radius);
116 position: absolute;116 position: absolute;
117 top: 20px;117 top: 20px;
118 right: 35px;118 right: 70px;
119 width: @inspector-width;119 width: @inspector-width;
120 min-height: 250px;120 min-height: 250px;
121 border: 0;121 border: 0;
@@ -248,6 +248,9 @@
248 padding-left: 10px;248 padding-left: 10px;
249 cursor: pointer;249 cursor: pointer;
250 }250 }
251 .editable-constraints {
252 padding-left: 10px;
253 }
251 .unit-constraints-confirm {254 .unit-constraints-confirm {
252 overflow: hidden;255 overflow: hidden;
253 height: 130px;256 height: 130px;
@@ -272,6 +275,9 @@
272 .unit-constraints-confirm.closed {275 .unit-constraints-confirm.closed {
273 height: 0;276 height: 0;
274 }277 }
278 .unit-constraints-confirm.editing {
279 height: 300px;
280 }
275 }281 }
276282
277 .settings-wrapper {283 .settings-wrapper {
@@ -290,6 +296,9 @@
290 textarea {296 textarea {
291 width: 100%;297 width: 100%;
292 }298 }
299 input[type=text] {
300 width: 150px;
301 }
293 .settings-description {302 .settings-description {
294 font-size: 12px;303 font-size: 12px;
295304
@@ -389,7 +398,7 @@
389 }398 }
390399
391 /* Resetting panel styles. Can be removed after old inspector styles are400 /* Resetting panel styles. Can be removed after old inspector styles are
392 removed. */401 removed. (ServiceInspector) */
393 .charm-panel-configure {402 .charm-panel-configure {
394 background: transparent;403 background: transparent;
395 float: none;404 float: none;
@@ -468,11 +477,11 @@
468 input.service-name {477 input.service-name {
469 width: 125px;478 width: 125px;
470 }479 }
471 .charm-icon {480 .icon {
472 width: 64px;481 width: 64px;
473 height: 64px;482 height: 64px;
474 margin-right: 1em;483 margin-right: 1em;
475 background: transparent url(/juju-ui/assets/images/charm_64.png) left top no-repeat;484
476 img {485 img {
477 /* bootstrap sets this to middle */486 /* bootstrap sets this to middle */
478 vertical-align: baseline;487 vertical-align: baseline;
@@ -582,8 +591,7 @@
582 font-size: 18px;591 font-size: 18px;
583 padding-left: 0;592 padding-left: 0;
584 }593 }
585 .settings-wrapper,594 .settings-wrapper {
586 .control-group {
587 position: relative;595 position: relative;
588 padding: 0 10px;596 padding: 0 10px;
589597
590598
=== added file 'test/data/wp-deployer.yaml'
--- test/data/wp-deployer.yaml 1970-01-01 00:00:00 +0000
+++ test/data/wp-deployer.yaml 2013-08-30 19:48:17 +0000
@@ -0,0 +1,41 @@
1envExport:
2 series: precise
3 services:
4 mysql:
5 charm: "cs:precise/mysql-27"
6 num_units: 1
7 options:
8 "binlog-format": MIXED
9 "block-size": "5"
10 "dataset-size": "80%"
11 flavor: distro
12 'gui-x': 50
13 'gui-y': 50
14 "ha-bindiface": eth0
15 "ha-mcastport": "5411"
16 "max-connections": "-1"
17 "preferred-storage-engine": InnoDB
18 "query-cache-size": "-1"
19 "query-cache-type": "OFF"
20 "rbd-name": mysql1
21 "tuning-level": safest
22 vip: ""
23 vip_cidr: "24"
24 vip_iface: eth0
25 annotations:
26 "gui-x": 115
27 "gui-y": 89
28 wordpress:
29 charm: "cs:precise/wordpress-16"
30 num_units: 1
31 options:
32 debug: "no"
33 engine: nginx
34 tuning: single
35 "wp-content": ""
36 annotations:
37 "gui-x": 510
38 "gui-y": 184
39 relations:
40 - - "wordpress:db"
41 - "mysql:db"
042
=== modified file 'test/index.html'
--- test/index.html 2013-08-06 04:35:06 +0000
+++ test/index.html 2013-08-30 19:48:17 +0000
@@ -32,7 +32,7 @@
32 should = chai.should();32 should = chai.should();
33 mocha.reporter('html');33 mocha.reporter('html');
34 mocha.ui('bdd');34 mocha.ui('bdd');
35 mocha.setup({ignoreLeaks: false, timeout: 10000})35 mocha.setup({ignoreLeaks: false, timeout: 10000});
36 </script>36 </script>
3737
38 <!-- Load up YUI base, app modules, and test utils -->38 <!-- Load up YUI base, app modules, and test utils -->
@@ -56,6 +56,7 @@
56 <script src="test_browser_models.js"></script>56 <script src="test_browser_models.js"></script>
57 <script src="test_browser_search_view.js"></script>57 <script src="test_browser_search_view.js"></script>
58 <script src="test_browser_search_widget.js"></script>58 <script src="test_browser_search_widget.js"></script>
59 <script src="test_bundle_module.js"></script>
59 <script src="test_charm_configuration.js"></script>60 <script src="test_charm_configuration.js"></script>
60 <script src="test_charm_container.js"></script>61 <script src="test_charm_container.js"></script>
61 <script src="test_charm_panel.js"></script>62 <script src="test_charm_panel.js"></script>
6263
=== modified file 'test/test_app.js'
--- test/test_app.js 2013-07-31 14:30:47 +0000
+++ test/test_app.js 2013-08-30 19:48:17 +0000
@@ -160,7 +160,7 @@
160160
161 it('should be able to route objects to internal URLs', function() {161 it('should be able to route objects to internal URLs', function() {
162 constructAppInstance({162 constructAppInstance({
163 env: juju.newEnvironment({ conn: new utils.SocketStub() })163 env: juju.newEnvironment({conn: new utils.SocketStub()}, 'python')
164 });164 });
165 // Take handles to database objects and ensure we can route to the view165 // Take handles to database objects and ensure we can route to the view
166 // needed to show them.166 // needed to show them.
@@ -241,6 +241,14 @@
241 })241 })
242 });242 });
243243
244 // XXX bug:1217383
245 // Force an app._controlEvents so that we don't try to bind viewmode
246 // controls.
247 var fakeEv = {
248 detach: function() {}
249 };
250 app._controlEvents = [fakeEv, fakeEv];
251
244 var checkUrls = [{252 var checkUrls = [{
245 url: ':gui:/service/memcached/',253 url: ':gui:/service/memcached/',
246 hidden: true254 hidden: true
@@ -329,6 +337,12 @@
329 return app;337 return app;
330 };338 };
331339
340 // Ensure the given message is a login request.
341 var assertIsLogin = function(message) {
342 assert.equal('Admin', message.Type);
343 assert.equal('Login', message.Request);
344 };
345
332 it('avoids trying to login if the env is not connected', function(done) {346 it('avoids trying to login if the env is not connected', function(done) {
333 var app = makeApp(false); // Create a disconnected app.347 var app = makeApp(false); // Create a disconnected app.
334 app.after('ready', function() {348 app.after('ready', function() {
@@ -341,7 +355,7 @@
341 var app = makeApp(true); // Create a connected app.355 var app = makeApp(true); // Create a connected app.
342 app.after('ready', function() {356 app.after('ready', function() {
343 assert.equal(1, conn.messages.length);357 assert.equal(1, conn.messages.length);
344 assert.equal('login', conn.last_message().op);358 assertIsLogin(conn.last_message());
345 done();359 done();
346 });360 });
347 });361 });
@@ -360,21 +374,22 @@
360 it('displays the login view if credentials are not valid', function(done) {374 it('displays the login view if credentials are not valid', function(done) {
361 var app = makeApp(true); // Create a connected app.375 var app = makeApp(true); // Create a connected app.
362 app.after('ready', function() {376 app.after('ready', function() {
363 // Mimic a login failed response.377 app.env.login();
364 conn.msg({op: 'login', result: false});378 // Mimic a login failed response assuming login is the first request.
379 conn.msg({RequestId: 1, Error: 'Invalid user or password'});
365 assert.equal(1, conn.messages.length);380 assert.equal(1, conn.messages.length);
366 assert.equal('login', conn.last_message().op);381 assertIsLogin(conn.last_message());
367 assert.equal(LOGIN_VIEW_NAME, app.get('activeView').name);382 assert.equal(LOGIN_VIEW_NAME, app.get('activeView').name);
368 done();383 done();
369 });384 });
370 });385 });
371386
372 it('login method hanlder is called after successful login', function(done) {387 it('login method handler is called after successful login', function(done) {
373 var oldOnLogin = Y.juju.App.onLogin;388 var oldOnLogin = Y.juju.App.onLogin;
374 Y.juju.App.prototype.onLogin = function(e) {389 Y.juju.App.prototype.onLogin = function(e) {
375 assert.equal(conn.messages.length, 1);390 assert.equal(conn.messages.length, 1);
376 assert.equal(conn.last_message().op, 'login');391 assertIsLogin(conn.last_message());
377 assert.equal(e.data.result, true);392 assert.isTrue(e.data.result, true);
378 Y.juju.App.onLogin = oldOnLogin;393 Y.juju.App.onLogin = oldOnLogin;
379 done();394 done();
380 };395 };
@@ -391,7 +406,7 @@
391 app.after('ready', function() {406 app.after('ready', function() {
392 env.connect();407 env.connect();
393 assert.equal(1, conn.messages.length);408 assert.equal(1, conn.messages.length);
394 assert.equal('login', conn.last_message().op);409 assertIsLogin(conn.last_message());
395 done();410 done();
396 });411 });
397 });412 });
@@ -403,10 +418,8 @@
403 // Disconnect and reconnect the WebSocket.418 // Disconnect and reconnect the WebSocket.
404 conn.transient_close();419 conn.transient_close();
405 conn.open();420 conn.open();
406 assert.equal(2, conn.messages.length);421 assert.equal(1, conn.messages.length);
407 Y.each(conn.messages, function(message) {422 assertIsLogin(conn.last_message());
408 assert.equal('login', message.op);
409 });
410 done();423 done();
411 });424 });
412 });425 });
413426
=== modified file 'test/test_browser_app.js'
--- test/test_browser_app.js 2013-08-19 17:15:16 +0000
+++ test/test_browser_app.js 2013-08-30 19:48:17 +0000
@@ -181,6 +181,54 @@
181 })();181 })();
182182
183 (function() {183 (function() {
184 describe('browser minimzed view', function() {
185 var Y, browser, container, view, views, Minimized;
186
187 before(function(done) {
188 Y = YUI(GlobalConfig).use(
189 'juju-browser',
190 'juju-models',
191 'juju-views',
192 'juju-tests-utils',
193 'subapp-browser-minimized',
194 function(Y) {
195 browser = Y.namespace('juju.browser');
196 views = Y.namespace('juju.browser.views');
197 Minimized = views.MinimizedView;
198 done();
199 });
200 });
201
202 beforeEach(function() {
203 container = Y.namespace('juju-tests.utils').makeContainer('container');
204 addBrowserContainer(Y, container);
205 // Mock out a dummy location for the Store used in view instances.
206 window.juju_config = {
207 charmworldURL: 'http://localhost'
208 };
209 });
210
211 afterEach(function() {
212 view.destroy();
213 Y.one('#subapp-browser').remove(true);
214 delete window.juju_config;
215 container.remove(true);
216 });
217
218 it('toggles to sidebar', function(done) {
219 var container = Y.one('#subapp-browser');
220 view = new Minimized();
221 view.on('viewNavigate', function(ev) {
222 assert(ev.change.viewmode === 'sidebar');
223 done();
224 });
225 view.render(container);
226 view.controls._toggleViewable({halt: function() {}});
227 });
228 });
229 })();
230
231 (function() {
184 describe('browser sidebar view', function() {232 describe('browser sidebar view', function() {
185 var Y, browser, container, view, views, Sidebar;233 var Y, browser, container, view, views, Sidebar;
186234
@@ -332,13 +380,14 @@
332380
333 (function() {381 (function() {
334 describe('browser app', function() {382 describe('browser app', function() {
335 var Y, app, browser, Charmworld2, next;383 var Y, app, browser, Charmworld2, container, next;
336384
337 before(function(done) {385 before(function(done) {
338 Y = YUI(GlobalConfig).use(386 Y = YUI(GlobalConfig).use(
339 'app-subapp-extension',387 'app-subapp-extension',
340 'juju-browser',388 'juju-browser',
341 'juju-charm-store',389 'juju-charm-store',
390 'juju-tests-utils',
342 'juju-views',391 'juju-views',
343 'subapp-browser', function(Y) {392 'subapp-browser', function(Y) {
344 browser = Y.namespace('juju.subapps');393 browser = Y.namespace('juju.subapps');
@@ -353,12 +402,16 @@
353 window.juju_config = {402 window.juju_config = {
354 charmworldURL: 'http://localhost'403 charmworldURL: 'http://localhost'
355 };404 };
405 container = Y.namespace('juju-tests.utils').makeContainer('container');
406 addBrowserContainer(Y, container);
407
356 });408 });
357409
358 afterEach(function() {410 afterEach(function() {
359 if (app) {411 if (app) {
360 app.destroy();412 app.destroy();
361 }413 }
414 container.remove(true);
362 window.juju_config = undefined;415 window.juju_config = undefined;
363 });416 });
364417
@@ -383,6 +436,28 @@
383 assert.isTrue(called);436 assert.isTrue(called);
384 });437 });
385438
439 it('resets using initState', function() {
440 app = new browser.Browser();
441 var mockView = {
442 destroy: function() {}
443 };
444 app._sidebar = mockView;
445 app._minimized = mockView;
446 app._fullscreen = mockView;
447
448 // Setup some previous state to check for clearing.
449 app._oldState.viewmode = 'fullscreen';
450 app._viewState.viewmode = 'sidebar';
451
452 app.initState();
453
454 assert.equal(app._sidebar, undefined, 'sidebar is removed');
455 assert.equal(app._fullscreen, undefined, 'fullscreen is removed');
456 assert.equal(app._minimized, undefined, 'minimized is removed');
457 assert.equal(app._oldState.viewmode, null, 'old state is reset');
458 assert.equal(app._viewState.viewmode, null, 'view state is reset');
459 });
460
386 it('correctly strips viewmode from the charmID', function() {461 it('correctly strips viewmode from the charmID', function() {
387 app = new browser.Browser();462 app = new browser.Browser();
388 var paths = [463 var paths = [
@@ -609,6 +684,7 @@
609 'app-subapp-extension',684 'app-subapp-extension',
610 'juju-views',685 'juju-views',
611 'juju-browser',686 'juju-browser',
687 'juju-tests-utils',
612 'subapp-browser', function(Y) {688 'subapp-browser', function(Y) {
613 browser = Y.namespace('juju.subapps');689 browser = Y.namespace('juju.subapps');
614690
@@ -723,6 +799,8 @@
723 it('resets filters when navigating away from search', function() {799 it('resets filters when navigating away from search', function() {
724 browser._viewState.search = true;800 browser._viewState.search = true;
725 browser._filter.set('text', 'foo');801 browser._filter.set('text', 'foo');
802 // Set the state before changing up.
803 browser._saveState();
726 browser._getStateUrl({search: false});804 browser._getStateUrl({search: false});
727 assert.equal('', browser._filter.get('text'));805 assert.equal('', browser._filter.get('text'));
728 });806 });
@@ -1143,6 +1221,18 @@
11431221
1144 it('when hidden the browser avoids routing', function() {1222 it('when hidden the browser avoids routing', function() {
1145 browser.hidden = true;1223 browser.hidden = true;
1224 // XXX bug:1217383
1225 // We also want to verify that the old views are cleared to avoid
1226 // having hidden views doing UX work for us.
1227 var hitCount = 0;
1228 var mockView = {
1229 destroy: function() {
1230 hitCount = hitCount + 1;
1231 }
1232 };
1233 browser._sidebar = mockView;
1234 browser._minimized = mockView;
1235 browser._fullscreen = mockView;
11461236
1147 var req = {1237 var req = {
1148 path: '/minimized',1238 path: '/minimized',
@@ -1161,6 +1251,13 @@
11611251
1162 minNode.getComputedStyle('display').should.eql('none');1252 minNode.getComputedStyle('display').should.eql('none');
1163 browserNode.getComputedStyle('display').should.eql('none');1253 browserNode.getComputedStyle('display').should.eql('none');
1254
1255 assert.equal(hitCount, 3);
1256
1257 // The view state needs to also be sync'd and updated even though
1258 // we're hidden so that we can detect changes in the app state across
1259 // requests while hidden.
1260 assert.equal(browser._oldState.viewmode, 'minimized');
1164 });1261 });
11651262
1166 it('knows when the search cache should be updated', function() {1263 it('knows when the search cache should be updated', function() {
@@ -1169,16 +1266,19 @@
1169 'querystring': 'text=apache'1266 'querystring': 'text=apache'
1170 });1267 });
1171 assert.isTrue(browser._searchChanged());1268 assert.isTrue(browser._searchChanged());
1269 browser._saveState();
1172 browser._getStateUrl({1270 browser._getStateUrl({
1173 'search': true,1271 'search': true,
1174 'querystring': 'text=apache'1272 'querystring': 'text=apache'
1175 });1273 });
1176 assert.isFalse(browser._searchChanged());1274 assert.isFalse(browser._searchChanged());
1275 browser._saveState();
1177 browser._getStateUrl({1276 browser._getStateUrl({
1178 'search': true,1277 'search': true,
1179 'querystring': 'text=ceph'1278 'querystring': 'text=ceph'
1180 });1279 });
1181 assert.isTrue(browser._searchChanged());1280 assert.isTrue(browser._searchChanged());
1281 browser._saveState();
1182 });1282 });
11831283
1184 it('permits a filter clear command', function() {1284 it('permits a filter clear command', function() {
11851285
=== modified file 'test/test_browser_editorial.js'
--- test/test_browser_editorial.js 2013-07-16 19:05:46 +0000
+++ test/test_browser_editorial.js 2013-08-30 19:48:17 +0000
@@ -140,26 +140,6 @@
140 assert(node.all('.yui3-charmtoken-hidden').size() === 14);140 assert(node.all('.yui3-charmtoken-hidden').size() === 14);
141 });141 });
142142
143 it('does a search when a category link is clicked', function(done) {
144 view = new EditorialView({
145 renderTo: Y.one('.bws-content')
146 });
147 var results = {
148 featuredCharms: [],
149 newCharms: [],
150 popularCharms: []
151 };
152 view.on('viewNavigate', function(ev) {
153 assert.isTrue(ev.change.search);
154 assert.equal(1, ev.change.filter.categories.length);
155 assert.equal('databases', ev.change.filter.categories[0]);
156 assert.equal(ev.change.filter.replace, true);
157 done();
158 });
159 view.render(results);
160 Y.one('#category-links').one('a').simulate('click');
161 });
162
163 it('clicking a charm navigates for fullscreen', function(done) {143 it('clicking a charm navigates for fullscreen', function(done) {
164 fakeStore = new Y.juju.Charmworld2({});144 fakeStore = new Y.juju.Charmworld2({});
165 fakeStore.set('datasource', {145 fakeStore.set('datasource', {
166146
=== added file 'test/test_bundle_module.js'
--- test/test_bundle_module.js 1970-01-01 00:00:00 +0000
+++ test/test_bundle_module.js 2013-08-30 19:48:17 +0000
@@ -0,0 +1,98 @@
1/*
2This file is part of the Juju GUI, which lets users view and manage Juju
3environments within a graphical interface (https://launchpad.net/juju-gui).
4Copyright (C) 2012-2013 Canonical Ltd.
5
6This program is free software: you can redistribute it and/or modify it under
7the terms of the GNU Affero General Public License version 3, as published by
8the Free Software Foundation.
9
10This program is distributed in the hope that it will be useful, but WITHOUT
11ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
12SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
13General Public License for more details.
14
15You should have received a copy of the GNU Affero General Public License along
16with this program. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19'use strict';
20
21describe('bundle module', function() {
22 var db, juju, models, utils, views, Y, bundleModule;
23 var bundle, container, fakeStore;
24
25 before(function(done) {
26 Y = YUI(GlobalConfig).use([
27 'juju-topology',
28 'juju-view-bundle',
29 'juju-charm-store',
30 'juju-models',
31 'juju-tests-utils'
32 ],
33 function(Y) {
34 juju = Y.namespace('juju');
35 models = Y.namespace('juju.models');
36 utils = Y.namespace('juju-tests.utils');
37 views = Y.namespace('juju.views');
38 done();
39 });
40 });
41
42 beforeEach(function() {
43 container = utils.makeContainer();
44 db = new models.Database();
45 });
46
47 afterEach(function() {
48 if (container) { container.remove(true); }
49 if (bundle) { bundle.destroy(); }
50
51 });
52
53 function promiseBundle() {
54 db.environment.set('defaultSeries', 'precise');
55 var fakeStore = utils.makeFakeStore(db.charms);
56 fakeStore.iconpath = function() { return 'fake.svg'; };
57
58 return db.importDeployer(
59 jsyaml.safeLoad(utils.loadFixture('data/wp-deployer.yaml')),
60 fakeStore, {useGhost: true, targetBundle: 'wordpress-prod'})
61 .then(function() {
62 bundle = new views.BundleTopology({
63 db: db,
64 container: container,
65 store: fakeStore
66 }).render();
67 bundleModule = bundle.topology.modules.BundleModule;
68 bundleModule.set('useTransitions', false);
69 return bundle;
70 });
71 }
72
73 it('should create a proper service for each model', function(done) {
74 promiseBundle()
75 .then(function(bundle) {
76 // The size of the element should reflect the passed in params
77 var svg = d3.select(container.getDOMNode()).select('svg');
78 assert.equal(svg.attr('width'), 640);
79 assert.equal(svg.attr('height'), 480);
80
81 // We should have the two rendered services
82 assert.equal(container.all('.service').size(), 2);
83 assert.deepEqual(container.all('tspan.name').get('text'),
84 ['mysql', 'wordpress']);
85 var service = svg.select('.service');
86 assert.equal(service.attr('width'), '96');
87 assert.equal(service.attr('transform'), 'translate(115,89)');
88
89
90 container.remove(true);
91 bundle.destroy();
92 done();
93 }).then(undefined, done);
94
95 });
96
97});
98
099
=== modified file 'test/test_charm_panel.js'
--- test/test_charm_panel.js 2013-08-12 14:58:23 +0000
+++ test/test_charm_panel.js 2013-08-30 19:48:17 +0000
@@ -118,7 +118,8 @@
118 serviceName = 'membase';118 serviceName = 'membase';
119 // Mock the relevant environment calls.119 // Mock the relevant environment calls.
120 env = {120 env = {
121 deploy: function(url, service, config, config_raw, units, callback) {121 deploy: function(url, service, config, config_raw, units,
122 constraints, callback) {
122 callback({err: false});123 callback({err: false});
123 },124 },
124 update_annotations: function(service, type, annotations, callback) {125 update_annotations: function(service, type, annotations, callback) {
125126
=== modified file 'test/test_charm_store.js'
--- test/test_charm_store.js 2013-08-21 16:02:10 +0000
+++ test/test_charm_store.js 2013-08-30 19:48:17 +0000
@@ -21,15 +21,17 @@
21(function() {21(function() {
2222
23 describe('juju Charmworld2 api', function() {23 describe('juju Charmworld2 api', function() {
24 var Y, models, conn, env, app, container, data, juju;24 var Y, models, conn, env, app, container, data, juju, utils;
2525
26 before(function(done) {26 before(function(done) {
27 Y = YUI(GlobalConfig).use(27 Y = YUI(GlobalConfig).use(
28 'datasource-local', 'json-stringify', 'juju-charm-store',28 'datasource-local', 'json-stringify', 'juju-charm-store',
29 'datasource-io', 'io', 'array-extras', 'juju-charm-models',29 'datasource-io', 'io', 'array-extras', 'juju-charm-models',
30 'juju-tests-utils',
30 function(Y) {31 function(Y) {
31 juju = Y.namespace('juju');32 juju = Y.namespace('juju');
32 models = Y.namespace('juju.models');33 models = Y.namespace('juju.models');
34 utils = Y.namespace('juju-tests').utils;
33 done();35 done();
34 });36 });
35 });37 });
@@ -210,6 +212,33 @@
210 assert.equal(api.normalizeCharmId('cs:precise/wordpress-10'),212 assert.equal(api.normalizeCharmId('cs:precise/wordpress-10'),
211 'precise/wordpress-10');213 'precise/wordpress-10');
212 });214 });
215
216 it('finds upgrades for charms - upgrade available', function(done) {
217 var store = utils.makeFakeStore();
218 var charm = new models.BrowserCharm({url: 'cs:precise/wordpress-10'});
219 store.promiseUpgradeAvailability(charm)
220 .then(function(upgrade) {
221 assert.equal(upgrade, 'precise/wordpress-15');
222 done();
223 }, function(error) {
224 assert.isTrue(false, 'We should not get here');
225 done();
226 });
227 });
228
229 it('finds upgrades for charms - no upgrade available', function(done) {
230 var store = utils.makeFakeStore();
231 var charm = new models.BrowserCharm({url: 'cs:precise/wordpress-15'});
232 store.promiseUpgradeAvailability(charm)
233 .then(function(upgrade) {
234 assert.isUndefined(upgrade);
235 done();
236 }, function(error) {
237 assert.isTrue(false, 'We should not get here');
238 done();
239 });
240 });
241
213 });242 });
214243
215})();244})();
216245
=== modified file 'test/test_charm_view.js'
--- test/test_charm_view.js 2013-07-26 22:05:56 +0000
+++ test/test_charm_view.js 2013-08-30 19:48:17 +0000
@@ -138,14 +138,12 @@
138 });138 });
139 var deployInput = charmView.get('container').one('#charm-deploy');139 var deployInput = charmView.get('container').one('#charm-deploy');
140 deployInput.after('click', function() {140 deployInput.after('click', function() {
141 var msg = conn.last_message(),141 var msg = conn.last_message();
142 charm = charmResults.charm,
143 expected = charm.series + '/' + charm.name;
144 // Ensure the websocket received the `deploy` message.142 // Ensure the websocket received the `deploy` message.
145 msg.op.should.equal('deploy');143 assert.equal('ServiceDeploy', msg.Request);
146 msg.charm_url.should.contain(expected);144 assert.equal(charmResults.charm.id, msg.Params.CharmUrl);
147 // A click to the deploy button redirects to the root page.145 // A click to the deploy button redirects to the root page.
148 redirected.should.equal(true);146 assert.isTrue(redirected);
149 done();147 done();
150 });148 });
151 deployInput.simulate('click');149 deployInput.simulate('click');
@@ -164,8 +162,8 @@
164 // Assertions are in a callback, so set them up first.162 // Assertions are in a callback, so set them up first.
165 deployButton.after('click', function() {163 deployButton.after('click', function() {
166 var msg = conn.last_message();164 var msg = conn.last_message();
167 assert.equal(msg.op, 'deploy');165 assert.equal('ServiceDeploy', msg.Request);
168 assert.equal(msg.service_name, serviceName);166 assert.equal(serviceName, msg.Params.ServiceName);
169 done();167 done();
170 });168 });
171 var serviceNameField = container.one('#service-name');169 var serviceNameField = container.one('#service-name');
@@ -188,9 +186,10 @@
188 // Assertions are in a callback, so set them up first.186 // Assertions are in a callback, so set them up first.
189 deployButton.after('click', function() {187 deployButton.after('click', function() {
190 var msg = conn.last_message();188 var msg = conn.last_message();
191 assert.equal(msg.op, 'deploy');189 assert.equal('ServiceDeploy', msg.Request);
192 assert.property(msg.config, 'option0');190 var config = msg.Params.Config;
193 assert.equal(msg.config.option0, option0Value);191 assert.property(config, 'option0');
192 assert.equal(option0Value, config.option0);
194 done();193 done();
195 });194 });
196 container.one('#input-option0').set('value', option0Value);195 container.one('#input-option0').set('value', option0Value);
@@ -225,7 +224,7 @@
225 // Assertions are in a callback, so set them up first.224 // Assertions are in a callback, so set them up first.
226 deployButton.after('click', function() {225 deployButton.after('click', function() {
227 var msg = conn.last_message();226 var msg = conn.last_message();
228 assert.equal(msg.op, 'deploy');227 assert.equal('ServiceDeploy', msg.Request);
229 done();228 done();
230 });229 });
231 deployButton.simulate('click');230 deployButton.simulate('click');
232231
=== modified file 'test/test_endpoints.js'
--- test/test_endpoints.js 2013-08-09 14:44:16 +0000
+++ test/test_endpoints.js 2013-08-30 19:48:17 +0000
@@ -378,6 +378,7 @@
378378
379 it('should update endpoints map when non-pending services are added',379 it('should update endpoints map when non-pending services are added',
380 function(done) {380 function(done) {
381 var store = utils.makeFakeStore();
381 var service_name = 'wordpress';382 var service_name = 'wordpress';
382 var charm_id = 'cs:precise/wordpress-2';383 var charm_id = 'cs:precise/wordpress-2';
383 app.db.charms.add({id: charm_id});384 app.db.charms.add({id: charm_id});
@@ -388,6 +389,7 @@
388 id: service_name,389 id: service_name,
389 pending: true,390 pending: true,
390 loaded: true,391 loaded: true,
392 store: store,
391 charm: charm_id});393 charm: charm_id});
392394
393 controller.on('endpointMapAdded', function() {395 controller.on('endpointMapAdded', function() {
@@ -535,7 +537,13 @@
535 });537 });
536 });538 });
537539
538 it('should not call get_service when a pending service is added',540 // Ensure the last message in the connection is a ServiceGet request.
541 var assertServiceGetCalled = function() {
542 assert.equal(1, conn.messages.length);
543 assert.equal('ServiceGet', conn.last_message().Request);
544 };
545
546 it('should not call ServiceGet when a pending service is added',
539 function() {547 function() {
540 var charm_id = 'cs:precise/wordpress-2';548 var charm_id = 'cs:precise/wordpress-2';
541 app.db.services.add({549 app.db.services.add({
@@ -545,7 +553,7 @@
545 assert.equal(0, conn.messages.length);553 assert.equal(0, conn.messages.length);
546 });554 });
547555
548 it('should call get_service when non-pending services are added',556 it('should call ServiceGet when non-pending services are added',
549 function() {557 function() {
550 var service_name = 'wordpress';558 var service_name = 'wordpress';
551 var charm_id = 'cs:precise/wordpress-2';559 var charm_id = 'cs:precise/wordpress-2';
@@ -558,11 +566,10 @@
558 charm: charm_id});566 charm: charm_id});
559 var svc = app.db.services.getById(service_name);567 var svc = app.db.services.getById(service_name);
560 svc.set('pending', false);568 svc.set('pending', false);
561 assert.equal(1, conn.messages.length);569 assertServiceGetCalled();
562 assert.equal('get_service', conn.last_message().op);
563 });570 });
564571
565 it('should call get_service when a service\'s charm changes', function() {572 it('should call ServiceGet when a service\'s charm changes', function() {
566 var service_name = 'wordpress';573 var service_name = 'wordpress';
567 var charm_id = 'cs:precise/wordpress-2';574 var charm_id = 'cs:precise/wordpress-2';
568 var charm = app.db.charms.add({id: charm_id});575 var charm = app.db.charms.add({id: charm_id});
@@ -572,15 +579,13 @@
572 id: service_name,579 id: service_name,
573 pending: false,580 pending: false,
574 charm: charm_id});581 charm: charm_id});
575 assert.equal(1, conn.messages.length);582 assertServiceGetCalled();
576 assert.equal('get_service', conn.last_message().op);
577 var svc = app.db.services.getById(service_name);583 var svc = app.db.services.getById(service_name);
578 charm_id = 'cs:precise/wordpress-3';584 charm_id = 'cs:precise/wordpress-3';
579 var charm2 = app.db.charms.add({id: charm_id, loaded: true});585 var charm2 = app.db.charms.add({id: charm_id, loaded: true});
580 destroyMe.push(charm2);586 destroyMe.push(charm2);
581 svc.set('charm', charm_id);587 svc.set('charm', charm_id);
582 assert.equal(1, conn.messages.length);588 assertServiceGetCalled();
583 assert.equal('get_service', conn.last_message().op);
584 });589 });
585590
586});591});
587592
=== modified file 'test/test_env.js'
--- test/test_env.js 2013-07-25 16:59:30 +0000
+++ test/test_env.js 2013-08-30 19:48:17 +0000
@@ -43,12 +43,12 @@
4343
44 it('returns the default env if none is specified', function() {44 it('returns the default env if none is specified', function() {
45 var env = juju.newEnvironment({});45 var env = juju.newEnvironment({});
46 assert.equal('python-env', env.name);46 assert.equal('go-env', env.name);
47 });47 });
4848
49 it('returns the default env if an invalid one is specified', function() {49 it('returns the default env if an invalid one is specified', function() {
50 var env = juju.newEnvironment({}, 'invalid-api-backend');50 var env = juju.newEnvironment({}, 'invalid-api-backend');
51 assert.equal('python-env', env.name);51 assert.equal('go-env', env.name);
52 });52 });
5353
54 it('sets up the env using the provided options', function() {54 it('sets up the env using the provided options', function() {
5555
=== modified file 'test/test_env_go.js'
--- test/test_env_go.js 2013-06-12 18:11:05 +0000
+++ test/test_env_go.js 2013-08-30 19:48:17 +0000
@@ -55,6 +55,7 @@
5555
56 });56 });
5757
58
58 describe('Go Juju JSON replacer', function() {59 describe('Go Juju JSON replacer', function() {
59 var cleanUpJSON, Y;60 var cleanUpJSON, Y;
6061
@@ -417,6 +418,7 @@
417 Request: 'ServiceDeploy',418 Request: 'ServiceDeploy',
418 Params: {419 Params: {
419 Config: {},420 Config: {},
421 Constraints: {},
420 CharmUrl: 'precise/mysql'422 CharmUrl: 'precise/mysql'
421 },423 },
422 RequestId: 1424 RequestId: 1
@@ -432,6 +434,7 @@
432 Params: {434 Params: {
433 // Configuration values are sent as strings.435 // Configuration values are sent as strings.
434 Config: {debug: 'true', logo: 'example.com/mylogo.png'},436 Config: {debug: 'true', logo: 'example.com/mylogo.png'},
437 Constraints: {},
435 CharmUrl: 'precise/mediawiki'438 CharmUrl: 'precise/mediawiki'
436 },439 },
437 RequestId: 1440 RequestId: 1
@@ -450,6 +453,7 @@
450 Request: 'ServiceDeploy',453 Request: 'ServiceDeploy',
451 Params: {454 Params: {
452 Config: {},455 Config: {},
456 Constraints: {},
453 ConfigYAML: config_raw,457 ConfigYAML: config_raw,
454 CharmUrl: 'precise/mysql'458 CharmUrl: 'precise/mysql'
455 },459 },
@@ -460,15 +464,29 @@
460 assert.deepEqual(expected, msg);464 assert.deepEqual(expected, msg);
461 });465 });
462466
467 it('successfully deploys a service with constraints', function() {
468 var constraints = {
469 'cpu-cores': 1,
470 'cpu-power': 0,
471 'mem': '512M',
472 'arch': 'i386'
473 };
474 env.deploy('precise/mediawiki', null, null, null, 1, constraints);
475 msg = conn.last_message();
476 assert.deepEqual(msg.Params.Constraints, constraints);
477 });
478
463 it('successfully deploys a service storing charm data', function() {479 it('successfully deploys a service storing charm data', function() {
464 var charm_url;480 var charm_url;
465 var err;481 var err;
466 var service_name;482 var service_name;
467 env.deploy('precise/mysql', 'mysql', null, null, null, function(data) {483 env.deploy(
468 charm_url = data.charm_url;484 'precise/mysql', 'mysql', null, null, null, null, function(data) {
469 err = data.err;485 charm_url = data.charm_url;
470 service_name = data.service_name;486 err = data.err;
471 });487 service_name = data.service_name;
488 }
489 );
472 // Mimic response.490 // Mimic response.
473 conn.msg({491 conn.msg({
474 RequestId: 1,492 RequestId: 1,
@@ -481,9 +499,11 @@
481499
482 it('handles failed service deploy', function() {500 it('handles failed service deploy', function() {
483 var err;501 var err;
484 env.deploy('precise/mysql', 'mysql', null, null, null, function(data) {502 env.deploy(
485 err = data.err;503 'precise/mysql', 'mysql', null, null, null, null, function(data) {
486 });504 err = data.err;
505 }
506 );
487 // Mimic response.507 // Mimic response.
488 conn.msg({508 conn.msg({
489 RequestId: 1,509 RequestId: 1,
490510
=== modified file 'test/test_env_python.js'
--- test/test_env_python.js 2013-05-17 14:51:05 +0000
+++ test/test_env_python.js 2013-08-30 19:48:17 +0000
@@ -37,7 +37,7 @@
37 testUtils = Y.namespace('juju-tests.utils');37 testUtils = Y.namespace('juju-tests.utils');
38 conn = new testUtils.SocketStub();38 conn = new testUtils.SocketStub();
39 juju = Y.namespace('juju');39 juju = Y.namespace('juju');
40 env = juju.newEnvironment({conn: conn});40 env = juju.newEnvironment({conn: conn}, 'python');
41 env.connect();41 env.connect();
42 conn.open();42 conn.open();
43 done();43 done();
@@ -70,6 +70,17 @@
70 msg.config_raw.should.equal(config_raw);70 msg.config_raw.should.equal(config_raw);
71 });71 });
7272
73 it('successfully deploys a service with constraints', function() {
74 var constraints = {
75 'cpu': 1,
76 'mem': '512M',
77 'arch': 'i386'
78 };
79 env.deploy('precise/mysql', null, null, null, 1, constraints);
80 msg = conn.last_message();
81 assert.deepEqual(msg.constraints, constraints);
82 });
83
73 it('can add a unit', function() {84 it('can add a unit', function() {
74 env.add_unit('mysql', 3);85 env.add_unit('mysql', 3);
75 msg = conn.last_message();86 msg = conn.last_message();
@@ -442,7 +453,7 @@
442453
443 it('denies deploying a charm if the GUI is read-only', function() {454 it('denies deploying a charm if the GUI is read-only', function() {
444 assertOperationDenied(455 assertOperationDenied(
445 'deploy', ['cs:precise/haproxy', 'haproxy', {}, null, 3]);456 'deploy', ['cs:precise/haproxy', 'haproxy', {}, null, 3, null]);
446 });457 });
447458
448 it('denies exposing a service if the GUI is read-only', function() {459 it('denies exposing a service if the GUI is read-only', function() {
449460
=== modified file 'test/test_fakebackend.js'
--- test/test_fakebackend.js 2013-07-30 21:00:47 +0000
+++ test/test_fakebackend.js 2013-08-30 19:48:17 +0000
@@ -134,7 +134,9 @@
134 pending: false,134 pending: false,
135 life: 'alive',135 life: 'alive',
136 subordinate: false,136 subordinate: false,
137 unit_count: undefined137 unit_count: undefined,
138 upgrade_available: false,
139 upgrade_to: undefined
138 });140 });
139 var units = fakebackend.db.units.get_units_for_service(service);141 var units = fakebackend.db.units.get_units_for_service(service);
140 assert.lengthOf(units, 1);142 assert.lengthOf(units, 1);
@@ -143,6 +145,30 @@
143 assert.equal(units[0].service, 'wordpress');145 assert.equal(units[0].service, 'wordpress');
144 });146 });
145147
148 it('deploys a charm with constraints', function() {
149 var options = {
150 constraints: {
151 cpu: 1,
152 mem: '4G',
153 arch: 'i386'
154 }
155 };
156 assert.isNull(
157 fakebackend.db.charms.getById('cs:precise/wordpress-15'));
158 fakebackend.deploy('cs:precise/wordpress-15', callback, options);
159 var service = fakebackend.db.services.getById('wordpress');
160 assert.isObject(
161 service,
162 'Null returend when a service was expected.');
163 assert.strictEqual(service, result.service);
164 var attrs = service.getAttrs();
165 var deployedConstraints = attrs.constraints;
166 assert.deepEqual(
167 options.constraints,
168 deployedConstraints
169 );
170 });
171
146 it('rejects names that duplicate an existing service', function() {172 it('rejects names that duplicate an existing service', function() {
147 fakebackend.deploy('cs:precise/wordpress-15', callback);173 fakebackend.deploy('cs:precise/wordpress-15', callback);
148 assert.isUndefined(result.error);174 assert.isUndefined(result.error);
149175
=== modified file 'test/test_ghost_inspector.js'
--- test/test_ghost_inspector.js 2013-08-15 00:40:51 +0000
+++ test/test_ghost_inspector.js 2013-08-30 19:48:17 +0000
@@ -70,8 +70,8 @@
70 service = db.services.ghostService(charm);70 service = db.services.ghostService(charm);
7171
72 var fakeStore = new Y.juju.Charmworld2({});72 var fakeStore = new Y.juju.Charmworld2({});
73 fakeStore.iconpath = function() {73 fakeStore.iconpath = function(id) {
74 return 'charm icon url';74 return '/icon/' + id;
75 };75 };
7676
77 view = new jujuViews.environment({77 view = new jujuViews.environment({
@@ -118,6 +118,14 @@
118 serviceNameInput.set('value', 'foo');118 serviceNameInput.set('value', 'foo');
119 });119 });
120120
121 it('displays the charms icon when rendered', function() {
122 inspector = setUpInspector();
123 var icon = container.one('.icon img');
124
125 // The icon url is from the fakestore we manually defined.
126 assert.equal(icon.getAttribute('src'), '/icon/cs:precise/mediawiki-8');
127 });
128
121 it('deploys a service with the specified unit count & config', function() {129 it('deploys a service with the specified unit count & config', function() {
122 inspector = setUpInspector();130 inspector = setUpInspector();
123 env.connect();131 env.connect();
@@ -129,16 +137,74 @@
129 vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click');137 vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click');
130138
131 var message = env.ws.last_message();139 var message = env.ws.last_message();
132 assert.equal(message.num_units, numUnits);140 var params = message.Params;
133 assert.equal(message.op, 'deploy');141 var config = {
134 assert.equal(message.service_name, 'mediawiki');
135 assert.deepEqual(message.config, {
136 admins: '',142 admins: '',
137 debug: false,143 debug: 'false',
138 logo: '',144 logo: '',
139 name: 'foo',145 name: 'foo',
140 skin: 'vector'146 skin: 'vector'
141 });147 };
148 assert.equal('ServiceDeploy', message.Request);
149 assert.equal('mediawiki', params.ServiceName);
150 assert.equal(numUnits, params.NumUnits);
151 assert.deepEqual(config, params.Config);
152 });
153
154 it('presents the contraints to the user in python env', function() {
155 // Create our own env to make sure we know which backend we're creating it
156 // against.
157 env.destroy();
158 env = juju.newEnvironment({conn: conn}, 'python');
159 inspector = setUpInspector();
160 var constraintsNode = container.all('.service-constraints');
161 assert.equal(constraintsNode.size(), 1);
162
163 var inputNodes = container.all('.service-constraints input');
164 assert.equal(inputNodes.size(), 3);
165 });
166
167 it('presents the contraints to the user in go env', function() {
168 // Create our own env to make sure we know which backend we're creating it
169 // against.
170 env.destroy();
171 env = juju.newEnvironment({conn: conn}, 'go');
172 inspector = setUpInspector();
173 var constraintsNode = container.all('.service-constraints');
174 assert.equal(constraintsNode.size(), 1);
175
176 var inputNodes = container.all('.service-constraints input');
177 assert.equal(inputNodes.size(), 4);
178 });
179
180 it('deploys with constraints in python env', function() {
181 env.destroy();
182 env = juju.newEnvironment({conn: conn}, 'python');
183 inspector = setUpInspector();
184 env.connect();
185 var vmContainer = inspector.viewletManager.get('container');
186
187 vmContainer.one('input[name=cpu]').set('value', 2);
188 // Called the deploy button, but the css if confirm.
189 vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click');
190
191 var message = env.ws.last_message();
192 assert.equal(message.constraints.cpu, '2');
193 });
194
195 it('deploys with constraints in go env', function() {
196 env.destroy();
197 env = juju.newEnvironment({conn: conn}, 'go');
198 inspector = setUpInspector();
199 env.connect();
200 var vmContainer = inspector.viewletManager.get('container');
201
202 vmContainer.one('input[name=cpu-power]').set('value', 2);
203 // Called the deploy button, but the css if confirm.
204 vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click');
205
206 var message = env.ws.last_message();
207 assert.equal(message.Params.Constraints['cpu-power'], '2');
142 });208 });
143209
144 it('disables and resets input fields when \'use default config\' is active',210 it('disables and resets input fields when \'use default config\' is active',
145211
=== modified file 'test/test_inspector_constraints.js'
--- test/test_inspector_constraints.js 2013-08-19 21:07:02 +0000
+++ test/test_inspector_constraints.js 2013-08-30 19:48:17 +0000
@@ -85,14 +85,15 @@
85 return inspector;85 return inspector;
86 };86 };
8787
88 // Create a fake response from the API server.88 // Create a fake response from the juju-core API server.
89 var makeResponse = function(service, error) {89 var makeResponse = function(service, error) {
90 return {90 var response = {RequestId: 1};
91 err: error,91 if (error) {
92 op: 'set_constraints',92 response.Error = 'bad wolf';
93 request_id: 1,93 } else {
94 service_name: service.get('id')94 response.Response = {};
95 };95 }
96 return response;
96 };97 };
9798
98 // Retrieve and return the constraints viewlet.99 // Retrieve and return the constraints viewlet.
@@ -131,7 +132,7 @@
131 var constraintDescriptions = viewUtils.constraintDescriptions;132 var constraintDescriptions = viewUtils.constraintDescriptions;
132133
133 Y.Array.each(env.genericConstraints, function(key) {134 Y.Array.each(env.genericConstraints, function(key) {
134 var node = container.one('div[for=' + key + '].control-label');135 var node = container.one('label[for=' + key + ']');
135 var expectedTitle = constraintDescriptions[key].title;136 var expectedTitle = constraintDescriptions[key].title;
136 assert.strictEqual(expectedTitle, node.getHTML());137 assert.strictEqual(expectedTitle, node.getHTML());
137 });138 });
@@ -156,7 +157,7 @@
156 });157 });
157158
158 it('can save constraints', function() {159 it('can save constraints', function() {
159 var expected = {arch: 'amd64', cpu: 'photon', mem: '1 teraflop'};160 var expected = {arch: 'amd64', 'cpu-power': 100, mem: 4};
160 // Change values in the form.161 // Change values in the form.
161 Y.Object.each(expected, function(value, key) {162 Y.Object.each(expected, function(value, key) {
162 var node = container.one('input[name=' + key + '].constraint-field');163 var node = container.one('input[name=' + key + '].constraint-field');
@@ -167,14 +168,10 @@
167 saveButton.simulate('click');168 saveButton.simulate('click');
168 var lastMessage = env.ws.last_message();169 var lastMessage = env.ws.last_message();
169 // The set_constraint API method is correctly called.170 // The set_constraint API method is correctly called.
170 assert.equal('set_constraints', lastMessage.op);171 assert.equal('SetServiceConstraints', lastMessage.Request);
171 // The expected constraints are passed in the API call.172 // The expected constraints are passed in the API call.
172 var obtained = Object.create(null);173 var obtained = Object.create(null);
173 Y.Array.each(lastMessage.constraints, function(value) {174 assert.deepEqual(expected, lastMessage.Params.Constraints);
174 var pair = value.split('=');
175 obtained[pair[0]] = pair[1];
176 });
177 assert.deepEqual(expected, obtained);
178 });175 });
179176
180 it('handles error responses from the environment', function() {177 it('handles error responses from the environment', function() {
181178
=== modified file 'test/test_inspector_overview.js'
--- test/test_inspector_overview.js 2013-08-19 15:14:33 +0000
+++ test/test_inspector_overview.js 2013-08-30 19:48:17 +0000
@@ -20,7 +20,9 @@
20describe('Inspector Overview', function() {20describe('Inspector Overview', function() {
2121
22 var view, service, db, models, utils, juju, env, conn, container,22 var view, service, db, models, utils, juju, env, conn, container,
23 inspector, Y, jujuViews, ENTER, charmConfig;23 inspector, Y, jujuViews, ENTER, charmConfig,
24
25 client, backendJuju, state;
2426
25 before(function(done) {27 before(function(done) {
26 var requires = ['juju-gui', 'juju-views', 'juju-tests-utils',28 var requires = ['juju-gui', 'juju-views', 'juju-tests-utils',
@@ -60,6 +62,16 @@
60 env.destroy();62 env.destroy();
61 container.remove(true);63 container.remove(true);
62 window.flags = {};64 window.flags = {};
65
66 if (client) {
67 client.destroy();
68 }
69 if (backendJuju) {
70 backendJuju.destroy();
71 }
72 if (state) {
73 state.destroy();
74 }
63 });75 });
6476
65 var setUpInspector = function() {77 var setUpInspector = function() {
@@ -78,8 +90,8 @@
78 ['unit', 'add', {id: 'mediawiki/2', agent_state: 'pending'}]90 ['unit', 'add', {id: 'mediawiki/2', agent_state: 'pending'}]
79 ]}});91 ]}});
80 var fakeStore = new Y.juju.Charmworld2({});92 var fakeStore = new Y.juju.Charmworld2({});
81 fakeStore.iconpath = function() {93 fakeStore.iconpath = function(id) {
82 return 'charm icon url';94 return '/icon/' + id;
83 };95 };
84 view = new jujuViews.environment({96 view = new jujuViews.environment({
85 container: container,97 container: container,
@@ -96,6 +108,14 @@
96 return inspector;108 return inspector;
97 };109 };
98110
111 it('should show the proper icon based off the charm model', function() {
112 inspector = setUpInspector();
113 var icon = container.one('.icon img');
114
115 // The icon url comes from the fake store and the service charm attribute.
116 assert.equal(icon.getAttribute('src'), '/icon/precise/mediawiki-4');
117 });
118
99 it('should start with the proper number of units shown in the text field',119 it('should start with the proper number of units shown in the text field',
100 function() {120 function() {
101 inspector = setUpInspector();121 inspector = setUpInspector();
@@ -110,8 +130,9 @@
110 control.set('value', 1);130 control.set('value', 1);
111 control.simulate('keydown', { keyCode: ENTER }); // Simulate Enter.131 control.simulate('keydown', { keyCode: ENTER }); // Simulate Enter.
112 var message = conn.last_message();132 var message = conn.last_message();
113 message.op.should.equal('remove_units');133 assert.equal('DestroyServiceUnits', message.Request);
114 message.unit_names.should.eql(['mediawiki/2', 'mediawiki/1']);134 assert.deepEqual(
135 ['mediawiki/2', 'mediawiki/1'], message.Params.UnitNames);
115 });136 });
116137
117 it('should not do anything if requested is < 1',138 it('should not do anything if requested is < 1',
@@ -132,10 +153,39 @@
132 control.simulate('keydown', { keyCode: ENTER });153 control.simulate('keydown', { keyCode: ENTER });
133 // confirm the 'please confirm constraints' dialogue154 // confirm the 'please confirm constraints' dialogue
134 container.one('.confirm-num-units').simulate('click');155 container.one('.confirm-num-units').simulate('click');
135 var message = conn.last_message();156 assert.equal(container.one('.unit-constraints-confirm')
136 message.op.should.equal('add_unit');157 .one('span:first-child')
137 message.service_name.should.equal('mediawiki');158 .getHTML(), 'Scale up with the following constraints?');
138 message.num_units.should.equal(4);159 var message = conn.last_message();
160 assert.equal('AddServiceUnits', message.Request);
161 assert.equal('mediawiki', message.Params.ServiceName);
162 assert.equal(4, message.Params.NumUnits);
163 });
164
165 it('should set the constraints before deploying any more units',
166 function() {
167 setUpInspector(true);
168 var control = container.one('.num-units-control');
169 control.set('value', 7);
170 control.simulate('keydown', { keyCode: ENTER });
171 var editConstraintsButton = container.one('.edit-constraints');
172 editConstraintsButton.simulate('click');
173 // It should be hidden after being clicked to display the constraints
174 assert.equal(editConstraintsButton.getStyle('display'), 'none');
175 var constraintsWrapper = container.one('.editable-constraints');
176 assert.equal(constraintsWrapper.getStyle('display'), 'block');
177 var constraints = {arch: 'amd64', 'cpu-cores': 4, mem: 8};
178 Y.Object.each(constraints, function(value, key) {
179 var node = constraintsWrapper.one('input[name=' + key + ']');
180 node.set('value', value);
181 });
182
183 // confirm the 'please confirm constraints' dialogue
184 container.one('.confirm-num-units').simulate('click');
185 var message = conn.last_message();
186 assert.equal('SetServiceConstraints', message.Request);
187 assert.equal('mediawiki', message.Params.ServiceName);
188 assert.deepEqual(constraints, message.Params.Constraints);
139 });189 });
140190
141 it('generates a proper statuses object', function() {191 it('generates a proper statuses object', function() {
@@ -283,15 +333,14 @@
283333
284 unit.simulate('click');334 unit.simulate('click');
285 retryButton.simulate('click');335 retryButton.simulate('click');
286 var msg = env.ws.last_message();
287336
288 assert.deepEqual(msg, {337 var expected = {
289 op: 'resolved',338 Params: {Retry: false, UnitName: 'mediawiki/7'},
290 unit_name: 'mediawiki/7',339 Request: 'Resolved',
291 relation_name: null,340 RequestId: 1,
292 retry: false,341 Type: 'Client'
293 request_id: 1342 };
294 });343 assert.deepEqual(expected, env.ws.last_message());
295 });344 });
296345
297 it('sends the retry command to the env for the selected unit', function() {346 it('sends the retry command to the env for the selected unit', function() {
@@ -311,15 +360,14 @@
311360
312 unit.simulate('click');361 unit.simulate('click');
313 retryButton.simulate('click');362 retryButton.simulate('click');
314 var msg = env.ws.last_message();
315363
316 assert.deepEqual(msg, {364 var expected = {
317 op: 'resolved',365 Params: {Retry: true, UnitName: 'mediawiki/7'},
318 unit_name: 'mediawiki/7',366 Request: 'Resolved',
319 relation_name: null,367 RequestId: 1,
320 retry: true,368 Type: 'Client'
321 request_id: 1369 };
322 });370 assert.deepEqual(expected, env.ws.last_message());
323 });371 });
324372
325 it('generates the button display map for each unit category', function() {373 it('generates the button display map for each unit category', function() {
326374
=== modified file 'test/test_inspector_settings.js'
--- test/test_inspector_settings.js 2013-08-15 15:37:52 +0000
+++ test/test_inspector_settings.js 2013-08-30 19:48:17 +0000
@@ -307,7 +307,8 @@
307 input.set('value', 'foo');307 input.set('value', 'foo');
308308
309 button.simulate('click');309 button.simulate('click');
310 assert.equal(env.ws.last_message().config.admins, 'foo');310 var message = env.ws.last_message();
311 assert.equal('foo', message.Params.Config.admins);
311 assert.equal(button.getHTML(), 'Save Changes');312 assert.equal(button.getHTML(), 'Save Changes');
312 });313 });
313314
314315
=== modified file 'test/test_login.js'
--- test/test_login.js 2013-05-17 14:51:05 +0000
+++ test/test_login.js 2013-08-30 19:48:17 +0000
@@ -45,26 +45,25 @@
45 });45 });
4646
47 test('the user is initially assumed to be unauthenticated', function() {47 test('the user is initially assumed to be unauthenticated', function() {
48 assert.equal(env.userIsAuthenticated, false);48 assert.isFalse(env.userIsAuthenticated);
49 });49 });
5050
51 test('successful login event marks user as authenticated', function() {51 test('successful login event marks user as authenticated', function() {
52 var evt = {data: {op: 'login', result: true}};52 var data = {Response: {}};
53 env.handleLoginEvent(evt);53 env.handleLogin(data);
54 assert.equal(env.userIsAuthenticated, true);54 assert.isTrue(env.userIsAuthenticated);
55 });55 });
5656
57 test('unsuccessful login event keeps user unauthenticated', function() {57 test('unsuccessful login event keeps user unauthenticated', function() {
58 var evt = {data: {op: 'login'}};58 var data = {Error: 'who are you?'};
59 env.handleLoginEvent(evt);59 env.handleLogin(data);
60 assert.equal(env.userIsAuthenticated, false);60 assert.isFalse(env.userIsAuthenticated);
61 });61 });
6262
63 test('bad credentials are removed', function() {63 test('bad credentials are removed', function() {
64 var evt = {data: {op: 'login'}};64 var data = {Error: 'who are you?'};
65 env.handleLoginEvent(evt);65 env.handleLogin(data);
66 var credentials = env.getCredentials();66 assert.isNull(env.getCredentials());
67 assert.equal(credentials, null);
68 });67 });
6968
70 test('credentials passed to the constructor are stored', function() {69 test('credentials passed to the constructor are stored', function() {
@@ -93,11 +92,12 @@
93 });92 });
9493
95 test('with credentials set, login() sends an RPC message', function() {94 test('with credentials set, login() sends an RPC message', function() {
96 env.setCredentials({ user: 'user', password: 'password' });95 env.setCredentials({user: 'user', password: 'password'});
97 env.login();96 env.login();
98 assert.equal(conn.last_message().op, 'login');97 var message = conn.last_message();
99 assert.equal(conn.last_message().user, 'user');98 assert.equal('Login', message.Request);
100 assert.equal(conn.last_message().password, 'password');99 assert.equal('user', message.Params.AuthTag);
100 assert.equal('password', message.Params.Password);
101 });101 });
102102
103 });103 });
@@ -143,9 +143,10 @@
143 container.appendChild('<input/>').set('type', 'password').set(143 container.appendChild('<input/>').set('type', 'password').set(
144 'value', 'password');144 'value', 'password');
145 loginView.login(ev);145 loginView.login(ev);
146 assert.equal(conn.last_message().op, 'login');146 var message = conn.last_message();
147 assert.equal(conn.last_message().user, 'user');147 assert.equal('Login', message.Request);
148 assert.equal(conn.last_message().password, 'password');148 assert.equal('user', message.Params.AuthTag);
149 assert.equal('password', message.Params.Password);
149 });150 });
150151
151 test('the view render method adds the login form', function() {152 test('the view render method adds the login form', function() {
152153
=== modified file 'test/test_model.js'
--- test/test_model.js 2013-08-21 16:02:10 +0000
+++ test/test_model.js 2013-08-30 19:48:17 +0000
@@ -522,7 +522,7 @@
522 it('must send request to juju environment for local charms', function() {522 it('must send request to juju environment for local charms', function() {
523 var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load(env);523 var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load(env);
524 assert(!charm.loaded);524 assert(!charm.loaded);
525 conn.last_message().op.should.equal('get_charm');525 assert.equal('CharmInfo', conn.last_message().Request);
526 });526 });
527527
528 it('must handle success from local charm request', function(done) {528 it('must handle success from local charm request', function(done) {
@@ -530,57 +530,47 @@
530 env,530 env,
531 function(err, response) {531 function(err, response) {
532 assert(!err);532 assert(!err);
533 charm.get('summary').should.equal('wowza');533 assert.equal('wowza', charm.get('summary'));
534 assert(charm.loaded);534 assert(charm.loaded);
535 done();535 done();
536 });536 });
537 var response = conn.last_message();537 var response = {
538 response.result = { summary: 'wowza' };538 RequestId: conn.last_message().RequestId,
539 env.dispatch_result(response);539 Response: {Meta: {Summary: 'wowza'}, Config: {}}
540 // The test in the callback above should run.540 };
541 });541 env.dispatch_result(response);
542542 // The test in the callback above should run.
543 it('parses the old charm model options location correctly', function(done) {543 });
544 var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load(544
545 env,545 it('parses charm model options correctly', function(done) {
546 function(err, response) {546 var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load(
547 assert(!err);547 env,
548 // This checks to make sure the parse mechanism is working properly548 function(err, response) {
549 // for both the old ane new charm browser.549 assert(!err);
550 assert.equal(charm.get('options').default_log['default'], 'global');550 // This checks to make sure the parse mechanism is working properly
551 done();551 // for both the old ane new charm browser.
552 });552 var option = charm.get('options').default_log;
553 var response = conn.last_message();553 assert.equal('global', option['default']);
554 response.result = {554 assert.equal('Default log', option.description);
555 config: {555 done();
556 options: {556 });
557 default_log: {557 var response = {
558 'default': 'global',558 RequestId: conn.last_message().RequestId,
559 description: 'Default log',559 Response: {
560 type: 'string'560 Meta: {},
561 }}}};561 Config: {
562 env.dispatch_result(response);562 Options: {
563 });563 default_log: {
564564 Default: 'global',
565 it('parses the new charm model options location correctly', function(done) {565 Description: 'Default log',
566 var charm = new models.BrowserCharm({id: 'local:precise/foo-4'}).load(566 Type: 'string'
567 env,567 }
568 function(err, response) {568 }
569 assert(!err);569 }
570 // This checks to make sure the parse mechanism is working properly570 }
571 // for both the old ane new charm browser.571 };
572 assert.equal(charm.get('options').default_log['default'], 'global');572 env.dispatch_result(response);
573 done();573 // The test in the callback above should run.
574 });
575 var response = conn.last_message();
576 response.result = {
577 options: {
578 default_log: {
579 'default': 'global',
580 description: 'Default log',
581 type: 'string'
582 }}};
583 env.dispatch_result(response);
584 });574 });
585575
586 it('must handle failure from local charm request', function(done) {576 it('must handle failure from local charm request', function(done) {
@@ -592,8 +582,10 @@
592 assert(!charm.loaded);582 assert(!charm.loaded);
593 done();583 done();
594 });584 });
595 var response = conn.last_message();585 var response = {
596 response.err = true;586 RequestId: conn.last_message().RequestId,
587 Error: 'error'
588 };
597 env.dispatch_result(response);589 env.dispatch_result(response);
598 // The test in the callback above should run.590 // The test in the callback above should run.
599 });591 });
600592
=== modified file 'test/test_model_controller.js'
--- test/test_model_controller.js 2013-08-12 14:56:38 +0000
+++ test/test_model_controller.js 2013-08-30 19:48:17 +0000
@@ -20,7 +20,7 @@
2020
21describe('Model Controller Promises', function() {21describe('Model Controller Promises', function() {
22 var modelController, yui, env, db, conn, environment, load, serviceError,22 var modelController, yui, env, db, conn, environment, load, serviceError,
23 getService, cleanups, aEach;23 getService, cleanups, aEach, utils;
2424
25 before(function(done) {25 before(function(done) {
26 YUI(GlobalConfig).use(26 YUI(GlobalConfig).use(
@@ -31,12 +31,13 @@
31 load = Y.juju.models.BrowserCharm.prototype.load;31 load = Y.juju.models.BrowserCharm.prototype.load;
32 getService = environments.PythonEnvironment.prototype.get_service;32 getService = environments.PythonEnvironment.prototype.get_service;
33 aEach = Y.Array.each;33 aEach = Y.Array.each;
34 utils = Y.namespace('juju-tests.utils');
34 done();35 done();
35 });36 });
36 });37 });
3738
38 beforeEach(function() {39 beforeEach(function() {
39 conn = new yui['juju-tests'].utils.SocketStub();40 conn = new utils.SocketStub();
40 environment = env = yui.juju.newEnvironment(41 environment = env = yui.juju.newEnvironment(
41 {conn: conn});42 {conn: conn});
42 db = new yui.juju.models.Database();43 db = new yui.juju.models.Database();
@@ -56,6 +57,7 @@
56 yui.Array.each(cleanups, function(cleanup) {57 yui.Array.each(cleanups, function(cleanup) {
57 cleanup();58 cleanup();
58 });59 });
60 window.flags = {};
59 });61 });
6062
61 /**63 /**
@@ -91,7 +93,7 @@
91 @static93 @static
92 */94 */
93 function clobberGetService() {95 function clobberGetService() {
94 yui.juju.environments.PythonEnvironment.prototype.get_service = function(96 yui.juju.environments.GoEnvironment.prototype.get_service = function(
95 serviceName, callback) {97 serviceName, callback) {
96 assert(typeof serviceName, 'string');98 assert(typeof serviceName, 'string');
97 // This is to test the error reject path of the getService tests99 // This is to test the error reject path of the getService tests
@@ -244,4 +246,30 @@
244 done();246 done();
245 });247 });
246 });248 });
249
250 it('can check for available upgrades', function(done) {
251 clobberLoad();
252 clobberGetService();
253 var serviceId = 'wordpress',
254 charmId = 'cs:precise/wordpress-7';
255 db.services.add({
256 id: serviceId,
257 loaded: true,
258 charm: charmId
259 });
260 window.flags.upgradeCharm = true;
261 modelController.set('store', utils.makeFakeStore());
262 var promise = modelController.getServiceWithCharm(serviceId);
263 promise.then(
264 function(result) {
265 var service = db.services.getById(serviceId);
266 assert(service.get('upgrade_available'), true);
267 assert(service.get('upgrade_to'), 'precise/wordpress-15');
268 done();
269 },
270 function() {
271 assert.fail('This should not have failed.');
272 done();
273 });
274 });
247});275});
248276
=== modified file 'test/test_notifications.js'
--- test/test_notifications.js 2013-07-31 14:30:47 +0000
+++ test/test_notifications.js 2013-08-30 19:48:17 +0000
@@ -263,7 +263,7 @@
263 var container = Y.Node.create(263 var container = Y.Node.create(
264 '<div id="test" class="container"></div>'),264 '<div id="test" class="container"></div>'),
265 conn = new(Y.namespace('juju-tests.utils')).SocketStub(),265 conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
266 env = juju.newEnvironment({conn: conn});266 env = juju.newEnvironment({conn: conn}, 'python');
267 app = new Y.juju.App({267 app = new Y.juju.App({
268 env: env,268 env: env,
269 container: container,269 container: container,
@@ -307,7 +307,7 @@
307 var container = Y.Node.create(307 var container = Y.Node.create(
308 '<div id="test" class="container"></div>');308 '<div id="test" class="container"></div>');
309 var conn = new(Y.namespace('juju-tests.utils')).SocketStub();309 var conn = new(Y.namespace('juju-tests.utils')).SocketStub();
310 var env = juju.newEnvironment({conn: conn});310 var env = juju.newEnvironment({conn: conn}, 'python');
311 env.connect();311 env.connect();
312 app = new Y.juju.App({312 app = new Y.juju.App({
313 env: env,313 env: env,
314314
=== modified file 'test/test_sandbox_go.js'
--- test/test_sandbox_go.js 2013-08-14 19:15:01 +0000
+++ test/test_sandbox_go.js 2013-08-30 19:48:17 +0000
@@ -277,6 +277,32 @@
277 {llama: 'pajama'},277 {llama: 'pajama'},
278 null,278 null,
279 1,279 1,
280 null,
281 callback);
282 });
283
284 it('can deploy with constraints', function(done) {
285 var constraints = {
286 'cpu-cores': 1,
287 'cpu-power': 0,
288 'mem': '512M',
289 'arch': 'i386'
290 };
291
292 env.connect();
293 // We begin logged in. See utils.makeFakeBackend.
294 var callback = function(result) {
295 var service = state.db.services.getById('kumquat');
296 assert.deepEqual(service.get('constraints'), constraints);
297 done();
298 };
299 env.deploy(
300 'cs:precise/wordpress-15',
301 'kumquat',
302 {llama: 'pajama'},
303 null,
304 1,
305 constraints,
280 callback);306 callback);
281 });307 });
282308
@@ -288,8 +314,8 @@
288 result.err, 'A service with this name already exists.');314 result.err, 'A service with this name already exists.');
289 done();315 done();
290 };316 };
291 env.deploy('cs:precise/wordpress-15', undefined, undefined, undefined, 1,317 env.deploy('cs:precise/wordpress-15', undefined, undefined, undefined,
292 callback);318 1, null, callback);
293 });319 });
294320
295 it('can destroy a service', function(done) {321 it('can destroy a service', function(done) {
@@ -609,6 +635,7 @@
609 {llama: 'pajama'},635 {llama: 'pajama'},
610 null,636 null,
611 1,637 1,
638 null,
612 localCb);639 localCb);
613 }640 }
614641
@@ -661,6 +688,7 @@
661 {llama: 'pajama'},688 {llama: 'pajama'},
662 null,689 null,
663 1,690 1,
691 null,
664 localCb);692 localCb);
665 }693 }
666694
@@ -904,19 +932,23 @@
904932
905 it('can add a relation (integration)', function(done) {933 it('can add a relation (integration)', function(done) {
906 env.connect();934 env.connect();
907 env.deploy('cs:precise/wordpress-15', null, null, null, 1, function() {935 env.deploy(
908 env.deploy('cs:precise/mysql-26', null, null, null, 1, function() {936 'cs:precise/wordpress-15', null, null, null, 1, null, function() {
909 var endpointA = ['wordpress', {name: 'db', role: 'client'}],937 env.deploy(
910 endpointB = ['mysql', {name: 'db', role: 'server'}];938 'cs:precise/mysql-26', null, null, null, 1, null, function() {
911 env.add_relation(endpointA, endpointB, function(recData) {939 var endpointA = ['wordpress', {name: 'db', role: 'client'}],
912 assert.equal(recData.err, undefined);940 endpointB = ['mysql', {name: 'db', role: 'server'}];
913 assert.equal(recData.endpoint_a, 'wordpress:db');941 env.add_relation(endpointA, endpointB, function(recData) {
914 assert.equal(recData.endpoint_b, 'mysql:db');942 assert.equal(recData.err, undefined);
915 assert.isObject(recData.result);943 assert.equal(recData.endpoint_a, 'wordpress:db');
916 done();944 assert.equal(recData.endpoint_b, 'mysql:db');
917 });945 assert.isObject(recData.result);
918 });946 done();
919 });947 });
948 }
949 );
950 }
951 );
920 });952 });
921953
922 it('is able to add a relation with a subordinate service', function(done) {954 it('is able to add a relation with a subordinate service', function(done) {
@@ -1020,20 +1052,26 @@
10201052
1021 it('can remove a relation(integration)', function(done) {1053 it('can remove a relation(integration)', function(done) {
1022 env.connect();1054 env.connect();
1023 env.deploy('cs:precise/wordpress-15', null, null, null, 1, function() {1055 env.deploy(
1024 env.deploy('cs:precise/mysql-26', null, null, null, 1, function() {1056 'cs:precise/wordpress-15', null, null, null, 1, null, function() {
1025 var endpointA = ['wordpress', {name: 'db', role: 'client'}],1057 env.deploy(
1026 endpointB = ['mysql', {name: 'db', role: 'server'}];1058 'cs:precise/mysql-26', null, null, null, 1, null, function() {
1027 env.add_relation(endpointA, endpointB, function() {1059 var endpointA = ['wordpress', {name: 'db', role: 'client'}],
1028 env.remove_relation(endpointA, endpointB, function(recData) {1060 endpointB = ['mysql', {name: 'db', role: 'server'}];
1029 assert.equal(recData.err, undefined);1061 env.add_relation(endpointA, endpointB, function() {
1030 assert.equal(recData.endpoint_a, 'wordpress:db');1062 env.remove_relation(
1031 assert.equal(recData.endpoint_b, 'mysql:db');1063 endpointA, endpointB, function(recData) {
1032 done();1064 assert.equal(recData.err, undefined);
1033 });1065 assert.equal(recData.endpoint_a, 'wordpress:db');
1034 });1066 assert.equal(recData.endpoint_b, 'mysql:db');
1035 });1067 done();
1036 });1068 }
1069 );
1070 });
1071 }
1072 );
1073 }
1074 );
1037 });1075 });
10381076
1039 });1077 });
10401078
=== modified file 'test/test_sandbox_python.js'
--- test/test_sandbox_python.js 2013-07-30 21:00:47 +0000
+++ test/test_sandbox_python.js 2013-08-30 19:48:17 +0000
@@ -154,6 +154,7 @@
154 {llama: 'pajama'},154 {llama: 'pajama'},
155 null,155 null,
156 1,156 1,
157 null,
157 localCb);158 localCb);
158 });159 });
159 env.connect();160 env.connect();
@@ -209,6 +210,7 @@
209 {llama: 'pajama'},210 {llama: 'pajama'},
210 null,211 null,
211 1,212 1,
213 null,
212 localCb);214 localCb);
213 });215 });
214 env.connect();216 env.connect();
@@ -383,6 +385,7 @@
383 {llama: 'pajama'},385 {llama: 'pajama'},
384 null,386 null,
385 1,387 1,
388 null,
386 callback);389 callback);
387 });390 });
388 env.connect();391 env.connect();
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: