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

Hongliang Wang hlwang at linux.vnet.ibm.com
Mon Sep 15 07:46:25 UTC 2014


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

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

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.ovirt.org/pipermail/kimchi-devel/attachments/20140915/240d8383/attachment.html>


More information about the Kimchi-devel mailing list