Merge lp:~bcsaller/juju-gui/export-ui into lp:juju-gui/experimental
- export-ui
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 666 |
Proposed branch: | lp:~bcsaller/juju-gui/export-ui |
Merge into: | lp:juju-gui/experimental |
Diff against target: |
455 lines (+312/-34) 9 files modified
.jshintignore (+1/-0) .jshintrc (+2/-1) Makefile (+1/-1) app/app.js (+27/-2) app/assets/javascripts/FileSaver.js (+216/-0) app/index.html (+2/-2) app/modules-debug.js (+9/-0) app/views/topology/importexport.js (+53/-28) bin/merge-files (+1/-0) |
To merge this branch: | bzr merge lp:~bcsaller/juju-gui/export-ui |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
Drag out to export
Dragging the environment icon in the nav to another supporting
GUI app will create DnD export/import chain.
DnD export is very limited now and only supports dragging from one
GUI instance to another. Because of this its hidden behind a feature
flag. /:flags:/dndexport/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
- 654. By Benjamin Saller
-
restore hotkey with a skip jshint on the ugly library code
- 655. By Benjamin Saller
-
FileSave w/hotkey passing tests again
- 656. By Benjamin Saller
-
missing file
- 657. By Benjamin Saller
-
lint
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
LGTM, with protecting the drag story (which surprised me in QA, as we
discussed, because I thought I could drag to the desktop) behind a
feature flag. If you change the feature flag rules, please doc the
intent of the new rules, like in the blog or something.
Thank you!
Gary
- 658. By Benjamin Saller
-
hide dndexport behind feature flag
- 659. By Benjamin Saller
-
lint
- 660. By Benjamin Saller
-
taking the easy way out with lint
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
- 661. By Benjamin Saller
-
move the feature flag to only guard the dnd export, not import as well
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Nicola Larosa (teknico) wrote : | # |
LGTM. I did not review FileSaver.js, I don't know how it's supposed to
work and the wide indentation makes it unreadable anyway.
Preview Diff
1 | === modified file '.jshintignore' |
2 | --- .jshintignore 2013-05-08 23:25:07 +0000 |
3 | +++ .jshintignore 2013-05-15 05:17:23 +0000 |
4 | @@ -1,2 +1,3 @@ |
5 | app/assets/javascripts/prettify.js |
6 | +app/assets/javascripts/FileSaver.js |
7 | app/assets/javascripts/unscaled-pack-layout.js |
8 | |
9 | === modified file '.jshintrc' |
10 | --- .jshintrc 2013-03-11 20:46:31 +0000 |
11 | +++ .jshintrc 2013-05-15 05:17:23 +0000 |
12 | @@ -124,7 +124,8 @@ |
13 | "jsyaml", |
14 | "consoleManager", |
15 | "it", |
16 | - "YUI" |
17 | + "YUI", |
18 | + "saveAs" |
19 | ], |
20 | // "indent" : 2, // Specify indentation spacing |
21 | "quotmark": "single" |
22 | |
23 | === modified file 'Makefile' |
24 | --- Makefile 2013-05-02 20:28:08 +0000 |
25 | +++ Makefile 2013-05-15 05:17:23 +0000 |
26 | @@ -35,7 +35,7 @@ |
27 | -e '^app/assets/javascripts/gallery-.*\.js$$' \ |
28 | -e '^server.js$$') |
29 | THIRD_PARTY_JS=app/assets/javascripts/reconnecting-websocket.js |
30 | -LINT_IGNORE='app/assets/javascripts/prettify.js' |
31 | +LINT_IGNORE='app/assets/javascripts/prettify.js, app/assets/javascripts/FileSaver.js' |
32 | NODE_TARGETS=node_modules/chai node_modules/cryptojs node_modules/d3 \ |
33 | node_modules/expect.js node_modules/express \ |
34 | node_modules/graceful-fs node_modules/grunt node_modules/jshint \ |
35 | |
36 | === modified file 'app/app.js' |
37 | --- app/app.js 2013-05-14 15:09:07 +0000 |
38 | +++ app/app.js 2013-05-15 05:17:23 +0000 |
39 | @@ -149,6 +149,18 @@ |
40 | focus: true, |
41 | help: 'Select the charm Search' |
42 | }, |
43 | + 'S-d': { |
44 | + callback: function(evt) { |
45 | + /* global saveAs: false */ |
46 | + this.env.exportEnvironment(function(r) { |
47 | + var exportData = JSON.stringify(r.result, undefined, 2); |
48 | + var exportBlob = new Blob([exportData], |
49 | + {type: 'application/json;charset=utf-8'}); |
50 | + saveAs(exportBlob, 'export.json'); |
51 | + }); |
52 | + }, |
53 | + help: 'Export the environment' |
54 | + }, |
55 | 'S-/': { |
56 | target: '#shortcut-help', |
57 | toggle: true, |
58 | @@ -935,7 +947,7 @@ |
59 | nsRouter: this.nsRouter, |
60 | landscape: this.landscape, |
61 | endpointsController: this.endpointsController, |
62 | - useDragDropImport: this.get('sandbox') || false, |
63 | + useDragDropImport: this.get('sandbox'), |
64 | db: this.db, |
65 | env: this.env}; |
66 | |
67 | @@ -974,6 +986,17 @@ |
68 | > The name looks like dotted python identifiers, with the form |
69 | > APP.FEATURE.EFFECT. The value is a Unicode string. |
70 | |
71 | + A shortened version of key can be used if they follow this pattern: |
72 | + - The feature flag applies to the gui. |
73 | + - The presence of the flag indicates Boolean enablement |
74 | + - The (default) absence of the flag indicates the feature will be |
75 | + unavailable. |
76 | + |
77 | + If those conditions are met then you may simply use the descriptive name of |
78 | + the feature taking care it uniquely defines the feature. An example is |
79 | + rather than specifying gui.dndexport.enable you can specify dndexport as a |
80 | + flag. |
81 | + |
82 | @method featureFlags |
83 | @param {object} req The request object. |
84 | @param {object} res The response object. |
85 | @@ -1195,5 +1218,7 @@ |
86 | 'subapp-browser', |
87 | 'event-key', |
88 | 'event-touch', |
89 | - 'model-controller'] |
90 | + 'model-controller', |
91 | + 'FileSaver' |
92 | + ] |
93 | }); |
94 | |
95 | === added file 'app/assets/javascripts/FileSaver.js' |
96 | --- app/assets/javascripts/FileSaver.js 1970-01-01 00:00:00 +0000 |
97 | +++ app/assets/javascripts/FileSaver.js 2013-05-15 05:17:23 +0000 |
98 | @@ -0,0 +1,216 @@ |
99 | +/* FileSaver.js |
100 | + * A saveAs() FileSaver implementation. |
101 | + * 2013-01-23 |
102 | + * |
103 | + * By Eli Grey, http://eligrey.com |
104 | + * License: X11/MIT |
105 | + * See LICENSE.md |
106 | + */ |
107 | + |
108 | +/*global self */ |
109 | +/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, |
110 | + plusplus: true */ |
111 | + |
112 | +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ |
113 | + |
114 | +var saveAs = saveAs |
115 | + || (navigator.msSaveBlob && navigator.msSaveBlob.bind(navigator)) |
116 | + || (function(view) { |
117 | + "use strict"; |
118 | + var |
119 | + doc = view.document |
120 | + // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet |
121 | + , get_URL = function() { |
122 | + return view.URL || view.webkitURL || view; |
123 | + } |
124 | + , URL = view.URL || view.webkitURL || view |
125 | + , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") |
126 | + , can_use_save_link = "download" in save_link |
127 | + , click = function(node) { |
128 | + var event = doc.createEvent("MouseEvents"); |
129 | + event.initMouseEvent( |
130 | + "click", true, false, view, 0, 0, 0, 0, 0 |
131 | + , false, false, false, false, 0, null |
132 | + ); |
133 | + node.dispatchEvent(event); |
134 | + } |
135 | + , webkit_req_fs = view.webkitRequestFileSystem |
136 | + , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem |
137 | + , throw_outside = function (ex) { |
138 | + (view.setImmediate || view.setTimeout)(function() { |
139 | + throw ex; |
140 | + }, 0); |
141 | + } |
142 | + , force_saveable_type = "application/octet-stream" |
143 | + , fs_min_size = 0 |
144 | + , deletion_queue = [] |
145 | + , process_deletion_queue = function() { |
146 | + var i = deletion_queue.length; |
147 | + while (i--) { |
148 | + var file = deletion_queue[i]; |
149 | + if (typeof file === "string") { // file is an object URL |
150 | + URL.revokeObjectURL(file); |
151 | + } else { // file is a File |
152 | + file.remove(); |
153 | + } |
154 | + } |
155 | + deletion_queue.length = 0; // clear queue |
156 | + } |
157 | + , dispatch = function(filesaver, event_types, event) { |
158 | + event_types = [].concat(event_types); |
159 | + var i = event_types.length; |
160 | + while (i--) { |
161 | + var listener = filesaver["on" + event_types[i]]; |
162 | + if (typeof listener === "function") { |
163 | + try { |
164 | + listener.call(filesaver, event || filesaver); |
165 | + } catch (ex) { |
166 | + throw_outside(ex); |
167 | + } |
168 | + } |
169 | + } |
170 | + } |
171 | + , FileSaver = function(blob, name) { |
172 | + // First try a.download, then web filesystem, then object URLs |
173 | + var |
174 | + filesaver = this |
175 | + , type = blob.type |
176 | + , blob_changed = false |
177 | + , object_url |
178 | + , target_view |
179 | + , get_object_url = function() { |
180 | + var object_url = get_URL().createObjectURL(blob); |
181 | + deletion_queue.push(object_url); |
182 | + return object_url; |
183 | + } |
184 | + , dispatch_all = function() { |
185 | + dispatch(filesaver, "writestart progress write writeend".split(" ")); |
186 | + } |
187 | + // on any filesys errors revert to saving with object URLs |
188 | + , fs_error = function() { |
189 | + // don't create more object URLs than needed |
190 | + if (blob_changed || !object_url) { |
191 | + object_url = get_object_url(blob); |
192 | + } |
193 | + if (target_view) { |
194 | + target_view.location.href = object_url; |
195 | + } |
196 | + filesaver.readyState = filesaver.DONE; |
197 | + dispatch_all(); |
198 | + } |
199 | + , abortable = function(func) { |
200 | + return function() { |
201 | + if (filesaver.readyState !== filesaver.DONE) { |
202 | + return func.apply(this, arguments); |
203 | + } |
204 | + }; |
205 | + } |
206 | + , create_if_not_found = {create: true, exclusive: false} |
207 | + , slice |
208 | + ; |
209 | + filesaver.readyState = filesaver.INIT; |
210 | + if (!name) { |
211 | + name = "download"; |
212 | + } |
213 | + if (can_use_save_link) { |
214 | + object_url = get_object_url(blob); |
215 | + save_link.href = object_url; |
216 | + save_link.download = name; |
217 | + click(save_link); |
218 | + filesaver.readyState = filesaver.DONE; |
219 | + dispatch_all(); |
220 | + return; |
221 | + } |
222 | + // Object and web filesystem URLs have a problem saving in Google Chrome when |
223 | + // viewed in a tab, so I force save with application/octet-stream |
224 | + // http://code.google.com/p/chromium/issues/detail?id=91158 |
225 | + if (view.chrome && type && type !== force_saveable_type) { |
226 | + slice = blob.slice || blob.webkitSlice; |
227 | + blob = slice.call(blob, 0, blob.size, force_saveable_type); |
228 | + blob_changed = true; |
229 | + } |
230 | + // Since I can't be sure that the guessed media type will trigger a download |
231 | + // in WebKit, I append .download to the filename. |
232 | + // https://bugs.webkit.org/show_bug.cgi?id=65440 |
233 | + if (webkit_req_fs && name !== "download") { |
234 | + name += ".download"; |
235 | + } |
236 | + if (type === force_saveable_type || webkit_req_fs) { |
237 | + target_view = view; |
238 | + } else { |
239 | + target_view = view.open(); |
240 | + } |
241 | + if (!req_fs) { |
242 | + fs_error(); |
243 | + return; |
244 | + } |
245 | + fs_min_size += blob.size; |
246 | + req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { |
247 | + fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { |
248 | + var save = function() { |
249 | + dir.getFile(name, create_if_not_found, abortable(function(file) { |
250 | + file.createWriter(abortable(function(writer) { |
251 | + writer.onwriteend = function(event) { |
252 | + target_view.location.href = file.toURL(); |
253 | + deletion_queue.push(file); |
254 | + filesaver.readyState = filesaver.DONE; |
255 | + dispatch(filesaver, "writeend", event); |
256 | + }; |
257 | + writer.onerror = function() { |
258 | + var error = writer.error; |
259 | + if (error.code !== error.ABORT_ERR) { |
260 | + fs_error(); |
261 | + } |
262 | + }; |
263 | + "writestart progress write abort".split(" ").forEach(function(event) { |
264 | + writer["on" + event] = filesaver["on" + event]; |
265 | + }); |
266 | + writer.write(blob); |
267 | + filesaver.abort = function() { |
268 | + writer.abort(); |
269 | + filesaver.readyState = filesaver.DONE; |
270 | + }; |
271 | + filesaver.readyState = filesaver.WRITING; |
272 | + }), fs_error); |
273 | + }), fs_error); |
274 | + }; |
275 | + dir.getFile(name, {create: false}, abortable(function(file) { |
276 | + // delete file if it already exists |
277 | + file.remove(); |
278 | + save(); |
279 | + }), abortable(function(ex) { |
280 | + if (ex.code === ex.NOT_FOUND_ERR) { |
281 | + save(); |
282 | + } else { |
283 | + fs_error(); |
284 | + } |
285 | + })); |
286 | + }), fs_error); |
287 | + }), fs_error); |
288 | + } |
289 | + , FS_proto = FileSaver.prototype |
290 | + , saveAs = function(blob, name) { |
291 | + return new FileSaver(blob, name); |
292 | + } |
293 | + ; |
294 | + FS_proto.abort = function() { |
295 | + var filesaver = this; |
296 | + filesaver.readyState = filesaver.DONE; |
297 | + dispatch(filesaver, "abort"); |
298 | + }; |
299 | + FS_proto.readyState = FS_proto.INIT = 0; |
300 | + FS_proto.WRITING = 1; |
301 | + FS_proto.DONE = 2; |
302 | + |
303 | + FS_proto.error = |
304 | + FS_proto.onwritestart = |
305 | + FS_proto.onprogress = |
306 | + FS_proto.onwrite = |
307 | + FS_proto.onabort = |
308 | + FS_proto.onerror = |
309 | + FS_proto.onwriteend = |
310 | + null; |
311 | + |
312 | + view.addEventListener("unload", process_deletion_queue, false); |
313 | + return saveAs; |
314 | +}(self)); |
315 | |
316 | === modified file 'app/index.html' |
317 | --- app/index.html 2013-05-13 16:58:10 +0000 |
318 | +++ app/index.html 2013-05-15 05:17:23 +0000 |
319 | @@ -83,8 +83,8 @@ |
320 | </span> |
321 | <span class="nav-container"> |
322 | <span class="nav-section"> |
323 | - <i class="sprite environment_icon"></i> |
324 | - <span id="environment-name"></span> |
325 | + <i class="sprite environment_icon"> </i> |
326 | + <span id="environment-name" draggable="true"></span> |
327 | <span id="provider-type" class="provider-type"></span> |
328 | </span> |
329 | </span> |
330 | |
331 | === modified file 'app/modules-debug.js' |
332 | --- app/modules-debug.js 2013-05-14 14:46:24 +0000 |
333 | +++ app/modules-debug.js 2013-05-15 05:17:23 +0000 |
334 | @@ -76,6 +76,15 @@ |
335 | } |
336 | } |
337 | }, |
338 | + |
339 | + filesaver: { |
340 | + modules: { |
341 | + 'FileSaver': { |
342 | + fullpath: '/juju-ui/assets/javascripts/FileSaver.js' |
343 | + } |
344 | + } |
345 | + }, |
346 | + |
347 | juju: { |
348 | modules: { |
349 | // Primitives |
350 | |
351 | === modified file 'app/views/topology/importexport.js' |
352 | --- app/views/topology/importexport.js 2013-05-07 17:52:05 +0000 |
353 | +++ app/views/topology/importexport.js 2013-05-15 05:17:23 +0000 |
354 | @@ -55,36 +55,61 @@ |
355 | notifications = topo.get('db').notifications, |
356 | env = topo.get('env'), |
357 | fileSources = evt._event.dataTransfer.files; |
358 | - |
359 | - Y.Array.each(fileSources, function(file) { |
360 | - var reader = new FileReader(); |
361 | - reader.onload = function(e) { |
362 | - // Import each into the environment |
363 | - console.log('Importing ' + file.name); |
364 | - env.importEnvironment(e.target.result, function(result) { |
365 | - if (!result.error) { |
366 | - notifications.add({ |
367 | - title: 'Imported Environment', |
368 | - message: 'Import from "' + file.name + '" successful', |
369 | - level: 'important' |
370 | - }); |
371 | - } else { |
372 | - notifications.add({ |
373 | - title: 'Import Environment Failed', |
374 | - message: 'Import from "' + file.name + |
375 | - '" failed.<br/>' + result.error, |
376 | - level: 'error' |
377 | - }); |
378 | - } |
379 | - }); |
380 | - }; |
381 | - reader.onerror = function(err) { |
382 | - console.warn(err); |
383 | - }; |
384 | - reader.readAsText(file); |
385 | - }); |
386 | + if (fileSources.length) { |
387 | + Y.Array.each(fileSources, function(file) { |
388 | + var reader = new FileReader(); |
389 | + reader.onload = function(e) { |
390 | + // Import each into the environment |
391 | + env.importEnvironment(e.target.result, function(result) { |
392 | + if (!result.error) { |
393 | + notifications.add({ |
394 | + title: 'Imported Environment', |
395 | + message: 'Import from "' + file.name + '" successful', |
396 | + level: 'important' |
397 | + }); |
398 | + } else { |
399 | + notifications.add({ |
400 | + title: 'Import Environment Failed', |
401 | + message: 'Import from "' + file.name + |
402 | + '" failed.<br/>' + result.error, |
403 | + level: 'error' |
404 | + }); |
405 | + } |
406 | + }); |
407 | + }; |
408 | + reader.readAsText(file); |
409 | + }); |
410 | + } else { |
411 | + env.importEnvironment(evt._event.dataTransfer.getData('Text')); |
412 | + } |
413 | evt.preventDefault(); |
414 | evt.stopPropagation(); |
415 | + }, |
416 | + |
417 | + /** |
418 | + * Update lifecycle phase |
419 | + * @method update |
420 | + */ |
421 | + update: function() { |
422 | + // Check the feature flag |
423 | + if (!this._dragHandle && window.flags.dndexport) { |
424 | + var env = this.get('component').get('env'); |
425 | + this._dragHandle = Y.one('#environment-name') |
426 | + .on('dragstart', function(evt) { |
427 | + env.exportEnvironment(function(r) { |
428 | + var ev = evt._event; |
429 | + ev.dataTransfer.dragEffect = 'copy'; |
430 | + var json = JSON.stringify(r.result); |
431 | + ev.dataTransfer.setData('Text', json); |
432 | + }); |
433 | + evt.stopPropagation(); |
434 | + }, this); |
435 | + |
436 | + this.get('component') |
437 | + .recordSubscription(this, this._dragHandle); |
438 | + |
439 | + } |
440 | + ImportExportModule.superclass.update.call(this); |
441 | } |
442 | }, { |
443 | ATTRS: {} |
444 | |
445 | === modified file 'bin/merge-files' |
446 | --- bin/merge-files 2013-05-08 22:00:24 +0000 |
447 | +++ bin/merge-files 2013-05-15 05:17:23 +0000 |
448 | @@ -81,6 +81,7 @@ |
449 | 'app/assets/javascripts/prettify.js', |
450 | 'app/assets/javascripts/reconnecting-websocket.js', |
451 | 'app/assets/javascripts/resizing_textarea.js', |
452 | + 'app/assets/javascripts/FileSaver.js', |
453 | 'app/assets/javascripts/sub-app.js', |
454 | 'app/assets/javascripts/unscaled-pack-layout.js' |
455 | ]); |
Reviewers: mp+162691_ code.launchpad. net,
Message:
Please take a look.
Description:
Drag out to export
Dragging the environment icon in the nav to another supporting
GUI app will create DnD export/import chain.
https:/ /code.launchpad .net/~bcsaller/ juju-gui/ export- ui/+merge/ 162691
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/9252043/
Affected files: topology/ importexport. js
A [revision details]
M app/index.html
M app/views/
Index: [revision details]
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision: <email address hidden>
+New revision: <email address hidden>
Index: app/index.html
</span>
<span class=" nav-container" >
< span class=" nav-section" > icon">< /i> -name"> </span> -name" draggable= "true"> </span>
<span id="provider-type" class=" provider- type">< /span>
< /span>
</span>
=== modified file 'app/index.html'
--- app/index.html 2013-05-04 20:11:20 +0000
+++ app/index.html 2013-05-06 20:31:34 +0000
@@ -83,8 +83,8 @@
- <i class="sprite environment_
- <span id="environment
+ <i class="sprite environment_icon"> </i>
+ <span id="environment
Index: app/views/ topology/ importexport. js topology/ importexport. js' topology/ importexport. js 2013-05-02 20:13:10 +0000 topology/ importexport. js 2013-05-07 00:32:53 +0000
notifications = topo.get( 'db').notificat ions,
fileSources = evt._event. dataTransfer. files; each(fileSource s, function(file) { onment( e.target. result) ; readAsText( file); length) { each(fileSource s, function(file) { onment( e.target. result) ; readAsText( file);
=== modified file 'app/views/
--- app/views/
+++ app/views/
@@ -55,24 +55,52 @@
env = topo.get('env'),
-
- Y.Array.
- var reader = new FileReader();
- reader.onload = (function(fileData) {
- return function(e) {
- // Import each into the environment
- env.importEnvir
- notifications.add({
- title: 'Imported Environment',
- message: 'Import from "' + file.name + '" successful',
- level: 'important'
- });
- };
- })(file);
- reader.
- });
+ if (fileSources.
+ Y.Array.
+ var reader = new FileReader();
+ reader.onload = (function(fileData) {
+ return function(e) {
+ // Import each into the environment
+ env.importEnvir
+ notifications.add({
+ title: 'Imported Environment',
+ message: 'Import from "' + file.name + '" successful',
+ level: 'important'
+ });
+ };
+ })(file);
+ reader.
+ });
+ } e...