You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2019/10/01 16:47:42 UTC

[airavata-django-portal] branch master updated: AIRAVATA-3081 Tus uploads: switch to Drag & Drop UI

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git


The following commit(s) were added to refs/heads/master by this push:
     new 5352078  AIRAVATA-3081 Tus uploads: switch to Drag & Drop UI
5352078 is described below

commit 5352078866be1da27d9b19597a9927714749a902
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Oct 1 12:44:31 2019 -0400

    AIRAVATA-3081 Tus uploads: switch to Drag & Drop UI
---
 django_airavata/apps/workspace/package.json        |   2 +-
 .../experiment/input-editors/FileInputEditor.vue   | 107 +++-------------
 .../experiment/input-editors/InputFileSelector.vue | 138 +++++++++++++++++++++
 .../input-editors/MultiFileInputEditor.vue         | 114 ++++++-----------
 .../components/experiment/input-editors/Uppy.vue   |  44 +++++--
 .../js/containers/UserStorageContainer.vue         |   1 +
 6 files changed, 224 insertions(+), 182 deletions(-)

diff --git a/django_airavata/apps/workspace/package.json b/django_airavata/apps/workspace/package.json
index aadd26f..c3e3a63 100644
--- a/django_airavata/apps/workspace/package.json
+++ b/django_airavata/apps/workspace/package.json
@@ -15,7 +15,7 @@
   },
   "dependencies": {
     "@uppy/core": "^1.2.0",
-    "@uppy/file-input": "^1.2.0",
+    "@uppy/drag-drop": "^1.3.0",
     "@uppy/status-bar": "^1.2.0",
     "@uppy/tus": "^1.3.0",
     "@uppy/xhr-upload": "^1.2.0",
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
index fcc3c06..d3059ce 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
@@ -40,36 +40,13 @@
         ></i>
       </b-link>
     </div>
-    <div v-if="isSelectingFile">
-      <user-storage-file-selection-container
-        @file-selected="fileSelected"
-        @cancel="cancelFileSelection"
-        :selected-data-product-uris="selectedDataProductURIs"
-      />
-    </div>
-    <div
-      class="d-flex align-items-baseline"
-      v-if="!isSelectingFile && !isDataProductURI"
-    >
-      <b-button
-        @click="isSelectingFile=true"
-        class="input-file-option"
-      >Select file from storage</b-button>
-      <span class="text-muted mx-3">OR</span>
-      <b-form-group
-        :description="maxFileUploadSizeMessage"
-        :state="fileUploadState"
-        :invalid-feedback="fileUploadInvalidFeedback"
-        class="input-file-option"
-      >
-        <uppy
-          xhr-upload-endpoint="/api/upload"
-          tus-upload-finish-endpoint="/api/tus-upload-finish"
-          @upload-success="uploadSuccess"
-          @upload-started="$emit('uploadstart')"
-        />
-      </b-form-group>
-    </div>
+    <input-file-selector
+      v-if="!isDataProductURI"
+      :selectedDataProductURIs="selectedDataProductURIs"
+      @uploadstart="$emit('uploadstart')"
+      @uploadend="$emit('uploadend')"
+      @selected="fileSelected"
+    />
   </div>
 </template>
 
@@ -77,8 +54,7 @@
 import { models, services, utils } from "django-airavata-api";
 import { InputEditorMixin } from "django-airavata-workspace-plugin-api";
 import { components } from "django-airavata-common-ui";
-import UserStorageFileSelectionContainer from "../../storage/UserStorageFileSelectionContainer";
-import Uppy from "./Uppy";
+import InputFileSelector from "./InputFileSelector";
 
 export default {
   name: "file-input-editor",
@@ -86,8 +62,7 @@ export default {
   components: {
     "data-product-viewer": components.DataProductViewer,
     "delete-link": components.DeleteLink,
-    UserStorageFileSelectionContainer,
-    Uppy
+    InputFileSelector
   },
   computed: {
     isDataProductURI() {
@@ -110,54 +85,11 @@ export default {
       } else {
         return [];
       }
-    },
-    maxFileUploadSizeMB() {
-      return this.settings
-        ? this.settings.fileUploadMaxFileSize / 1024 / 1024
-        : 0;
-    },
-    maxFileUploadSizeMessage() {
-      if (this.maxFileUploadSizeMB) {
-        return (
-          "Max file upload size is " +
-          Math.round(this.maxFileUploadSizeMB) +
-          " MB"
-        );
-      } else {
-        return null;
-      }
-    },
-    fileTooLarge() {
-      return (
-        this.settings &&
-        this.settings.fileUploadMaxFileSize &&
-        this.file &&
-        this.file.size > this.settings.fileUploadMaxFileSize
-      );
-    },
-    fileUploadState() {
-      if (this.fileTooLarge) {
-        return false;
-      } else {
-        return null;
-      }
-    },
-    fileUploadInvalidFeedback() {
-      if (this.fileTooLarge) {
-        return (
-          "File selected is larger than " + this.maxFileUploadSizeMB + " MB"
-        );
-      } else {
-        return null;
-      }
     }
   },
   data() {
     return {
       dataProduct: null,
-      file: null,
-      isSelectingFile: false,
-      settings: null,
       fileContent: null
     };
   },
@@ -165,7 +97,6 @@ export default {
     if (this.isDataProductURI) {
       this.loadDataProduct(this.value);
     }
-    services.SettingsService.get().then(s => (this.settings = s));
   },
   methods: {
     loadDataProduct(dataProductURI) {
@@ -195,20 +126,18 @@ export default {
         .catch(utils.FetchUtils.reportError);
     },
     unselect() {
-      this.file = null;
       this.data = null;
       this.valueChanged();
     },
-    fileSelected(dataProductURI) {
+    fileSelected(dataProductURI, dataProduct) {
       this.data = dataProductURI;
-      this.isSelectingFile = false;
-      this.loadDataProduct(dataProductURI);
+      if (!dataProduct) {
+        this.loadDataProduct(dataProductURI);
+      } else {
+        this.dataProduct = dataProduct;
+      }
       this.valueChanged();
     },
-    cancelFileSelection() {
-      this.isSelectingFile = false;
-      this.unselect();
-    },
     viewFile() {
       this.fileContent = null;
       fetch(this.dataProduct.downloadURL, {
@@ -219,12 +148,6 @@ export default {
           this.fileContent = text;
           this.$refs.modal.show();
         });
-    },
-    uploadSuccess(result) {
-      this.dataProduct = new models.DataProduct(result["data-product"]);
-      this.data = this.dataProduct.productUri;
-      this.valueChanged();
-      this.$emit('uploadend');
     }
   }
 };
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/InputFileSelector.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/InputFileSelector.vue
new file mode 100644
index 0000000..645a198
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/InputFileSelector.vue
@@ -0,0 +1,138 @@
+<template>
+  <div v-if="isSelectingFile">
+    <user-storage-file-selection-container
+      @file-selected="fileSelected"
+      @cancel="cancelFileSelection"
+      :selected-data-product-uris="selectedDataProductURIs"
+    />
+  </div>
+  <div
+    class="d-flex align-items-baseline"
+    v-else
+  >
+    <b-button
+      @click="isSelectingFile=true"
+      class="input-file-option"
+    >Select file from storage</b-button>
+    <span class="text-muted mx-3">OR</span>
+    <b-form-group
+      :description="maxFileUploadSizeMessage"
+      :state="fileUploadState"
+      :invalid-feedback="fileUploadInvalidFeedback"
+      class="input-file-option"
+    >
+      <uppy
+        ref="uppy"
+        xhr-upload-endpoint="/api/upload"
+        tus-upload-finish-endpoint="/api/tus-upload-finish"
+        @upload-success="uploadSuccess"
+        @upload-started="$emit('uploadstart')"
+        @upload-finished="uploadFinished"
+        :multiple="multiple"
+      />
+    </b-form-group>
+  </div>
+</template>
+
+<script>
+import { models, services } from "django-airavata-api";
+import UserStorageFileSelectionContainer from "../../storage/UserStorageFileSelectionContainer";
+import Uppy from "./Uppy";
+
+export default {
+  name: "input-file-selector",
+  props: {
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    selectedDataProductURIs: {
+      type: Array,
+      default: () => []
+    }
+  },
+  components: {
+    UserStorageFileSelectionContainer,
+    Uppy
+  },
+  computed: {
+    maxFileUploadSizeMB() {
+      return this.settings
+        ? this.settings.fileUploadMaxFileSize / 1024 / 1024
+        : 0;
+    },
+    maxFileUploadSizeMessage() {
+      if (this.maxFileUploadSizeMB) {
+        return (
+          "Max file upload size is " +
+          Math.round(this.maxFileUploadSizeMB) +
+          " MB"
+        );
+      } else {
+        return null;
+      }
+    },
+    fileTooLarge() {
+      return (
+        this.settings &&
+        this.settings.fileUploadMaxFileSize &&
+        this.file &&
+        this.file.size > this.settings.fileUploadMaxFileSize
+      );
+    },
+    fileUploadState() {
+      if (this.fileTooLarge) {
+        return false;
+      } else {
+        return null;
+      }
+    },
+    fileUploadInvalidFeedback() {
+      if (this.fileTooLarge) {
+        return (
+          "File selected is larger than " + this.maxFileUploadSizeMB + " MB"
+        );
+      } else {
+        return null;
+      }
+    }
+  },
+  data() {
+    return {
+      file: null,
+      isSelectingFile: false,
+      settings: null
+    };
+  },
+  created() {
+    services.SettingsService.get().then(s => (this.settings = s));
+  },
+  methods: {
+    unselect() {
+      this.file = null;
+    },
+    fileSelected(dataProductURI) {
+      this.isSelectingFile = false;
+      this.$emit("selected", dataProductURI);
+    },
+    cancelFileSelection() {
+      this.isSelectingFile = false;
+      this.unselect();
+    },
+    uploadSuccess(result) {
+      const dataProduct = new models.DataProduct(result["data-product"]);
+      this.$emit("selected", dataProduct.productUri, dataProduct);
+    },
+    uploadFinished() {
+      this.$emit('uploadend');
+      this.$refs.uppy.reset();
+    }
+  }
+};
+</script>
+
+<style scoped>
+.input-file-option {
+  flex: 1 1 50%;
+}
+</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/MultiFileInputEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/MultiFileInputEditor.vue
index 30204e3..38dfc47 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/MultiFileInputEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/MultiFileInputEditor.vue
@@ -2,25 +2,31 @@
   <div>
     <div
       class="mb-2"
-      v-for="fileEntry in fileEntries"
-      :key="fileEntry.id"
+      v-for="dataProductURI in selectedDataProductURIs"
+      :key="dataProductURI"
     >
       <file-input-editor
-        :value="fileEntry.value"
-        :id="fileEntry.id"
+        :value="dataProductURI"
+        :id="dataProductURI"
         :experiment="experiment"
         :experiment-input="experimentInput"
-        @input="updatedFile($event, fileEntry)"
-        @uploadstart="uploadStart(fileEntry)"
-        @uploadend="uploadEnd(fileEntry)"
+        @input="updatedFile($event, dataProductURI)"
       />
     </div>
+    <input-file-selector
+      :selectedDataProductURIs="selectedDataProductURIs"
+      @selected="fileSelected"
+      @uploadstart="$emit('uploadstart')"
+      @uploadend="$emit('uploadend')"
+      multiple
+    />
   </div>
 </template>
 
 <script>
 import { InputEditorMixin } from "django-airavata-workspace-plugin-api";
 import FileInputEditor from "./FileInputEditor.vue";
+import InputFileSelector from "./InputFileSelector";
 
 export default {
   name: "multi-file-input-editor",
@@ -31,38 +37,32 @@ export default {
     }
   },
   components: {
-    FileInputEditor
+    FileInputEditor,
+    InputFileSelector
   },
   data() {
-    return {
-      newFileCount: 0,
-      fileEntries: this.createFileEntries(this.value)
-    };
+    return {};
+  },
+  computed: {
+    selectedDataProductURIs() {
+      return this.createValueArray(this.value);
+    }
   },
-  computed: {},
   methods: {
-    addFile() {
-      this.fileEntries.push({
-        id: this.id + "-" + this.newFileCount++,
-        value: null,
-        uploading: false
-      });
-    },
-    updatedFile(newValue, fileEntry) {
-      if (!newValue && fileEntry.value) {
-        this.removeFile(fileEntry);
-      } else {
-        fileEntry.value = newValue;
+    updatedFile(newValue, dataProductURI) {
+      // Only remove is handled here, input-file-selector handles adding
+      if (!newValue) {
+        this.removeFile(dataProductURI);
       }
-      this.data = this.fileEntries
-        .filter(e => e.value) // exclude null entries
-        .map(e => e.value)
-        .join(",");
-      this.valueChanged();
     },
-    removeFile(fileEntry) {
-      const index = this.fileEntries.findIndex(e => e.id === fileEntry.id);
-      this.fileEntries.splice(index, 1);
+    removeFile(dataProductURI) {
+      const index = this.selectedDataProductURIs.findIndex(
+        u => u === dataProductURI
+      );
+      const copyDataProductURIs = this.selectedDataProductURIs.slice();
+      copyDataProductURIs.splice(index, 1);
+      this.data = copyDataProductURIs.join(",");
+      this.valueChanged();
     },
     createValueArray(value) {
       if (value && typeof value === "string") {
@@ -71,49 +71,11 @@ export default {
         return [];
       }
     },
-    createFileEntries(value) {
-      const valueArray = this.createValueArray(value);
-      const fileEntries = valueArray.map(v => {
-        return {
-          id: this.id + "-" + v,
-          value: v,
-          uploading: false
-        };
-      });
-      // Add a null entry to accept an additional upload
-      fileEntries.push(this.getNullFileEntry());
-      return fileEntries;
-    },
-    getNullFileEntry() {
-      // Reuse the old null entry if it exists, otherwise create a new one
-      if (
-        this.fileEntries &&
-        this.fileEntries.length > 0 &&
-        this.fileEntries[this.fileEntries.length - 1].value === null
-      ) {
-        return this.fileEntries[this.fileEntries.length - 1];
-      } else {
-        return {
-          id: this.id + "-" + this.newFileCount++,
-          value: null,
-          uploading: false
-        };
-      }
-    },
-    uploadStart(fileEntry) {
-      fileEntry.uploading = true;
-      this.$emit("uploadstart");
-    },
-    uploadEnd(fileEntry) {
-      fileEntry.uploading = false;
-      if (this.fileEntries.every(fe => !fe.uploading)) {
-        this.$emit("uploadend");
-      }
-    }
-  },
-  watch: {
-    value(newValue) {
-      this.fileEntries = this.createFileEntries(newValue);
+    fileSelected(dataProductURI) {
+      const values = this.createValueArray(this.value);
+      values.push(dataProductURI);
+      this.data = values.join(",");
+      this.valueChanged();
     }
   }
 };
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/Uppy.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/Uppy.vue
index 98c96c5..39cfb2f 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/Uppy.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/Uppy.vue
@@ -1,6 +1,6 @@
 <template>
-  <div>
-    <div ref="fileInput" />
+  <div class="custom-Uppy">
+    <div ref="dragDrop" />
     <div ref="statusBar" />
   </div>
 </template>
@@ -9,17 +9,15 @@
 import { services, utils } from "django-airavata-api";
 
 import Uppy from "@uppy/core";
-import FileInput from "@uppy/file-input";
+import DragDrop from "@uppy/drag-drop";
 import StatusBar from "@uppy/status-bar";
 import Tus from "@uppy/tus";
 import XHRUpload from "@uppy/xhr-upload";
 
-import "@uppy/core/dist/style.css";
-import "@uppy/status-bar/dist/style.css";
-import "@uppy/file-input/dist/style.css";
+import "@uppy/core/dist/style.min.css";
+import "@uppy/status-bar/dist/style.min.css";
+import "@uppy/drag-drop/dist/style.min.css";
 
-// TODO: dispatch upload start
-// TODO: maybe use dragdrop UI?
 export default {
   name: "uppy",
   props: {
@@ -32,6 +30,10 @@ export default {
     tusUploadFinishEndpoint: {
       type: String,
       required: false
+    },
+    multiple: {
+      type: Boolean,
+      default: false
     }
   },
   mounted() {
@@ -46,7 +48,7 @@ export default {
   data() {
     return {
       uppy: null
-    }
+    };
   },
   methods: {
     initUppy() {
@@ -54,9 +56,13 @@ export default {
         // TODO: set id
         autoProceed: true,
         // TODO: add maxFileSize restriction
-        debug: true
+        debug: true,
+        restrictions: {
+          maxNumberOfFiles: this.multiple ? null : 1,
+        }
       });
-      this.uppy.use(FileInput, { target: this.$refs.fileInput, pretty: false });
+      // TODO: add 'note' here as passed in through prop (Max file upload size is 64MB)
+      this.uppy.use(DragDrop, { target: this.$refs.dragDrop });
       this.uppy.use(StatusBar, {
         target: this.$refs.statusBar,
         hideUploadButton: true,
@@ -78,9 +84,9 @@ export default {
           endpoint: this.xhrUploadEndpoint,
           withCredentials: true,
           headers: {
-            'X-CSRFToken': utils.FetchUtils.getCSRFToken()
+            "X-CSRFToken": utils.FetchUtils.getCSRFToken()
           },
-          fieldName: 'file'
+          fieldName: "file"
         });
         this.uppy.on("upload-success", (file, response) => {
           this.$emit("upload-success", response.body);
@@ -99,3 +105,15 @@ export default {
   }
 };
 </script>
+
+<style scoped>
+.custom-Uppy >>> .uppy-DragDrop-inner {
+  padding: 5px 0px;
+}
+.custom-Uppy >>> .UppyIcon {
+  display: none;
+}
+.custom-Uppy >>> .uppy-DragDrop-label {
+  margin-bottom: 0px;
+}
+</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
index a47ff71..b7f34ef 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
@@ -22,6 +22,7 @@
             :xhr-upload-endpoint="uploadEndpoint"
             :tus-upload-finish-endpoint="uploadEndpoint"
             @upload-success="uploadSuccess"
+            multiple
           />
         </b-form-group>
       </div>