<html>
<head>
<meta content="text/html; charset=windows-1252"
http-equiv="Content-Type">
</head>
<body bgcolor="#FFFFFF" text="#000000">
<br>
<div class="moz-cite-prefix">On 09/15/2014 04:46 AM, Hongliang Wang
wrote:<br>
</div>
<blockquote cite="mid:54169951.5050805@linux.vnet.ibm.com"
type="cite">
<meta content="text/html; charset=windows-1252"
http-equiv="Content-Type">
<br>
<div class="moz-cite-prefix">On 09/12/2014 11:32 PM, Aline Manera
wrote:<br>
</div>
<blockquote cite="mid:5413121C.1050804@linux.vnet.ibm.com"
type="cite"> <br>
On 09/12/2014 06:42 AM, Hongliang Wang wrote: <br>
<blockquote type="cite">Implemented download and upload volumes
functions. <br>
<br>
Signed-off-by: Hongliang Wang <a moz-do-not-send="true"
class="moz-txt-link-rfc2396E"
href="mailto:hlwang@linux.vnet.ibm.com"><hlwang@linux.vnet.ibm.com></a>
<br>
--- <br>
ui/css/theme-default/storagepool-add-volume.css | 36 ++++ <br>
ui/js/src/kimchi.storagepool_add_volume_main.js | 243
++++++++++++++++++++++++ <br>
ui/pages/storagepool-add-volume.html.tmpl | 80
++++++++ <br>
3 files changed, 359 insertions(+) <br>
create mode 100644
ui/css/theme-default/storagepool-add-volume.css <br>
create mode 100644
ui/js/src/kimchi.storagepool_add_volume_main.js <br>
create mode 100644 ui/pages/storagepool-add-volume.html.tmpl
<br>
<br>
diff --git a/ui/css/theme-default/storagepool-add-volume.css
b/ui/css/theme-default/storagepool-add-volume.css <br>
new file mode 100644 <br>
index 0000000..6e8a551 <br>
--- /dev/null <br>
+++ b/ui/css/theme-default/storagepool-add-volume.css <br>
@@ -0,0 +1,36 @@ <br>
+/* <br>
+ * Project Kimchi <br>
+ * <br>
+ * Copyright IBM, Corp. 2014 <br>
+ * <br>
+ * Licensed under the Apache License, Version 2.0 (the
"License"); <br>
+ * you may not use this file except in compliance with the
License. <br>
+ * You may obtain a copy of the License at <br>
+ * <br>
+ * <a moz-do-not-send="true"
class="moz-txt-link-freetext"
href="http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0</a>
<br>
+ * <br>
+ * Unless required by applicable law or agreed to in writing,
software <br>
+ * distributed under the License is distributed on an "AS IS"
BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. <br>
+ * See the License for the specific language governing
permissions and <br>
+ * limitations under the License. <br>
+ */ <br>
+#sp-add-volume-window { <br>
+ height: 400px; <br>
+ width: 500px; <br>
+} <br>
+ <br>
+#sp-add-volume-window .textbox-wrapper input[type="text"] { <br>
+ box-sizing: border-box; <br>
+ width: 100%; <br>
+} <br>
+ <br>
+#sp-add-volume-window .textbox-wrapper label { <br>
+ vertical-align: middle; <br>
+} <br>
+ <br>
+#sp-add-volume-window input[type="text"][disabled] { <br>
+ color: #bbb; <br>
+ background-color: #fafafa; <br>
+ cursor: not-allowed; <br>
+} <br>
diff --git a/ui/js/src/kimchi.storagepool_add_volume_main.js
b/ui/js/src/kimchi.storagepool_add_volume_main.js <br>
new file mode 100644 <br>
index 0000000..9435e28 <br>
--- /dev/null <br>
+++ b/ui/js/src/kimchi.storagepool_add_volume_main.js <br>
@@ -0,0 +1,243 @@ <br>
+/* <br>
+ * Project Kimchi <br>
+ * <br>
+ * Copyright IBM, Corp. 2014 <br>
+ * <br>
+ * Licensed under the Apache License, Version 2.0 (the
'License'); <br>
+ * you may not use this file except in compliance with the
License. <br>
+ * You may obtain a copy of the License at <br>
+ * <br>
+ * <a moz-do-not-send="true"
class="moz-txt-link-freetext"
href="http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0</a>
<br>
+ * <br>
+ * Unless required by applicable law or agreed to in writing,
software <br>
+ * distributed under the License is distributed on an 'AS IS'
BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. <br>
+ * See the License for the specific language governing
permissions and <br>
+ * limitations under the License. <br>
+ */ <br>
+kimchi.sp_add_volume_main = function() { <br>
+ // download from remote server or upload from local file
<br>
+ var type = 'download'; <br>
+ <br>
+ var addButton = $('#sp-add-volume-button'); <br>
+ var remoteURLBox = $('#volume-remote-url'); <br>
+ var localFileBox = $('#volume-input-file'); <br>
+ var typeRadios = $('input.volume-type'); <br>
+ <br>
+ var isValidURL = function() { <br>
+ var url = $(remoteURLBox).val(); <br>
+ return kimchi.template_check_url(url); <br>
+ }; <br>
+ <br>
</blockquote>
<br>
<br>
<blockquote type="cite">+ var isValidFile = function() { <br>
+ var fileName = $(localFileBox).val(); <br>
+ return fileName && <br>
+ /[Ii][Ss][Oo]/g.test(fileName.split('.').pop());
<br>
+ }; <br>
</blockquote>
<br>
The user can download and upload any type of file. <br>
So you just need to check it a file was selected. <br>
</blockquote>
ACK.<br>
<blockquote cite="mid:5413121C.1050804@linux.vnet.ibm.com"
type="cite"> <br>
<blockquote type="cite">+ <br>
+ $(typeRadios).change(function(event) { <br>
+ $('.volume-input').prop('disabled', true); <br>
+ $('.volume-input.' + this.value).prop('disabled',
false); <br>
+ type = this.value; <br>
+ if(type == 'download') { <br>
+ $(addButton).prop('disabled', !isValidURL()); <br>
+ } <br>
+ else { <br>
+ $(addButton).prop('disabled', !isValidFile()); <br>
+ } <br>
+ }); <br>
+ <br>
+ $(remoteURLBox).on('input propertychange',
function(event) { <br>
+ $(addButton).prop('disabled', !isValidURL()); <br>
+ }); <br>
+ <br>
+ $(localFileBox).on('change', function(event) { <br>
+ $(addButton).prop('disabled', !isValidFile()); <br>
+ }); <br>
+ <br>
</blockquote>
<br>
<br>
<blockquote type="cite">+ if(!kimchi.volumeTransferTracker) {
<br>
+ kimchi.volumeTransferTracker = (function() { <br>
+ var tasks = {}, <br>
+ sps = {}; <br>
+ var addTask = function(task) { <br>
+ var taskID = task['id']; <br>
+ tasks[taskID] = task; <br>
+ var sp = task['sp']; <br>
+ if(sps[sp] === undefined) { <br>
+ sps[sp] = 1; <br>
+ } <br>
+ else { <br>
+ sps[sp]++; <br>
+ } <br>
+ }; <br>
+ var getTask = function(taskID) { <br>
+ return tasks[taskID]; <br>
+ }; <br>
+ var removeTask = function(task) { <br>
+ var taskID = task['id']; <br>
+ var sp = tasks[taskID]['sp']; <br>
+ delete tasks[taskID]; <br>
+ if(--sps[sp] === 0) { <br>
+ delete sps[sp]; <br>
+
kimchi.topic('kimchi/allVolumeTasksFinished').publish({ <br>
+ sp: sp <br>
+ }); <br>
+ } <br>
+ }; <br>
+ return { <br>
+ add: addTask, <br>
+ get: getTask, <br>
+ remove: removeTask <br>
+ }; <br>
+ })(); <br>
+ } <br>
+ var taskTracker = kimchi.volumeTransferTracker; <br>
</blockquote>
<br>
Why the above code is needed? <br>
</blockquote>
This code is used for:<br>
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).<br>
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.<br>
3) Performance. Keeping the mapping of storage pool name and task
in memory can save Ajax requests to improve performance.<br>
</blockquote>
<br>
All that information is hold by backend in the Task element.<br>
<br>
When querying a task related to storage volumes you should do:<br>
<br>
GET
/tasks?status=running&target_uri=^/storagepools/<pool-name>/storagevolumes/*<br>
<br>
This will return a list of running tasks related to storage volumes<br>
<br>
{ id: 1,<br>
status: running,<br>
message: ...,<br>
target_uri: /storagepools/default/storagevolumes/new-vol<br>
}<br>
...<br>
<br>
With the target_uri parameter you can know the pool and the new
volume name.<br>
<br>
I have done that design on the patch "[Kimchi-devel] [PATCH]
Adjustments on upload/download UI"<br>
With this approach every user logged into kimchi will have the same
view of volumes in progress.<br>
<br>
<blockquote cite="mid:54169951.5050805@linux.vnet.ibm.com"
type="cite"> <br>
<blockquote cite="mid:5413121C.1050804@linux.vnet.ibm.com"
type="cite"> <br>
The flow should be: <br>
<br>
1) User selects a file to upload or download and click on "Add"
button <br>
2) Add handler will only start the process <br>
POST
/storagepools/<pool>/storagevolumes/<volume>
{<data>} <br>
<br>
While listing the storage volumes in a storage pool you need to:
<br>
<br>
GET /storagepools/<pool>/storagevolumes/ <br>
<br>
*AND* get the storage volumes in progress by calling: <br>
<br>
GET
/tasks?status=running&target_uri=^/storagepools/<pool>/storagevolumes/*<br>
<br>
So when listing the volumes you will know which ones are in
progress. <br>
<br>
volumes = GET
/storagepools/<pool>/storagevolumes/<volume> <br>
pendingVolumes = GET
/tasks?status=running&target_uri=^/storagepools/<pool>/storagevolumes/*<br>
<br>
for volumes in volumes: <br>
#create volume box html <br>
<br>
# check the volume is in progress <br>
if volume in pendingVolumes: <br>
# add progress bar according to task <br>
# add taskTrack for this task id <br>
<br>
<br>
By now the Task resource can only return target_uri and message,
so there is no way to differ download from upload. <br>
What we know is only if a storage volume is pending. <br>
So I suggest to change the progress bar message to a generic
one, example, "In progress" <br>
</blockquote>
ACK.<br>
Already save the download/upload information when making the Ajax
callback in <b>makeCallback()</b> 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".<br>
</blockquote>
<br>
We should use the same "In progress" message in all cases so every
user will have the same view on it.<br>
<br>
<blockquote cite="mid:54169951.5050805@linux.vnet.ibm.com"
type="cite">
<blockquote cite="mid:5413121C.1050804@linux.vnet.ibm.com"
type="cite"> <br>
<br>
<br>
<blockquote type="cite">+ <br>
+ var makeCallback = function(trackType, sp, transType,
callback) { <br>
+ return function(resp) { <br>
+ var taskID = resp['id']; <br>
+ var volumeName =
resp['target_uri'].split('/').pop(); <br>
+ if(trackType === 'add') { <br>
+ taskTracker.add({ <br>
+ id: taskID, <br>
+ sp: sp, <br>
+ volume: volumeName <br>
+ }); <br>
+ } <br>
+ callback(transType, resp); <br>
+ }; <br>
+ }; <br>
+ <br>
+ var extractProgressData = function(data) { <br>
+ var sizeArray = /(\d+)\/(\d+)/g.exec(data) || [0, 0,
0]; <br>
+ var downloaded = sizeArray[1]; <br>
+ var percent = 0; <br>
+ if(downloaded) { <br>
+ var total = sizeArray[2]; <br>
+ if(!isNaN(total)) { <br>
+ percent = downloaded / total * 100; <br>
+ } <br>
+ } <br>
+ var formatted = kimchi.formatMeasurement(downloaded);
<br>
+ var size = (1.0 * formatted['v']).toFixed(1) +
formatted['s']; <br>
+ return { <br>
+ size: size, <br>
+ percent: percent <br>
+ }; <br>
+ }; <br>
+ <br>
+ var onFinished = function(type, result) { <br>
+ var progress = extractProgressData(resp['message']);
<br>
+ var task = taskTracker.get([result['id']]); <br>
+
kimchi.topic('kimchi/volumeTransferFinished').publish($.extend(progress,
{ <br>
+ sp: task['sp'], <br>
+ type: type, <br>
+ volume: task['volume'] <br>
+ })); <br>
+ taskTracker.remove({ <br>
+ id: result['id'] <br>
+ }); <br>
+ }; <br>
+ <br>
+ var onProgress = function(type, resp) { <br>
+ var progress = extractProgressData(resp['message']);
<br>
+ var task = taskTracker.get([resp['id']]); <br>
+
kimchi.topic('kimchi/volumeTransferProgress').publish($.extend(progress,
{ <br>
+ sp: task['sp'], <br>
+ type: type, <br>
+ volume: task['volume'] <br>
+ })); <br>
+ }; <br>
+ <br>
+ var onTransferError = function(type, result) { <br>
+ if(!result) { <br>
+ return; <br>
+ } <br>
+ var msg = result && (result['message'] || ( <br>
+ result['responseJSON'] &&
result['responseJSON']['reason']) <br>
+ ); <br>
+ kimchi.message.error(msg); <br>
+ <br>
+ if(!result['target_uri']) { <br>
+ return; <br>
+ } <br>
+ var task = taskTracker.get(result['id']); <br>
+ kimchi.topic('kimchi/volumeTransferError').publish({
<br>
+ sp: task['sp'], <br>
+ type: type, <br>
+ volume: task['volume'] <br>
+ }); <br>
+ taskTracker.remove({ <br>
+ id: result['id'] <br>
+ }); <br>
+ }; <br>
+ <br>
+ var onAccepted = function(type, resp) { <br>
+ var taskID = resp['id']; <br>
+ var task = taskTracker.get(taskID); <br>
+ kimchi.window.close(); <br>
+
kimchi.topic('kimchi/volumeTransferStarted').publish({ <br>
+ sp: task['sp'], <br>
+ type: type, <br>
+ volume: task['volume'] <br>
+ }); <br>
+ <br>
+ kimchi.trackTask(taskID, function(resp) { <br>
+ onFinished(type, resp); <br>
+ }, function(resp) { <br>
+ onTransferError(type, resp); <br>
+ }, function(resp) { <br>
+ onProgress(type, resp); <br>
+ }); <br>
+ }; <br>
+ <br>
+ var onError = function(result) { <br>
+ $(this).prop('disabled', false); <br>
+ $(typeRadios).prop('disabled', false); <br>
+ if(!result) { <br>
+ return; <br>
+ } <br>
+ var msg = result['message'] || ( <br>
+ result['responseJSON'] &&
result['responseJSON']['reason'] <br>
+ ); <br>
+ kimchi.message.error(msg); <br>
+ }; <br>
+ <br>
+ var fetchRemoteFile = function() { <br>
+ var volumeURL = remoteURLBox.val(); <br>
+ var volumeName = volumeURL.split(/(\\|\/)/g).pop(); <br>
+ kimchi.downloadVolumeToSP({ <br>
+ sp: kimchi.selectedSP, <br>
+ name: volumeName, <br>
+ url: volumeURL <br>
+ }, makeCallback('add', kimchi.selectedSP, 'download',
onAccepted), <br>
+ onError <br>
+ ); <br>
+ }; <br>
+ <br>
</blockquote>
<br>
<br>
<blockquote type="cite">+ var uploadFile = function() { <br>
+ var blobFile = $(localFileBox)[0].files[0]; <br>
+ var fileName = blobFile.name; <br>
+ var fd = new FormData(); <br>
+ fd.append('name', fileName); <br>
+ fd.append('file', blobFile); <br>
+ kimchi.uploadVolumeToSP({ <br>
+ sp: kimchi.selectedSP, <br>
+ formData: fd <br>
+ }, makeCallback('add', kimchi.selectedSP, 'upload',
onAccepted), <br>
+ onError <br>
+ ); <br>
+ }; <br>
+ <br>
</blockquote>
<br>
When selecting a file to upload the UI will need to read the
selected file to build the request. <br>
If the file is large it takes some time to complete and the UI
freezes. <br>
<br>
I have to suggestions: <br>
<br>
OPTION 1: <br>
<br>
1) Use select a file to upload <br>
2) Close the add volume window and display the volume in
progress with the progress bar label "Verifying file" <br>
3) Once the request is built, send it and update the volume box
accordingly <br>
<br>
OPTION 2: <br>
<br>
1) Use select a file to upload <br>
2) Disable "Add" button and rename it to: "Verifying file" <br>
3) Once the request is built, send it and close the add volume
window. <br>
4) Update volume list <br>
</blockquote>
ACK. Thanks for the testing for large files which I didn't. Option
1 sounds good.<br>
<blockquote cite="mid:5413121C.1050804@linux.vnet.ibm.com"
type="cite"> <br>
<blockquote type="cite">+ $(addButton).on('click',
function(event) { <br>
+ $(this).prop('disabled', true); <br>
+ $(typeRadios).prop('disabled', true); <br>
+ if(type === 'download') { <br>
+ fetchRemoteFile(); <br>
+ } <br>
+ else { <br>
+ uploadFile(); <br>
+ } <br>
+ event.preventDefault(); <br>
+ }); <br>
+}; <br>
diff --git a/ui/pages/storagepool-add-volume.html.tmpl
b/ui/pages/storagepool-add-volume.html.tmpl <br>
new file mode 100644 <br>
index 0000000..b01c942 <br>
--- /dev/null <br>
+++ b/ui/pages/storagepool-add-volume.html.tmpl <br>
@@ -0,0 +1,80 @@ <br>
+#* <br>
+ * Project Kimchi <br>
+ * <br>
+ * Copyright IBM, Corp. 2014 <br>
+ * <br>
+ * Authors: <br>
+ * Hongliang Wang <a moz-do-not-send="true"
class="moz-txt-link-rfc2396E"
href="mailto:hlwang@linux.vnet.ibm.com"><hlwang@linux.vnet.ibm.com></a>
<br>
+ * <br>
+ * Licensed under the Apache License, Version 2.0 (the
"License"); <br>
+ * you may not use this file except in compliance with the
License. <br>
+ * You may obtain a copy of the License at <br>
+ * <br>
+ * <a moz-do-not-send="true"
class="moz-txt-link-freetext"
href="http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0</a>
<br>
+ * <br>
+ * Unless required by applicable law or agreed to in writing,
software <br>
+ * distributed under the License is distributed on an "AS IS"
BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. <br>
+ * See the License for the specific language governing
permissions and <br>
+ * limitations under the License. <br>
+ *# <br>
+#unicode UTF-8 <br>
+#import gettext <br>
+#from kimchi.cachebust import href <br>
+#silent t = gettext.translation($lang.domain,
$lang.localedir, languages=$lang.lang) <br>
+#silent _ = t.gettext <br>
+#silent _t = t.gettext <br>
+<div id="sp-add-volume-window" class="window"> <br>
+ <form id="form-sp-add-volume"> <br>
+ <header class="window-header"> <br>
+ <h1 class="title">$_("Add a Volume to
Storage Pool")</h1> <br>
+ <div class="close">X</div> <br>
+ </header> <br>
+ <section> <br>
+ <div class="content"> <br>
+ <div class="form-section"> <br>
+ <h2> <br>
+ <input type="radio"
id="volume-type-download" class="volume-type"
name="volumeType" value="download" checked="checked" /> <br>
+ <label
for="volume-type-download"> <br>
+ $_("Fetch from remote URL") <br>
+ </label> <br>
+ </h2> <br>
+ <div class="field"> <br>
+ <p class="text-help"> <br>
+ $_("Enter the remote URL here.")
<br>
+ </p> <br>
+ <div class="textbox-wrapper"> <br>
+ <input type="text"
id="volume-remote-url" class="text volume-input download"
name="volumeRemoteURL" /> <br>
+ </div> <br>
+ </div> <br>
+ </div> <br>
+ <div class="form-section"> <br>
+ <h2> <br>
+ <input type="radio"
id="volume-type-upload" class="volume-type" name="volumeType"
value="upload"/> <br>
+ <label
for="volume-type-upload"> <br>
+ $_("Upload an file") <br>
+ </label> <br>
+ </h2> <br>
+ <div class="field"> <br>
+ <p class="text-help"> <br>
+ $_("Choose the ISO file (with
.iso suffix) you want to upload.") <br>
+ </p> <br>
+ <div class="textbox-wrapper"> <br>
+ <input type="file"
class="volume-input upload" id="volume-input-file"
name="volumeLocalFile" disabled="disabled" /> <br>
+ </div> <br>
+ </div> <br>
+ </div> <br>
+ </div> <br>
+ </section> <br>
+ <footer> <br>
+ <div class="btn-group"> <br>
+ <button type="submit"
id="sp-add-volume-button" class="btn-normal"
disabled="disabled"> <br>
+ <span
class="text">$_("Add")</span> <br>
+ </button> <br>
+ </div> <br>
+ </footer> <br>
+ </form> <br>
+</div> <br>
+<script type="text/javascript"> <br>
+ kimchi.sp_add_volume_main(); <br>
+</script> <br>
</blockquote>
<br>
</blockquote>
<br>
</blockquote>
<br>
</body>
</html>