<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">&lt;hlwang@linux.vnet.ibm.com&gt;</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 &amp;&amp; <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&amp;target_uri=^/storagepools/&lt;pool-name&gt;/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/&lt;pool&gt;/storagevolumes/&lt;volume&gt;
        {&lt;data&gt;} <br>
        <br>
        While listing the storage volumes in a storage pool you need to:
        <br>
        <br>
        GET /storagepools/&lt;pool&gt;/storagevolumes/ <br>
        <br>
        *AND* get the storage volumes in progress by calling: <br>
        <br>
        GET
/tasks?status=running&amp;target_uri=^/storagepools/&lt;pool&gt;/storagevolumes/*<br>
        <br>
        So when listing the volumes you will know which ones are in
        progress. <br>
        <br>
        volumes = GET
        /storagepools/&lt;pool&gt;/storagevolumes/&lt;volume&gt; <br>
        pendingVolumes = GET
/tasks?status=running&amp;target_uri=^/storagepools/&lt;pool&gt;/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 &amp;&amp; (result['message'] || ( <br>
          +            result['responseJSON'] &amp;&amp;
          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'] &amp;&amp;
          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">&lt;hlwang@linux.vnet.ibm.com&gt;</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>
          +&lt;div id="sp-add-volume-window" class="window"&gt; <br>
          +    &lt;form id="form-sp-add-volume"&gt; <br>
          +        &lt;header class="window-header"&gt; <br>
          +            &lt;h1 class="title"&gt;$_("Add a Volume to
          Storage Pool")&lt;/h1&gt; <br>
          +            &lt;div class="close"&gt;X&lt;/div&gt; <br>
          +        &lt;/header&gt; <br>
          +        &lt;section&gt; <br>
          +            &lt;div class="content"&gt; <br>
          +                &lt;div class="form-section"&gt; <br>
          +                    &lt;h2&gt; <br>
          +                        &lt;input type="radio"
          id="volume-type-download" class="volume-type"
          name="volumeType" value="download" checked="checked" /&gt; <br>
          +                        &lt;label
          for="volume-type-download"&gt; <br>
          +                            $_("Fetch from remote URL") <br>
          +                        &lt;/label&gt; <br>
          +                    &lt;/h2&gt; <br>
          +                    &lt;div class="field"&gt; <br>
          +                        &lt;p class="text-help"&gt; <br>
          +                            $_("Enter the remote URL here.")
          <br>
          +                        &lt;/p&gt; <br>
          +                        &lt;div class="textbox-wrapper"&gt; <br>
          +                            &lt;input type="text"
          id="volume-remote-url" class="text volume-input download"
          name="volumeRemoteURL" /&gt; <br>
          +                        &lt;/div&gt; <br>
          +                    &lt;/div&gt; <br>
          +                &lt;/div&gt; <br>
          +                &lt;div class="form-section"&gt; <br>
          +                    &lt;h2&gt; <br>
          +                        &lt;input type="radio"
          id="volume-type-upload" class="volume-type" name="volumeType"
          value="upload"/&gt; <br>
          +                        &lt;label
          for="volume-type-upload"&gt; <br>
          +                        $_("Upload an file") <br>
          +                        &lt;/label&gt; <br>
          +                    &lt;/h2&gt; <br>
          +                    &lt;div class="field"&gt; <br>
          +                        &lt;p class="text-help"&gt; <br>
          +                            $_("Choose the ISO file (with
          .iso suffix) you want to upload.") <br>
          +                        &lt;/p&gt; <br>
          +                        &lt;div class="textbox-wrapper"&gt; <br>
          +                            &lt;input type="file"
          class="volume-input upload" id="volume-input-file"
          name="volumeLocalFile" disabled="disabled" /&gt; <br>
          +                        &lt;/div&gt; <br>
          +                    &lt;/div&gt; <br>
          +                &lt;/div&gt; <br>
          +            &lt;/div&gt; <br>
          +        &lt;/section&gt; <br>
          +        &lt;footer&gt; <br>
          +            &lt;div class="btn-group"&gt; <br>
          +                &lt;button type="submit"
          id="sp-add-volume-button" class="btn-normal"
          disabled="disabled"&gt; <br>
          +                    &lt;span
          class="text"&gt;$_("Add")&lt;/span&gt; <br>
          +                &lt;/button&gt; <br>
          +            &lt;/div&gt; <br>
          +        &lt;/footer&gt; <br>
          +    &lt;/form&gt; <br>
          +&lt;/div&gt; <br>
          +&lt;script type="text/javascript"&gt; <br>
          +    kimchi.sp_add_volume_main(); <br>
          +&lt;/script&gt; <br>
        </blockquote>
        <br>
      </blockquote>
      <br>
    </blockquote>
    <br>
  </body>
</html>