[PATCH v2 0/4] UI: Download Remote Image Feature

Implemented download remote image feature. v1 -> v2: 2a) Disabled add volume for inactive and iscsi/scsi pools (Aline's comment) 2b) Removed progress bar after upload/download finished (Aline & Cristian's comment) 2c) Changed the button label to "Add" from "OK" (Cristian's comment) 2d) Made add volume button behave correctly (enabled/disabled) (Cristian's comment) 2e) Removed some code redundent (Aline & Cristian's comment) Hongliang Wang (4): Storage Pool Add Volume UI: Add APIs to kimchi.api.js Storage Pool Add Volume UI: Add i18n Strings Storage Pool Add Volume UI: Implement Download/Upload Volume Function Storage Pool Add Volume UI: Connect the Feature in Storage Tab ui/css/theme-default/storage.css | 31 ++- ui/css/theme-default/storagepool-add-volume.css | 36 ++++ ui/js/src/kimchi.api.js | 37 ++++ ui/js/src/kimchi.storage_main.js | 116 ++++++++++- ui/js/src/kimchi.storagepool_add_volume_main.js | 243 ++++++++++++++++++++++++ ui/pages/i18n.json.tmpl | 4 + ui/pages/storagepool-add-volume.html.tmpl | 80 ++++++++ ui/pages/tabs/storage.html.tmpl | 12 +- 8 files changed, 549 insertions(+), 10 deletions(-) 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 -- 1.8.1.4

Added APIs. Signed-off-by: Hongliang Wang <hlwang@linux.vnet.ibm.com> --- ui/js/src/kimchi.api.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ui/js/src/kimchi.api.js b/ui/js/src/kimchi.api.js index 681f573..0e27987 100644 --- a/ui/js/src/kimchi.api.js +++ b/ui/js/src/kimchi.api.js @@ -1142,5 +1142,42 @@ var kimchi = { kimchi.message.error(data.responseJSON.reason); } }); + }, + + /** + * Add a volume to a given storage pool. + */ + uploadVolumeToSP: function(settings, suc, err) { + var fd = settings['formData']; + var sp = encodeURIComponent(settings['sp']); + kimchi.requestJSON({ + url : kimchi.url + 'storagepools/' + sp + '/storagevolumes', + type : 'POST', + data : fd, + processData : false, + contentType : false, + headers : { + Accept : 'application/json; charset=utf-8' + }, + success : suc, + error : err + }); + }, + + /** + * Add a volume to a given storage pool by URL. + */ + downloadVolumeToSP: function(settings, suc, err) { + var sp = encodeURIComponent(settings['sp']); + delete settings['sp']; + kimchi.requestJSON({ + url : kimchi.url + 'storagepools/' + sp + '/storagevolumes', + type : 'POST', + data : JSON.stringify(settings), + contentType : 'application/json', + dataType : 'json', + success : suc, + error : err + }); } }; -- 1.8.1.4

On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Added APIs.
Signed-off-by: Hongliang Wang <hlwang@linux.vnet.ibm.com> --- ui/js/src/kimchi.api.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+)
diff --git a/ui/js/src/kimchi.api.js b/ui/js/src/kimchi.api.js index 681f573..0e27987 100644 --- a/ui/js/src/kimchi.api.js +++ b/ui/js/src/kimchi.api.js @@ -1142,5 +1142,42 @@ var kimchi = { kimchi.message.error(data.responseJSON.reason); } }); + }, + + /** + * Add a volume to a given storage pool. + */ + uploadVolumeToSP: function(settings, suc, err) { + var fd = settings['formData']; + var sp = encodeURIComponent(settings['sp']); + kimchi.requestJSON({ + url : kimchi.url + 'storagepools/' + sp + '/storagevolumes', + type : 'POST', + data : fd, + processData : false, + contentType : false,
+ headers : { + Accept : 'application/json; charset=utf-8' + },
Instead of headers, you can only set "dataType: json"
+ success : suc, + error : err + }); + }, + + /** + * Add a volume to a given storage pool by URL. + */ + downloadVolumeToSP: function(settings, suc, err) { + var sp = encodeURIComponent(settings['sp']); + delete settings['sp']; + kimchi.requestJSON({ + url : kimchi.url + 'storagepools/' + sp + '/storagevolumes', + type : 'POST', + data : JSON.stringify(settings), + contentType : 'application/json', + dataType : 'json', + success : suc, + error : err + }); } };

On 09/12/2014 09:20 PM, Aline Manera wrote:
On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Added APIs.
Signed-off-by: Hongliang Wang <hlwang@linux.vnet.ibm.com> --- ui/js/src/kimchi.api.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+)
diff --git a/ui/js/src/kimchi.api.js b/ui/js/src/kimchi.api.js index 681f573..0e27987 100644 --- a/ui/js/src/kimchi.api.js +++ b/ui/js/src/kimchi.api.js @@ -1142,5 +1142,42 @@ var kimchi = { kimchi.message.error(data.responseJSON.reason); } }); + }, + + /** + * Add a volume to a given storage pool. + */ + uploadVolumeToSP: function(settings, suc, err) { + var fd = settings['formData']; + var sp = encodeURIComponent(settings['sp']); + kimchi.requestJSON({ + url : kimchi.url + 'storagepools/' + sp + '/storagevolumes', + type : 'POST', + data : fd, + processData : false, + contentType : false,
+ headers : { + Accept : 'application/json; charset=utf-8' + },
Instead of headers, you can only set "dataType: json"
ACK.
+ success : suc, + error : err + }); + }, + + /** + * Add a volume to a given storage pool by URL. + */ + downloadVolumeToSP: function(settings, suc, err) { + var sp = encodeURIComponent(settings['sp']); + delete settings['sp']; + kimchi.requestJSON({ + url : kimchi.url + 'storagepools/' + sp + '/storagevolumes', + type : 'POST', + data : JSON.stringify(settings), + contentType : 'application/json', + dataType : 'json', + success : suc, + error : err + }); } };

Add i18n strings for volume download/upload feature. Signed-off-by: Hongliang Wang <hlwang@linux.vnet.ibm.com> --- ui/pages/i18n.json.tmpl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index b552101..fb1cb56 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -169,6 +169,10 @@ "KCHPOOL6011M": "$_("No available partitions found.")", "KCHPOOL6012M": "$_("This storage pool is not persistent. Instead of deactivate, this action will permanently delete it. Would you like to continue?")", "KCHPOOL6013M": "$_("Unable to retrieve partitions information.")", + "KCHPOOL6014M": "$_("Downloading...")", + "KCHPOOL6015M": "$_("Done!")", + "KCHPOOL6016M": "$_("Failed!")", + "KCHPOOL6017M": "$_("Uploading...")", "KCHVMSTOR0001E": "$_("CDROM path needs to be a valid local/remote path and cannot be blank.")", "KCHVMSTOR0002E": "$_("Disk pool or volume cannot be blank.")" -- 1.8.1.4

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()); + }; + + $(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; + + 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 + ); + }; + + $(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> -- 1.8.1.4

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.
+ + $(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@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>

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

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>

Connect download/upload volume window with storage pool add button. Signed-off-by: Hongliang Wang <hlwang@linux.vnet.ibm.com> --- ui/css/theme-default/storage.css | 31 ++++++++++- ui/js/src/kimchi.storage_main.js | 116 ++++++++++++++++++++++++++++++++++++--- ui/pages/tabs/storage.html.tmpl | 12 +++- 3 files changed, 149 insertions(+), 10 deletions(-) diff --git a/ui/css/theme-default/storage.css b/ui/css/theme-default/storage.css index f635c2f..e0ab290 100644 --- a/ui/css/theme-default/storage.css +++ b/ui/css/theme-default/storage.css @@ -336,7 +336,6 @@ float: left; padding: 4px; margin-bottom: 5px; - height: 40px; width: 130px; } @@ -622,3 +621,33 @@ #iSCSITarget input { width: 493px; } + +/* Progress bar */ +.volume-progress { + clear: both; + width: 140px; +} + +.volume-progress .progress-bar-outer { + background: #ccc; + height: 4px; + overflow: hidden; + width: 100%; +} + +.volume-progress .progress-bar-inner { + background: #090; + height: 100%; + width: 0%; +} + +.volume-progress .progress-label { + color: #999; + font-size: 10px; + line-height: 16px; +} + +.volume-progress .progress-transferred { + float: right; +} +/* End of Progress bar */ diff --git a/ui/js/src/kimchi.storage_main.js b/ui/js/src/kimchi.storage_main.js index ae3f963..6c1aade 100644 --- a/ui/js/src/kimchi.storage_main.js +++ b/ui/js/src/kimchi.storage_main.js @@ -79,6 +79,19 @@ kimchi.storageBindClick = function() { } }); + $('.pool-add-volume').each(function(index) { + var canAddVolume = + $(this).data('stat') === 'active' && + $(this).data('type') !== 'iscsi' && + $(this).data('type') !== 'scsi'; + if(canAddVolume) { + $(this).removeClass('hidden'); + } + else { + $(this).addClass('hidden'); + } + }); + if(kimchi.tabMode['storage'] === 'admin') { $('.pool-delete').on('click', function(event) { var $pool = $(this); @@ -135,6 +148,12 @@ kimchi.storageBindClick = function() { } }); + $('.pool-add-volume').on('click', function(event) { + var poolName = $(this).data('name'); + kimchi.selectedSP = poolName; + kimchi.window.open('storagepool-add-volume.html'); + }); + $('.storage-action').on('click', function() { var storage_action = $(this); var deleteButton = storage_action.find('.pool-delete'); @@ -149,10 +168,6 @@ kimchi.storageBindClick = function() { $("#logicalPoolExtend").dialog("option", "poolName", $(this).data('name')); $("#logicalPoolExtend").dialog("open"); }); - - $('#volume-doAdd').on('click', function() { - kimchi.window.open('storagevolume-add.html'); - }); } $('.storage-li').on('click', function(event) { @@ -173,8 +188,24 @@ kimchi.storageBindClick = function() { }); } +kimchi._generateVolumeHTML = function(volume) { + if(volume['type'] === 'kimchi-iso') { + return ''; + } + var volumeHtml = $('#volumeTmpl').html(); + volume.capacity = kimchi.changetoProperUnit(volume.capacity,1); + volume.allocation = kimchi.changetoProperUnit(volume.allocation,1); + return kimchi.substitute(volumeHtml, volume); +}; + +kimchi._updateVolumeBoxUI = function(box, volume) { + var html = kimchi._generateVolumeHTML(volume); + $(box).replaceWith(html); +}; + kimchi.doListVolumes = function(poolObj) { var volumeDiv = $('#volume' + poolObj.data('name')); + $(volumeDiv).empty(); var slide = poolObj.next('.volumes'); var handleArrow = poolObj.children().last().children(); kimchi.listStorageVolumes(poolObj.data('name'), function(result) { @@ -184,9 +215,7 @@ kimchi.doListVolumes = function(poolObj) { var listHtml = ''; $.each(result, function(index, value) { value.poolname = poolObj.data('name'); - value.capacity = kimchi.changetoProperUnit(value.capacity,1); - value.allocation = kimchi.changetoProperUnit(value.allocation,1); - listHtml += kimchi.substitute(volumeHtml, value); + listHtml += kimchi._generateVolumeHTML(value); }); volumeDiv.html(listHtml); } else { @@ -255,7 +284,78 @@ kimchi.storage_main = function() { } kimchi.doListStoragePools(); kimchi.initLogicalPoolExtend(); -} + + kimchi.topic('kimchi/allVolumeTasksFinished').subscribe(function(data) { + var sp = data['sp']; + var poolNode = $('.storage-li[data-name="' + sp + '"]'); + kimchi.doListVolumes(poolNode); + }); + + kimchi.topic('kimchi/volumeTransferStarted').subscribe(function(data) { + var sp = data['sp'], + type = data['type'], + volumeName = data['volume']; + var volumesContainer = $('#volume' + sp); + var volume = { + poolName: sp, + ref_cnt: 0, + capacity: 0, + name: volumeName, + format: '', + bootable: true, + os_distro: '', + allocation: 0, + os_version: '', + path: '', + type: 'file' + }; + if($('.volume-box', volumesContainer).length === 0) { + volumesContainer.empty(); + } + volumesContainer.prepend(kimchi._generateVolumeHTML(volume)); + var volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.volume-progress', volumeBox).removeClass('hidden'); + $('.progress-status', volumeBox).text( + type === 'download' ? i18n['KCHPOOL6014M'] : i18n['KCHPOOL6017M'] + ); + }); + + kimchi.topic('kimchi/volumeTransferProgress').subscribe(function(data) { + var sp = data['sp'], + type = data['type'], + volumeName = data['volume'], + size = data['size'], + percent = data['percent']; + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.progress-bar-inner', volumeBox).css({ + width: percent + '%' + }); + $('.progress-status', volumeBox).text( + type === 'download' ? i18n['KCHPOOL6014M'] : i18n['KCHPOOL6017M'] + ); + $('.progress-transferred', volumeBox).text(size); + $('.volume-progress', volumeBox).removeClass('hidden'); + }); + + kimchi.topic('kimchi/volumeTransferFinished').subscribe(function(data) { + var sp = data['sp'], + volumeName = data['volume'], + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.volume-progress', volumeBox).addClass('hidden'); + kimchi.getStoragePoolVolume(sp, volumeName, function(volume) { + kimchi._updateVolumeBoxUI(volumeBox, volume); + }, function(err) { + kimchi.message.error(err.responseJSON.reason); + }); + }); + + kimchi.topic('kimchi/volumeTransferError').subscribe(function(data) { + var sp = data['sp'], + volumeName = data['volume'], + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.progress-status', volumeBox).text(i18n['KCHPOOL6016M']); + }); +}; kimchi.changeArrow = function(obj) { if ($(obj).hasClass('arrow-down')) { diff --git a/ui/pages/tabs/storage.html.tmpl b/ui/pages/tabs/storage.html.tmpl index 87205bd..523f480 100644 --- a/ui/pages/tabs/storage.html.tmpl +++ b/ui/pages/tabs/storage.html.tmpl @@ -82,6 +82,7 @@ <div class="popover actionsheet right-side" style="width: 250px"> <button class="button-big pool-deactivate" data-stat="{state}" data-name="{name}" data-persistent="{persistent}"><span class="text">$_("Deactivate")</span></button> <button class="button-big pool-activate" data-stat="{state}" data-name="{name}"><span class="text">$_("Activate")</span></button> + <button class="button-big pool-add-volume" data-stat="{state}" data-name="{name}" data-type="{type}"><span class="text">$_("Add Volume")</span></button> <button class="button-big pool-extend {enableExt}" data-stat="{state}" data-name="{name}"><span class="text">$_("Extend")</span></button> <button class="button-big red pool-delete" data-stat="{state}" data-name="{name}"><span class="text">$_("Undefine")</span></button> </div> @@ -98,11 +99,20 @@ </li> </script> <script id="volumeTmpl" type="html/text"> - <div class="volume-box white-box"> + <div class="volume-box white-box" data-volume-name="{name}"> <div class="storage-icon volume-default icon-{format} "> </div> <div class="volume-title"> <div class="volume-name" title="{name}">{name}</div> + <div class="volume-progress hidden"> + <div class="progress-bar-outer"> + <div class="progress-bar-inner"></div> + </div> + <div class="progress-label"> + <span class="progress-status"></span> + <span class="progress-transferred"></span> + </div> + </div> </div> <div class="volume-setting"> </div> -- 1.8.1.4

On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Connect download/upload volume window with storage pool add button.
Signed-off-by: Hongliang Wang <hlwang@linux.vnet.ibm.com> --- ui/css/theme-default/storage.css | 31 ++++++++++- ui/js/src/kimchi.storage_main.js | 116 ++++++++++++++++++++++++++++++++++++--- ui/pages/tabs/storage.html.tmpl | 12 +++- 3 files changed, 149 insertions(+), 10 deletions(-)
diff --git a/ui/css/theme-default/storage.css b/ui/css/theme-default/storage.css index f635c2f..e0ab290 100644 --- a/ui/css/theme-default/storage.css +++ b/ui/css/theme-default/storage.css @@ -336,7 +336,6 @@ float: left; padding: 4px; margin-bottom: 5px; - height: 40px; width: 130px; }
@@ -622,3 +621,33 @@ #iSCSITarget input { width: 493px; } + +/* Progress bar */ +.volume-progress { + clear: both; + width: 140px; +} + +.volume-progress .progress-bar-outer { + background: #ccc; + height: 4px; + overflow: hidden; + width: 100%; +} + +.volume-progress .progress-bar-inner { + background: #090; + height: 100%; + width: 0%; +} + +.volume-progress .progress-label { + color: #999; + font-size: 10px; + line-height: 16px; +} + +.volume-progress .progress-transferred { + float: right; +} +/* End of Progress bar */ diff --git a/ui/js/src/kimchi.storage_main.js b/ui/js/src/kimchi.storage_main.js index ae3f963..6c1aade 100644 --- a/ui/js/src/kimchi.storage_main.js +++ b/ui/js/src/kimchi.storage_main.js @@ -79,6 +79,19 @@ kimchi.storageBindClick = function() { } });
+ $('.pool-add-volume').each(function(index) { + var canAddVolume = + $(this).data('stat') === 'active' && + $(this).data('type') !== 'iscsi' && + $(this).data('type') !== 'scsi'; + if(canAddVolume) { + $(this).removeClass('hidden'); + } + else { + $(this).addClass('hidden'); + } + });
It is not working for me. I needed to change to .show() and .hide() to get it working as planned.
+ if(kimchi.tabMode['storage'] === 'admin') { $('.pool-delete').on('click', function(event) { var $pool = $(this); @@ -135,6 +148,12 @@ kimchi.storageBindClick = function() { } });
+ $('.pool-add-volume').on('click', function(event) { + var poolName = $(this).data('name'); + kimchi.selectedSP = poolName; + kimchi.window.open('storagepool-add-volume.html'); + }); + $('.storage-action').on('click', function() { var storage_action = $(this); var deleteButton = storage_action.find('.pool-delete'); @@ -149,10 +168,6 @@ kimchi.storageBindClick = function() { $("#logicalPoolExtend").dialog("option", "poolName", $(this).data('name')); $("#logicalPoolExtend").dialog("open"); }); - - $('#volume-doAdd').on('click', function() { - kimchi.window.open('storagevolume-add.html'); - }); }
$('.storage-li').on('click', function(event) { @@ -173,8 +188,24 @@ kimchi.storageBindClick = function() { }); }
+kimchi._generateVolumeHTML = function(volume) { + if(volume['type'] === 'kimchi-iso') { + return ''; + } + var volumeHtml = $('#volumeTmpl').html(); + volume.capacity = kimchi.changetoProperUnit(volume.capacity,1); + volume.allocation = kimchi.changetoProperUnit(volume.allocation,1); + return kimchi.substitute(volumeHtml, volume); +}; + +kimchi._updateVolumeBoxUI = function(box, volume) { + var html = kimchi._generateVolumeHTML(volume); + $(box).replaceWith(html); +}; + kimchi.doListVolumes = function(poolObj) { var volumeDiv = $('#volume' + poolObj.data('name')); + $(volumeDiv).empty(); var slide = poolObj.next('.volumes'); var handleArrow = poolObj.children().last().children(); kimchi.listStorageVolumes(poolObj.data('name'), function(result) { @@ -184,9 +215,7 @@ kimchi.doListVolumes = function(poolObj) { var listHtml = ''; $.each(result, function(index, value) { value.poolname = poolObj.data('name'); - value.capacity = kimchi.changetoProperUnit(value.capacity,1); - value.allocation = kimchi.changetoProperUnit(value.allocation,1); - listHtml += kimchi.substitute(volumeHtml, value); + listHtml += kimchi._generateVolumeHTML(value); }); volumeDiv.html(listHtml); } else { @@ -255,7 +284,78 @@ kimchi.storage_main = function() { } kimchi.doListStoragePools(); kimchi.initLogicalPoolExtend(); -} + + kimchi.topic('kimchi/allVolumeTasksFinished').subscribe(function(data) { + var sp = data['sp']; + var poolNode = $('.storage-li[data-name="' + sp + '"]'); + kimchi.doListVolumes(poolNode); + }); + + kimchi.topic('kimchi/volumeTransferStarted').subscribe(function(data) { + var sp = data['sp'], + type = data['type'], + volumeName = data['volume']; + var volumesContainer = $('#volume' + sp); + var volume = { + poolName: sp, + ref_cnt: 0, + capacity: 0, + name: volumeName, + format: '', + bootable: true, + os_distro: '', + allocation: 0, + os_version: '', + path: '', + type: 'file' + }; + if($('.volume-box', volumesContainer).length === 0) { + volumesContainer.empty(); + } + volumesContainer.prepend(kimchi._generateVolumeHTML(volume)); + var volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.volume-progress', volumeBox).removeClass('hidden'); + $('.progress-status', volumeBox).text( + type === 'download' ? i18n['KCHPOOL6014M'] : i18n['KCHPOOL6017M'] + ); + }); + + kimchi.topic('kimchi/volumeTransferProgress').subscribe(function(data) { + var sp = data['sp'], + type = data['type'], + volumeName = data['volume'], + size = data['size'], + percent = data['percent']; + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.progress-bar-inner', volumeBox).css({ + width: percent + '%' + }); + $('.progress-status', volumeBox).text( + type === 'download' ? i18n['KCHPOOL6014M'] : i18n['KCHPOOL6017M'] + ); + $('.progress-transferred', volumeBox).text(size); + $('.volume-progress', volumeBox).removeClass('hidden'); + }); + + kimchi.topic('kimchi/volumeTransferFinished').subscribe(function(data) { + var sp = data['sp'], + volumeName = data['volume'], + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.volume-progress', volumeBox).addClass('hidden'); + kimchi.getStoragePoolVolume(sp, volumeName, function(volume) { + kimchi._updateVolumeBoxUI(volumeBox, volume); + }, function(err) { + kimchi.message.error(err.responseJSON.reason); + }); + }); + + kimchi.topic('kimchi/volumeTransferError').subscribe(function(data) { + var sp = data['sp'], + volumeName = data['volume'], + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.progress-status', volumeBox).text(i18n['KCHPOOL6016M']); + }); +};
kimchi.changeArrow = function(obj) { if ($(obj).hasClass('arrow-down')) { diff --git a/ui/pages/tabs/storage.html.tmpl b/ui/pages/tabs/storage.html.tmpl index 87205bd..523f480 100644 --- a/ui/pages/tabs/storage.html.tmpl +++ b/ui/pages/tabs/storage.html.tmpl @@ -82,6 +82,7 @@ <div class="popover actionsheet right-side" style="width: 250px"> <button class="button-big pool-deactivate" data-stat="{state}" data-name="{name}" data-persistent="{persistent}"><span class="text">$_("Deactivate")</span></button> <button class="button-big pool-activate" data-stat="{state}" data-name="{name}"><span class="text">$_("Activate")</span></button> + <button class="button-big pool-add-volume" data-stat="{state}" data-name="{name}" data-type="{type}"><span class="text">$_("Add Volume")</span></button> <button class="button-big pool-extend {enableExt}" data-stat="{state}" data-name="{name}"><span class="text">$_("Extend")</span></button> <button class="button-big red pool-delete" data-stat="{state}" data-name="{name}"><span class="text">$_("Undefine")</span></button> </div> @@ -98,11 +99,20 @@ </li> </script> <script id="volumeTmpl" type="html/text"> - <div class="volume-box white-box"> + <div class="volume-box white-box" data-volume-name="{name}"> <div class="storage-icon volume-default icon-{format} "> </div> <div class="volume-title"> <div class="volume-name" title="{name}">{name}</div> + <div class="volume-progress hidden"> + <div class="progress-bar-outer"> + <div class="progress-bar-inner"></div> + </div> + <div class="progress-label"> + <span class="progress-status"></span> + <span class="progress-transferred"></span> + </div> + </div> </div> <div class="volume-setting"> </div>

On 09/12/2014 11:33 PM, Aline Manera wrote:
On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Connect download/upload volume window with storage pool add button.
Signed-off-by: Hongliang Wang <hlwang@linux.vnet.ibm.com> --- ui/css/theme-default/storage.css | 31 ++++++++++- ui/js/src/kimchi.storage_main.js | 116 ++++++++++++++++++++++++++++++++++++--- ui/pages/tabs/storage.html.tmpl | 12 +++- 3 files changed, 149 insertions(+), 10 deletions(-)
diff --git a/ui/css/theme-default/storage.css b/ui/css/theme-default/storage.css index f635c2f..e0ab290 100644 --- a/ui/css/theme-default/storage.css +++ b/ui/css/theme-default/storage.css @@ -336,7 +336,6 @@ float: left; padding: 4px; margin-bottom: 5px; - height: 40px; width: 130px; }
@@ -622,3 +621,33 @@ #iSCSITarget input { width: 493px; } + +/* Progress bar */ +.volume-progress { + clear: both; + width: 140px; +} + +.volume-progress .progress-bar-outer { + background: #ccc; + height: 4px; + overflow: hidden; + width: 100%; +} + +.volume-progress .progress-bar-inner { + background: #090; + height: 100%; + width: 0%; +} + +.volume-progress .progress-label { + color: #999; + font-size: 10px; + line-height: 16px; +} + +.volume-progress .progress-transferred { + float: right; +} +/* End of Progress bar */ diff --git a/ui/js/src/kimchi.storage_main.js b/ui/js/src/kimchi.storage_main.js index ae3f963..6c1aade 100644 --- a/ui/js/src/kimchi.storage_main.js +++ b/ui/js/src/kimchi.storage_main.js @@ -79,6 +79,19 @@ kimchi.storageBindClick = function() { } });
+ $('.pool-add-volume').each(function(index) { + var canAddVolume = + $(this).data('stat') === 'active' && + $(this).data('type') !== 'iscsi' && + $(this).data('type') !== 'scsi'; + if(canAddVolume) { + $(this).removeClass('hidden'); + } + else { + $(this).addClass('hidden'); + } + });
It is not working for me. I needed to change to .show() and .hide() to get it working as planned.
OK.
+ if(kimchi.tabMode['storage'] === 'admin') { $('.pool-delete').on('click', function(event) { var $pool = $(this); @@ -135,6 +148,12 @@ kimchi.storageBindClick = function() { } });
+ $('.pool-add-volume').on('click', function(event) { + var poolName = $(this).data('name'); + kimchi.selectedSP = poolName; + kimchi.window.open('storagepool-add-volume.html'); + }); + $('.storage-action').on('click', function() { var storage_action = $(this); var deleteButton = storage_action.find('.pool-delete'); @@ -149,10 +168,6 @@ kimchi.storageBindClick = function() { $("#logicalPoolExtend").dialog("option", "poolName", $(this).data('name')); $("#logicalPoolExtend").dialog("open"); }); - - $('#volume-doAdd').on('click', function() { - kimchi.window.open('storagevolume-add.html'); - }); }
$('.storage-li').on('click', function(event) { @@ -173,8 +188,24 @@ kimchi.storageBindClick = function() { }); }
+kimchi._generateVolumeHTML = function(volume) { + if(volume['type'] === 'kimchi-iso') { + return ''; + } + var volumeHtml = $('#volumeTmpl').html(); + volume.capacity = kimchi.changetoProperUnit(volume.capacity,1); + volume.allocation = kimchi.changetoProperUnit(volume.allocation,1); + return kimchi.substitute(volumeHtml, volume); +}; + +kimchi._updateVolumeBoxUI = function(box, volume) { + var html = kimchi._generateVolumeHTML(volume); + $(box).replaceWith(html); +}; + kimchi.doListVolumes = function(poolObj) { var volumeDiv = $('#volume' + poolObj.data('name')); + $(volumeDiv).empty(); var slide = poolObj.next('.volumes'); var handleArrow = poolObj.children().last().children(); kimchi.listStorageVolumes(poolObj.data('name'), function(result) { @@ -184,9 +215,7 @@ kimchi.doListVolumes = function(poolObj) { var listHtml = ''; $.each(result, function(index, value) { value.poolname = poolObj.data('name'); - value.capacity = kimchi.changetoProperUnit(value.capacity,1); - value.allocation = kimchi.changetoProperUnit(value.allocation,1); - listHtml += kimchi.substitute(volumeHtml, value); + listHtml += kimchi._generateVolumeHTML(value); }); volumeDiv.html(listHtml); } else { @@ -255,7 +284,78 @@ kimchi.storage_main = function() { } kimchi.doListStoragePools(); kimchi.initLogicalPoolExtend(); -} + + kimchi.topic('kimchi/allVolumeTasksFinished').subscribe(function(data) { + var sp = data['sp']; + var poolNode = $('.storage-li[data-name="' + sp + '"]'); + kimchi.doListVolumes(poolNode); + }); + + kimchi.topic('kimchi/volumeTransferStarted').subscribe(function(data) { + var sp = data['sp'], + type = data['type'], + volumeName = data['volume']; + var volumesContainer = $('#volume' + sp); + var volume = { + poolName: sp, + ref_cnt: 0, + capacity: 0, + name: volumeName, + format: '', + bootable: true, + os_distro: '', + allocation: 0, + os_version: '', + path: '', + type: 'file' + }; + if($('.volume-box', volumesContainer).length === 0) { + volumesContainer.empty(); + } + volumesContainer.prepend(kimchi._generateVolumeHTML(volume)); + var volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.volume-progress', volumeBox).removeClass('hidden'); + $('.progress-status', volumeBox).text( + type === 'download' ? i18n['KCHPOOL6014M'] : i18n['KCHPOOL6017M'] + ); + }); + + kimchi.topic('kimchi/volumeTransferProgress').subscribe(function(data) { + var sp = data['sp'], + type = data['type'], + volumeName = data['volume'], + size = data['size'], + percent = data['percent']; + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.progress-bar-inner', volumeBox).css({ + width: percent + '%' + }); + $('.progress-status', volumeBox).text( + type === 'download' ? i18n['KCHPOOL6014M'] : i18n['KCHPOOL6017M'] + ); + $('.progress-transferred', volumeBox).text(size); + $('.volume-progress', volumeBox).removeClass('hidden'); + }); + + kimchi.topic('kimchi/volumeTransferFinished').subscribe(function(data) { + var sp = data['sp'], + volumeName = data['volume'], + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.volume-progress', volumeBox).addClass('hidden'); + kimchi.getStoragePoolVolume(sp, volumeName, function(volume) { + kimchi._updateVolumeBoxUI(volumeBox, volume); + }, function(err) { + kimchi.message.error(err.responseJSON.reason); + }); + }); + + kimchi.topic('kimchi/volumeTransferError').subscribe(function(data) { + var sp = data['sp'], + volumeName = data['volume'], + volumeBox = $('#volume' + sp + ' [data-volume-name="' + volumeName + '"]'); + $('.progress-status', volumeBox).text(i18n['KCHPOOL6016M']); + }); +};
kimchi.changeArrow = function(obj) { if ($(obj).hasClass('arrow-down')) { diff --git a/ui/pages/tabs/storage.html.tmpl b/ui/pages/tabs/storage.html.tmpl index 87205bd..523f480 100644 --- a/ui/pages/tabs/storage.html.tmpl +++ b/ui/pages/tabs/storage.html.tmpl @@ -82,6 +82,7 @@ <div class="popover actionsheet right-side" style="width: 250px"> <button class="button-big pool-deactivate" data-stat="{state}" data-name="{name}" data-persistent="{persistent}"><span class="text">$_("Deactivate")</span></button> <button class="button-big pool-activate" data-stat="{state}" data-name="{name}"><span class="text">$_("Activate")</span></button> + <button class="button-big pool-add-volume" data-stat="{state}" data-name="{name}" data-type="{type}"><span class="text">$_("Add Volume")</span></button> <button class="button-big pool-extend {enableExt}" data-stat="{state}" data-name="{name}"><span class="text">$_("Extend")</span></button> <button class="button-big red pool-delete" data-stat="{state}" data-name="{name}"><span class="text">$_("Undefine")</span></button> </div> @@ -98,11 +99,20 @@ </li> </script> <script id="volumeTmpl" type="html/text"> - <div class="volume-box white-box"> + <div class="volume-box white-box" data-volume-name="{name}"> <div class="storage-icon volume-default icon-{format} "> </div> <div class="volume-title"> <div class="volume-name" title="{name}">{name}</div> + <div class="volume-progress hidden"> + <div class="progress-bar-outer"> + <div class="progress-bar-inner"></div> + </div> + <div class="progress-label"> + <span class="progress-status"></span> + <span class="progress-transferred"></span> + </div> + </div> </div> <div class="volume-setting"> </div>

Hi Hongliang, Most of issues I commented in this patch set are fixed in "[Kimchi-devel] [PATCH] Adjustments on upload/download UI" We still need to fix the upload to do not freeze the UI and crash browser when uploading big files. To fix it, I think, we will need to send the file part by part, let's say 5MB per request. It will also require changes on backend but it can solve our problems. Regards, Aline Manera On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Implemented download remote image feature.
v1 -> v2: 2a) Disabled add volume for inactive and iscsi/scsi pools (Aline's comment)
2b) Removed progress bar after upload/download finished (Aline & Cristian's comment)
2c) Changed the button label to "Add" from "OK" (Cristian's comment)
2d) Made add volume button behave correctly (enabled/disabled) (Cristian's comment)
2e) Removed some code redundent (Aline & Cristian's comment)
Hongliang Wang (4): Storage Pool Add Volume UI: Add APIs to kimchi.api.js Storage Pool Add Volume UI: Add i18n Strings Storage Pool Add Volume UI: Implement Download/Upload Volume Function Storage Pool Add Volume UI: Connect the Feature in Storage Tab
ui/css/theme-default/storage.css | 31 ++- ui/css/theme-default/storagepool-add-volume.css | 36 ++++ ui/js/src/kimchi.api.js | 37 ++++ ui/js/src/kimchi.storage_main.js | 116 ++++++++++- ui/js/src/kimchi.storagepool_add_volume_main.js | 243 ++++++++++++++++++++++++ ui/pages/i18n.json.tmpl | 4 + ui/pages/storagepool-add-volume.html.tmpl | 80 ++++++++ ui/pages/tabs/storage.html.tmpl | 12 +- 8 files changed, 549 insertions(+), 10 deletions(-) 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

On 09/12/2014 05:55 PM, Aline Manera wrote:
Hi Hongliang,
Most of issues I commented in this patch set are fixed in "[Kimchi-devel] [PATCH] Adjustments on upload/download UI"
We still need to fix the upload to do not freeze the UI and crash browser when uploading big files. To fix it, I think, we will need to send the file part by part, let's say 5MB per request. It will also require changes on backend but it can solve our problems.
Regards, Aline Manera
I forgot to mention you also need to apply the same Kimchi style to the input box and the browser button. You can merge this patch set with mine and do the style changes. And to do not block the UI patch, disable the upload option by now until we get the backend updated. Royce, For the backend I imagine we can allow receive multiple POST request to get the whole file while uploading it. Example: POST /storagepools/<pool>/storagevolumes/ {file: fd} this fd object will have : - fd.file: a part of 10MB of the file to be uploaded - fd.part: the correspond part number - fd.total: total number of parts to be received The UI will do something like: fd.total = round(fd.size / 10MB) transfered = 0 part = 0 while part < fd.total: fd.file = read more 10MB of the file fd.part = part POST POST /storagepools/<pool>/storagevolumes/ {file: fd} fd.part++ On backend: if os.path.exists(filepath) and fd.part = 0: raise InvalidParemeter("file already exists") localfile = open(filepath, a) localfile.seek(fd.part * 10MB) localfile.write(fd.file)
On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Implemented download remote image feature.
v1 -> v2: 2a) Disabled add volume for inactive and iscsi/scsi pools (Aline's comment)
2b) Removed progress bar after upload/download finished (Aline & Cristian's comment)
2c) Changed the button label to "Add" from "OK" (Cristian's comment)
2d) Made add volume button behave correctly (enabled/disabled) (Cristian's comment)
2e) Removed some code redundent (Aline & Cristian's comment)
Hongliang Wang (4): Storage Pool Add Volume UI: Add APIs to kimchi.api.js Storage Pool Add Volume UI: Add i18n Strings Storage Pool Add Volume UI: Implement Download/Upload Volume Function Storage Pool Add Volume UI: Connect the Feature in Storage Tab
ui/css/theme-default/storage.css | 31 ++- ui/css/theme-default/storagepool-add-volume.css | 36 ++++ ui/js/src/kimchi.api.js | 37 ++++ ui/js/src/kimchi.storage_main.js | 116 ++++++++++- ui/js/src/kimchi.storagepool_add_volume_main.js | 243 ++++++++++++++++++++++++ ui/pages/i18n.json.tmpl | 4 + ui/pages/storagepool-add-volume.html.tmpl | 80 ++++++++ ui/pages/tabs/storage.html.tmpl | 12 +- 8 files changed, 549 insertions(+), 10 deletions(-) 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
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

On 09/14/2014 10:09 PM, Aline Manera wrote:
On 09/12/2014 05:55 PM, Aline Manera wrote:
Hi Hongliang,
Most of issues I commented in this patch set are fixed in "[Kimchi-devel] [PATCH] Adjustments on upload/download UI"
We still need to fix the upload to do not freeze the UI and crash browser when uploading big files. To fix it, I think, we will need to send the file part by part, let's say 5MB per request. It will also require changes on backend but it can solve our problems.
Regards, Aline Manera
I forgot to mention you also need to apply the same Kimchi style to the input box and the browser button.
ACK. The file selector button styling is somewhat different. Let me do some investigation.
You can merge this patch set with mine and do the style changes. And to do not block the UI patch, disable the upload option by now until we get the backend updated.
Royce,
For the backend I imagine we can allow receive multiple POST request to get the whole file while uploading it. Example:
POST /storagepools/<pool>/storagevolumes/ {file: fd}
this fd object will have :
- fd.file: a part of 10MB of the file to be uploaded - fd.part: the correspond part number - fd.total: total number of parts to be received
The UI will do something like:
fd.total = round(fd.size / 10MB) transfered = 0 part = 0
while part < fd.total: fd.file = read more 10MB of the file fd.part = part POST POST /storagepools/<pool>/storagevolumes/ {file: fd} fd.part++
On backend: if os.path.exists(filepath) and fd.part = 0: raise InvalidParemeter("file already exists")
localfile = open(filepath, a) localfile.seek(fd.part * 10MB) localfile.write(fd.file) What's the problem here for large files? Seems your solution will introduce lots of requests. I tried a large ISO file and the window was not closed and finally the request timed out.
On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Implemented download remote image feature.
v1 -> v2: 2a) Disabled add volume for inactive and iscsi/scsi pools (Aline's comment)
2b) Removed progress bar after upload/download finished (Aline & Cristian's comment)
2c) Changed the button label to "Add" from "OK" (Cristian's comment)
2d) Made add volume button behave correctly (enabled/disabled) (Cristian's comment)
2e) Removed some code redundent (Aline & Cristian's comment)
Hongliang Wang (4): Storage Pool Add Volume UI: Add APIs to kimchi.api.js Storage Pool Add Volume UI: Add i18n Strings Storage Pool Add Volume UI: Implement Download/Upload Volume Function Storage Pool Add Volume UI: Connect the Feature in Storage Tab
ui/css/theme-default/storage.css | 31 ++- ui/css/theme-default/storagepool-add-volume.css | 36 ++++ ui/js/src/kimchi.api.js | 37 ++++ ui/js/src/kimchi.storage_main.js | 116 ++++++++++- ui/js/src/kimchi.storagepool_add_volume_main.js | 243 ++++++++++++++++++++++++ ui/pages/i18n.json.tmpl | 4 + ui/pages/storagepool-add-volume.html.tmpl | 80 ++++++++ ui/pages/tabs/storage.html.tmpl | 12 +- 8 files changed, 549 insertions(+), 10 deletions(-) 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
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

On 09/15/2014 06:47 AM, Hongliang Wang wrote:
On 09/14/2014 10:09 PM, Aline Manera wrote:
On 09/12/2014 05:55 PM, Aline Manera wrote:
Hi Hongliang,
Most of issues I commented in this patch set are fixed in "[Kimchi-devel] [PATCH] Adjustments on upload/download UI"
We still need to fix the upload to do not freeze the UI and crash browser when uploading big files. To fix it, I think, we will need to send the file part by part, let's say 5MB per request. It will also require changes on backend but it can solve our problems.
Regards, Aline Manera
I forgot to mention you also need to apply the same Kimchi style to the input box and the browser button.
ACK. The file selector button styling is somewhat different. Let me do some investigation.
You can merge this patch set with mine and do the style changes. And to do not block the UI patch, disable the upload option by now until we get the backend updated.
Royce,
For the backend I imagine we can allow receive multiple POST request to get the whole file while uploading it. Example:
POST /storagepools/<pool>/storagevolumes/ {file: fd}
this fd object will have :
- fd.file: a part of 10MB of the file to be uploaded - fd.part: the correspond part number - fd.total: total number of parts to be received
The UI will do something like:
fd.total = round(fd.size / 10MB) transfered = 0 part = 0
while part < fd.total: fd.file = read more 10MB of the file fd.part = part POST POST /storagepools/<pool>/storagevolumes/ {file: fd} fd.part++
On backend: if os.path.exists(filepath) and fd.part = 0: raise InvalidParemeter("file already exists")
localfile = open(filepath, a) localfile.seek(fd.part * 10MB) localfile.write(fd.file) What's the problem here for large files?
I wouldn't say 900MB is a large file.
Seems your solution will introduce lots of requests. I tried a large ISO file and the window was not closed and finally the request timed out.
Yes - it will require more requests but I can't see other solution on it. Ideas?
On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Implemented download remote image feature.
v1 -> v2: 2a) Disabled add volume for inactive and iscsi/scsi pools (Aline's comment)
2b) Removed progress bar after upload/download finished (Aline & Cristian's comment)
2c) Changed the button label to "Add" from "OK" (Cristian's comment)
2d) Made add volume button behave correctly (enabled/disabled) (Cristian's comment)
2e) Removed some code redundent (Aline & Cristian's comment)
Hongliang Wang (4): Storage Pool Add Volume UI: Add APIs to kimchi.api.js Storage Pool Add Volume UI: Add i18n Strings Storage Pool Add Volume UI: Implement Download/Upload Volume Function Storage Pool Add Volume UI: Connect the Feature in Storage Tab
ui/css/theme-default/storage.css | 31 ++- ui/css/theme-default/storagepool-add-volume.css | 36 ++++ ui/js/src/kimchi.api.js | 37 ++++ ui/js/src/kimchi.storage_main.js | 116 ++++++++++- ui/js/src/kimchi.storagepool_add_volume_main.js | 243 ++++++++++++++++++++++++ ui/pages/i18n.json.tmpl | 4 + ui/pages/storagepool-add-volume.html.tmpl | 80 ++++++++ ui/pages/tabs/storage.html.tmpl | 12 +- 8 files changed, 549 insertions(+), 10 deletions(-) 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
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

Hongliang, I've just sent a V3 of this patch set (it is your V2 + [Kimchi-devel] [PATCH] Adjustments on upload/download UI). Please, review and test it so we can merge it for 1.3 release. As many issues were found on upload feature it will be disabled for 1.3 release due the time. After 1.3 release we can focus on solve those issues and then enable upload option. The download UI *must* be merged by Web to be on 1.3. Please, take a look on V3. On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Implemented download remote image feature.
v1 -> v2: 2a) Disabled add volume for inactive and iscsi/scsi pools (Aline's comment)
2b) Removed progress bar after upload/download finished (Aline & Cristian's comment)
2c) Changed the button label to "Add" from "OK" (Cristian's comment)
2d) Made add volume button behave correctly (enabled/disabled) (Cristian's comment)
2e) Removed some code redundent (Aline & Cristian's comment)
Hongliang Wang (4): Storage Pool Add Volume UI: Add APIs to kimchi.api.js Storage Pool Add Volume UI: Add i18n Strings Storage Pool Add Volume UI: Implement Download/Upload Volume Function Storage Pool Add Volume UI: Connect the Feature in Storage Tab
ui/css/theme-default/storage.css | 31 ++- ui/css/theme-default/storagepool-add-volume.css | 36 ++++ ui/js/src/kimchi.api.js | 37 ++++ ui/js/src/kimchi.storage_main.js | 116 ++++++++++- ui/js/src/kimchi.storagepool_add_volume_main.js | 243 ++++++++++++++++++++++++ ui/pages/i18n.json.tmpl | 4 + ui/pages/storagepool-add-volume.html.tmpl | 80 ++++++++ ui/pages/tabs/storage.html.tmpl | 12 +- 8 files changed, 549 insertions(+), 10 deletions(-) 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

On 09/16/2014 05:33 AM, Aline Manera wrote:
Hongliang,
I've just sent a V3 of this patch set (it is your V2 + [Kimchi-devel] [PATCH] Adjustments on upload/download UI). Please, review and test it so we can merge it for 1.3 release.
As many issues were found on upload feature it will be disabled for 1.3 release due the time. After 1.3 release we can focus on solve those issues and then enable upload option.
The download UI *must* be merged by Web to be on 1.3. Please, take a look on V3. OK. Thanks for the patches.
On 09/12/2014 06:42 AM, Hongliang Wang wrote:
Implemented download remote image feature.
v1 -> v2: 2a) Disabled add volume for inactive and iscsi/scsi pools (Aline's comment)
2b) Removed progress bar after upload/download finished (Aline & Cristian's comment)
2c) Changed the button label to "Add" from "OK" (Cristian's comment)
2d) Made add volume button behave correctly (enabled/disabled) (Cristian's comment)
2e) Removed some code redundent (Aline & Cristian's comment)
Hongliang Wang (4): Storage Pool Add Volume UI: Add APIs to kimchi.api.js Storage Pool Add Volume UI: Add i18n Strings Storage Pool Add Volume UI: Implement Download/Upload Volume Function Storage Pool Add Volume UI: Connect the Feature in Storage Tab
ui/css/theme-default/storage.css | 31 ++- ui/css/theme-default/storagepool-add-volume.css | 36 ++++ ui/js/src/kimchi.api.js | 37 ++++ ui/js/src/kimchi.storage_main.js | 116 ++++++++++- ui/js/src/kimchi.storagepool_add_volume_main.js | 243 ++++++++++++++++++++++++ ui/pages/i18n.json.tmpl | 4 + ui/pages/storagepool-add-volume.html.tmpl | 80 ++++++++ ui/pages/tabs/storage.html.tmpl | 12 +- 8 files changed, 549 insertions(+), 10 deletions(-) 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
participants (2)
-
Aline Manera
-
Hongliang Wang