Merge lp:~bcsaller/juju-gui/export-ui into lp:juju-gui/experimental

Proposed by Benjamin Saller
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
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+162691@code.launchpad.net

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/

https://codereview.appspot.com/9252043/

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

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:
   A [revision details]
   M app/index.html
   M app/views/topology/importexport.js

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
=== 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 @@
                  </span>
                  <span class="nav-container">
                    <span class="nav-section">
- <i class="sprite environment_icon"></i>
- <span id="environment-name"></span>
+ <i class="sprite environment_icon"> </i>
+ <span id="environment-name" draggable="true"></span>
                      <span id="provider-type" class="provider-type"></span>
                    </span>
                  </span>

Index: app/views/topology/importexport.js
=== modified file 'app/views/topology/importexport.js'
--- app/views/topology/importexport.js 2013-05-02 20:13:10 +0000
+++ app/views/topology/importexport.js 2013-05-07 00:32:53 +0000
@@ -55,24 +55,52 @@
                notifications = topo.get('db').notifications,
                env = topo.get('env'),
                fileSources = evt._event.dataTransfer.files;
-
- Y.Array.each(fileSources, function(file) {
- var reader = new FileReader();
- reader.onload = (function(fileData) {
- return function(e) {
- // Import each into the environment
- env.importEnvironment(e.target.result);
- notifications.add({
- title: 'Imported Environment',
- message: 'Import from "' + file.name + '" successful',
- level: 'important'
- });
- };
- })(file);
- reader.readAsText(file);
- });
+ if (fileSources.length) {
+ Y.Array.each(fileSources, function(file) {
+ var reader = new FileReader();
+ reader.onload = (function(fileData) {
+ return function(e) {
+ // Import each into the environment
+ env.importEnvironment(e.target.result);
+ notifications.add({
+ title: 'Imported Environment',
+ message: 'Import from "' + file.name + '" successful',
+ level: 'important'
+ });
+ };
+ })(file);
+ reader.readAsText(file);
+ });
+ } e...

Read more...

lp:~bcsaller/juju-gui/export-ui updated
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

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Revision history for this message
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

https://codereview.appspot.com/9252043/

lp:~bcsaller/juju-gui/export-ui updated
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

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

move the feature flag to only guard the dnd export, not import as well

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Revision history for this message
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.

https://codereview.appspot.com/9252043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 ]);

Subscribers

People subscribed via source and target branches