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

Aline Manera alinefm at linux.vnet.ibm.com
Mon Sep 15 19:45:17 UTC 2014


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

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 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/c2653d5a/attachment.html>


More information about the Kimchi-devel mailing list