瀏覽代碼

[ui-sb] move fileUploadQueue at global level (#4136)

Ram Prasad Agarwal 6 月之前
父節點
當前提交
8ed894a0a5

+ 19 - 17
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx

@@ -32,15 +32,12 @@ import {
   FileSystem
 } from '../types';
 import formatBytes from '../../../utils/formatBytes';
-
-import './StorageDirectoryPage.scss';
 import { formatTimestamp } from '../../../utils/dateTimeUtils';
 import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
 import { DEFAULT_PAGE_SIZE, DEFAULT_POLLING_TIME } from '../../../utils/constants/storageBrowser';
 import DragAndDrop from '../../../reactComponents/DragAndDrop/DragAndDrop';
 import UUID from '../../../utils/string/UUID';
-import { RegularFile, FileStatus } from '../../../utils/hooks/useFileUpload/types';
-import FileUploadQueue from '../../../reactComponents/FileUploadQueue/FileUploadQueue';
+import { FileStatus } from '../../../utils/hooks/useFileUpload/types';
 import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 import StorageDirectoryActions from './StorageDirectoryActions/StorageDirectoryActions';
 import PaginatedTable, {
@@ -48,6 +45,14 @@ import PaginatedTable, {
   ColumnProps
 } from '../../../reactComponents/PaginatedTable/PaginatedTable';
 import { getLastDirOrFileNameFromPath } from '../../../reactComponents/PathBrowser/PathBrowser.util';
+import huePubSub from '../../../utils/huePubSub';
+import { useHuePubSub } from '../../../utils/hooks/useHuePubSub/useHuePubSub';
+import {
+  FILE_UPLOAD_START_EVENT,
+  FILE_UPLOAD_SUCCESS_EVENT
+} from '../../../reactComponents/FileUploadQueue/event';
+
+import './StorageDirectoryPage.scss';
 
 interface StorageDirectoryPageProps {
   fileStats: FileStats;
@@ -69,7 +74,6 @@ const StorageDirectoryPage = ({
   reloadTrashPath
 }: StorageDirectoryPageProps): JSX.Element => {
   const [selectedFiles, setSelectedFiles] = useState<StorageDirectoryTableData[]>([]);
-  const [filesToUpload, setFilesToUpload] = useState<RegularFile[]>([]);
   const [polling, setPolling] = useState<boolean>(false);
 
   const [pageSize, setPageSize] = useState<number>(DEFAULT_PAGE_SIZE);
@@ -151,9 +155,18 @@ const StorageDirectoryPage = ({
       };
     });
     setPolling(true);
-    setFilesToUpload(prevFiles => [...prevFiles, ...newUploadItems]);
+    huePubSub.publish(FILE_UPLOAD_START_EVENT, {
+      files: newUploadItems
+    });
   };
 
+  useHuePubSub({
+    topic: FILE_UPLOAD_SUCCESS_EVENT,
+    callback: () => {
+      setPolling(false);
+    }
+  });
+
   const errorConfig = [
     {
       enabled: !!listDirectoryError,
@@ -271,17 +284,6 @@ const StorageDirectoryPage = ({
           />
         </LoadingErrorWrapper>
       </DragAndDrop>
-
-      {filesToUpload.length > 0 && (
-        <FileUploadQueue
-          filesQueue={filesToUpload}
-          onClose={() => setFilesToUpload([])}
-          onComplete={() => {
-            reloadFilesData();
-            setPolling(false);
-          }}
-        />
-      )}
     </div>
   );
 };

+ 34 - 24
desktop/core/src/desktop/js/reactComponents/FileUploadQueue/FileUploadQueue.scss

@@ -16,34 +16,44 @@
 
 @use 'variables' as vars;
 
+.hue-upload-queue-container {
+  display: flex;
+  flex-direction: column;
+  position: absolute;
+  bottom: 0;
+  right: 16px;
+  width: 520px;
+  box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.2);
+  z-index: 1000;
+}
+
 .antd.cuix {
-  .upload-queue {
-    position: fixed;
-    bottom: 0;
-    right: 16px;
-    width: 520px;
-    background-color: vars.$fluidx-white;
-    box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.2);
-    z-index: 1000;
+  .hue-upload-queue-container__header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px 16px;
+    color: vars.$fluidx-gray-900;
+    background-color: vars.$fluidx-gray-050;
+    font-size: 16px;
 
-    &__header {
+    &__button-group {
       display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: 16px;
-      color: vars.$fluidx-gray-900;
-      background-color: vars.$fluidx-gray-050;
-      cursor: pointer;
-      font-size: 16px;
-    }
+      gap: 8px;
 
-    &__list {
-      display: flex;
-      flex-direction: column;
-      max-height: 40vh;
-      overflow: auto;
-      padding: 16px;
-      gap: 16px;
+      button {
+        color: vars.$fluidx-black;
+      }
     }
   }
+
+  .hue-upload-queue-container__list {
+    display: flex;
+    flex-direction: column;
+    max-height: 40vh;
+    background-color: vars.$fluidx-white;
+    overflow: auto;
+    padding: 16px;
+    gap: 16px;
+  }
 }

+ 23 - 18
desktop/core/src/desktop/js/reactComponents/FileUploadQueue/FileUploadQueue.test.tsx

@@ -15,10 +15,13 @@
 // limitations under the License.
 
 import React from 'react';
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, fireEvent } from '@testing-library/react';
 import '@testing-library/jest-dom';
 import FileUploadQueue from './FileUploadQueue';
 import { FileStatus, RegularFile } from '../../utils/hooks/useFileUpload/types';
+import { act } from 'react-dom/test-utils';
+import huePubSub from '../../utils/huePubSub';
+import { FILE_UPLOAD_START_EVENT } from './event';
 
 const mockFilesQueue: RegularFile[] = [
   {
@@ -35,40 +38,42 @@ const mockFilesQueue: RegularFile[] = [
   }
 ];
 
-const mockOnCancel = jest.fn();
 jest.mock('../../utils/hooks/useFileUpload/useFileUpload', () => ({
   __esModule: true,
   default: jest.fn(() => ({
     uploadQueue: mockFilesQueue,
-    onCancel: mockOnCancel
+    onCancel: jest.fn(),
+    addFiles: jest.fn()
   }))
 }));
 
 describe('FileUploadQueue', () => {
   it('should render the component with initial files in the queue', () => {
-    const { getByText } = render(
-      <FileUploadQueue filesQueue={mockFilesQueue} onClose={() => {}} onComplete={() => {}} />
-    );
+    const { getByText } = render(<FileUploadQueue />);
+
+    act(() => huePubSub.publish(FILE_UPLOAD_START_EVENT, { files: mockFilesQueue }));
 
     expect(getByText('file1.txt')).toBeInTheDocument();
     expect(getByText('file2.txt')).toBeInTheDocument();
   });
 
   it('should toggle the visibility of the queue when the header is clicked', () => {
-    const { getByTestId } = render(
-      <FileUploadQueue filesQueue={mockFilesQueue} onClose={() => {}} onComplete={() => {}} />
-    );
+    const { getByText, getByTestId, queryByText } = render(<FileUploadQueue />);
+
+    act(() => huePubSub.publish(FILE_UPLOAD_START_EVENT, { files: mockFilesQueue }));
+
+    const header = getByTestId('hue-upload-queue-container__expand-button');
+    expect(getByText('file1.txt')).toBeVisible();
+    expect(getByText('file2.txt')).toBeVisible();
+
+    fireEvent.click(header);
 
-    const header = getByTestId('upload-queue__header');
-    expect(screen.getByText('file1.txt')).toBeVisible();
-    expect(screen.getByText('file2.txt')).toBeVisible();
+    expect(queryByText('file1.txt')).toBeNull();
+    expect(queryByText('file2.txt')).toBeNull();
 
-    fireEvent.click(header!);
-    expect(screen.queryByText('file1.txt')).not.toBeVisible();
-    expect(screen.queryByText('file2.txt')).not.toBeVisible();
+    fireEvent.click(header);
 
-    fireEvent.click(header!);
-    expect(screen.getByText('file1.txt')).toBeVisible();
-    expect(screen.getByText('file2.txt')).toBeVisible();
+    expect(getByText('file1.txt')).toBeVisible();
+    expect(getByText('file2.txt')).toBeVisible();
   });
 });

+ 85 - 32
desktop/core/src/desktop/js/reactComponents/FileUploadQueue/FileUploadQueue.tsx

@@ -14,21 +14,25 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import React from 'react';
-import CloseIcon from '../../components/icons/CloseIcon';
+import React, { useState } from 'react';
+import CloseIcon from '@cloudera/cuix-core/icons/react/CloseIcon';
+import CaratDownIcon from '@cloudera/cuix-core/icons/react/CaratDownIcon';
+import CaratUpIcon from '@cloudera/cuix-core/icons/react/CaratUpIcon';
 import { i18nReact } from '../../utils/i18nReact';
 import { RegularFile, FileStatus } from '../../utils/hooks/useFileUpload/types';
 import useFileUpload from '../../utils/hooks/useFileUpload/useFileUpload';
 import { DEFAULT_ENABLE_CHUNK_UPLOAD } from '../../utils/constants/storageBrowser';
 import { getLastKnownConfig } from '../../config/hueConfig';
 import FileUploadRow from './FileUploadRow/FileUploadRow';
+import { useHuePubSub } from '../../utils/hooks/useHuePubSub/useHuePubSub';
+import huePubSub from '../../utils/huePubSub';
+import { BorderlessButton } from 'cuix/dist/components/Button';
+import { FILE_UPLOAD_START_EVENT, FILE_UPLOAD_SUCCESS_EVENT } from './event';
 
 import './FileUploadQueue.scss';
 
-interface FileUploadQueueProps {
-  filesQueue: RegularFile[];
-  onClose: () => void;
-  onComplete: () => void;
+interface FileUploadEvent {
+  files: RegularFile[];
 }
 
 const sortOrder = [
@@ -42,48 +46,97 @@ const sortOrder = [
   return acc;
 }, {});
 
-const FileUploadQueue: React.FC<FileUploadQueueProps> = ({ filesQueue, onClose, onComplete }) => {
+const FileUploadQueue = (): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  const [expandQueue, setExpandQueue] = useState<boolean>(true);
+  const [isVisible, setIsVisible] = useState<boolean>(false);
+
+  const onComplete = () => {
+    huePubSub.publish(FILE_UPLOAD_SUCCESS_EVENT);
+  };
+
   const config = getLastKnownConfig();
   const isChunkUpload =
     (config?.storage_browser.enable_chunked_file_upload ?? DEFAULT_ENABLE_CHUNK_UPLOAD) &&
     !!config?.hue_config.enable_task_server;
 
-  const { t } = i18nReact.useTranslation();
-
-  const { uploadQueue, onCancel } = useFileUpload(filesQueue, {
+  const { uploadQueue, cancelFile, addFiles } = useFileUpload({
     isChunkUpload,
     onComplete
   });
 
+  useHuePubSub<FileUploadEvent>({
+    topic: FILE_UPLOAD_START_EVENT,
+    callback: (data?: FileUploadEvent) => {
+      if (data?.files) {
+        setIsVisible(true);
+        addFiles(data.files);
+      }
+    }
+  });
+
+  const onClose = () => {
+    uploadQueue.forEach(file => cancelFile(file));
+    setIsVisible(false);
+  };
+
   const uploadedCount = uploadQueue.filter(item => item.status === FileStatus.Uploaded).length;
   const pendingCount = uploadQueue.filter(
     item => item.status === FileStatus.Pending || item.status === FileStatus.Uploading
   ).length;
+  const failedCount = uploadQueue.filter(item => item.status === FileStatus.Failed).length;
+
+  if (!isVisible) {
+    return <></>;
+  }
+
+  const getHeaderText = () => {
+    const fileText = uploadQueue.length > 1 ? 'files' : 'file';
+    const uploadedText = `{{uploadedCount}} ${fileText} uploaded`;
+    const pendingText = pendingCount > 0 ? `{{pendingCount}} ${fileText} remaining` : '';
+    const failedText = failedCount > 0 ? `, {{failedCount}} failed` : '';
+    if (pendingCount > 0) {
+      return `${pendingText}${failedText}`;
+    }
+    return `${uploadedText}${failedText}`;
+  };
 
   return (
-    <details className="upload-queue cuix antd" open>
-      <summary className="upload-queue__header" data-testid="upload-queue__header">
-        {pendingCount > 0
-          ? t('{{count}} file(s) remaining', {
-              count: pendingCount
-            })
-          : t('{{count}} file(s) uploaded', {
-              count: uploadedCount
-            })}
-        <CloseIcon onClick={onClose} height={16} width={16} />
-      </summary>
-      <div className="upload-queue__list">
-        {uploadQueue
-          .sort((a, b) => sortOrder[a.status] - sortOrder[b.status])
-          .map((row: RegularFile) => (
-            <FileUploadRow
-              key={`${row.filePath}__${row.file.name}`}
-              data={row}
-              onCancel={() => onCancel(row)}
-            />
-          ))}
+    <div className="hue-upload-queue-container antd cuix">
+      <div className="hue-upload-queue-container__header" data-testid="hue-upload-queue__header">
+        {t(getHeaderText(), {
+          pendingCount,
+          uploadedCount,
+          failedCount
+        })}
+        <div className="hue-upload-queue-container__header__button-group">
+          <BorderlessButton
+            onClick={() => setExpandQueue(!expandQueue)}
+            data-testid="hue-upload-queue-container__expand-button"
+            icon={expandQueue ? <CaratDownIcon /> : <CaratUpIcon />}
+          />
+          <BorderlessButton
+            onClick={onClose}
+            data-testid="hue-upload-queue-container__close-button"
+            icon={<CloseIcon />}
+          />
+        </div>
       </div>
-    </details>
+      {expandQueue && (
+        <div className="hue-upload-queue-container__list">
+          {uploadQueue
+            .sort((a, b) => sortOrder[a.status] - sortOrder[b.status])
+            .map((row: RegularFile) => (
+              <FileUploadRow
+                key={`${row.filePath}__${row.file.name}`}
+                data={row}
+                onCancel={() => cancelFile(row)}
+              />
+            ))}
+        </div>
+      )}
+    </div>
   );
 };
 

+ 18 - 0
desktop/core/src/desktop/js/reactComponents/FileUploadQueue/event.ts

@@ -0,0 +1,18 @@
+// 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.
+
+export const FILE_UPLOAD_START_EVENT = 'hue.file.upload.start';
+export const FILE_UPLOAD_SUCCESS_EVENT = 'hue.file.upload.success';

+ 3 - 0
desktop/core/src/desktop/js/reactComponents/imports.js

@@ -38,6 +38,9 @@ export async function loadComponent(name) {
     case 'TaskServer':
       return (await import('./TaskServer/TaskServer')).default;
 
+    case 'FileUploadQueue':
+      return (await import('./FileUploadQueue/FileUploadQueue')).default;
+
     default:
       console.error(`A placeholder component is rendered because you probably forgot to include your new component in the 
       loadComponent function of reactComponents/imports.js`);

+ 28 - 30
desktop/core/src/desktop/js/utils/hooks/useFileUpload/useFileUpload.ts

@@ -14,17 +14,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { useCallback, useEffect, useState } from 'react';
+import { useState } from 'react';
 import useRegularUpload from './useRegularUpload';
 import useChunkUpload from './useChunkUpload';
 import { DEFAULT_CONCURRENT_MAX_CONNECTIONS } from '../../constants/storageBrowser';
 import { getLastKnownConfig } from '../../../config/hueConfig';
-import { getNewRegularFiles } from './utils';
 import { FileStatus, RegularFile, FileVariables } from './types';
+import { i18nReact } from '../../../utils/i18nReact';
 
 interface UseUploadQueueResponse {
   uploadQueue: RegularFile[];
-  onCancel: (item: RegularFile) => void;
+  addFiles: (newFiles: RegularFile[]) => void;
+  cancelFile: (item: RegularFile) => void;
   isLoading: boolean;
 }
 
@@ -33,10 +34,12 @@ interface UploadQueueOptions {
   onComplete: () => RegularFile[] | void;
 }
 
-const useFileUpload = (
-  filesQueue: RegularFile[],
-  { isChunkUpload = false, onComplete }: UploadQueueOptions
-): UseUploadQueueResponse => {
+const useFileUpload = ({
+  isChunkUpload = false,
+  onComplete
+}: UploadQueueOptions): UseUploadQueueResponse => {
+  const { t } = i18nReact.useTranslation();
+
   const config = getLastKnownConfig();
   const concurrentProcess =
     config?.storage_browser.concurrent_max_connection ?? DEFAULT_CONCURRENT_MAX_CONNECTIONS;
@@ -82,38 +85,33 @@ const useFileUpload = (
     onComplete
   });
 
-  const onCancel = useCallback(
-    (item: RegularFile) => {
-      const queueItem = uploadQueue.find(q => q.uuid === item.uuid);
-      if (queueItem?.status === FileStatus.Pending) {
-        const error = new Error('Upload cancelled');
-        updateFileVariables(item.uuid, { status: FileStatus.Cancelled, error });
+  const cancelFile = (fileItem: RegularFile) => {
+    const queueItem = uploadQueue.find(q => q.uuid === fileItem.uuid);
+    if (queueItem?.status === FileStatus.Pending) {
+      const error = new Error(t('Upload cancelled'));
+      updateFileVariables(fileItem.uuid, { status: FileStatus.Cancelled, error });
 
-        if (isChunkUpload) {
-          cancelFromChunkUpload(item.uuid);
-        } else {
-          cancelFromRegularUpload(item.uuid);
-        }
+      if (isChunkUpload) {
+        cancelFromChunkUpload(fileItem.uuid);
+      } else {
+        cancelFromRegularUpload(fileItem.uuid);
       }
-    },
-    [isChunkUpload, updateFileVariables, cancelFromChunkUpload, cancelFromRegularUpload]
-  );
-
-  useEffect(() => {
-    const newQueueItems = getNewRegularFiles(filesQueue, uploadQueue);
+    }
+  };
 
-    if (newQueueItems.length > 0) {
-      setUploadQueue(prev => [...prev, ...newQueueItems]);
+  const addFiles = (fileItems: RegularFile[]) => {
+    if (fileItems.length > 0) {
+      setUploadQueue(prev => [...prev, ...fileItems]);
 
       if (isChunkUpload) {
-        addToChunkUpload(newQueueItems);
+        addToChunkUpload(fileItems);
       } else {
-        addToRegularUpload(newQueueItems);
+        addToRegularUpload(fileItems);
       }
     }
-  }, [filesQueue, uploadQueue, isChunkUpload]);
+  };
 
-  return { uploadQueue, onCancel, isLoading: isChunkLoading || isRegularLoading };
+  return { uploadQueue, cancelFile, addFiles, isLoading: isChunkLoading || isRegularLoading };
 };
 
 export default useFileUpload;

+ 0 - 12
desktop/core/src/desktop/js/utils/hooks/useFileUpload/utils.ts

@@ -29,18 +29,6 @@ import {
   RegularFile
 } from './types';
 
-export const getNewRegularFiles = (
-  newQueue: RegularFile[],
-  oldQueue: RegularFile[]
-): RegularFile[] => {
-  return newQueue.filter(
-    newItem =>
-      !oldQueue.some(
-        oldItem => oldItem.file.name === newItem.file.name && oldItem.filePath === newItem.filePath
-      )
-  );
-};
-
 export const getTotalChunk = (fileSize: number, DEFAULT_CHUNK_SIZE: number): number => {
   return Math.ceil(fileSize / DEFAULT_CHUNK_SIZE);
 };

+ 1 - 0
desktop/core/src/desktop/templates/hue.mako

@@ -138,6 +138,7 @@ ${ hueIcons.symbols() }
     <AppBanner data-reactcomponent='AppBanner'></AppBanner>
     <GlobalAlert data-reactcomponent='GlobalAlert'></GlobalAlert>
     <WelcomeTour data-reactcomponent='WelcomeTour'></WelcomeTour>
+    <FileUploadQueue data-reactcomponent='FileUploadQueue'></FileUploadQueue>
 
     <nav class="navbar navbar-default">
       <div class="navbar-inner top-nav">