浏览代码

[ui-sb] show loading spinner in action modal button instead of page (#4094)

Ram Prasad Agarwal 7 月之前
父节点
当前提交
30b315827c
共有 37 个文件被更改,包括 250 次插入275 次删除
  1. 18 12
      desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.tsx
  2. 5 11
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserTab/StorageBrowserTab.tsx
  3. 0 2
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.test.tsx
  4. 3 5
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.tsx
  5. 24 11
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangeOwnerAndGroupModal/ChangeOwnerAndGroupModal.tsx
  6. 12 6
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangePermissionModal/ChangePermissionModal.tsx
  7. 38 24
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/CompressionModal/CompressionModal.tsx
  8. 16 10
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/DeletionModal/DeletionModal.tsx
  9. 1 8
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.test.tsx
  10. 19 8
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.tsx
  11. 2 4
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/FileAndFolderActions.test.tsx
  12. 3 11
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/FileAndFolderActions.tsx
  13. 15 19
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/MoveCopyModal/MoveCopyModal.test.tsx
  14. 2 5
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/MoveCopyModal/MoveCopyModal.tsx
  15. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/RenameModal/RenameModal.tsx
  16. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ReplicationModal/ReplicationModal.tsx
  17. 1 13
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/SummaryModal/SummaryModal.test.tsx
  18. 5 7
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/SummaryModal/SummaryModal.tsx
  19. 2 8
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/StorageDirectoryActions.test.tsx
  20. 1 8
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/StorageDirectoryActions.tsx
  21. 0 13
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/Trash/TrashActions.test.tsx
  22. 7 12
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/Trash/TrashActions.tsx
  23. 1 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.scss
  24. 1 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx
  25. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/types.ts
  26. 4 4
      desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.test.tsx
  27. 13 10
      desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.tsx
  28. 0 9
      desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.scss
  29. 20 14
      desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.tsx
  30. 1 1
      desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.test.tsx
  31. 14 10
      desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.tsx
  32. 1 2
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.scss
  33. 1 8
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.test.tsx
  34. 2 10
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.tsx
  35. 6 4
      desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.ts
  36. 0 0
      desktop/core/src/desktop/static/desktop/css/hue.css
  37. 9 0
      desktop/core/src/desktop/static/desktop/less/root-wrapped-antd.less

+ 18 - 12
desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.tsx

@@ -16,7 +16,7 @@
 
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import Modal from 'cuix/dist/components/Modal';
-import { Input, Spin, Tooltip } from 'antd';
+import { Input, Tooltip } from 'antd';
 import Table, { ColumnProps } from 'cuix/dist/components/Table';
 import classNames from 'classnames';
 
@@ -30,12 +30,13 @@ import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
 import { BrowserViewType, ListDirectory } from '../types';
 import { LIST_DIRECTORY_API_URL } from '../api';
 import PathBrowser from '../../../reactComponents/PathBrowser/PathBrowser';
+import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 import './FileChooserModal.scss';
 
 interface FileChooserModalProps {
   onClose: () => void;
-  onSubmit: (destination_path: string) => void;
+  onSubmit: (destination_path: string) => Promise<void>;
   showModal: boolean;
   title: string;
   sourcePath: string;
@@ -60,6 +61,7 @@ const FileChooserModal = ({
   const { t } = i18nReact.useTranslation();
   const { cancelText = t('Cancel'), submitText = t('Submit') } = i18n;
   const [destPath, setDestPath] = useState<string>(sourcePath);
+  const [submitLoading, setSubmitLoading] = useState<boolean>(false);
   const [searchTerm, setSearchTerm] = useState<string>('');
 
   useEffect(() => {
@@ -130,19 +132,23 @@ const FileChooserModal = ({
     emptyText: t('Folder is empty')
   };
 
+  const handleOk = async () => {
+    setSubmitLoading(true);
+    await onSubmit(destPath);
+    setSubmitLoading(false);
+    onClose();
+  };
+
   return (
     <Modal
-      cancelText={cancelText}
+      open={showModal}
+      title={title}
       className="hue-filechooser-modal cuix antd"
       okText={submitText}
-      title={title}
-      open={showModal}
+      onOk={handleOk}
+      okButtonProps={{ disabled: sourcePath === destPath, loading: submitLoading }}
+      cancelText={cancelText}
       onCancel={onClose}
-      onOk={() => {
-        onSubmit(destPath);
-        onClose();
-      }}
-      okButtonProps={{ disabled: sourcePath === destPath }}
     >
       <div className="hue-filechooser-modal__body">
         <div className="hue-filechooser-modal__path-browser-panel">
@@ -156,7 +162,7 @@ const FileChooserModal = ({
             handleSearch(event.target.value);
           }}
         />
-        <Spin spinning={loading}>
+        <LoadingErrorWrapper loading={loading}>
           <Table
             className="hue-filechooser-modal__table"
             dataSource={tableData}
@@ -173,7 +179,7 @@ const FileChooserModal = ({
             locale={locale}
             showHeader={false}
           />
-        </Spin>
+        </LoadingErrorWrapper>
       </div>
     </Modal>
   );

+ 5 - 11
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserTab/StorageBrowserTab.tsx

@@ -62,11 +62,7 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
 
   const { t } = i18nReact.useTranslation();
 
-  const {
-    data: trashData,
-    loading: trashLoading,
-    reloadData: onTrashPathReload
-  } = useLoadData<TrashData>(TRASH_PATH, {
+  const { data: trashData, reloadData: onTrashPathReload } = useLoadData<TrashData>(TRASH_PATH, {
     params: { path: fileSystem.userHomeDirectory },
     skip: !fileSystem.config?.isTrashEnabled || !fileSystem.userHomeDirectory
   });
@@ -77,7 +73,7 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
   };
 
   const reloadTrashPath = () => {
-    if (trashData?.trashPath) {
+    if (trashData?.trashPath || !fileSystem.config?.isTrashEnabled) {
       return;
     }
     onTrashPathReload();
@@ -122,10 +118,8 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
     }
   ];
 
-  const isLoading = loading || trashLoading;
-
   return (
-    <LoadingErrorWrapper loading={isLoading} errors={errorConfig}>
+    <LoadingErrorWrapper loading={loading} errors={errorConfig}>
       <div className="hue-storage-browser-tab" data-testid={testId}>
         <div
           className="hue-storage-browser-tab__title-bar-container"
@@ -192,7 +186,7 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
         >
           <PathBrowser filePath={filePath} onFilepathChange={setFilePath} />
         </div>
-        {fileStats?.type === BrowserViewType.dir && !isLoading && (
+        {fileStats?.type === BrowserViewType.dir && !loading && (
           <StorageDirectoryPage
             fileStats={fileStats}
             onFilePathChange={setFilePath}
@@ -200,7 +194,7 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
             reloadTrashPath={reloadTrashPath}
           />
         )}
-        {fileStats?.type === BrowserViewType.file && !isLoading && (
+        {fileStats?.type === BrowserViewType.file && !loading && (
           <StorageFilePage fileStats={fileStats} onReload={reloadData} />
         )}
       </div>

+ 0 - 2
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.test.tsx

@@ -21,7 +21,6 @@ describe('CreateAndUploadAction', () => {
   const currentPath = '/some/path';
   const onActionSuccess = jest.fn();
   const onActionError = jest.fn();
-  const setLoadingFiles = jest.fn();
   const mockFilesUpload = jest.fn();
 
   beforeEach(() => {
@@ -29,7 +28,6 @@ describe('CreateAndUploadAction', () => {
       <CreateAndUploadAction
         currentPath={currentPath}
         onActionSuccess={onActionSuccess}
-        setLoadingFiles={setLoadingFiles}
         onFilesUpload={mockFilesUpload}
         onActionError={onActionError}
       />

+ 3 - 5
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.tsx

@@ -29,13 +29,13 @@ import { CREATE_DIRECTORY_API_URL, CREATE_FILE_API_URL } from '../../../api';
 import { FileStats } from '../../../types';
 import useSaveData from '../../../../../utils/hooks/useSaveData/useSaveData';
 import InputModal from '../../../../../reactComponents/InputModal/InputModal';
-import './CreateAndUploadAction.scss';
 import DragAndDrop from '../../../../../reactComponents/DragAndDrop/DragAndDrop';
 
+import './CreateAndUploadAction.scss';
+
 interface CreateAndUploadActionProps {
   currentPath: FileStats['path'];
   onActionSuccess: () => void;
-  setLoadingFiles: (value: boolean) => void;
   onFilesUpload: (files: File[]) => void;
   onActionError: (error: Error) => void;
 }
@@ -49,7 +49,6 @@ enum ActionType {
 const CreateAndUploadAction = ({
   currentPath,
   onActionSuccess,
-  setLoadingFiles,
   onFilesUpload,
   onActionError
 }: CreateAndUploadActionProps): JSX.Element => {
@@ -125,7 +124,6 @@ const CreateAndUploadAction = ({
     if (!url) {
       return;
     }
-    setLoadingFiles(true);
     save({ path: currentPath, name }, { url });
   };
 
@@ -154,7 +152,7 @@ const CreateAndUploadAction = ({
           submitText={t('Create')}
           onSubmit={handleCreate}
           onClose={onModalClose}
-          buttonDisabled={loading}
+          loading={loading}
         />
       )}
       <Modal

+ 24 - 11
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangeOwnerAndGroupModal/ChangeOwnerAndGroupModal.tsx

@@ -21,6 +21,7 @@ import useSaveData from '../../../../../../utils/hooks/useSaveData/useSaveData';
 import { Checkbox, Input, Select } from 'antd';
 import { HDFSFileSystemConfig, StorageDirectoryTableData } from '../../../../types';
 import { BULK_CHANGE_OWNER_API_URL } from '../../../../api';
+
 import './ChangeOwnerAndGroupModal.scss';
 
 interface ChangeOwnerAndGroupModalProps {
@@ -30,7 +31,6 @@ interface ChangeOwnerAndGroupModalProps {
   groups?: HDFSFileSystemConfig['groups'];
   isOpen?: boolean;
   files: StorageDirectoryTableData[];
-  setLoading: (value: boolean) => void;
   onSuccess: () => void;
   onError: (error: Error) => void;
   onClose: () => void;
@@ -53,7 +53,6 @@ const ChangeOwnerAndGroupModal = ({
   groups = [],
   isOpen = true,
   files,
-  setLoading,
   onSuccess,
   onError,
   onClose
@@ -73,8 +72,6 @@ const ChangeOwnerAndGroupModal = ({
   });
 
   const handleChangeOwner = () => {
-    setLoading(true);
-
     const formData = new FormData();
     if (selectedUser === OTHERS_KEY && userOther) {
       formData.append('user', userOther);
@@ -124,15 +121,16 @@ const ChangeOwnerAndGroupModal = ({
 
   return (
     <Modal
-      cancelText={t('Cancel')}
+      open={isOpen}
+      title={t('Change Onwer / Group')}
       className="cuix antd"
       okText={t('Submit')}
-      onCancel={onClose}
       onOk={handleChangeOwner}
-      open={isOpen}
-      title={t('Change Onwer / Group')}
-      okButtonProps={{ disabled: loading || !isSubmitEnabled }}
+      okButtonProps={{ disabled: !isSubmitEnabled, loading }}
+      cancelText={t('Cancel')}
+      onCancel={onClose}
       cancelButtonProps={{ disabled: loading }}
+      closable={!loading}
     >
       <div className="hue-change-owner-group">
         <span className="hue-change-owner-group__header-note">
@@ -149,12 +147,19 @@ const ChangeOwnerAndGroupModal = ({
           <div className="hue-change-owner-group__entity">
             <div className="hue-change-owner-group__label">{t('User')}</div>
             <div className="hue-change-owner-group__dropdown">
-              <Select options={usersOptions} onChange={setSelectedUser} value={selectedUser} />
+              <Select
+                options={usersOptions}
+                onChange={setSelectedUser}
+                value={selectedUser}
+                disabled={loading}
+                getPopupContainer={triggerNode => triggerNode.parentElement}
+              />
               {selectedUser === OTHERS_KEY && (
                 <Input
                   placeholder={t('Enter user')}
                   value={userOther}
                   onChange={e => setUserOther(e.target.value)}
+                  disabled={loading}
                   required
                 />
               )}
@@ -164,12 +169,19 @@ const ChangeOwnerAndGroupModal = ({
           <div className="hue-change-owner-group__entity">
             <div className="hue-change-owner-group__label">{t('Group')}</div>
             <div className="hue-change-owner-group__dropdown">
-              <Select options={groupOptions} onChange={setSelectedGroup} value={selectedGroup} />
+              <Select
+                options={groupOptions}
+                onChange={setSelectedGroup}
+                value={selectedGroup}
+                disabled={loading}
+                getPopupContainer={triggerNode => triggerNode.parentElement}
+              />
               {selectedGroup === OTHERS_KEY && (
                 <Input
                   placeholder={t('Enter group')}
                   value={groupOther}
                   onChange={e => setGroupOther(e.target.value)}
+                  disabled={loading}
                   required
                 />
               )}
@@ -181,6 +193,7 @@ const ChangeOwnerAndGroupModal = ({
             <Checkbox
               checked={isRecursive}
               onChange={() => setIsRecursive(prev => !prev)}
+              disabled={loading}
               name="recursive"
             />
           </div>

+ 12 - 6
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangePermissionModal/ChangePermissionModal.tsx

@@ -31,7 +31,6 @@ import './ChangePermissionModal.scss';
 interface ChangePermissionModalProps {
   isOpen?: boolean;
   files: StorageDirectoryTableData[];
-  setLoading: (value: boolean) => void;
   onSuccess: () => void;
   onError: (error: Error) => void;
   onClose: () => void;
@@ -40,7 +39,6 @@ interface ChangePermissionModalProps {
 const ChangePermissionModal = ({
   isOpen = true,
   files,
-  setLoading,
   onSuccess,
   onError,
   onClose
@@ -57,8 +55,6 @@ const ChangePermissionModal = ({
   });
 
   const handleChangeOwner = () => {
-    setLoading(true);
-
     const formData = new FormData();
     const perm = permissions.reduce((acc, { key, user, group, other, common }) => {
       if (user) {
@@ -93,7 +89,13 @@ const ChangePermissionModal = ({
 
   const renderTableCheckbox = (key: keyof Permission) => (value: boolean, record: Permission) => {
     if (value !== undefined) {
-      return <Checkbox checked={value} onChange={() => handleCheckboxChange(record.key, key)} />;
+      return (
+        <Checkbox
+          checked={value}
+          onChange={() => handleCheckboxChange(record.key, key)}
+          disabled={loading}
+        />
+      );
     }
   };
 
@@ -138,8 +140,12 @@ const ChangePermissionModal = ({
       onOk={handleChangeOwner}
       open={isOpen}
       title={t('Change Permissions')}
-      okButtonProps={{ disabled: loading }}
+      okButtonProps={{
+        loading,
+        disabled: JSON.stringify(initialPermissions) === JSON.stringify(permissions)
+      }}
       cancelButtonProps={{ disabled: loading }}
+      closable={!loading}
     >
       <PaginatedTable<Permission> data={permissions} columns={columns} rowKey="key" />
     </Modal>

+ 38 - 24
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/CompressionModal/CompressionModal.tsx

@@ -23,12 +23,12 @@ import { COMPRESS_API_URL } from '../../../../api';
 import { Input } from 'antd';
 
 import './CompressionModal.scss';
+import LoadingErrorWrapper from '../../../../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 interface CompressionModalProps {
   currentPath: string;
   isOpen?: boolean;
   files: StorageDirectoryTableData[];
-  setLoading: (value: boolean) => void;
   onSuccess: () => void;
   onError: (error: Error) => void;
   onClose: () => void;
@@ -38,7 +38,6 @@ const CompressionModal = ({
   currentPath,
   isOpen = true,
   files,
-  setLoading,
   onSuccess,
   onError,
   onClose
@@ -47,15 +46,19 @@ const CompressionModal = ({
   const [value, setValue] = useState<string>(initialName);
   const { t } = i18nReact.useTranslation();
 
-  const { save: saveForm, loading } = useSaveData(COMPRESS_API_URL, {
+  const {
+    save: saveForm,
+    loading,
+    error
+  } = useSaveData(COMPRESS_API_URL, {
+    // TODO: remove silenceErrors once it is default to true in the hook
+    postOptions: { silenceErrors: true },
     skip: !files.length,
     onSuccess,
     onError
   });
 
   const handleCompress = () => {
-    setLoading(true);
-
     const formData = new FormData();
     files.forEach(selectedFile => {
       formData.append('file_name', selectedFile.name);
@@ -66,36 +69,47 @@ const CompressionModal = ({
     saveForm(formData);
   };
 
+  const errors = [
+    {
+      enabled: error?.response?.status === 500,
+      message: t('An error occurred during compression. Please check configuration.'),
+      action: t('Retry'),
+      onClick: handleCompress
+    }
+  ];
+
   return (
     <Modal
       cancelText={t('Cancel')}
       className="cuix antd"
       okText={t('Compress')}
       onCancel={onClose}
-      onOk={() => handleCompress()}
+      onOk={handleCompress}
       open={isOpen}
       title={t('Compress files and folders')}
-      okButtonProps={{ disabled: loading }}
+      okButtonProps={{ disabled: !!error || !value, loading }}
       cancelButtonProps={{ disabled: loading }}
+      closable={!loading}
     >
-      {t('Compressed file name')}
-      <Input
-        value={value}
-        type="text"
-        onPressEnter={() => handleCompress()}
-        onChange={e => {
-          setValue(e.target.value);
-        }}
-      />
+      <LoadingErrorWrapper errors={errors}>
+        {t('Compressed file name')}
+        <Input
+          value={value}
+          type="text"
+          onPressEnter={handleCompress}
+          onChange={e => setValue(e.target.value)}
+          disabled={loading}
+        />
 
-      <div className="compress-action">
-        {t('Following files and folders will be compressed:')}
-        <ul className="compress-action__list">
-          {files.map(file => (
-            <li key={file.path}>{file.name}</li>
-          ))}
-        </ul>
-      </div>
+        <div className="compress-action">
+          {t('Following files and folders will be compressed:')}
+          <ul className="compress-action__list">
+            {files.map(file => (
+              <li key={file.path}>{file.name}</li>
+            ))}
+          </ul>
+        </div>
+      </LoadingErrorWrapper>
     </Modal>
   );
 };

+ 16 - 10
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/DeletionModal/DeletionModal.tsx

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import React from 'react';
+import React, { useState } from 'react';
 import Modal from 'cuix/dist/components/Modal';
 import { i18nReact } from '../../../../../../utils/i18nReact';
 import useSaveData from '../../../../../../utils/hooks/useSaveData/useSaveData';
@@ -25,7 +25,6 @@ interface DeletionModalProps {
   isOpen?: boolean;
   isTrashEnabled?: boolean;
   files: StorageDirectoryTableData[];
-  setLoading: (value: boolean) => void;
   onSuccess: () => void;
   onError: (error: Error) => void;
   onClose: () => void;
@@ -35,13 +34,14 @@ const DeletionModal = ({
   isOpen = true,
   isTrashEnabled = false,
   files,
-  setLoading,
   onSuccess,
   onError,
   onClose
 }: DeletionModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
+  const [isMoveTrashClicked, setIsMoveTrashClicked] = useState<boolean>(false);
+
   const { save, loading } = useSaveData(undefined, {
     skip: !files.length,
     onSuccess,
@@ -49,8 +49,8 @@ const DeletionModal = ({
   });
 
   const handleDeletion = (isForedSkipTrash: boolean = false) => {
-    setLoading(true);
     const isSkipTrash = !isTrashEnabled || isForedSkipTrash;
+    setIsMoveTrashClicked(!isSkipTrash);
 
     const formData = new FormData();
     files.forEach(selectedFile => {
@@ -69,17 +69,23 @@ const DeletionModal = ({
 
   return (
     <Modal
-      cancelText={t('Cancel')}
+      open={isOpen}
+      title={t('Confirm Delete')}
       className="hue-input-modal cuix antd"
+      cancelText={t('Cancel')}
       okText={isTrashEnabled ? t('Move to Trash') : t('Delete Permanently')}
-      onCancel={onClose}
       onOk={() => handleDeletion()}
-      open={isOpen}
-      title={t('Confirm Delete')}
+      okButtonProps={{
+        disabled: loading && !isMoveTrashClicked,
+        loading: loading && isMoveTrashClicked
+      }}
       secondaryButtonText={isTrashEnabled ? t('Delete Permanently') : undefined}
       onSecondary={() => handleDeletion(true)}
-      secondaryButtonProps={{ disabled: loading }}
-      okButtonProps={{ disabled: loading }}
+      secondaryButtonProps={{
+        disabled: loading && isMoveTrashClicked,
+        loading: loading && !isMoveTrashClicked
+      }}
+      onCancel={onClose}
       cancelButtonProps={{ disabled: loading }}
     >
       {isTrashEnabled

+ 1 - 8
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.test.tsx

@@ -46,7 +46,6 @@ describe('ExtractAction Component', () => {
   const mockOnSuccess = jest.fn();
   const mockOnError = jest.fn();
   const mockOnClose = jest.fn();
-  const setLoading = jest.fn();
 
   beforeEach(() => {
     jest.clearAllMocks();
@@ -57,7 +56,6 @@ describe('ExtractAction Component', () => {
       <ExtractAction
         isOpen={true}
         file={mockFile}
-        setLoading={setLoading}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -76,7 +74,6 @@ describe('ExtractAction Component', () => {
       <ExtractAction
         isOpen={true}
         file={mockFile}
-        setLoading={setLoading}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -101,7 +98,6 @@ describe('ExtractAction Component', () => {
       <ExtractAction
         isOpen={true}
         file={mockFile}
-        setLoading={setLoading}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -122,7 +118,6 @@ describe('ExtractAction Component', () => {
       <ExtractAction
         isOpen={true}
         file={mockFile}
-        setLoading={setLoading}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -139,7 +134,6 @@ describe('ExtractAction Component', () => {
       <ExtractAction
         isOpen={true}
         file={mockFile}
-        setLoading={setLoading}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -158,7 +152,6 @@ describe('ExtractAction Component', () => {
       <ExtractAction
         isOpen={true}
         file={mockFile}
-        setLoading={setLoading}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -166,6 +159,6 @@ describe('ExtractAction Component', () => {
       />
     );
 
-    expect(getByRole('button', { name: 'Extract' })).toBeDisabled();
+    expect(getByRole('button', { name: 'loading Extract' })).toBeInTheDocument();
   });
 });

+ 19 - 8
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.tsx

@@ -20,12 +20,12 @@ import { i18nReact } from '../../../../../../utils/i18nReact';
 import useSaveData from '../../../../../../utils/hooks/useSaveData/useSaveData';
 import { StorageDirectoryTableData } from '../../../../types';
 import { EXTRACT_API_URL } from '../../../../api';
+import LoadingErrorWrapper from '../../../../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 interface ExtractActionProps {
   currentPath: string;
   isOpen?: boolean;
   file: StorageDirectoryTableData;
-  setLoading: (value: boolean) => void;
   onSuccess: () => void;
   onError: (error: Error) => void;
   onClose: () => void;
@@ -35,29 +35,37 @@ const ExtractionModal = ({
   currentPath,
   isOpen = true,
   file,
-  setLoading,
   onSuccess,
   onError,
   onClose
 }: ExtractActionProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { save, loading } = useSaveData(EXTRACT_API_URL, {
-    postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
+  const { save, loading, error } = useSaveData(EXTRACT_API_URL, {
+    // TODO: Remove qsEncodeData once API supports RAW JSON payload
+    // TODO: remove silenceErrors once it is default to true in the hook
+    postOptions: { qsEncodeData: true, silenceErrors: true },
     skip: !file,
     onSuccess,
     onError
   });
 
   const handleExtract = () => {
-    setLoading(true);
-
     save({
       upload_path: currentPath,
       archive_name: file.name
     });
   };
 
+  const errors = [
+    {
+      enabled: error?.response?.status === 500,
+      message: t('An error occurred during extraction. Please check configuration.'),
+      action: t('Retry'),
+      onClick: handleExtract
+    }
+  ];
+
   return (
     <Modal
       cancelText={t('Cancel')}
@@ -67,10 +75,13 @@ const ExtractionModal = ({
       onOk={handleExtract}
       open={isOpen}
       title={t('Extract Archive')}
-      okButtonProps={{ disabled: loading }}
+      okButtonProps={{ disabled: !!error, loading }}
       cancelButtonProps={{ disabled: loading }}
+      closable={!loading}
     >
-      {t('Are you sure you want to extract "{{fileName}}" file?', { fileName: file.name })}
+      <LoadingErrorWrapper errors={errors}>
+        {t('Are you sure you want to extract "{{fileName}}" file?', { fileName: file.name })}
+      </LoadingErrorWrapper>
     </Modal>
   );
 };

+ 2 - 4
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/FileAndFolderActions.test.tsx

@@ -77,12 +77,11 @@ describe('FileAndFolderActions', () => {
 
   const mockRecord: StorageDirectoryTableData = mockTwoRecords[0];
 
-  const setLoadingFiles = jest.fn();
   const mockOnActionSuccess = jest.fn();
   const mockOnActionError = jest.fn();
   const mockConfig = {
-    is_trash_enabled: true,
-    is_hdfs_superuser: true,
+    isTrashEnabled: true,
+    isHdfsSuperuser: true,
     groups: ['hue'],
     users: ['hue'],
     superuser: 'hue',
@@ -109,7 +108,6 @@ describe('FileAndFolderActions', () => {
       <FileAndFolderActions
         config={mockConfig}
         onActionError={mockOnActionError}
-        setLoadingFiles={setLoadingFiles}
         onActionSuccess={mockOnActionSuccess}
         selectedFiles={selectedFiles}
         currentPath="/path/to/folder"

+ 3 - 11
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/FileAndFolderActions.tsx

@@ -57,12 +57,11 @@ interface ActionConfig {
   superuser: HDFSFileSystemConfig['superuser'];
 }
 interface StorageBrowserRowActionsProps {
-  config: ActionConfig;
+  config?: ActionConfig;
   currentPath: FileStats['path'];
   selectedFiles: StorageDirectoryTableData[];
   onActionSuccess: () => void;
   onActionError: (error: Error) => void;
-  setLoadingFiles: (value: boolean) => void;
 }
 
 const iconsMap: Record<ActionType, JSX.Element> = {
@@ -84,8 +83,7 @@ const FileAndFolderActions = ({
   currentPath,
   selectedFiles,
   onActionSuccess,
-  onActionError,
-  setLoadingFiles
+  onActionError
 }: StorageBrowserRowActionsProps): JSX.Element => {
   const [selectedAction, setSelectedAction] = useState<ActionType>();
 
@@ -133,7 +131,7 @@ const FileAndFolderActions = ({
         trigger={['click']}
         disabled={!actionItems.length}
       >
-        <Button data-event="">
+        <Button>
           {t('Actions')}
           <DropDownIcon />
         </Button>
@@ -166,7 +164,6 @@ const FileAndFolderActions = ({
             onSuccess={onApiSuccess}
             onError={onActionError}
             onClose={closeModal}
-            setLoadingFiles={setLoadingFiles}
           />
         )}
       {selectedAction === ActionType.Delete && !!selectedFiles.length && (
@@ -176,7 +173,6 @@ const FileAndFolderActions = ({
           onSuccess={onApiSuccess}
           onError={onActionError}
           onClose={closeModal}
-          setLoading={setLoadingFiles}
         />
       )}
       {selectedAction === ActionType.Compress && !!selectedFiles.length && (
@@ -186,7 +182,6 @@ const FileAndFolderActions = ({
           onSuccess={onApiSuccess}
           onError={onActionError}
           onClose={closeModal}
-          setLoading={setLoadingFiles}
         />
       )}
       {selectedAction === ActionType.Extract && !!selectedFiles.length && (
@@ -196,7 +191,6 @@ const FileAndFolderActions = ({
           onSuccess={onApiSuccess}
           onError={onActionError}
           onClose={closeModal}
-          setLoading={setLoadingFiles}
         />
       )}
       {selectedAction === ActionType.ChangeOwnerAndGroup && !!selectedFiles.length && (
@@ -209,7 +203,6 @@ const FileAndFolderActions = ({
           onSuccess={onApiSuccess}
           onError={onActionError}
           onClose={closeModal}
-          setLoading={setLoadingFiles}
         />
       )}
       {selectedAction === ActionType.ChangePermission && !!selectedFiles.length && (
@@ -218,7 +211,6 @@ const FileAndFolderActions = ({
           onSuccess={onApiSuccess}
           onError={onActionError}
           onClose={closeModal}
-          setLoading={setLoadingFiles}
         />
       )}
     </>

+ 15 - 19
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/MoveCopyModal/MoveCopyModal.test.tsx

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
+import { render, fireEvent, waitFor } from '@testing-library/react';
 import '@testing-library/jest-dom';
 import MoveCopyModal from './MoveCopyModal';
 import { ActionType } from '../FileAndFolderActions.util';
@@ -83,7 +83,6 @@ describe('MoveCopy Action Component', () => {
           action={ActionType.Copy}
           currentPath={currentPath}
           files={mockFiles}
-          setLoadingFiles={jest.fn()}
           onSuccess={mockOnSuccess}
           onError={mockOnError}
           onClose={mockOnClose}
@@ -103,19 +102,17 @@ describe('MoveCopy Action Component', () => {
           action={ActionType.Copy}
           currentPath={currentPath}
           files={mockFiles}
-          setLoadingFiles={jest.fn()}
           onSuccess={mockOnSuccess}
           onError={mockOnError}
           onClose={mockOnClose}
         />
       );
       fireEvent.click(getByText('folder1'));
-
       const copyButton = getByText('Copy');
-      expect(copyButton).not.toBeDisabled();
+      await waitFor(() => expect(copyButton).not.toBeDisabled());
       fireEvent.click(copyButton);
 
-      expect(mockSave).toHaveBeenCalledTimes(1);
+      await waitFor(() => expect(mockSave).toHaveBeenCalledTimes(1));
 
       const formData = new FormData();
       mockFiles.forEach(file => {
@@ -134,7 +131,6 @@ describe('MoveCopy Action Component', () => {
           action={ActionType.Copy}
           currentPath={currentPath}
           files={mockFiles}
-          setLoadingFiles={jest.fn()}
           onSuccess={mockOnSuccess}
           onError={mockOnError}
           onClose={mockOnClose}
@@ -144,8 +140,10 @@ describe('MoveCopy Action Component', () => {
       fireEvent.click(getByText('folder1'));
       fireEvent.click(getByText('Copy'));
 
-      expect(mockSave).toHaveBeenCalledTimes(1);
-      expect(mockOnSuccess).toHaveBeenCalledTimes(1);
+      await waitFor(() => {
+        expect(mockSave).toHaveBeenCalledTimes(1);
+        expect(mockOnSuccess).toHaveBeenCalledTimes(1);
+      });
     });
 
     it('should call onError when the request fails', async () => {
@@ -158,7 +156,6 @@ describe('MoveCopy Action Component', () => {
           action={ActionType.Copy}
           currentPath={currentPath}
           files={mockFiles}
-          setLoadingFiles={jest.fn()}
           onSuccess={mockOnSuccess}
           onError={mockOnError}
           onClose={mockOnClose}
@@ -168,18 +165,19 @@ describe('MoveCopy Action Component', () => {
       fireEvent.click(getByText('folder1'));
       fireEvent.click(getByText('Copy'));
 
-      expect(mockSave).toHaveBeenCalledTimes(1);
-      expect(mockOnError).toHaveBeenCalledTimes(1);
+      await waitFor(() => {
+        expect(mockSave).toHaveBeenCalledTimes(1);
+        expect(mockOnError).toHaveBeenCalledTimes(1);
+      });
     });
 
-    it('should call onClose when the modal is closed', () => {
+    it('should call onClose when the modal is closed', async () => {
       const { getByText } = render(
         <MoveCopyModal
           isOpen={true}
           action={ActionType.Copy}
           currentPath={currentPath}
           files={mockFiles}
-          setLoadingFiles={jest.fn()}
           onSuccess={mockOnSuccess}
           onError={mockOnError}
           onClose={mockOnClose}
@@ -188,7 +186,7 @@ describe('MoveCopy Action Component', () => {
 
       fireEvent.click(getByText('Cancel'));
 
-      expect(mockOnClose).toHaveBeenCalledTimes(1);
+      await waitFor(() => expect(mockOnClose).toHaveBeenCalledTimes(1));
     });
   });
 
@@ -200,7 +198,6 @@ describe('MoveCopy Action Component', () => {
           action={ActionType.Move}
           currentPath={currentPath}
           files={mockFiles}
-          setLoadingFiles={jest.fn()}
           onSuccess={mockOnSuccess}
           onError={mockOnError}
           onClose={mockOnClose}
@@ -220,7 +217,6 @@ describe('MoveCopy Action Component', () => {
           action={ActionType.Move}
           currentPath={currentPath}
           files={mockFiles}
-          setLoadingFiles={jest.fn()}
           onSuccess={mockOnSuccess}
           onError={mockOnError}
           onClose={mockOnClose}
@@ -229,10 +225,10 @@ describe('MoveCopy Action Component', () => {
       fireEvent.click(getByText('folder1'));
 
       const moveButton = getByText('Move');
-      expect(moveButton).not.toBeDisabled();
+      await waitFor(() => expect(moveButton).not.toBeDisabled());
       fireEvent.click(moveButton);
 
-      expect(mockSave).toHaveBeenCalledTimes(1);
+      await waitFor(() => expect(mockSave).toHaveBeenCalledTimes(1));
 
       const formData = new FormData();
       mockFiles.forEach(file => {

+ 2 - 5
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/MoveCopyModal/MoveCopyModal.tsx

@@ -27,7 +27,6 @@ interface MoveCopyModalProps {
   action: ActionType.Copy | ActionType.Move;
   currentPath: FileStats['path'];
   files: StorageDirectoryTableData[];
-  setLoadingFiles: (value: boolean) => void;
   onSuccess: () => void;
   onError: (error: Error) => void;
   onClose: () => void;
@@ -38,7 +37,6 @@ const MoveCopyModal = ({
   action,
   currentPath,
   files,
-  setLoadingFiles,
   onSuccess,
   onError,
   onClose
@@ -51,7 +49,7 @@ const MoveCopyModal = ({
     onError: onError
   });
 
-  const handleCopyOrMove = (destination_path: string) => {
+  const handleCopyOrMove = async (destination_path: string) => {
     const url = {
       [ActionType.Copy]: BULK_COPY_API_URL,
       [ActionType.Move]: BULK_MOVE_API_URL
@@ -67,8 +65,7 @@ const MoveCopyModal = ({
     });
     formData.append('destination_path', destination_path);
 
-    setLoadingFiles(true);
-    save(formData, { url });
+    return save(formData, { url });
   };
 
   return (

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/RenameModal/RenameModal.tsx

@@ -59,7 +59,7 @@ const RenameModal = ({
       onClose={onClose}
       inputType="text"
       initialValue={file.name}
-      buttonDisabled={loading}
+      loading={loading}
     />
   );
 };

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ReplicationModal/ReplicationModal.tsx

@@ -59,7 +59,7 @@ const ReplicationModal = ({
       onClose={onClose}
       inputType="number"
       initialValue={file.replication}
-      buttonDisabled={loading}
+      loading={loading}
     />
   );
 };

+ 1 - 13
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/SummaryModal/SummaryModal.test.tsx

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import React from 'react';
-import { waitFor, screen, fireEvent, render } from '@testing-library/react';
+import { waitFor, screen, render } from '@testing-library/react';
 import '@testing-library/jest-dom';
 
 import { get } from '../../../../../../api/utils';
@@ -77,16 +77,4 @@ describe('SummaryModal', () => {
       expect(spaceConsumed[0]).toBeInTheDocument();
     });
   });
-
-  it('should call onClose function when close button is clicked', async () => {
-    const mockOnClose = jest.fn();
-    const { getByText } = render(<SummaryModal onClose={mockOnClose} path="some/path" />);
-
-    const closeButton = getByText('Close');
-    expect(mockOnClose).not.toHaveBeenCalled();
-    fireEvent.click(closeButton);
-    await waitFor(() => {
-      expect(mockOnClose).toHaveBeenCalledTimes(1);
-    });
-  });
 });

+ 5 - 7
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/SummaryModal/SummaryModal.tsx

@@ -16,7 +16,6 @@
 
 import React from 'react';
 import Modal from 'cuix/dist/components/Modal';
-import { Spin } from 'antd';
 
 import huePubSub from '../../../../../../utils/huePubSub';
 import { i18nReact } from '../../../../../../utils/i18nReact';
@@ -24,6 +23,7 @@ import formatBytes from '../../../../../../utils/formatBytes';
 import useLoadData from '../../../../../../utils/hooks/useLoadData/useLoadData';
 import { CONTENT_SUMMARY_API_URL } from '../../../../api';
 import { ContentSummary, StorageDirectoryTableData } from '../../../../types';
+import LoadingErrorWrapper from '../../../../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 import './SummaryModal.scss';
 
@@ -80,15 +80,13 @@ const SummaryModal = ({ isOpen = true, onClose, path }: SummaryModalProps): JSX.
 
   return (
     <Modal
-      className="hue-summary-modal cuix antd"
-      okText={t('Close')}
-      onOk={onClose}
       open={isOpen}
       title={t('Summary for ') + shortendPath}
-      cancellable={false}
+      className="cuix antd"
       onCancel={onClose}
+      footer={false}
     >
-      <Spin spinning={loading}>
+      <LoadingErrorWrapper loading={loading}>
         <div className="hue-summary-modal__grid">
           {summary?.map(item => (
             <div key={item.key} className="hue-summary-modal__grid__summary-item">
@@ -97,7 +95,7 @@ const SummaryModal = ({ isOpen = true, onClose, path }: SummaryModalProps): JSX.
             </div>
           ))}
         </div>
-      </Spin>
+      </LoadingErrorWrapper>
     </Modal>
   );
 };

+ 2 - 8
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/StorageDirectoryActions.test.tsx

@@ -20,7 +20,6 @@ import '@testing-library/jest-dom';
 import StorageDirectoryActions from './StorageDirectoryActions';
 
 const mockOnActionSuccess = jest.fn();
-const mockSetLoadingFiles = jest.fn();
 const mockOnFilesDrop = jest.fn();
 const mockOnFilePathChange = jest.fn();
 
@@ -60,20 +59,17 @@ describe('StorageDirectoryActions Component', () => {
   };
 
   const mockFileSystem = {
-    file_system: 'hdfs',
-    user_home_directory: '/user/hue'
+    name: 'hdfs',
+    userHomeDirectory: '/user/hue'
   };
 
   const mockTrashPath = '/user/path/.Trash/Current';
-  const mockIsTrashEmpty = !mockSelectedFiles.length;
 
   it('should render the Trash actions when path is in Trash', () => {
     const { getByRole, queryByRole } = render(
       <StorageDirectoryActions
         selectedFiles={mockSelectedFiles}
-        isFolderEmpty={mockIsTrashEmpty}
         onActionSuccess={mockOnActionSuccess}
-        setLoadingFiles={mockSetLoadingFiles}
         fileStats={{ ...mockFileStats, path: mockTrashPath }}
         fileSystem={mockFileSystem}
         onFilePathChange={mockOnFilePathChange}
@@ -91,9 +87,7 @@ describe('StorageDirectoryActions Component', () => {
     const { getByRole, queryByRole } = render(
       <StorageDirectoryActions
         selectedFiles={mockSelectedFiles}
-        isFolderEmpty={mockIsTrashEmpty}
         onActionSuccess={mockOnActionSuccess}
-        setLoadingFiles={mockSetLoadingFiles}
         fileStats={mockFileStats}
         fileSystem={mockFileSystem}
         onFilePathChange={mockOnFilePathChange}

+ 1 - 8
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/StorageDirectoryActions.tsx

@@ -27,7 +27,6 @@ interface StorageDirectoryActionsProps {
   fileSystem: FileSystem;
   onFilePathChange: (path: string) => void;
   onActionSuccess: () => void;
-  setLoadingFiles: (value: boolean) => void;
   selectedFiles: StorageDirectoryTableData[];
   onFilesDrop: (files: File[]) => void;
 }
@@ -37,17 +36,14 @@ const StorageDirectoryActions = ({
   fileSystem,
   onFilePathChange,
   onActionSuccess,
-  setLoadingFiles,
   selectedFiles,
   onFilesDrop
 }: StorageDirectoryActionsProps): JSX.Element => {
   const onApiSuccess = () => {
-    setLoadingFiles(false);
     onActionSuccess();
   };
 
   const onApiError = (error: Error) => {
-    setLoadingFiles(false);
     huePubSub.publish('hue.error', error);
   };
 
@@ -56,10 +52,9 @@ const StorageDirectoryActions = ({
       <TrashActions
         selectedFiles={selectedFiles}
         currentPath={fileStats.path}
-        setLoadingFiles={setLoadingFiles}
         onActionSuccess={onApiSuccess}
         onActionError={onApiError}
-        onTrashEmptySuccess={() => onFilePathChange(fileSystem.user_home_directory)}
+        onTrashEmptySuccess={() => onFilePathChange(fileSystem.userHomeDirectory)}
       />
     );
   }
@@ -70,13 +65,11 @@ const StorageDirectoryActions = ({
         config={fileSystem.config}
         currentPath={fileStats.path}
         selectedFiles={selectedFiles}
-        setLoadingFiles={setLoadingFiles}
         onActionSuccess={onApiSuccess}
         onActionError={onApiError}
       />
       <CreateAndUploadAction
         currentPath={fileStats.path}
-        setLoadingFiles={setLoadingFiles}
         onActionSuccess={onApiSuccess}
         onActionError={onApiError}
         onFilesUpload={onFilesDrop}

+ 0 - 13
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/Trash/TrashActions.test.tsx

@@ -30,7 +30,6 @@ jest.mock('../../../../../utils/hooks/useSaveData/useSaveData', () => ({
 const mockOnActionSuccess = jest.fn();
 const mockOnActionError = jest.fn();
 const mockOnTrashEmptySuccess = jest.fn();
-const mockSetLoadingFiles = jest.fn();
 
 describe('TrashActions Component', () => {
   beforeEach(() => {
@@ -60,7 +59,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -77,7 +75,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -97,7 +94,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -119,7 +115,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -127,7 +122,6 @@ describe('TrashActions Component', () => {
     fireEvent.click(getByRole('button', { name: 'Restore' }));
     fireEvent.click(getByRole('button', { name: 'Yes' }));
 
-    expect(mockSetLoadingFiles).toHaveBeenCalledWith(true);
     expect(mockSave).toHaveBeenCalled();
   });
 
@@ -138,7 +132,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -146,7 +139,6 @@ describe('TrashActions Component', () => {
     fireEvent.click(getByRole('button', { name: 'Empty trash' }));
     fireEvent.click(getByRole('button', { name: 'Yes' }));
 
-    expect(mockSetLoadingFiles).toHaveBeenCalledWith(true);
     expect(mockSave).toHaveBeenCalled();
   });
 
@@ -157,7 +149,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -175,7 +166,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -190,7 +180,6 @@ describe('TrashActions Component', () => {
         currentPath={'path/not/restorable'}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -209,7 +198,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );
@@ -231,7 +219,6 @@ describe('TrashActions Component', () => {
         currentPath={mockCurrentPath}
         onActionSuccess={mockOnActionSuccess}
         onActionError={mockOnActionError}
-        setLoadingFiles={mockSetLoadingFiles}
         onTrashEmptySuccess={mockOnTrashEmptySuccess}
       />
     );

+ 7 - 12
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/Trash/TrashActions.tsx

@@ -29,7 +29,6 @@ interface TrashActionsProps {
   currentPath: FileStats['path'];
   onActionSuccess: () => void;
   onActionError: (error: Error) => void;
-  setLoadingFiles: (value: boolean) => void;
   onTrashEmptySuccess: () => void;
 }
 
@@ -43,7 +42,6 @@ const TrashActions = ({
   currentPath,
   onActionSuccess,
   onActionError,
-  setLoadingFiles,
   onTrashEmptySuccess
 }: TrashActionsProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
@@ -62,14 +60,11 @@ const TrashActions = ({
     selectedFiles.forEach(selectedFile => {
       formData.append('path', selectedFile.path);
     });
-    setLoadingFiles(true);
 
     save(formData, { url: TRASH_RESTORE_BULK, onSuccess: onActionSuccess });
   };
 
   const onTrashEmpty = () => {
-    setLoadingFiles(true);
-
     save(undefined, {
       url: TRASH_PURGE,
       onSuccess: onTrashEmptySuccess
@@ -98,25 +93,25 @@ const TrashActions = ({
   return (
     <>
       <BorderlessButton
-        data-event=""
         disabled={!selectedFiles.length || !inRestorableTrash(currentPath)}
         onClick={() => handleActionClick(Actions.restore)}
       >
         {t('Restore')}
       </BorderlessButton>
-      <BorderlessButton data-event="" onClick={() => handleActionClick(Actions.trashEmpty)}>
+      <BorderlessButton onClick={() => handleActionClick(Actions.trashEmpty)}>
         {t('Empty trash')}
       </BorderlessButton>
       <Modal
-        cancelText={t('No')}
+        open={isModalOpen}
+        title={selectedAction === Actions.restore ? t('Restore') : t('Empty trash')}
         className="cuix antd"
         okText={t('Yes')}
-        onCancel={handleModalCancel}
         onOk={handleModalConfirm}
-        open={isModalOpen}
-        title={selectedAction === Actions.restore ? t('Restore') : t('Empty trash')}
-        okButtonProps={{ disabled: loading }}
+        okButtonProps={{ loading }}
+        cancelText={t('No')}
+        onCancel={handleModalCancel}
         cancelButtonProps={{ disabled: loading }}
+        closable={!loading}
       >
         {selectedAction === Actions.restore
           ? t('Are you sure you want to restore these files?')

+ 1 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.scss

@@ -52,6 +52,7 @@ $icon-margin: 5px;
       display: flex;
       gap: 6px;
       color: vars.$fluidx-blue-700;
+      cursor: pointer;
 
       &__icon {
         display: flex;

+ 1 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx

@@ -68,7 +68,6 @@ const StorageDirectoryPage = ({
   testId,
   reloadTrashPath
 }: StorageDirectoryPageProps): JSX.Element => {
-  const [loadingFiles, setLoadingFiles] = useState<boolean>(false);
   const [selectedFiles, setSelectedFiles] = useState<StorageDirectoryTableData[]>([]);
   const [filesToUpload, setFilesToUpload] = useState<RegularFile[]>([]);
   const [polling, setPolling] = useState<boolean>(false);
@@ -240,7 +239,6 @@ const StorageDirectoryPage = ({
               reloadFilesData();
               reloadTrashPath();
             }}
-            setLoadingFiles={setLoadingFiles}
             onFilesDrop={onFilesDrop}
           />
         </div>
@@ -249,7 +247,7 @@ const StorageDirectoryPage = ({
       <DragAndDrop onDrop={onFilesDrop}>
         <LoadingErrorWrapper errors={errorConfig}>
           <PaginatedTable<StorageDirectoryTableData>
-            loading={(loadingFiles || listDirectoryLoading) && !polling}
+            loading={listDirectoryLoading && !polling}
             data={tableData}
             columns={columnsConfig}
             rowKey={r => `${r.path}_${r.type}_${r.mtime}`}

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/types.ts

@@ -28,7 +28,7 @@ export interface HDFSFileSystemConfig {
 export interface FileSystem {
   name: string;
   userHomeDirectory: string;
-  config: HDFSFileSystemConfig;
+  config?: HDFSFileSystemConfig;
 }
 
 export interface FileStats {

+ 4 - 4
desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.test.tsx

@@ -216,7 +216,7 @@ describe('InputModal', () => {
     expect(cancelButton).toHaveFocus();
   });
 
-  it('should disable the submit button when buttonDisabled is true', () => {
+  it('should disable the submit button when isLoading is true', () => {
     render(
       <InputModal
         title="Create File"
@@ -227,11 +227,11 @@ describe('InputModal', () => {
         onClose={mockOnClose}
         initialValue=""
         inputType="text"
-        buttonDisabled={true}
+        loading={true}
       />
     );
-    const submitButton = screen.getByRole('button', { name: 'Create' });
-    expect(submitButton).toBeDisabled();
+    const submitButton = screen.getByRole('button', { name: 'loading Create' });
+    expect(submitButton).toBeInTheDocument();
   });
 
   it('should disable the submit button when input value is same as initial value', async () => {

+ 13 - 10
desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.tsx

@@ -32,7 +32,7 @@ interface InputModalProps {
   submitText?: string;
   showModal: boolean;
   title: string;
-  buttonDisabled?: boolean;
+  loading?: boolean;
 }
 
 const InputModal = ({
@@ -43,7 +43,7 @@ const InputModal = ({
   onSubmit,
   showModal,
   title,
-  buttonDisabled,
+  loading = false,
   ...i18n
 }: InputModalProps): JSX.Element => {
   const inputRef = useRef<InputRef>(null);
@@ -62,23 +62,26 @@ const InputModal = ({
 
   return (
     <Modal
-      cancelText={cancelText}
+      open={showModal}
+      title={title}
       className="hue-input-modal cuix antd"
       okText={submitText}
-      onCancel={onClose}
       onOk={handleSubmit}
-      open={showModal}
-      title={title}
-      secondaryButtonProps={{ disabled: buttonDisabled }}
-      okButtonProps={{ disabled: buttonDisabled || initialValue === value }}
-      cancelButtonProps={{ disabled: buttonDisabled }}
+      okButtonProps={{
+        disabled: initialValue === value,
+        loading
+      }}
+      cancelText={cancelText}
+      onCancel={onClose}
+      cancelButtonProps={{ disabled: loading }}
+      closable={!loading}
     >
       <div className="hue-input-modal__input-label">{inputLabel}</div>
       <Input
         className="hue-input-modal__input"
         defaultValue={value}
         type={inputType}
-        disabled={buttonDisabled}
+        disabled={loading}
         onPressEnter={handleSubmit}
         ref={inputRef}
         onChange={e => {

+ 0 - 9
desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.scss

@@ -14,15 +14,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-.loading-error-wrapper__error {
-  display: flex;
-  flex: 1;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  gap: 16px;
-}
-
 .antd.cuix {
   .loading-error-wrapper__spinner {
     // this overrides the max-height: 400px in antd library

+ 20 - 14
desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.tsx

@@ -15,17 +15,19 @@
 // limitations under the License.
 
 import React from 'react';
-import { Spin } from 'antd';
-import { PrimaryButton } from 'cuix/dist/components/Button';
+import { Alert, AlertProps, Spin } from 'antd';
+import { BorderlessButton } from 'cuix/dist/components/Button';
+import SpinnerIcon from 'cuix/dist/components/SpinnerIcon';
 
-import { i18nReact } from '../../utils/i18nReact';
 import './LoadingErrorWrapper.scss';
 
 interface WrapperError {
   enabled: boolean;
-  message: string;
+  message: AlertProps['message'];
+  description?: AlertProps['description'];
   action?: string;
-  onClick?: () => void;
+  onClick?: AlertProps['onClick'];
+  closable?: AlertProps['closable'];
 }
 
 interface LoadingErrorWrapperProps {
@@ -41,12 +43,11 @@ const LoadingErrorWrapper = ({
   children,
   hideChildren = false
 }: LoadingErrorWrapperProps): JSX.Element => {
-  const { t } = i18nReact.useTranslation();
-
   if (loading) {
     return (
       <Spin
         spinning={loading}
+        indicator={<SpinnerIcon size="default" />}
         data-testid="loading-error-wrapper__spinner"
         className="loading-error-wrapper__spinner"
       >
@@ -60,14 +61,19 @@ const LoadingErrorWrapper = ({
     return (
       <>
         {enabledErrors.map(error => (
-          <div className="loading-error-wrapper__error" key={error.message}>
-            <div>{t(error.message)}</div>
-            {error.onClick && (
-              <PrimaryButton onClick={error.onClick} data-event="">
+          <Alert
+            key={error.message?.toString()}
+            type="error"
+            message={error.message}
+            description={error.description}
+            closable={error.closable}
+            onClick={error.onClick}
+            action={
+              <BorderlessButton onClick={error.onClick} data-event="">
                 {error.action}
-              </PrimaryButton>
-            )}
-          </div>
+              </BorderlessButton>
+            }
+          />
         ))}
       </>
     );

+ 1 - 1
desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.test.tsx

@@ -96,7 +96,7 @@ describe('ScheduleTaskModal', () => {
     const input = screen.getByRole('spinbutton');
     fireEvent.change(input, { target: { value: '10' } });
 
-    const submitButton = screen.getByRole('button', { name: /submit/i });
+    const submitButton = screen.getByRole('button', { name: /schedule/i });
 
     fireEvent.click(submitButton);
 

+ 14 - 10
desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.tsx

@@ -74,37 +74,40 @@ const ScheduleTaskModal = ({ onClose, open = true }: ScheduleTaskModalProps): JS
   const errors = [
     {
       enabled: !!error,
-      message: error?.message ?? 'An unknown error occurred.'
+      message: error?.message ?? t('An unknown error occurred.')
     }
   ];
 
   const selectedTaskChildren =
     scheduleTasksCategory.find(task => task.value === selectedTask)?.children ?? [];
 
-  const isSubmitDisabled =
-    loading || !(selectedTask && selectedTaskChildren.every(task => params[task.value] > 0));
+  const isSubmitDisabled = !(
+    selectedTask && selectedTaskChildren.every(task => params[task.value] > 0)
+  );
 
   return (
     <Modal
       title={t('Schedule Task')}
       open={open}
+      className="hue-schedule-task__modal"
+      data-testid="hue-schedule-task__modal"
+      width={530}
+      okText={t('Schedule')}
       onOk={handleSubmit}
+      okButtonProps={{ disabled: isSubmitDisabled, loading }}
+      cancelText={t('Cancel')}
       onCancel={onClose}
-      okText={t('Submit')}
-      width={530}
-      cancellable={false}
-      okButtonProps={{ disabled: isSubmitDisabled }}
       cancelButtonProps={{ disabled: loading }}
-      className="hue-schedule-task__modal"
-      data-testid="hue-schedule-task__modal"
+      closable={!loading}
     >
-      <LoadingErrorWrapper loading={loading} errors={errors}>
+      <LoadingErrorWrapper errors={errors}>
         <div className="hue-schedule-task-selection">
           <Form.Item label={'Task'}>
             <Radio.Group
               options={dropdownOptions}
               value={selectedTask}
               onChange={e => setSelectedTask(e.target.value)}
+              disabled={loading}
             />
           </Form.Item>
         </div>
@@ -119,6 +122,7 @@ const ScheduleTaskModal = ({ onClose, open = true }: ScheduleTaskModalProps): JS
                     placeholder={param.label}
                     onChange={handleChange}
                     value={params[param.value]}
+                    disabled={loading}
                   />
                 </Form.Item>
               </div>

+ 1 - 2
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.scss

@@ -17,10 +17,9 @@
 .hue-task-server-logs {
   max-height: 60vh;
   overflow: auto;
-  margin-right: -16px;
-  padding-right: 16px;
 
   pre {
     padding: 16px;
+    margin: 0;
   }
 }

+ 1 - 8
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.test.tsx

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import React from 'react';
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
 import '@testing-library/jest-dom';
 import TaskLogsModal from './TaskLogsModal';
 import { TaskServerResponse } from '../types';
@@ -43,11 +43,4 @@ describe('TaskLogsModal', () => {
     expect(screen.getByText('Task Logs')).toBeInTheDocument();
     expect(screen.getByText('Sample log data')).toBeInTheDocument();
   });
-
-  it('calls onClose when the close button is clicked', () => {
-    render(<TaskLogsModal taskId={taskId} onClose={mockOnClose} />);
-
-    fireEvent.click(screen.getByText('Close'));
-    expect(mockOnClose).toHaveBeenCalledTimes(1);
-  });
 });

+ 2 - 10
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.tsx

@@ -40,20 +40,12 @@ const TaskLogsModal: React.FC<TaskLogsModalProps> = ({ taskId, onClose }): JSX.E
   const errors = [
     {
       enabled: !!error,
-      message: error?.message ?? 'An error occurred while fetching task logs.'
+      message: error?.message ?? t('An error occurred while fetching task logs.')
     }
   ];
 
   return (
-    <Modal
-      title={t('Task Logs')}
-      open={!!taskId}
-      onOk={onClose}
-      onCancel={onClose}
-      width={830}
-      okText={t('Close')}
-      cancellable={false}
-    >
+    <Modal open={!!taskId} title={t('Task Logs')} width={830} onCancel={onClose} footer={false}>
       <LoadingErrorWrapper loading={loading} errors={errors}>
         <div className="hue-task-server-logs">
           <pre>{data}</pre>

+ 6 - 4
desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.ts

@@ -15,13 +15,14 @@
 // limitations under the License.
 
 import { useCallback, useEffect, useState } from 'react';
+import { AxiosError } from 'axios';
 import { ApiFetchOptions, post } from '../../../api/utils';
 import { isJSON } from '../../jsonUtils';
 
 interface saveOptions<T> {
   url?: string;
   onSuccess?: (data: T) => void;
-  onError?: (error: Error) => void;
+  onError?: (error: AxiosError) => void;
   postOptions?: ApiFetchOptions<T>;
 }
 
@@ -32,7 +33,7 @@ export interface Options<T> extends saveOptions<T> {
 interface UseSaveData<T, U> {
   data?: T;
   loading: boolean;
-  error?: Error;
+  error?: AxiosError;
   save: (body: U, saveOption?: saveOptions<T>) => void;
 }
 
@@ -40,11 +41,12 @@ const useSaveData = <T, U = unknown>(url?: string, options?: Options<T>): UseSav
   const [localOptions, setLocalOptions] = useState<Options<T> | undefined>(options);
   const [data, setData] = useState<T | undefined>();
   const [loading, setLoading] = useState<boolean>(false);
-  const [error, setError] = useState<Error | undefined>();
+  const [error, setError] = useState<AxiosError | undefined>();
 
   const postOptionsDefault: ApiFetchOptions<T> = {
     silenceErrors: false,
-    ignoreSuccessErrors: true
+    ignoreSuccessErrors: true,
+    isRawError: true
   };
 
   const saveData = useCallback(

文件差异内容过多而无法显示
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


+ 9 - 0
desktop/core/src/desktop/static/desktop/less/root-wrapped-antd.less

@@ -57,4 +57,13 @@ body.ant-scrolling-effect {
       }
     }
   }
+
+  // Fix the modal loading button gap
+  .ant-btn-loading-icon {
+    margin-right: 8px;
+  }
+
+  .ant-btn-loading {
+    cursor: not-allowed;
+  }
 }

部分文件因为文件数量过多而无法显示