On 09/15/2014 04:46 AM, Hongliang Wang wrote:

On 09/12/2014 11:32 PM, Aline Manera wrote:

On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Implemented download and upload volumes functions.

Signed-off-by: Hongliang Wang <hlwang@linux.vnet.ibm.com>
---
  ui/css/theme-default/storagepool-add-volume.css |  36 ++++
  ui/js/src/kimchi.storagepool_add_volume_main.js | 243 ++++++++++++++++++++++++
  ui/pages/storagepool-add-volume.html.tmpl       |  80 ++++++++
  3 files changed, 359 insertions(+)
  create mode 100644 ui/css/theme-default/storagepool-add-volume.css
  create mode 100644 ui/js/src/kimchi.storagepool_add_volume_main.js
  create mode 100644 ui/pages/storagepool-add-volume.html.tmpl

diff --git a/ui/css/theme-default/storagepool-add-volume.css b/ui/css/theme-default/storagepool-add-volume.css
new file mode 100644
index 0000000..6e8a551
--- /dev/null
+++ b/ui/css/theme-default/storagepool-add-volume.css
@@ -0,0 +1,36 @@
+/*
+ * Project Kimchi
+ *
+ * Copyright IBM, Corp. 2014
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#sp-add-volume-window {
+    height: 400px;
+    width: 500px;
+}
+
+#sp-add-volume-window .textbox-wrapper input[type="text"] {
+    box-sizing: border-box;
+    width: 100%;
+}
+
+#sp-add-volume-window .textbox-wrapper label {
+    vertical-align: middle;
+}
+
+#sp-add-volume-window input[type="text"][disabled] {
+    color: #bbb;
+    background-color: #fafafa;
+    cursor: not-allowed;
+}
diff --git a/ui/js/src/kimchi.storagepool_add_volume_main.js b/ui/js/src/kimchi.storagepool_add_volume_main.js
new file mode 100644
index 0000000..9435e28
--- /dev/null
+++ b/ui/js/src/kimchi.storagepool_add_volume_main.js
@@ -0,0 +1,243 @@
+/*
+ * Project Kimchi
+ *
+ * Copyright IBM, Corp. 2014
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+kimchi.sp_add_volume_main = function() {
+    // download from remote server or upload from local file
+    var type = 'download';
+
+    var addButton = $('#sp-add-volume-button');
+    var remoteURLBox = $('#volume-remote-url');
+    var localFileBox = $('#volume-input-file');
+    var typeRadios = $('input.volume-type');
+
+    var isValidURL = function() {
+        var url = $(remoteURLBox).val();
+        return kimchi.template_check_url(url);
+    };
+


+    var isValidFile = function() {
+        var fileName = $(localFileBox).val();
+        return fileName &&
+            /[Ii][Ss][Oo]/g.test(fileName.split('.').pop());
+    };

The user can download and upload any type of file.
So you just need to check it a file was selected.
ACK.

+
+    $(typeRadios).change(function(event) {
+        $('.volume-input').prop('disabled', true);
+        $('.volume-input.' + this.value).prop('disabled', false);
+        type = this.value;
+        if(type == 'download') {
+            $(addButton).prop('disabled', !isValidURL());
+        }
+        else {
+            $(addButton).prop('disabled', !isValidFile());
+        }
+    });
+
+    $(remoteURLBox).on('input propertychange', function(event) {
+        $(addButton).prop('disabled', !isValidURL());
+    });
+
+    $(localFileBox).on('change', function(event) {
+        $(addButton).prop('disabled', !isValidFile());
+    });
+


+    if(!kimchi.volumeTransferTracker) {
+        kimchi.volumeTransferTracker = (function() {
+            var tasks = {},
+                sps = {};
+            var addTask = function(task) {
+                var taskID = task['id'];
+                tasks[taskID] = task;
+                var sp = task['sp'];
+                if(sps[sp] === undefined) {
+                    sps[sp] = 1;
+                }
+                else {
+                    sps[sp]++;
+                }
+            };
+            var getTask = function(taskID) {
+                return tasks[taskID];
+            };
+            var removeTask = function(task) {
+                var taskID = task['id'];
+                var sp = tasks[taskID]['sp'];
+                delete tasks[taskID];
+                if(--sps[sp] === 0) {
+                    delete sps[sp];
+                    kimchi.topic('kimchi/allVolumeTasksFinished').publish({
+                        sp: sp
+                    });
+                }
+            };
+            return {
+                add: addTask,
+                get: getTask,
+                remove: removeTask
+            };
+        })();
+    }
+    var taskTracker = kimchi.volumeTransferTracker;

Why the above code is needed?
This code is used for:
1) Setup relationship between storage pool and task, and make it possible to retrieve the task ID for a volume and the storage pool name for a task. In current GET /tasks response, there is no storage pool name information so it's inconvenient when we want to update the progress information for a volume because we need to find which storage pool is this volume is located (volumes with same name can be located in different storage pools).
2) There may be several in-progress volumes at the same time within a same storage pool. So we need do a whole refresh of the storage pool when all volume transferring is done. We need a counter for each storage pools.
3) Performance. Keeping the mapping of storage pool name and task in memory can save Ajax requests to improve performance.

All that information is hold by backend in the Task element.

When querying a task related to storage volumes you should do:

GET /tasks?status=running&target_uri=^/storagepools/<pool-name>/storagevolumes/*

This will return a list of running tasks related to storage volumes

{ id: 1,
   status: running,
   message: ...,
   target_uri: /storagepools/default/storagevolumes/new-vol
}
...

With the target_uri parameter you can know the pool and the new volume name.

I have done that design on the patch "[Kimchi-devel] [PATCH] Adjustments on upload/download UI"
With this approach every user logged into kimchi will have the same view of volumes in progress.



The flow should be:

1) User selects a file to upload or download and click on "Add" button
2) Add handler will only start the process
    POST /storagepools/<pool>/storagevolumes/<volume> {<data>}

While listing the storage volumes in a storage pool you need to:

GET /storagepools/<pool>/storagevolumes/

*AND* get the storage volumes in progress by calling:

GET /tasks?status=running&target_uri=^/storagepools/<pool>/storagevolumes/*

So when listing the volumes you will know which ones are in progress.

volumes = GET /storagepools/<pool>/storagevolumes/<volume>
pendingVolumes = GET /tasks?status=running&target_uri=^/storagepools/<pool>/storagevolumes/*

for volumes in volumes:
    #create volume box html

    # check the volume is in progress
    if volume in pendingVolumes:
        # add progress bar according to task
        # add taskTrack for this task id


By now the Task resource can only return target_uri and message, so there is no way to differ download from upload.
What we know is only if a storage volume is pending.
So I suggest to change the progress bar message to a generic one, example, "In progress"
ACK.
Already save the download/upload information when making the Ajax callback in makeCallback() function. Though if the user refreshes the browser, the information will be lost. Then we'll lose the transfer type information and degrade it to "In progress".

We should use the same "In progress" message in all cases so every user will have the same view on it.




+
+    var makeCallback = function(trackType, sp, transType, callback) {
+        return function(resp) {
+            var taskID = resp['id'];
+            var volumeName = resp['target_uri'].split('/').pop();
+            if(trackType === 'add') {
+                taskTracker.add({
+                    id: taskID,
+                    sp: sp,
+                    volume: volumeName
+                });
+            }
+            callback(transType, resp);
+       };
+    };
+
+    var extractProgressData = function(data) {
+        var sizeArray = /(\d+)\/(\d+)/g.exec(data) || [0, 0, 0];
+        var downloaded = sizeArray[1];
+        var percent = 0;
+        if(downloaded) {
+            var total = sizeArray[2];
+            if(!isNaN(total)) {
+                percent = downloaded / total * 100;
+            }
+        }
+        var formatted = kimchi.formatMeasurement(downloaded);
+        var size = (1.0 * formatted['v']).toFixed(1) + formatted['s'];
+        return {
+            size: size,
+            percent: percent
+        };
+    };
+
+    var onFinished = function(type, result) {
+        var progress = extractProgressData(resp['message']);
+        var task = taskTracker.get([result['id']]);
+        kimchi.topic('kimchi/volumeTransferFinished').publish($.extend(progress, {
+            sp: task['sp'],
+            type: type,
+            volume: task['volume']
+        }));
+        taskTracker.remove({
+            id: result['id']
+        });
+    };
+
+    var onProgress = function(type, resp) {
+        var progress = extractProgressData(resp['message']);
+        var task = taskTracker.get([resp['id']]);
+        kimchi.topic('kimchi/volumeTransferProgress').publish($.extend(progress, {
+            sp: task['sp'],
+            type: type,
+            volume: task['volume']
+        }));
+    };
+
+    var onTransferError = function(type, result) {
+        if(!result) {
+            return;
+        }
+        var msg = result && (result['message'] || (
+            result['responseJSON'] && result['responseJSON']['reason'])
+        );
+        kimchi.message.error(msg);
+
+        if(!result['target_uri']) {
+            return;
+        }
+        var task = taskTracker.get(result['id']);
+        kimchi.topic('kimchi/volumeTransferError').publish({
+            sp: task['sp'],
+            type: type,
+            volume: task['volume']
+        });
+        taskTracker.remove({
+            id: result['id']
+        });
+    };
+
+    var onAccepted = function(type, resp) {
+        var taskID = resp['id'];
+        var task = taskTracker.get(taskID);
+        kimchi.window.close();
+        kimchi.topic('kimchi/volumeTransferStarted').publish({
+            sp: task['sp'],
+            type: type,
+            volume: task['volume']
+        });
+
+        kimchi.trackTask(taskID, function(resp) {
+            onFinished(type, resp);
+        }, function(resp) {
+            onTransferError(type, resp);
+        }, function(resp) {
+            onProgress(type, resp);
+        });
+    };
+
+    var onError = function(result) {
+        $(this).prop('disabled', false);
+        $(typeRadios).prop('disabled', false);
+        if(!result) {
+            return;
+        }
+        var msg = result['message'] || (
+            result['responseJSON'] && result['responseJSON']['reason']
+        );
+        kimchi.message.error(msg);
+    };
+
+    var fetchRemoteFile = function() {
+        var volumeURL = remoteURLBox.val();
+        var volumeName = volumeURL.split(/(\\|\/)/g).pop();
+        kimchi.downloadVolumeToSP({
+            sp: kimchi.selectedSP,
+            name: volumeName,
+            url: volumeURL
+        }, makeCallback('add', kimchi.selectedSP, 'download', onAccepted),
+            onError
+        );
+    };
+


+    var uploadFile = function() {
+        var blobFile = $(localFileBox)[0].files[0];
+        var fileName = blobFile.name;
+        var fd = new FormData();
+        fd.append('name', fileName);
+        fd.append('file', blobFile);
+        kimchi.uploadVolumeToSP({
+            sp: kimchi.selectedSP,
+            formData: fd
+        }, makeCallback('add', kimchi.selectedSP, 'upload', onAccepted),
+            onError
+        );
+    };
+

When selecting a file to upload the UI will need to read the selected file to build  the request.
If the file is large it takes some time to complete and the UI freezes.

I have to suggestions:

OPTION 1:

1) Use select a file to upload
2) Close the add volume window and display the volume in progress with the progress bar label "Verifying file"
3) Once the request is built, send it and update the volume box accordingly

OPTION 2:

1) Use select a file to upload
2) Disable "Add" button and rename it to: "Verifying file"
3) Once the request is built, send it and close the add volume window.
4) Update volume list
ACK. Thanks for the testing for large files which I didn't. Option 1 sounds good.

+    $(addButton).on('click', function(event) {
+        $(this).prop('disabled', true);
+        $(typeRadios).prop('disabled', true);
+        if(type === 'download') {
+            fetchRemoteFile();
+        }
+        else {
+            uploadFile();
+        }
+        event.preventDefault();
+    });
+};
diff --git a/ui/pages/storagepool-add-volume.html.tmpl b/ui/pages/storagepool-add-volume.html.tmpl
new file mode 100644
index 0000000..b01c942
--- /dev/null
+++ b/ui/pages/storagepool-add-volume.html.tmpl
@@ -0,0 +1,80 @@
+#*
+ * Project Kimchi
+ *
+ * Copyright IBM, Corp. 2014
+ *
+ * Authors:
+ *  Hongliang Wang <hlwang@linux.vnet.ibm.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *#
+#unicode UTF-8
+#import gettext
+#from kimchi.cachebust import href
+#silent t = gettext.translation($lang.domain, $lang.localedir, languages=$lang.lang)
+#silent _ = t.gettext
+#silent _t = t.gettext
+<div id="sp-add-volume-window" class="window">
+    <form id="form-sp-add-volume">
+        <header class="window-header">
+            <h1 class="title">$_("Add a Volume to Storage Pool")</h1>
+            <div class="close">X</div>
+        </header>
+        <section>
+            <div class="content">
+                <div class="form-section">
+                    <h2>
+                        <input type="radio" id="volume-type-download" class="volume-type" name="volumeType" value="download" checked="checked" />
+                        <label for="volume-type-download">
+                            $_("Fetch from remote URL")
+                        </label>
+                    </h2>
+                    <div class="field">
+                        <p class="text-help">
+                            $_("Enter the remote URL here.")
+                        </p>
+                        <div class="textbox-wrapper">
+                            <input type="text" id="volume-remote-url" class="text volume-input download" name="volumeRemoteURL" />
+                        </div>
+                    </div>
+                </div>
+                <div class="form-section">
+                    <h2>
+                        <input type="radio" id="volume-type-upload" class="volume-type" name="volumeType" value="upload"/>
+                        <label for="volume-type-upload">
+                        $_("Upload an file")
+                        </label>
+                    </h2>
+                    <div class="field">
+                        <p class="text-help">
+                            $_("Choose the ISO file (with .iso suffix) you want to upload.")
+                        </p>
+                        <div class="textbox-wrapper">
+                            <input type="file" class="volume-input upload" id="volume-input-file" name="volumeLocalFile" disabled="disabled" />
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </section>
+        <footer>
+            <div class="btn-group">
+                <button type="submit" id="sp-add-volume-button" class="btn-normal" disabled="disabled">
+                    <span class="text">$_("Add")</span>
+                </button>
+            </div>
+        </footer>
+    </form>
+</div>
+<script type="text/javascript">
+    kimchi.sp_add_volume_main();
+</script>