Browse Source

[frontend] Add generic FileUpload and ImportDocumentsModal Vue components

Johan Ahlen 4 years ago
parent
commit
b4ebf7f9a1

+ 25 - 3
desktop/core/src/desktop/js/api/utils.ts

@@ -14,7 +14,13 @@
 // See the License for the specific language governing permissions and
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
 
 
-import axios, { AxiosError, AxiosResponse, AxiosTransformer, CancelToken } from 'axios';
+import axios, {
+  AxiosError,
+  AxiosRequestConfig,
+  AxiosResponse,
+  AxiosTransformer,
+  CancelToken
+} from 'axios';
 import qs from 'qs';
 import qs from 'qs';
 import huePubSub from 'utils/huePubSub';
 import huePubSub from 'utils/huePubSub';
 
 
@@ -31,7 +37,7 @@ export interface DefaultApiResponse {
   traceback?: string;
   traceback?: string;
 }
 }
 
 
-export interface ApiFetchOptions<T, E = string> {
+export interface ApiFetchOptions<T, E = string> extends AxiosRequestConfig {
   silenceErrors?: boolean;
   silenceErrors?: boolean;
   ignoreSuccessErrors?: boolean;
   ignoreSuccessErrors?: boolean;
   transformResponse?: AxiosTransformer;
   transformResponse?: AxiosTransformer;
@@ -153,7 +159,7 @@ export const post = <T, U = unknown, E = string>(
     axios
     axios
       .post<T & DefaultApiResponse>(url, qs.stringify(data), {
       .post<T & DefaultApiResponse>(url, qs.stringify(data), {
         cancelToken,
         cancelToken,
-        transformResponse: options && options.transformResponse
+        ...options
       })
       })
       .then(response => {
       .then(response => {
         handleResponse(response, resolve, reject, options);
         handleResponse(response, resolve, reject, options);
@@ -210,6 +216,22 @@ export const get = <T, U = unknown>(
     });
     });
   });
   });
 
 
+export const upload = async <T>(
+  url: string,
+  data: FormData,
+  progressCallback?: (progress: number) => void
+): Promise<T> => {
+  const response = await axios.post<T>(url, data, {
+    headers: { 'Content-Type': 'multipart/form-data' },
+    onUploadProgress: progressEvent => {
+      if (progressCallback) {
+        progressCallback(Math.round((progressEvent.loaded * 100) / progressEvent.total));
+      }
+    }
+  });
+  return response.data;
+};
+
 export const cancelActiveRequest = (request?: JQuery.jqXHR): void => {
 export const cancelActiveRequest = (request?: JQuery.jqXHR): void => {
   if (request && request.readyState < 4) {
   if (request && request.readyState < 4) {
     request.abort();
     request.abort();

+ 63 - 0
desktop/core/src/desktop/js/components/FileUpload.vue

@@ -0,0 +1,63 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. licenses this file
+  to you 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.
+-->
+
+<template>
+  <form enctype="multipart/form-data" novalidate>
+    <input
+      type="file"
+      :accept="accept"
+      :disabled="disabled"
+      :multiple="multiple"
+      @change="onFileInputChange"
+    />
+  </form>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  export default defineComponent({
+    name: 'FileUpload',
+    props: {
+      modelValue: {
+        type: Object as PropType<FileList | null>,
+        default: null
+      },
+      accept: {
+        type: String,
+        default: '*'
+      },
+      multiple: {
+        type: Boolean,
+        default: false
+      },
+      disabled: {
+        type: Boolean,
+        default: false
+      }
+    },
+    emits: ['update:model-value'],
+    setup(props, { emit }) {
+      const onFileInputChange = (event: InputEvent) => {
+        emit('update:model-value', (<HTMLInputElement>event.target).files);
+      };
+
+      return { onFileInputChange };
+    }
+  });
+</script>

+ 142 - 0
desktop/core/src/desktop/js/components/ImportDocumentsModal.vue

@@ -0,0 +1,142 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. licenses this file
+  to you 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.
+-->
+
+<template>
+  <Modal v-if="modelValue" :header="header || I18n('Import Hue Documents')" @close="onModalClose">
+    <template #body>
+      <FileUpload
+        v-if="!uploadStats && !importingDocuments"
+        v-model="selectedFiles"
+        :accept="'.json'"
+        :disabled="fileSelectionDisabled"
+      />
+      <Spinner v-else-if="importingDocuments" :inline="true" :label="I18n('Importing....')" />
+      <div v-else>
+        <div>
+          {{ I18n('Imported: ') }} <span>{{ uploadStats.count }}</span>
+        </div>
+        <div>
+          {{ I18n('Created: ') }} <span>{{ uploadStats.created_count }}</span>
+        </div>
+        <div>
+          {{ I18n('Updated: ') }} <span>{{ uploadStats.updated_count }}</span>
+        </div>
+      </div>
+      <div v-if="failed">
+        {{ I18n('Import failed!') }}
+      </div>
+    </template>
+    <template #footer>
+      <HueButton :disabled="importingDocuments" @click="onModalClose">
+        {{ I18n('Close') }}
+      </HueButton>
+      <HueButton v-if="!uploadStats" :alert="true" :disabled="importDisabled" @click="onImport">
+        {{ I18n('Import') }}
+      </HueButton>
+    </template>
+  </Modal>
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, ref } from 'vue';
+
+  import FileUpload from './FileUpload.vue';
+  import HueButton from './HueButton.vue';
+  import Spinner from './Spinner.vue';
+  import Modal from './Modal.vue';
+  import { upload } from 'api/utils';
+  import I18n from 'utils/i18n';
+
+  interface UploadStats {
+    count: number;
+    created_count: number;
+    updated_count: number;
+  }
+
+  export default defineComponent({
+    name: 'ImportDocumentsModal',
+    components: {
+      Spinner,
+      FileUpload,
+      HueButton,
+      Modal
+    },
+    props: {
+      modelValue: {
+        type: Boolean,
+        default: false
+      },
+      header: {
+        type: String,
+        default: ''
+      }
+    },
+    emits: ['update:model-value', 'documents-imported'],
+    setup(props, { emit }) {
+      const importingDocuments = ref(false);
+      const isUploading = ref(false);
+      const selectedFiles = ref<FileList | null>(null);
+      const uploadStats = ref<UploadStats | null>(null);
+      const failed = ref(false);
+
+      const importDisabled = computed((): boolean => {
+        return isUploading.value || importingDocuments.value || !selectedFiles.value?.length;
+      });
+
+      const fileSelectionDisabled = computed((): boolean => {
+        return isUploading.value || importingDocuments.value;
+      });
+
+      const onModalClose = (): void => {
+        emit('update:model-value', false);
+        selectedFiles.value = null;
+        uploadStats.value = null;
+      };
+
+      const onImport = async (): Promise<void> => {
+        const fileList = selectedFiles.value;
+        if (!fileList || !fileList.length) {
+          return;
+        }
+        importingDocuments.value = true;
+
+        const formData = new FormData();
+        formData.append('documents', fileList[0]);
+
+        try {
+          uploadStats.value = await upload<UploadStats>('/desktop/api2/doc/import', formData);
+          emit('documents-imported');
+        } catch (err) {
+          failed.value = true;
+        }
+        importingDocuments.value = false;
+      };
+
+      return {
+        I18n,
+        fileSelectionDisabled,
+        importDisabled,
+        importingDocuments,
+        onImport,
+        onModalClose,
+        selectedFiles,
+        uploadStats
+      };
+    }
+  });
+</script>