[Kimchi-devel] [PATCH v2 3/4] Storage Pool Add Volume UI: Implement Download/Upload Volume Function

Aline Manera alinefm at linux.vnet.ibm.com
Fri Sep 12 15:32:44 UTC 2014


On 09/12/2014 06:42 AM, Hongliang Wang wrote:
> Implemented download and upload volumes functions.
>
> Signed-off-by: Hongliang Wang <hlwang at 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.

> +
> +    $(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?

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"



> +
> +    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

> +    $(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 at 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>




More information about the Kimchi-devel mailing list