Merge lp:~rharding/juju-gui/bundle-progress into lp:juju-gui/experimental

Proposed by Richard Harding
Status: Merged
Merged at revision: 1201
Proposed branch: lp:~rharding/juju-gui/bundle-progress
Merge into: lp:juju-gui/experimental
Diff against target: 1033 lines (+677/-129)
12 files modified
app/app.js (+27/-24)
app/assets/javascripts/bundle-import-helpers.js (+202/-30)
app/modules-debug.js (+2/-2)
app/store/env/go.js (+91/-0)
app/views/topology/service.js (+13/-13)
app/views/utils.js (+0/-18)
bin/merge-files (+1/-1)
test/index.html (+1/-0)
test/test_bundle_import_helpers.js (+303/-0)
test/test_env_go.js (+30/-0)
test/test_service_module.js (+7/-2)
test/test_utils.js (+0/-39)
To merge this branch: bzr merge lp:~rharding/juju-gui/bundle-progress
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+195084@code.launchpad.net

Description of the change

Add support for watching progress of bundle import

- Change the bundle-import-extension to a helpers lib that doesn't need to be
mixed in. It didn't need any state info.
- Move everyone to going through the single deployBundle function in the
helpers vs their own calls to the env.deployerImport
- Provide the ability to start a watch on a deployment.
- Add the methods to update with new notifications on each change to the watch
status of the import.
- Start a new test file for the bundle-import-helpers tests
- Remove the dupe idea of adding import notifications from the view utils,
it's part of the helpers.

QA
----

There are four QA methods that need testing as there are four ways to deploy
a bundle in the code. In order to QA you need to bring up a live environment.
I suggest that you use `juju quickstart` without any parameters to bring up
the environment.

Once it's up, you need to change the source to this branch with:

juju set juju-gui juju-gui-source='lp:~rharding/juju-gui/bundle-progress'

Now this takes a while. I found the best way to monitor it was to

juju ssh juju-gui/0
watch 'ps aux | grep config'

You then need to wait for the config-changed hook to finish running.

Once the config change hook is done, you can start to QA the four methods of
deploying a bundle.

1. Search for 'jorge' in the browser and drag-n-drop the discource bundle onto
the canvas.
2. Search for 'jorge' and click on the discourse bundle to view the details.
Then click on the deploy button and press "Yes I'm sure".
3. Use the import icon to import a local bundle file.
4. Drag-n-Drop a bundle file onto the canvas.

In each test case, what you're watching for are notifications that the bundle
has been requested, started, and completed. If the bundle is large the final
step might take a while.

Let me know if you've got any questions or issues qa'ing.

https://codereview.appspot.com/26290043/

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

Reviewers: mp+195084_code.launchpad.net,

Message:
Please take a look.

Description:
Add support for watching progress of bundle import

- Change the bundle-import-extension to a helpers lib that doesn't need
to be
mixed in. It didn't need any state info.
- Move everyone to going through the single deployBundle function in the
helpers vs their own calls to the env.deployerImport
- Provide the ability to start a watch on a deployment.
- Add the methods to update with new notifications on each change to the
watch
status of the import.
- Start a new test file for the bundle-import-helpers tests
- Remove the dupe idea of adding import notifications from the view
utils,
it's part of the helpers.

https://code.launchpad.net/~rharding/juju-gui/bundle-progress/+merge/195084

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/26290043/

Affected files (+609, -129 lines):
   A [revision details]
   M app/app.js
   M app/assets/javascripts/bundle-import-helpers.js
   M app/modules-debug.js
   M app/store/env/go.js
   M app/views/topology/service.js
   M app/views/utils.js
   M bin/merge-files
   M test/index.html
   A test/test_bundle_import_helpers.js
   M test/test_env_go.js
   M test/test_service_module.js
   M test/test_utils.js

1203. By Richard Harding

Add some debugging info

1204. By Richard Harding

Bind to the right callbacks dummy

1205. By Richard Harding

Update the deployBundle helper passed to subapp/views

1206. By Richard Harding

doh

1207. By Richard Harding

Update the deploy helper from the app.js

Revision history for this message
Richard Harding (rharding) wrote :
1208. By Richard Harding

Remove alerts used for debuggin

Revision history for this message
Richard Harding (rharding) wrote :
1209. By Richard Harding

Add error case for the watcher change

Revision history for this message
Richard Harding (rharding) wrote :
1210. By Richard Harding

Add a test for the failure

Revision history for this message
Richard Harding (rharding) wrote :
1211. By Richard Harding

Move the watch to the right section

Revision history for this message
Richard Harding (rharding) wrote :
Revision history for this message
Jeff Pihach (hatch) wrote :
Download full text (4.2 KiB)

This is looking really good I have some small comments mostly related to
naming but I think that we need some integration tests for the go
sandbox.

Do we have a story for the fakebackend for this functionality or if it's
simply supposed to go unanswered?

I'm curious as to why we can't initiate a watch rpc call and then have
the guiserver send fake deltas down for us to react to, this would get
rid of calling 'next' and essentially long-polling for updates. I'm sure
there is a good reason for it I just don't know what it is :)

https://codereview.appspot.com/26290043/diff/60001/app/app.js
File app/app.js (right):

https://codereview.appspot.com/26290043/diff/60001/app/app.js#newcode43
app/app.js:43: importHelpers = juju.BundleImport;
I'd probably change this to be something like `bundleHelpers`

https://codereview.appspot.com/26290043/diff/60001/app/app.js#newcode583
app/app.js:583: env,
this.env and this.db are available here because the callback is scoped
to app.js so you don't need the references above

https://codereview.appspot.com/26290043/diff/60001/app/app.js#newcode598
app/app.js:598: cfg.deployBundle = function(bundle) {
if you use .bind() for this function you do not need to maintain the
reference above for env, db

https://codereview.appspot.com/26290043/diff/60001/app/assets/javascripts/bundle-import-helpers.js
File app/assets/javascripts/bundle-import-helpers.js (right):

https://codereview.appspot.com/26290043/diff/60001/app/assets/javascripts/bundle-import-helpers.js#newcode55
app/assets/javascripts/bundle-import-helpers.js:55:
ns.BundleImport._watchDeployment(result.DeploymentId, env, db);
When I see utility methods that are directly tied together this it makes
me think this is a class trying really hard to be a utility object.
Everything in this object is directly related to a single instance but
instead of relying on that instance everything needs to be passed
around. I don't see the advantage to this structure vs relying on the
primary instance.

For testing you could unit test the class simply by creating an instance
of it with stubbed env/db like you do now. If you didn't want to create
a new class this would be handled by converting it to an extension and
then testing would be the same technique as prev mentioned.

https://codereview.appspot.com/26290043/diff/60001/app/assets/javascripts/bundle-import-helpers.js#newcode105
app/assets/javascripts/bundle-import-helpers.js:105:
ns.BundleImport.deployBundle(
I know this is repurposed but it feels like we are duplicating work here
just to supply different notification messages. I'd try to move these to
be handled in the deployBundle method, or offload both notifications to
a separate showNotification(file Boolean) {} method

https://codereview.appspot.com/26290043/diff/60001/app/assets/javascripts/bundle-import-helpers.js#newcode188
app/assets/javascripts/bundle-import-helpers.js:188:
_processWatchDeploymentUpdates: function(watchId, env, db) {
this is really a watchDeploymentUpdates method

https://codereview.appspot.com/26290043/diff/60001/app/store/env/go.js
File app/store/env/go.js (right):

https://codereview.appspot.com/26290043/diff/60001/app/store/env/go.js#newcode...

Read more...

Revision history for this message
Richard Harding (rharding) wrote :
Download full text (5.7 KiB)

On 2013/11/14 21:30:52, jeff.pihach wrote:
> This is looking really good I have some small comments mostly related
to naming
> but I think that we need some integration tests for the go sandbox.

Thanks, I should have brought this up. The next card is to figure out
how to implement this in the sandbox. This branch is purely about making
it work for users in a live environment.

> Do we have a story for the fakebackend for this functionality or if
it's simply
> supposed to go unanswered?

That is for the follow up card. My first instinct is to not show all of
these notifications to the sandbox user, but only the final "Competed"
one.

> I'm curious as to why we can't initiate a watch rpc call and then have
the
> guiserver send fake deltas down for us to react to, this would get rid
of
> calling 'next' and essentially long-polling for updates. I'm sure
there is a
> good reason for it I just don't know what it is :)

I'll ask around on this one. Not sure about the delta stuff myself.

> https://codereview.appspot.com/26290043/diff/60001/app/app.js
> File app/app.js (right):

https://codereview.appspot.com/26290043/diff/60001/app/app.js#newcode43
> app/app.js:43: importHelpers = juju.BundleImport;
> I'd probably change this to be something like `bundleHelpers`

Sure thing, thanks.

https://codereview.appspot.com/26290043/diff/60001/app/app.js#newcode583
> app/app.js:583: env,
> this.env and this.db are available here because the callback is scoped
to app.js
> so you don't need the references above

The trouble is that these callbacks get bound in the go env wrappings
and I preferred to be explicit. I'll test it out and make sure, but then
add a comment/note.

https://codereview.appspot.com/26290043/diff/60001/app/app.js#newcode598
> app/app.js:598: cfg.deployBundle = function(bundle) {
> if you use .bind() for this function you do not need to maintain the
reference
> above for env, db

https://codereview.appspot.com/26290043/diff/60001/app/assets/javascripts/bundle-import-helpers.js
> File app/assets/javascripts/bundle-import-helpers.js (right):

https://codereview.appspot.com/26290043/diff/60001/app/assets/javascripts/bundle-import-helpers.js#newcode55
> app/assets/javascripts/bundle-import-helpers.js:55:
> ns.BundleImport._watchDeployment(result.DeploymentId, env, db);
> When I see utility methods that are directly tied together this it
makes me
> think this is a class trying really hard to be a utility object.
Everything in
> this object is directly related to a single instance but instead of
relying on
> that instance everything needs to be passed around. I don't see the
advantage to
> this structure vs relying on the primary instance.

> For testing you could unit test the class simply by creating an
instance of it
> with stubbed env/db like you do now. If you didn't want to create a
new class
> this would be handled by converting it to an extension and then
testing would be
> the same technique as prev mentioned.

I thought about a class but then I stopped since this makes testing
really easy. Passing in arguments ftw which allowed me to write a
specific test case for the update method without dealing with anything
else. They al...

Read more...

1212. By Richard Harding

Sync from laptop

Revision history for this message
Richard Harding (rharding) wrote :
Revision history for this message
Jeff Pihach (hatch) wrote :
1213. By Richard Harding

Add debug info

1214. By Richard Harding

Fix debugging code

1215. By Richard Harding

Make the error check more explicit

Revision history for this message
Jeff Pihach (hatch) wrote :
Revision history for this message
Madison Scott-Clary (makyo) wrote :
Revision history for this message
Richard Harding (rharding) wrote :

*** Submitted:

Add support for watching progress of bundle import

- Change the bundle-import-extension to a helpers lib that doesn't need
to be
mixed in. It didn't need any state info.
- Move everyone to going through the single deployBundle function in the
helpers vs their own calls to the env.deployerImport
- Provide the ability to start a watch on a deployment.
- Add the methods to update with new notifications on each change to the
watch
status of the import.
- Start a new test file for the bundle-import-helpers tests
- Remove the dupe idea of adding import notifications from the view
utils,
it's part of the helpers.

QA
----

There are four QA methods that need testing as there are four ways to
deploy
a bundle in the code. In order to QA you need to bring up a live
environment.
I suggest that you use `juju quickstart` without any parameters to bring
up
the environment.

Once it's up, you need to change the source to this branch with:

juju set juju-gui
juju-gui-source='lp:~rharding/juju-gui/bundle-progress'

Now this takes a while. I found the best way to monitor it was to

juju ssh juju-gui/0
watch 'ps aux | grep config'

You then need to wait for the config-changed hook to finish running.

Once the config change hook is done, you can start to QA the four
methods of
deploying a bundle.

1. Search for 'jorge' in the browser and drag-n-drop the discource
bundle onto
the canvas.
2. Search for 'jorge' and click on the discourse bundle to view the
details.
Then click on the deploy button and press "Yes I'm sure".
3. Use the import icon to import a local bundle file.
4. Drag-n-Drop a bundle file onto the canvas.

In each test case, what you're watching for are notifications that the
bundle
has been requested, started, and completed. If the bundle is large the
final
step might take a while.

Let me know if you've got any questions or issues qa'ing.

R=jeff.pihach, matthew.scott
CC=
https://codereview.appspot.com/26290043

https://codereview.appspot.com/26290043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/app.js'
2--- app/app.js 2013-11-12 21:05:13 +0000
3+++ app/app.js 2013-11-15 19:04:35 +0000
4@@ -39,8 +39,8 @@
5 var juju = Y.namespace('juju'),
6 models = Y.namespace('juju.models'),
7 views = Y.namespace('juju.views'),
8- utils = views.utils,
9- widgets = Y.namespace('juju.widgets');
10+ widgets = Y.namespace('juju.widgets'),
11+ importHelpers = juju.BundleHelpers;
12
13 /**
14 * The main app class.
15@@ -51,8 +51,7 @@
16 Y.juju.SubAppRegistration,
17 Y.juju.NSRouter,
18 Y.juju.Cookies,
19- Y.juju.GhostDeployer,
20- Y.juju.BundleImport], {
21+ Y.juju.GhostDeployer], {
22
23 /*
24 Extension properties
25@@ -566,6 +565,7 @@
26
27 var importNode = Y.one('#import-trigger');
28 var importFileInput = Y.one('.import-export input[type=file]');
29+
30 // Tests won't have this node.
31 if (importNode && importFileInput) {
32 importNode.on('click', function(e) {
33@@ -575,8 +575,11 @@
34 });
35
36 importFileInput.on('change', function(e) {
37- this.sendToDeployer(this.env, this.db,
38- e.currentTarget.get('files')._nodes);
39+ importHelpers.deployBundleFiles(
40+ e.currentTarget.get('files')._nodes,
41+ this.env,
42+ this.db
43+ );
44 }, this);
45 }
46
47@@ -587,7 +590,22 @@
48 // from the Y.juju.GhostDeployer extension
49 cfg.deployService = Y.bind(this.deployService, this);
50
51- cfg.deployBundle = this.deployBundle.bind(this);
52+ // Provide the bundle deployment helper to the subapps and views to
53+ // access in case of an UX interaction that triggers a bundle deploy.
54+ // Grab a reference of these for the nested event calls below.
55+ var env = this.env;
56+ var db = this.db;
57+ cfg.deployBundle = function(bundle) {
58+ // The other views will hand us an Object vs a YAML string. The import
59+ // helpers want the yaml string instead.
60+ importHelpers.deployBundle(
61+ Y.JSON.stringify({
62+ bundle: bundle
63+ }),
64+ env,
65+ db
66+ );
67+ };
68
69 // Watch specific things, (add units), remove db.update above
70 // Note: This hides under the flag as tests don't properly clean
71@@ -630,21 +648,6 @@
72 },
73
74 /**
75- Calls the deployer import method with the bundle data
76- to deploy the bundle to the environment.
77-
78- @method deployBundle
79- @param {Object} bundle Bundle data.
80- */
81- deployBundle: function(bundle) {
82- var notifications = this.db.notifications;
83- this.env.deployerImport(
84- Y.JSON.stringify({
85- bundle: bundle
86- }), null, Y.bind(utils.deployBundleCallback, null, notifications));
87- },
88-
89- /**
90 Export the YAML for this environment.
91
92 @method exportYAML
93@@ -1349,6 +1352,7 @@
94 'app-base',
95 'app-transitions',
96 'base',
97+ 'bundle-import-helpers',
98 'node',
99 'model',
100 'app-cookies-extension',
101@@ -1364,7 +1368,6 @@
102 'juju-ghost-inspector',
103 'juju-view-bundle',
104 'viewmode-controls',
105- 'help-dropdown',
106- 'bundle-import-extension'
107+ 'help-dropdown'
108 ]
109 });
110
111=== renamed file 'app/assets/javascripts/bundle-import-extension.js' => 'app/assets/javascripts/bundle-import-helpers.js'
112--- app/assets/javascripts/bundle-import-extension.js 2013-11-07 15:52:10 +0000
113+++ app/assets/javascripts/bundle-import-helpers.js 2013-11-15 19:04:35 +0000
114@@ -18,13 +18,71 @@
115
116 'use strict';
117
118-YUI.add('bundle-import-extension', function(Y) {
119-
120- function BundleImport() {}
121-
122- BundleImport.prototype = {
123-
124- sendToDeployer: function(env, db, fileSources) {
125+YUI.add('bundle-import-helpers', function(Y) {
126+ var ns = Y.namespace('juju');
127+
128+ ns.BundleHelpers = {
129+ /**
130+ Calls the deployer import method with the bundle data
131+ to deploy the bundle to the environment.
132+
133+ @method deployBundle
134+ @param {Object} bundle Bundle data.
135+ @param {Environment} env Environment with access to the bundle back end.
136+ calls.
137+ @param {Database} db The app Database with access to the NotificationList.
138+ */
139+ deployBundle: function(bundle, env, db, customCallback) {
140+ var notifications = db.notifications;
141+
142+ var defaultCallback = function(result) {
143+ if (result.err) {
144+ console.log('bundle import failed:', result.err);
145+ notifications.add({
146+ title: 'Bundle deployment failed',
147+ message: 'Unable to deploy the bundle. The server returned the ' +
148+ 'following error: ' + result.err,
149+ level: 'error'
150+ });
151+ } else {
152+ notifications.add({
153+ title: 'Bundle deployment requested',
154+ message: 'Bundle deployment request successful. The full ' +
155+ 'deployment can take some time to complete',
156+ level: 'important'
157+ });
158+
159+ ns.BundleHelpers._watchDeployment(result.DeploymentId, env, db);
160+ }
161+ };
162+
163+ if (Y.Lang.isFunction(env.deployerImport)) {
164+ env.deployerImport(
165+ bundle,
166+ null,
167+ customCallback ? customCallback : defaultCallback
168+ );
169+ } else {
170+ notifications.add({
171+ title: 'Bundle deployment not available.',
172+ message: 'Bundle deployments are not currently available for your' +
173+ 'environment.',
174+ level: 'error'
175+ });
176+ }
177+ },
178+
179+ /**
180+ Deploy one or more files, generally from the import file picker
181+ utility.
182+
183+ @method deployBundleFiles
184+ @param {Array} fileSources The list of files from the import control.
185+ @param {Environment} env Environment with access to the bundle back end.
186+ calls.
187+ @param {Database} db The app Database with access to the NotificationList.
188+ */
189+ deployBundleFiles: function(fileSources, env, db) {
190 var notifications = db.notifications;
191 if (!Y.Lang.isFunction(env.deployerImport)) {
192 // Notify the user that their environment is too old and return.
193@@ -37,37 +95,151 @@
194 });
195 return;
196 }
197+
198 // Handle dropping Deployer files on the canvas.
199 Y.Array.each(fileSources, function(file) {
200 var reader = new FileReader();
201+
202+ // Once the file has been loaded, deploy it.
203 reader.onload = function(e) {
204- // Import each into the environment
205- env.deployerImport(e.target.result, null, function(result) {
206- if (!result.err) {
207- notifications.add({
208- title: 'Imported Deployer file',
209- message: 'Import from "' + file.name + '" successful. This ' +
210- 'can take some time to complete.',
211- level: 'important'
212- });
213- } else {
214- console.warn('import failed', file, result);
215- notifications.add({
216- title: 'Import Environment Failed',
217- message: 'Import from "' + file.name +
218- '" failed.<br/>' + result.err,
219- level: 'error'
220- });
221- }
222- });
223+ ns.BundleHelpers.deployBundle(
224+ e.target.result,
225+ env,
226+ db,
227+ function(result) {
228+ if (!result.err) {
229+
230+ notifications.add({
231+ title: 'Imported Deployer file',
232+ message: 'Import from "' + file.name +
233+ '" successful. This ' +
234+ 'can take some time to complete.',
235+ level: 'important'
236+ });
237+
238+ ns.BundleHelpers._watchDeployment(
239+ result.DeploymentId, env, db);
240+
241+ } else {
242+ console.warn('import failed', file, result);
243+ notifications.add({
244+ title: 'Import Environment Failed',
245+ message: 'Import from "' + file.name +
246+ '" failed.<br/>' + result.err,
247+ level: 'error'
248+ });
249+ }
250+ }
251+ );
252 };
253+
254+ // Start the loading process.
255 reader.readAsText(file);
256 });
257+ },
258+
259+ /**
260+ Watch a deployment for changes and notify the user of updates.
261+
262+ @method watchDeployment
263+ @param {Integer} deploymentId The ID returned from the deployment call.
264+ @param {Environment} env The environment so that we can call the back
265+ end.
266+ @param {Database} db The db which contains the NotificationList used
267+ for adding notifications to the system.
268+ */
269+ _watchDeployment: function(deploymentId, env, db) {
270+ var notifications = db.notifications;
271+
272+ // First generate a watch.
273+ env.deployerWatch(deploymentId, function(data) {
274+ if (data.err) {
275+ notifications.add({
276+ title: 'Unable to watch status of import.',
277+ message: 'Attempting to watch the deployment failed: ' +
278+ data.err,
279+ level: 'error'
280+ });
281+ } else {
282+ ns.BundleHelpers._watchDeploymentUpdates(
283+ data.WatchId, env, db);
284+ }
285+ });
286+ },
287+
288+ /**
289+ Once we've got a Watch, we need to continue to call Next on it to
290+ receive updates. Watch the deployment until it's either completed or
291+ we've gotten an error.
292+
293+ This will make nested calls upon each update from the server and could
294+ be a potential point for recursive callback issues. It appears that we
295+ should only get a couple of levels deep in the notification stack and
296+ will be safe.
297+
298+ @method watchDeployment
299+ @param {Integer} watchId The ID of the Watcher from the watchDeployment
300+ call.
301+ @param {Environment} env The environment so that we can call the
302+ back end.
303+ @param {Database} db The db which contains the NotificationList used
304+ for adding notifications to the system.
305+ */
306+ _watchDeploymentUpdates: function(watchId, env, db) {
307+ var notifications = db.notifications;
308+
309+ var processUpdate = function(data) {
310+ if (data.err) {
311+ notifications.add({
312+ title: 'Error watching deployment',
313+ message: 'The watch of the deployment errored:' +
314+ data.err,
315+ level: 'error'
316+ });
317+
318+ // Break the loop of calling for the next update from the server.
319+ return;
320+
321+ } else {
322+ // Just grab the latest change and notify the user of the
323+ // status.
324+ var newChange = data.Changes[0];
325+ // The change could be the result of an error.
326+ if (newChange.Error !== undefined) {
327+ notifications.add({
328+ title: 'Updated status for deployment: ' +
329+ newChange.DeploymentId,
330+ message: 'The deployment errored: ' +
331+ newChange.Error,
332+ level: 'error'
333+ });
334+ } else {
335+ notifications.add({
336+ title: 'Updated status for deployment: ' +
337+ newChange.DeploymentId,
338+ message: 'The deployment is currently: ' +
339+ newChange.Status,
340+ level: 'important'
341+ });
342+ }
343+
344+ // If the status is 'completed' then we're done watching this.
345+ if (newChange.Status === 'completed') {
346+ // There's nothing else to see here.
347+ return;
348+ } else {
349+ env.deployerWatchUpdate(watchId, processUpdate);
350+ }
351+ }
352+ };
353+
354+ // Make the first call to the env and the processUpdate callback will
355+ // handle re-calling on each update.
356+ env.deployerWatchUpdate(watchId, processUpdate);
357 }
358+
359 };
360
361- Y.namespace('juju').BundleImport = BundleImport;
362-
363-
364+}, '0.1.0', {
365+ requires: []
366 });
367-
368
369=== modified file 'app/modules-debug.js'
370--- app/modules-debug.js 2013-11-12 15:53:05 +0000
371+++ app/modules-debug.js 2013-11-15 19:04:35 +0000
372@@ -181,8 +181,8 @@
373 fullpath: '/juju-ui/assets/javascripts/app-cookies-extension.js'
374 },
375
376- 'bundle-import-extension': {
377- fullpath: '/juju-ui/assets/javascripts/bundle-import-extension.js'
378+ 'bundle-import-helpers': {
379+ fullpath: '/juju-ui/assets/javascripts/bundle-import-helpers.js'
380 },
381
382 'view-dropdown-extension': {
383
384=== modified file 'app/store/env/go.js'
385--- app/store/env/go.js 2013-10-14 19:38:47 +0000
386+++ app/store/env/go.js 2013-11-15 19:04:35 +0000
387@@ -496,6 +496,97 @@
388 userCallback(transformedData);
389 },
390
391+ /**
392+ Register a Watch with the deployment specified.
393+
394+ The callback will receive an {Object} An object with err, and the
395+ generated WatchId. .
396+
397+ @method deployerWatch
398+ @param {Integer} deploymentId The ID of the deployment from the
399+ original deployment call.
400+ @param {Function} callback A user callback to return the response data
401+ to.
402+ */
403+ deployerWatch: function(deploymentId, callback) {
404+ var intermediateCallback;
405+ if (callback) {
406+ intermediateCallback = Y.bind(this.handleDeployerWatch,
407+ this, callback);
408+ }
409+ this._send_rpc({
410+ Type: 'Deployer',
411+ Request: 'Watch',
412+ Params: {
413+ DeploymentId: deploymentId
414+ }
415+ }, intermediateCallback);
416+
417+ },
418+
419+ /**
420+ Callback to process the environments response to requested a watcher for
421+ the deployment specified above.
422+
423+ @method handleDeployerWatch
424+ @param {Function} userCallback The original callback to the
425+ deployerWatch function.
426+ @param {Object} data The servers response to the deployerWatch call.
427+ */
428+ handleDeployerWatch: function(userCallback, data) {
429+ var transformedData = {
430+ err: data.Error,
431+ WatchId: data.Response.WatcherId
432+ };
433+ userCallback(transformedData);
434+ },
435+
436+ /**
437+ Wait for an update to a deployer watch created earlier.
438+
439+ Note: This returns once the server has something to update on. It might
440+ wait a while.
441+
442+ The callback will receive an {Object} An object with err, and the
443+ list of Changes.
444+
445+ @method deployerWatchUpdate
446+ @param {Integer} watchId The ID of the watcher created in depployWatch.
447+ @param {Function} callback The caller's callback function to process
448+ the response.
449+ */
450+ deployerWatchUpdate: function(watchId, callback) {
451+ var intermediateCallback;
452+ if (callback) {
453+ intermediateCallback = Y.bind(this.handleDeployerWatchUpdate,
454+ this, callback);
455+ }
456+
457+ this._send_rpc({
458+ Type: 'Deployer',
459+ Request: 'Next',
460+ Params: {
461+ WatcherId: watchId
462+ }
463+ }, intermediateCallback);
464+ },
465+
466+ /**
467+ Wrapper for the deployerWatchUpdate call.
468+
469+ @method handleDeployerWatchUpdate
470+ @param {Function} userCallback The original callback to the
471+ deployerWatchUpdate function.
472+ @param {Object} data The servers response to the deployerWatchUpdate
473+ call.
474+ */
475+ handleDeployerWatchUpdate: function(userCallback, data) {
476+ var transformedData = {
477+ err: data.Error,
478+ Changes: data.Response.Changes
479+ };
480+ userCallback(transformedData);
481+ },
482
483 /**
484 Deploy a charm.
485
486=== modified file 'app/views/topology/service.js'
487--- app/views/topology/service.js 2013-11-11 18:00:20 +0000
488+++ app/views/topology/service.js 2013-11-15 19:04:35 +0000
489@@ -26,12 +26,12 @@
490 */
491
492 YUI.add('juju-topology-service', function(Y) {
493- var views = Y.namespace('juju.views'),
494+ var d3ns = Y.namespace('d3'),
495+ importHelpers = Y.namespace('juju').BundleHelpers,
496 models = Y.namespace('juju.models'),
497+ topoUtils = Y.namespace('juju.topology.utils'),
498 utils = Y.namespace('juju.views.utils'),
499- topoUtils = Y.namespace('juju.topology.utils'),
500- d3ns = Y.namespace('d3'),
501- Templates = views.Templates;
502+ views = Y.namespace('juju.views');
503
504 var ServiceModuleCommon = function() {};
505 /**
506@@ -285,8 +285,8 @@
507 @class ServiceModule
508 */
509 var ServiceModule = Y.Base.create('ServiceModule', d3ns.Module, [
510- ServiceModuleCommon,
511- Y.juju.BundleImport], {
512+ ServiceModuleCommon
513+ ], {
514 events: {
515 scene: {
516 '.service': {
517@@ -639,10 +639,8 @@
518 var fileSources = dataTransfer.files;
519 var env = topo.get('env');
520 var db = topo.get('db');
521- var notifications = db.notifications;
522 if (fileSources && fileSources.length) {
523- // provided by bundle-import-extension.js
524- this.sendToDeployer(env, db, fileSources);
525+ importHelpers.sendToDeployer(fileSources, env, db);
526 } else {
527 // Handle dropping charm/bundle tokens from the left side bar.
528 var dragData = JSON.parse(dataTransfer.getData('Text'));
529@@ -673,11 +671,13 @@
530 // data, so we wrap the entity data in a mapping. The deployer
531 // format is YAML, but JSON is a subset of YAML, so we can just
532 // encode it this way.
533- env.deployerImport(
534+ importHelpers.deployBundle(
535 Y.JSON.stringify({
536 bundle: entityData.data
537- }), null,
538- Y.bind(utils.deployBundleCallback, null, notifications));
539+ }),
540+ env,
541+ db
542+ );
543 }
544 }
545 }
546@@ -1299,6 +1299,6 @@
547 'juju-models',
548 'juju-env',
549 'unscaled-pack-layout',
550- 'bundle-import-extension'
551+ 'bundle-import-helpers'
552 ]
553 });
554
555=== modified file 'app/views/utils.js'
556--- app/views/utils.js 2013-11-06 14:44:35 +0000
557+++ app/views/utils.js 2013-11-15 19:04:35 +0000
558@@ -1789,24 +1789,6 @@
559 return charmIcons;
560 };
561
562- utils.deployBundleCallback = function(notifications, result) {
563- if (result.err) {
564- console.log('bundle import failed:', result.err);
565- notifications.add({
566- title: 'Bundle Deployment Failed',
567- message: 'Unable to deploy the bundle. The server returned the ' +
568- 'following error: ' + result.err,
569- level: 'error'
570- });
571- return;
572- }
573- notifications.add({
574- title: 'Bundle Deployment Requested',
575- message: 'Bundle deployment request successful. The full ' +
576- 'deployment can take some time to complete',
577- level: 'important'
578- });
579- };
580
581 }, '0.1.0', {
582 requires: [
583
584=== modified file 'bin/merge-files'
585--- bin/merge-files 2013-11-12 15:53:05 +0000
586+++ bin/merge-files 2013-11-15 19:04:35 +0000
587@@ -88,7 +88,7 @@
588 filesToLoad.js.push.apply(filesToLoad.js, [
589 'app/assets/javascripts/app-subapp-extension.js',
590 'app/assets/javascripts/app-cookies-extension.js',
591- 'app/assets/javascripts/bundle-import-extension.js',
592+ 'app/assets/javascripts/bundle-import-helpers.js',
593 'app/assets/javascripts/view-dropdown-extension.js',
594 'app/assets/javascripts/d3-components.js',
595 'app/assets/javascripts/d3.min.js',
596
597=== modified file 'test/index.html'
598--- test/index.html 2013-11-12 20:28:43 +0000
599+++ test/index.html 2013-11-15 19:04:35 +0000
600@@ -58,6 +58,7 @@
601 <script src="test_browser_search_widget.js"></script>
602 <script src="test_bundle_details_view.js"></script>
603 <script src="test_bundle_module.js"></script>
604+ <script src="test_bundle_import_helpers.js"></script>
605 <script src="test_charmworld.js"></script>
606 <script src="test_token_container.js"></script>
607 <script src="test_token.js"></script>
608
609=== added file 'test/test_bundle_import_helpers.js'
610--- test/test_bundle_import_helpers.js 1970-01-01 00:00:00 +0000
611+++ test/test_bundle_import_helpers.js 2013-11-15 19:04:35 +0000
612@@ -0,0 +1,303 @@
613+/*
614+This file is part of the Juju GUI, which lets users view and manage Juju
615+environments within a graphical interface (https://launchpad.net/juju-gui).
616+Copyright (C) 2012-2013 Canonical Ltd.
617+
618+This program is free software: you can redistribute it and/or modify it under
619+the terms of the GNU Affero General Public License version 3, as published by
620+the Free Software Foundation.
621+
622+This program is distributed in the hope that it will be useful, but WITHOUT
623+ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
624+SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
625+General Public License for more details.
626+
627+You should have received a copy of the GNU Affero General Public License along
628+with this program. If not, see <http://www.gnu.org/licenses/>.
629+*/
630+
631+'use strict';
632+
633+(function() {
634+
635+ describe('bundle-import-helpers', function() {
636+ var db, env, ns, Y;
637+
638+ before(function(done) {
639+ Y = YUI(GlobalConfig).use('bundle-import-helpers', function(Y) {
640+ ns = Y.namespace('juju');
641+ done();
642+ });
643+ });
644+
645+ beforeEach(function() {
646+ db = {
647+ notifications: {
648+ add: function(info) {
649+
650+ }
651+ }
652+ };
653+
654+ env = {};
655+ });
656+
657+ afterEach(function() {
658+ db = undefined;
659+ env = undefined;
660+ });
661+
662+ it('errors when the env does not support deployer', function(done) {
663+ db.notifications.add = function(info) {
664+ assert.equal(info.level, 'error');
665+ assert.notEqual(info.title.indexOf('not available'), -1);
666+ done();
667+ };
668+
669+ // Start the process by deploying the bundle.
670+ ns.BundleHelpers.deployBundle('test bundle', env, db);
671+ });
672+
673+ it('errors when the bundle import fails from the env', function(done) {
674+ db.notifications.add = function(info) {
675+ assert.equal(info.level, 'error');
676+ assert.notEqual(info.title.indexOf('deployment failed'), -1);
677+ done();
678+ };
679+
680+ env.deployerImport = function(bundle, name, callback) {
681+ callback({
682+ err: 'Abort abort!'
683+ });
684+ };
685+
686+ // Start the process by deploying the bundle.
687+ ns.BundleHelpers.deployBundle('test bundle', env, db);
688+ });
689+
690+ it('provides deployBundle helper for working through env', function(done) {
691+ // Watch the notification for the message that we're hitting the default
692+ // callback.
693+ var hitNotifications = false;
694+ db.notifications.add = function(info) {
695+ hitNotifications = true;
696+ assert.equal(info.level, 'important');
697+ assert.notEqual(info.title.indexOf('requested'), -1);
698+ };
699+
700+ // Stub out the env call to make sure we check the params and call the
701+ // provided callback.
702+ env.deployerImport = function(bundle, name, callback) {
703+ assert.equal(bundle, 'test bundle');
704+ assert.equal(
705+ name, null, 'The name is not currently supported or passed.');
706+ // This is the default callback from the deployBundle method.
707+ callback({
708+ err: undefined,
709+ DeploymentId: 10
710+ });
711+ };
712+
713+ // A watch should be created in the process of submitting the deployment
714+ // once there's an ID.
715+ var _watchDeployment = ns.BundleHelpers._watchDeployment;
716+ ns.BundleHelpers._watchDeployment = function(id, env, db) {
717+ assert.equal(id, 10);
718+ // At this point we've checked the env call, the notification is
719+ // correct, and now that we've requested a watcher with the right id.
720+ ns.BundleHelpers._watchDeployment = _watchDeployment;
721+
722+ // Make sure we did in fact post our notification to the user.
723+ assert.equal(hitNotifications, true);
724+ done();
725+ };
726+
727+ // Start the process by deploying the bundle.
728+ ns.BundleHelpers.deployBundle('test bundle', env, db);
729+ });
730+
731+ it('provides a notification when a deploy watch updates', function(done) {
732+ var watchId = 1;
733+ var callNumber = 0;
734+
735+ // This will get called twice. First with an update that it was
736+ // scheduled, then with a second call that it's complete which stops
737+ // the look in the watch.
738+ db.notifications.add = function(info) {
739+ if (callNumber === 0) {
740+ assert.equal('Updated status for deployment: 42', info.title);
741+ assert.equal(info.level, 'important');
742+ assert.isTrue(info.message.indexOf('scheduled') !== -1, info.message);
743+ } else {
744+ assert.equal(info.level, 'important');
745+ assert.isTrue(info.message.indexOf('completed') !== -1, info.message);
746+ done();
747+ }
748+ callNumber = callNumber + 1;
749+ };
750+
751+ var called = false;
752+ env.deployerWatchUpdate = function(watchId, callback) {
753+ if (!called) {
754+ called = true;
755+ callback({
756+ err: undefined,
757+ Changes: [
758+ // Copied right from the docs of the charm.
759+ {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080066,
760+ 'Queue': 2},
761+ {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080062,
762+ 'Queue': 1},
763+ {'DeploymentId': 42, 'Status': 'started', 'Time': 1377080000,
764+ 'Queue': 0}
765+ ]
766+ });
767+ } else {
768+ // Make that this is done now.
769+ callback({
770+ err: undefined,
771+ Changes: [
772+ // Copied right from the docs of the charm.
773+ {'DeploymentId': 42, 'Status': 'completed', 'Time': 1377080066,
774+ 'Queue': 2},
775+ {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080062,
776+ 'Queue': 1},
777+ {'DeploymentId': 42, 'Status': 'started', 'Time': 1377080000,
778+ 'Queue': 0}
779+ ]
780+ });
781+ }
782+ };
783+
784+ // Testing private method is evil, but it does a decent amount of
785+ // work and we want the aid in debugging issues.
786+ ns.BundleHelpers._watchDeploymentUpdates(
787+ watchId,
788+ env,
789+ db
790+ );
791+ });
792+
793+ it('provides a error when the deploy watch says so', function(done) {
794+ var watchId = 1;
795+
796+ // This will get called twice. First with an update that it was
797+ // scheduled, then with a second call that it's complete which stops
798+ // the look in the watch.
799+ db.notifications.add = function(info) {
800+ assert.equal(info.level, 'error');
801+ assert.isTrue(info.message.indexOf('boom') !== -1, info.message);
802+ done();
803+ };
804+
805+ env.deployerWatchUpdate = function(watchId, callback) {
806+ callback({
807+ err: undefined,
808+ Changes: [
809+ // Copied right from the docs of the charm.
810+ {'DeploymentId': 42, 'Status': 'completed', 'Time': 1377080066,
811+ 'Queue': 2, 'Error': 'Deploy go boom!'},
812+ {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080062,
813+ 'Queue': 1},
814+ {'DeploymentId': 42, 'Status': 'started', 'Time': 1377080000,
815+ 'Queue': 0}
816+ ]
817+ });
818+ };
819+
820+ // Testing private method is evil, but it does a decent amount of
821+ // work and we want the aid in debugging issues.
822+ ns.BundleHelpers._watchDeploymentUpdates(
823+ watchId,
824+ env,
825+ db
826+ );
827+ });
828+
829+
830+ it('the stack of deploy to watch integrate', function(done) {
831+ // By stubbing only the env calls to behave properly, we should be able to
832+ // verify that the bundle-import-helpers stack up and call in proper
833+ // succession to get a deployment id, then a watcher id, then the watch
834+ // status updates, and then completes.
835+
836+ // Stub out the env call to make sure we check the params and call the
837+ // provided callback.
838+ env.deployerImport = function(bundle, name, callback) {
839+ callback({
840+ err: undefined,
841+ DeploymentId: 10
842+ });
843+ };
844+
845+ env.deployerWatch = function(deploymentId, callback) {
846+ callback({
847+ WatchId: 1
848+ });
849+ };
850+
851+ var updated = false;
852+ env.deployerWatchUpdate = function(watchId, callback) {
853+ if (!updated) {
854+ updated = true;
855+ callback({
856+ err: undefined,
857+ Changes: [
858+ // Copied right from the docs of the charm.
859+ {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080066,
860+ 'Queue': 2},
861+ {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080062,
862+ 'Queue': 1},
863+ {'DeploymentId': 42, 'Status': 'started', 'Time': 1377080000,
864+ 'Queue': 0}
865+ ]
866+ });
867+ } else {
868+ // Make that this is done now.
869+ callback({
870+ err: undefined,
871+ Changes: [
872+ // Copied right from the docs of the charm.
873+ {'DeploymentId': 42, 'Status': 'completed', 'Time': 1377080066,
874+ 'Queue': 2},
875+ {'DeploymentId': 42, 'Status': 'scheduled', 'Time': 1377080062,
876+ 'Queue': 1},
877+ {'DeploymentId': 42, 'Status': 'started', 'Time': 1377080000,
878+ 'Queue': 0}
879+ ]
880+ });
881+ }
882+ };
883+
884+ // We'll be called several times.
885+ // 1. Deployment Requested.
886+ // 2. Watching will pass without a notification.
887+ // 3. The status will first be scheduled.
888+ // 4. The status will be completed.
889+ var called = 0;
890+ db.notifications.add = function(info) {
891+ switch (called) {
892+ case 0:
893+ assert.notEqual(
894+ info.title.indexOf('requested'), -1, 'not requested');
895+ break;
896+ case 1:
897+ assert.notEqual(
898+ info.message.indexOf('scheduled'), -1, 'not scheduled');
899+ break;
900+ case 2:
901+ assert.notEqual(
902+ info.message.indexOf('completed'), -1, 'not completed');
903+ done();
904+ break;
905+ }
906+ called = called + 1;
907+ };
908+
909+ // Start the process by deploying the bundle.
910+ ns.BundleHelpers.deployBundle('test bundle', env, db);
911+
912+ });
913+
914+ });
915+})();
916
917=== modified file 'test/test_env_go.js'
918--- test/test_env_go.js 2013-09-27 08:09:27 +0000
919+++ test/test_env_go.js 2013-11-15 19:04:35 +0000
920@@ -325,6 +325,7 @@
921 });
922 });
923
924+
925 describe('Deployer support', function() {
926 it('sends the correct messages on deployer imports', function() {
927 env.deployerImport('YAML BLOB');
928@@ -351,6 +352,35 @@
929 };
930 assert.deepEqual(expected, last_message);
931 });
932+
933+ it('builds a proper watch request', function() {
934+ env.deployerWatch(2);
935+ var last_message = conn.last_message();
936+ var expected = {
937+ Type: 'Deployer',
938+ Request: 'Watch',
939+ RequestId: 1,
940+ Params: {
941+ DeploymentId: 2
942+ }
943+ };
944+ assert.deepEqual(expected, last_message);
945+ });
946+
947+ it('builds a proper watch next request', function() {
948+ env.deployerWatchUpdate(5);
949+ var last_message = conn.last_message();
950+ var expected = {
951+ Type: 'Deployer',
952+ Request: 'Next',
953+ RequestId: 1,
954+ Params: {
955+ WatcherId: 5
956+ }
957+ };
958+ assert.deepEqual(expected, last_message);
959+ });
960+
961 });
962
963 it('sends the correct expose message', function() {
964
965=== modified file 'test/test_service_module.js'
966--- test/test_service_module.js 2013-11-05 18:10:05 +0000
967+++ test/test_service_module.js 2013-11-15 19:04:35 +0000
968@@ -328,10 +328,15 @@
969 }
970 };
971
972- view.topo.set('env', {deployerImport: function(deployerData) {
973+ // mock out the Y.BundleHelpers call.
974+ var _deployBundle = juju.BundleHelpers.deployBundle;
975+ juju.BundleHelpers.deployBundle = function(deployerData, env, db) {
976 assert.include(deployerData, 'BUNDLE DATA');
977+ // Restore the deployBundle call for future tests.
978+ juju.BundleHelpers.deployBundle = _deployBundle;
979 done();
980- }});
981+ };
982+
983 serviceModule.set('component', view.topo);
984 serviceModule.canvasDropHandler(fakeEventObject);
985 });
986
987=== modified file 'test/test_utils.js'
988--- test/test_utils.js 2013-11-06 14:39:56 +0000
989+++ test/test_utils.js 2013-11-15 19:04:35 +0000
990@@ -1451,43 +1451,4 @@
991 });
992 });
993
994- describe('utils.deployBundleCallback', function() {
995- var utils, Y;
996-
997- before(function(done) {
998- Y = YUI(GlobalConfig).use('juju-view-utils', function(Y) {
999- utils = Y.juju.views.utils;
1000- done();
1001- });
1002- });
1003-
1004- it('adds a notification if bundle import is successful', function(done) {
1005- var expected = {
1006- title: 'Bundle Deployment Requested',
1007- message: 'Bundle deployment request successful. The full deployment ' +
1008- 'can take some time to complete',
1009- level: 'important'
1010- };
1011- utils.deployBundleCallback({
1012- add: function(notification) {
1013- assert.deepEqual(notification, expected);
1014- done();
1015- }}, {});
1016- });
1017-
1018- it('adds a notification if a deployment error occurs', function(done) {
1019- var expected = {
1020- title: 'Bundle Deployment Failed',
1021- message: 'Unable to deploy the bundle. The server returned the ' +
1022- 'following error: bad wolf',
1023- level: 'error'
1024- };
1025- utils.deployBundleCallback({
1026- add: function(notification) {
1027- assert.deepEqual(notification, expected);
1028- done();
1029- }}, {err: 'bad wolf'});
1030- });
1031- });
1032-
1033 })();

Subscribers

People subscribed via source and target branches