瀏覽代碼

[ui-sb]: Add Error handling alerts in action modals (#4118)

Ram Prasad Agarwal 6 月之前
父節點
當前提交
e7311e0e68
共有 25 個文件被更改,包括 311 次插入300 次删除
  1. 12 12
      desktop/core/src/desktop/js/api/utils.ts
  2. 10 14
      desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx
  3. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage.tsx
  4. 83 72
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserTab/StorageBrowserTab.tsx
  5. 2 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.tsx
  6. 1 10
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangeOwnerAndGroupModal/ChangeOwnerAndGroupModal.test.tsx
  7. 64 59
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangeOwnerAndGroupModal/ChangeOwnerAndGroupModal.tsx
  8. 7 2
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangePermissionModal/ChangePermissionModal.tsx
  9. 2 6
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/CompressionModal/CompressionModal.tsx
  10. 3 6
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.tsx
  11. 4 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/RenameModal/RenameModal.tsx
  12. 3 2
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ReplicationModal/ReplicationModal.tsx
  13. 17 9
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/SummaryModal/SummaryModal.tsx
  14. 4 4
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx
  15. 2 2
      desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx
  16. 0 23
      desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.scss
  17. 0 2
      desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.test.tsx
  18. 22 15
      desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.tsx
  19. 7 0
      desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.scss
  20. 1 1
      desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.test.tsx
  21. 22 15
      desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.tsx
  22. 3 2
      desktop/core/src/desktop/js/reactComponents/PaginatedTable/PaginatedTable.tsx
  23. 2 0
      desktop/core/src/desktop/js/reactComponents/Pagination/Pagination.scss
  24. 22 24
      desktop/core/src/desktop/js/utils/hooks/useLoadData/useLoadData.ts
  25. 17 15
      desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.ts

+ 12 - 12
desktop/core/src/desktop/js/api/utils.ts

@@ -42,7 +42,7 @@ export interface DefaultApiResponse {
   content?: string;
 }
 
-export interface ApiFetchOptions<T, E = string> extends AxiosRequestConfig {
+export interface ApiFetchOptions<T, E = AxiosError<DefaultApiResponse>> extends AxiosRequestConfig {
   silenceErrors?: boolean;
   ignoreSuccessErrors?: boolean;
   transformResponse?: AxiosResponseTransformer;
@@ -54,9 +54,9 @@ export interface ApiFetchOptions<T, E = string> extends AxiosRequestConfig {
     reject: (err: unknown) => void
   ) => void;
   handleError?: (
-    errorResponse: AxiosError<E>,
+    errorResponse: AxiosError<DefaultApiResponse>,
     resolve: (val: T) => void,
-    reject: (err: unknown) => void
+    reject: (err: E) => void
   ) => void;
 }
 
@@ -105,7 +105,7 @@ export const successResponseIsError = (responseData?: DefaultApiResponse): boole
 const UNKNOWN_ERROR_MESSAGE = 'Unknown error occurred';
 
 export const extractErrorMessage = (
-  errorResponse?: DefaultApiResponse | AxiosError | string
+  errorResponse?: DefaultApiResponse | AxiosError<DefaultApiResponse> | string
 ): string => {
   if (!errorResponse) {
     return UNKNOWN_ERROR_MESSAGE;
@@ -168,11 +168,11 @@ const handleErrorResponse = <T>(
   notifyError(errorMessage, (err && err.response) || err, options);
 };
 
-const handleResponse = <T>(
+const handleResponse = <T, E = unknown>(
   response: AxiosResponse<T & DefaultApiResponse>,
   resolve: (value?: T) => void,
   reject: (reason?: unknown) => void,
-  options?: ApiFetchOptions<T>
+  options?: ApiFetchOptions<T, E>
 ): void => {
   if (options && options.handleSuccess) {
     options.handleSuccess(response.data, resolve, reason => {
@@ -193,10 +193,10 @@ const getCancelToken = (): { cancelToken: CancelToken; cancel: () => void } => {
   return { cancelToken: cancelTokenSource.token, cancel: cancelTokenSource.cancel };
 };
 
-export const post = <T, U = unknown>(
+export const post = <T, U = unknown, E = AxiosError>(
   url: string,
   data?: U,
-  options?: ApiFetchOptions<T>
+  options?: ApiFetchOptions<T, E>
 ): CancellablePromise<T> =>
   new CancellablePromise((resolve, reject, onCancel) => {
     const { cancelToken, cancel } = getCancelToken();
@@ -212,7 +212,7 @@ export const post = <T, U = unknown>(
       .then(response => {
         handleResponse(response, resolve, reject, options);
       })
-      .catch((err: AxiosError) => {
+      .catch((err: AxiosError<DefaultApiResponse>) => {
         if (options && options.handleError) {
           options.handleError(err, resolve, reason => {
             handleErrorResponse(err, reject, options);
@@ -233,10 +233,10 @@ export const post = <T, U = unknown>(
     });
   });
 
-export const get = <T, U = unknown>(
+export const get = <T, U = unknown, E = AxiosError<DefaultApiResponse>>(
   url: string,
   data?: U,
-  options?: ApiFetchOptions<T>
+  options?: ApiFetchOptions<T, E>
 ): CancellablePromise<T> =>
   new CancellablePromise((resolve, reject, onCancel) => {
     const { cancelToken, cancel } = getCancelToken();
@@ -250,7 +250,7 @@ export const get = <T, U = unknown>(
       .then(response => {
         handleResponse(response, resolve, reject, options);
       })
-      .catch((err: AxiosError) => {
+      .catch((err: AxiosError<DefaultApiResponse>) => {
         handleErrorResponse(err, reject, options);
       })
       .finally(() => {

+ 10 - 14
desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx

@@ -15,7 +15,6 @@
 // limitations under the License.
 
 import React, { useState } from 'react';
-import Alert from 'cuix/dist/components/Alert';
 import ServerLogsHeader from './ServerLogsHeader';
 import { i18nReact } from '../../../utils/i18nReact';
 import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
@@ -33,29 +32,26 @@ const ServerLogs: React.FC = (): JSX.Element => {
   const [filter, setFilter] = useState<string>('');
   const [wrapLogs, setWrapLogs] = useState(true);
   const { t } = i18nReact.useTranslation();
-  const { data, loading, error } = useLoadData<ServerLogsData>(SERVER_LOGS_API_URL, {
+  const { data, loading, error, reloadData } = useLoadData<ServerLogsData>(SERVER_LOGS_API_URL, {
     params: {
       reverse: true
     }
   });
 
-  if (error) {
-    return (
-      <div className="hue-server-logs-component">
-        <Alert
-          message={`${t('Error:')} ${error}`}
-          description={t('An error occurred while fetching server logs.')}
-          type="error"
-        />
-      </div>
-    );
-  }
+  const errors = [
+    {
+      enabled: !!error,
+      message: t('An error occurred while fetching server logs.'),
+      actionText: t('Retry'),
+      onClick: reloadData
+    }
+  ];
 
   const isEmptyLogs = !data?.logs || !data?.logs?.some(log => log.length);
 
   return (
     <div className="hue-server-logs-component">
-      <LoadingErrorWrapper loading={loading}>
+      <LoadingErrorWrapper loading={loading} errors={errors}>
         <ServerLogsHeader
           onFilterChange={setFilter}
           onWrapLogsChange={setWrapLogs}

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

@@ -43,7 +43,7 @@ const StorageBrowserPage = (): JSX.Element => {
     {
       enabled: !!error,
       message: t('An error occurred while fetching the filesystem'),
-      action: t('Retry'),
+      actionText: t('Retry'),
       onClick: reloadData
     }
   ];

+ 83 - 72
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserTab/StorageBrowserTab.tsx

@@ -16,13 +16,13 @@
 
 import React, { useEffect, useState } from 'react';
 import { Alert } from 'antd';
+import { AxiosError } from 'axios';
 import BucketIcon from '@cloudera/cuix-core/icons/react/BucketIcon';
 import RefreshIcon from '@cloudera/cuix-core/icons/react/RefreshIcon';
 import HomeIcon from '@cloudera/cuix-core/icons/react/HomeIcon';
 import DeleteIcon from '@cloudera/cuix-core/icons/react/DeleteIcon';
 
 import { i18nReact } from '../../../utils/i18nReact';
-
 import PathBrowser from '../../../reactComponents/PathBrowser/PathBrowser';
 import StorageDirectoryPage from '../StorageDirectoryPage/StorageDirectoryPage';
 import { FILE_STATS_API_URL, TRASH_PATH } from '../api';
@@ -84,7 +84,8 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
     loading,
     error,
     reloadData
-  } = useLoadData<FileStats>(FILE_STATS_API_URL, {
+  } = useLoadData<FileStats, { path: string }, AxiosError>(FILE_STATS_API_URL, {
+    fetchOptions: { isRawError: true },
     params: { path: filePath },
     skip: !filePath
   });
@@ -101,88 +102,98 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
     }
   }, [filePath, urlPathname, urlFilePath, window.location]);
 
-  const errorConfig = [
+  const errors = [
+    {
+      enabled: error?.response?.status === 404 && filePath !== fileSystem.userHomeDirectory,
+      message: t('Path "{{path}}" does not exist.', { path: filePath })
+    },
+    {
+      enabled: error?.response?.status === 404 && filePath === fileSystem.userHomeDirectory,
+      message: t('User home directory "{{path}}" does not exist.', {
+        path: filePath
+      })
+    },
     {
-      enabled: error?.response?.status === 404,
-      message: t('Error: Path "{{path}}" not found.', { path: filePath }),
-      action: t('Go to home directory'),
-      onClick: () => setFilePath(fileSystem.userHomeDirectory)
+      enabled: error?.response?.status === 403,
+      message: t('Unauthorized access to the path "{{path}}".', {
+        path: filePath
+      })
     },
     {
-      enabled: !!error && error?.response?.status !== 404,
-      message: t('An error occurred while fetching filesystem "{{fileSystem}}".', {
-        fileSystem: fileSystem.name.toUpperCase()
+      enabled: !!error && error?.response?.status !== 404 && error?.response?.status !== 403,
+      message: t('An unkown error occurred while fetching path "{{path}}".', {
+        path: filePath
       }),
-      action: t('Retry'),
+      actionText: t('Retry'),
       onClick: reloadData
     }
   ];
 
   return (
-    <LoadingErrorWrapper loading={loading} errors={errorConfig}>
-      <div className="hue-storage-browser-tab" data-testid={testId}>
-        <div
-          className="hue-storage-browser-tab__title-bar-container"
-          data-testid={`${testId}-title-bar`}
-        >
-          <div className="hue-storage-browser-tab__title-bar">
-            <BucketIcon
-              className="hue-storage-browser-tab__title-bar-icon"
-              data-testid={`${testId}-icon`}
-            />
-            <h3
-              className="hue-storage-browser-tab__title-bar-name"
-              data-testid={`${testId}-folder-namer`}
-            >
-              {fileName}
-            </h3>
-          </div>
-          <div className="hue-storage-browser-tab__title-bar-button-group">
-            <BorderlessButton
-              onClick={() => {
-                setFilePath(fileSystem.userHomeDirectory);
-              }}
-              className="hue-storage-browser-tab__title-bar-button"
-              title={t('Home')}
-              icon={<HomeIcon />}
-            >
-              {t('Home')}
-            </BorderlessButton>
-            {fileSystem.config?.isTrashEnabled && (
-              <BorderlessButton
-                onClick={onTrashClick}
-                className="hue-storage-browser-tab__title-bar-button"
-                title={t('Trash')}
-                icon={<DeleteIcon />}
-                disabled={!trashData?.trashPath}
-              >
-                {t('Trash')}
-              </BorderlessButton>
-            )}
+    <div className="hue-storage-browser-tab" data-testid={testId}>
+      <div
+        className="hue-storage-browser-tab__title-bar-container"
+        data-testid={`${testId}-title-bar`}
+      >
+        <div className="hue-storage-browser-tab__title-bar">
+          <BucketIcon
+            className="hue-storage-browser-tab__title-bar-icon"
+            data-testid={`${testId}-icon`}
+          />
+          <h3
+            className="hue-storage-browser-tab__title-bar-name"
+            data-testid={`${testId}-folder-namer`}
+          >
+            {fileName}
+          </h3>
+        </div>
+        <div className="hue-storage-browser-tab__title-bar-button-group">
+          <BorderlessButton
+            onClick={() => {
+              setFilePath(fileSystem.userHomeDirectory);
+            }}
+            className="hue-storage-browser-tab__title-bar-button"
+            title={t('Home')}
+            icon={<HomeIcon />}
+          >
+            {t('Home')}
+          </BorderlessButton>
+          {fileSystem.config?.isTrashEnabled && (
             <BorderlessButton
-              onClick={reloadData}
+              onClick={onTrashClick}
               className="hue-storage-browser-tab__title-bar-button"
-              title={t('Refresh')}
-              icon={<RefreshIcon />}
+              title={t('Trash')}
+              icon={<DeleteIcon />}
+              disabled={!trashData?.trashPath}
             >
-              {t('Refresh')}
+              {t('Trash')}
             </BorderlessButton>
-          </div>
-        </div>
-        {!!inTrash(filePath) && fileSystem.name === 'hdfs' && (
-          <Alert
-            type="warning"
-            message={t(
-              'This is Hadoop trash. Files will be under a checkpoint, or timestamp named, directory.'
-            )}
-          />
-        )}
-        <div
-          className="hue-storage-browser-tab__path-browser-container"
-          data-testid={`${testId}-path-browser-panel`}
-        >
-          <PathBrowser filePath={filePath} onFilepathChange={setFilePath} />
+          )}
+          <BorderlessButton
+            onClick={reloadData}
+            className="hue-storage-browser-tab__title-bar-button"
+            title={t('Refresh')}
+            icon={<RefreshIcon />}
+          >
+            {t('Refresh')}
+          </BorderlessButton>
         </div>
+      </div>
+      {!!inTrash(filePath) && fileSystem.name === 'hdfs' && (
+        <Alert
+          type="warning"
+          message={t(
+            'This is Hadoop trash. Files will be under a checkpoint, or timestamp named, directory.'
+          )}
+        />
+      )}
+      <div
+        className="hue-storage-browser-tab__path-browser-container"
+        data-testid={`${testId}-path-browser-panel`}
+      >
+        <PathBrowser filePath={filePath} onFilepathChange={setFilePath} />
+      </div>
+      <LoadingErrorWrapper loading={loading} errors={errors}>
         {fileStats?.type === BrowserViewType.dir && !loading && (
           <StorageDirectoryPage
             fileStats={fileStats}
@@ -194,8 +205,8 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
         {fileStats?.type === BrowserViewType.file && !loading && (
           <StorageFilePage fileStats={fileStats} onReload={reloadData} />
         )}
-      </div>
-    </LoadingErrorWrapper>
+      </LoadingErrorWrapper>
+    </div>
   );
 };
 

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

@@ -70,7 +70,7 @@ const CreateAndUploadAction = ({
     onActionSuccess();
   };
 
-  const { save, loading } = useSaveData(undefined, {
+  const { save, loading, error } = useSaveData(undefined, {
     postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     onSuccess: onApiSuccess,
     onError: onActionError
@@ -153,6 +153,7 @@ const CreateAndUploadAction = ({
           onSubmit={handleCreate}
           onClose={onModalClose}
           loading={loading}
+          error={error}
         />
       )}
       <Modal

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

@@ -65,14 +65,13 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
       />
     );
 
-    expect(getByText('Change Onwer / Group')).toBeInTheDocument();
+    expect(getByText('Change Owner / Group')).toBeInTheDocument();
     expect(getByText('Submit')).toBeInTheDocument();
     expect(getByText('Cancel')).toBeInTheDocument();
     expect(getByText('User')).toBeInTheDocument();
@@ -94,7 +93,6 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -123,7 +121,6 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -152,7 +149,6 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -176,7 +172,6 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -204,7 +199,6 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -231,7 +225,6 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -254,7 +247,6 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}
@@ -274,7 +266,6 @@ describe('ChangeOwnerAndGroupModal Component', () => {
         users={users}
         groups={groups}
         files={mockFiles}
-        setLoading={jest.fn()}
         onSuccess={mockOnSuccess}
         onError={mockOnError}
         onClose={mockOnClose}

+ 64 - 59
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 LoadingErrorWrapper from '../../../../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 import './ChangeOwnerAndGroupModal.scss';
 
@@ -65,7 +66,7 @@ const ChangeOwnerAndGroupModal = ({
   const [groupOther, setGroupOther] = useState<string>();
   const [isRecursive, setIsRecursive] = useState<boolean>(false);
 
-  const { save, loading } = useSaveData(BULK_CHANGE_OWNER_API_URL, {
+  const { save, loading, error } = useSaveData(BULK_CHANGE_OWNER_API_URL, {
     skip: !files.length,
     onSuccess,
     onError
@@ -119,10 +120,12 @@ const ChangeOwnerAndGroupModal = ({
     );
   }, [selectedUser, selectedGroup, userOther, groupOther]);
 
+  const errors = [{ enabled: !!error, message: error }];
+
   return (
     <Modal
       open={isOpen}
-      title={t('Change Onwer / Group')}
+      title={t('Change Owner / Group')}
       className="cuix antd"
       okText={t('Submit')}
       onOk={handleChangeOwner}
@@ -132,73 +135,75 @@ const ChangeOwnerAndGroupModal = ({
       cancelButtonProps={{ disabled: loading }}
       closable={!loading}
     >
-      <div className="hue-change-owner-group">
-        <span className="hue-change-owner-group__header-note">
-          {t(
-            'Note: Only the Hadoop superuser, "{{superuser}}" or the HDFS supergroup, "{{supergroup}}" on this file system, may change the owner of a file.',
-            {
-              superuser: superUser,
-              supergroup: superGroup
-            }
-          )}
-        </span>
-
-        <div className="hue-change-owner-group__form">
-          <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}
-                disabled={loading}
-                getPopupContainer={triggerNode => triggerNode.parentElement}
-              />
-              {selectedUser === OTHERS_KEY && (
-                <Input
-                  placeholder={t('Enter user')}
-                  value={userOther}
-                  onChange={e => setUserOther(e.target.value)}
+      <LoadingErrorWrapper errors={errors}>
+        <div className="hue-change-owner-group">
+          <span className="hue-change-owner-group__header-note">
+            {t(
+              'Note: Only the Hadoop superuser, "{{superuser}}" or the HDFS supergroup, "{{supergroup}}" on this file system, may change the owner of a file.',
+              {
+                superuser: superUser,
+                supergroup: superGroup
+              }
+            )}
+          </span>
+
+          <div className="hue-change-owner-group__form">
+            <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}
                   disabled={loading}
-                  required
+                  getPopupContainer={triggerNode => triggerNode.parentElement}
                 />
-              )}
+                {selectedUser === OTHERS_KEY && (
+                  <Input
+                    placeholder={t('Enter user')}
+                    value={userOther}
+                    onChange={e => setUserOther(e.target.value)}
+                    disabled={loading}
+                    required
+                  />
+                )}
+              </div>
             </div>
-          </div>
 
-          <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}
-                disabled={loading}
-                getPopupContainer={triggerNode => triggerNode.parentElement}
-              />
-              {selectedGroup === OTHERS_KEY && (
-                <Input
-                  placeholder={t('Enter group')}
-                  value={groupOther}
-                  onChange={e => setGroupOther(e.target.value)}
+            <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}
                   disabled={loading}
-                  required
+                  getPopupContainer={triggerNode => triggerNode.parentElement}
                 />
-              )}
+                {selectedGroup === OTHERS_KEY && (
+                  <Input
+                    placeholder={t('Enter group')}
+                    value={groupOther}
+                    onChange={e => setGroupOther(e.target.value)}
+                    disabled={loading}
+                    required
+                  />
+                )}
+              </div>
             </div>
-          </div>
 
-          <div className="hue-change-owner-group__checkbox">
-            <span className="hue-change-owner-group__label">{t('Recursive')}</span>
-            <Checkbox
-              checked={isRecursive}
-              onChange={() => setIsRecursive(prev => !prev)}
-              disabled={loading}
-              name="recursive"
-            />
+            <div className="hue-change-owner-group__checkbox">
+              <span className="hue-change-owner-group__label">{t('Recursive')}</span>
+              <Checkbox
+                checked={isRecursive}
+                onChange={() => setIsRecursive(prev => !prev)}
+                disabled={loading}
+                name="recursive"
+              />
+            </div>
           </div>
         </div>
-      </div>
+      </LoadingErrorWrapper>
     </Modal>
   );
 };

+ 7 - 2
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangePermissionModal/ChangePermissionModal.tsx

@@ -25,6 +25,7 @@ import PaginatedTable, {
 import { StorageDirectoryTableData } from '../../../../types';
 import { BULK_CHANGE_PERMISSION_API_URL } from '../../../../api';
 import { getInitialPermissions, Permission } from './ChangePermissionModal.util';
+import LoadingErrorWrapper from '../../../../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 import './ChangePermissionModal.scss';
 
@@ -48,7 +49,7 @@ const ChangePermissionModal = ({
   const initialPermissions = getInitialPermissions(files);
   const [permissions, setPermissions] = useState<Permission[]>(initialPermissions);
 
-  const { save, loading } = useSaveData(BULK_CHANGE_PERMISSION_API_URL, {
+  const { save, loading, error } = useSaveData(BULK_CHANGE_PERMISSION_API_URL, {
     skip: !files.length,
     onSuccess,
     onError
@@ -131,6 +132,8 @@ const ChangePermissionModal = ({
     }
   ];
 
+  const errors = [{ enabled: !!error, message: error }];
+
   return (
     <Modal
       cancelText={t('Cancel')}
@@ -147,7 +150,9 @@ const ChangePermissionModal = ({
       cancelButtonProps={{ disabled: loading }}
       closable={!loading}
     >
-      <PaginatedTable<Permission> data={permissions} columns={columns} rowKey="key" />
+      <LoadingErrorWrapper errors={errors}>
+        <PaginatedTable<Permission> data={permissions} columns={columns} rowKey="key" />
+      </LoadingErrorWrapper>
     </Modal>
   );
 };

+ 2 - 6
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/CompressionModal/CompressionModal.tsx

@@ -51,8 +51,6 @@ const CompressionModal = ({
     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
@@ -71,10 +69,8 @@ const CompressionModal = ({
 
   const errors = [
     {
-      enabled: error?.response?.status === 500,
-      message: t('An error occurred during compression. Please check configuration.'),
-      action: t('Retry'),
-      onClick: handleCompress
+      enabled: !!error,
+      message: error
     }
   ];
 

+ 3 - 6
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.tsx

@@ -43,8 +43,7 @@ const ExtractionModal = ({
 
   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 },
+    postOptions: { qsEncodeData: true },
     skip: !file,
     onSuccess,
     onError
@@ -59,10 +58,8 @@ const ExtractionModal = ({
 
   const errors = [
     {
-      enabled: error?.response?.status === 500,
-      message: t('An error occurred during extraction. Please check configuration.'),
-      action: t('Retry'),
-      onClick: handleExtract
+      enabled: !!error,
+      message: error
     }
   ];
 

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

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import React from 'react';
-import { StorageDirectoryTableData } from '../../../../types';
+import { BrowserViewType, StorageDirectoryTableData } from '../../../../types';
 import { i18nReact } from '../../../../../../utils/i18nReact';
 import useSaveData from '../../../../../../utils/hooks/useSaveData/useSaveData';
 import { RENAME_API_URL } from '../../../../api';
@@ -38,7 +38,7 @@ const RenameModal = ({
 }: RenameModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { save, loading } = useSaveData(RENAME_API_URL, {
+  const { save, loading, error } = useSaveData(RENAME_API_URL, {
     postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     skip: !file.path,
     onSuccess,
@@ -52,7 +52,7 @@ const RenameModal = ({
   return (
     <InputModal
       title={t('Rename')}
-      inputLabel={t('Enter new name')}
+      inputLabel={file.type === BrowserViewType.dir ? t('Folder name') : t('File name')}
       submitText={t('Rename')}
       showModal={isOpen}
       onSubmit={handleRename}
@@ -60,6 +60,7 @@ const RenameModal = ({
       inputType="text"
       initialValue={file.name}
       loading={loading}
+      error={error}
     />
   );
 };

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

@@ -38,7 +38,7 @@ const ReplicationModal = ({
 }: ReplicationModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { save, loading } = useSaveData(SET_REPLICATION_API_URL, {
+  const { save, loading, error } = useSaveData(SET_REPLICATION_API_URL, {
     postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     skip: !file.path,
     onSuccess,
@@ -52,7 +52,7 @@ const ReplicationModal = ({
   return (
     <InputModal
       title={t('Setting Replication factor for: ') + file.path}
-      inputLabel={t('Replication factor:')}
+      inputLabel={t('Replication factor')}
       submitText={t('Submit')}
       showModal={isOpen}
       onSubmit={handleReplication}
@@ -60,6 +60,7 @@ const ReplicationModal = ({
       inputType="number"
       initialValue={file.replication}
       loading={loading}
+      error={error}
     />
   );
 };

+ 17 - 9
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/SummaryModal/SummaryModal.tsx

@@ -17,7 +17,6 @@
 import React from 'react';
 import Modal from 'cuix/dist/components/Modal';
 
-import huePubSub from '../../../../../../utils/huePubSub';
 import { i18nReact } from '../../../../../../utils/i18nReact';
 import formatBytes from '../../../../../../utils/formatBytes';
 import useLoadData from '../../../../../../utils/hooks/useLoadData/useLoadData';
@@ -36,13 +35,13 @@ interface SummaryModalProps {
 const SummaryModal = ({ isOpen = true, onClose, path }: SummaryModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { data, loading } = useLoadData<ContentSummary>(CONTENT_SUMMARY_API_URL, {
-    params: { path: path },
-    onError: error => {
-      huePubSub.publish('hue.error', error);
-    },
-    skip: path === '' || path === undefined
-  });
+  const { data, loading, error, reloadData } = useLoadData<ContentSummary>(
+    CONTENT_SUMMARY_API_URL,
+    {
+      params: { path: path },
+      skip: path === '' || path === undefined
+    }
+  );
 
   const summary = [
     {
@@ -75,6 +74,15 @@ const SummaryModal = ({ isOpen = true, onClose, path }: SummaryModalProps): JSX.
     { key: 'numberOfFiles', label: t('Number of Files'), value: data?.fileCount }
   ];
 
+  const errors = [
+    {
+      enabled: !!error,
+      message: error,
+      actionText: t('Retry'),
+      onClick: reloadData
+    }
+  ];
+
   const shortendPath =
     path.split('/').length > 4 ? '...' + path.split('/').slice(-4).join('/') : path;
 
@@ -86,7 +94,7 @@ const SummaryModal = ({ isOpen = true, onClose, path }: SummaryModalProps): JSX.
       onCancel={onClose}
       footer={false}
     >
-      <LoadingErrorWrapper loading={loading}>
+      <LoadingErrorWrapper loading={loading} errors={errors} hideOnError>
         <div className="hue-summary-modal__grid">
           {summary?.map(item => (
             <div key={item.key} className="hue-summary-modal__grid__summary-item">

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

@@ -158,7 +158,7 @@ const StorageDirectoryPage = ({
     {
       enabled: !!listDirectoryError,
       message: t('An error occurred while fetching the data'),
-      action: t('Retry'),
+      actionText: t('Retry'),
       onClick: reloadFilesData
     }
   ];
@@ -168,7 +168,7 @@ const StorageDirectoryPage = ({
       title: t('Name'),
       dataIndex: 'name',
       key: 'name',
-      sorter: true,
+      sorter: (filesData?.files?.length ?? 0) > 1,
       width: '40%',
       render: (_, record) => (
         <Tooltip title={record.name} mouseEnterDelay={1.5}>
@@ -190,7 +190,7 @@ const StorageDirectoryPage = ({
       dataIndex: 'size',
       key: 'size',
       width: '10%',
-      sorter: true
+      sorter: (filesData?.files?.length ?? 0) > 1
     },
     {
       title: t('User'),
@@ -215,7 +215,7 @@ const StorageDirectoryPage = ({
       dataIndex: 'mtime',
       key: 'mtime',
       width: '20%',
-      sorter: true
+      sorter: (filesData?.files?.length ?? 0) > 1
     }
   ];
 

+ 2 - 2
desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx

@@ -129,7 +129,7 @@ const StorageFilePage = ({ fileStats, onReload }: StorageFilePageProps): JSX.Ele
       message: t('An error occurred while fetching file content for path "{{path}}".', {
         path: fileStats.path
       }),
-      action: t('Retry'),
+      actionText: t('Retry'),
       onClick: onReload
     }
   ];
@@ -150,7 +150,7 @@ const StorageFilePage = ({ fileStats, onReload }: StorageFilePageProps): JSX.Ele
           ))}
         </div>
 
-        <LoadingErrorWrapper loading={loading || isSaving} errors={errorConfig} hideChildren>
+        <LoadingErrorWrapper loading={loading || isSaving} errors={errorConfig} hideOnLoading>
           <div className="preview">
             <div className="preview__title-bar">
               {t('Content')}

+ 0 - 23
desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.scss

@@ -1,23 +0,0 @@
-// 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.
-@use 'variables' as vars;
-@use 'mixins';
-
-.antd.cuix {
-  .hue-input-modal__input-label {
-    color: vars.$fluidx-gray-700;
-  }
-}

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

@@ -117,7 +117,6 @@ describe('InputModal', () => {
       />
     );
     const input = screen.getByRole('textbox');
-    expect(input).toHaveClass('hue-input-modal__input');
     expect(input).toBeVisible();
   });
 
@@ -135,7 +134,6 @@ describe('InputModal', () => {
       />
     );
     const input = screen.getByRole('spinbutton');
-    expect(input).toHaveClass('hue-input-modal__input');
     expect(input).toBeVisible();
   });
 

+ 22 - 15
desktop/core/src/desktop/js/reactComponents/InputModal/InputModal.tsx

@@ -16,11 +16,11 @@
 
 import React, { useEffect, useRef, useState } from 'react';
 import Modal from 'cuix/dist/components/Modal';
-import { Input, InputRef } from 'antd';
+import { Input, InputRef, Form } from 'antd';
 
 import { i18nReact } from '../../utils/i18nReact';
 
-import './InputModal.scss';
+import LoadingErrorWrapper from '../LoadingErrorWrapper/LoadingErrorWrapper';
 
 interface InputModalProps {
   cancelText?: string;
@@ -33,6 +33,7 @@ interface InputModalProps {
   showModal: boolean;
   title: string;
   loading?: boolean;
+  error?: string;
 }
 
 const InputModal = ({
@@ -43,6 +44,7 @@ const InputModal = ({
   onSubmit,
   showModal,
   title,
+  error,
   loading = false,
   ...i18n
 }: InputModalProps): JSX.Element => {
@@ -60,11 +62,12 @@ const InputModal = ({
     inputRef?.current?.focus();
   }, [inputRef.current]);
 
+  const errors = [{ enabled: !!error, message: error }];
+
   return (
     <Modal
       open={showModal}
       title={title}
-      className="hue-input-modal cuix antd"
       okText={submitText}
       onOk={handleSubmit}
       okButtonProps={{
@@ -76,18 +79,22 @@ const InputModal = ({
       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={loading}
-        onPressEnter={handleSubmit}
-        ref={inputRef}
-        onChange={e => {
-          setValue(e.target.value);
-        }}
-      />
+      <LoadingErrorWrapper errors={errors}>
+        <Form layout="vertical">
+          <Form.Item label={inputLabel}>
+            <Input
+              defaultValue={value}
+              type={inputType}
+              disabled={loading}
+              onPressEnter={handleSubmit}
+              ref={inputRef}
+              onChange={e => {
+                setValue(e.target.value);
+              }}
+            />
+          </Form.Item>
+        </Form>
+      </LoadingErrorWrapper>
     </Modal>
   );
 };

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

@@ -19,4 +19,11 @@
     // this overrides the max-height: 400px in antd library
     max-height: 100% !important;
   }
+
+  .loading-error-wrapper__errors {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    margin: 8px 0;
+  }
 }

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

@@ -42,7 +42,7 @@ describe('LoadingErrorWrapper', () => {
   it('should render action button for errors with onClick', () => {
     const mockOnClick = jest.fn();
     const errors = [
-      { enabled: true, message: 'Error with action', action: 'Retry', onClick: mockOnClick }
+      { enabled: true, message: 'Error with action', actionText: 'Retry', onClick: mockOnClick }
     ];
 
     const { getByText } = render(

+ 22 - 15
desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.tsx

@@ -25,7 +25,7 @@ interface WrapperError {
   enabled: boolean;
   message: AlertProps['message'];
   description?: AlertProps['description'];
-  action?: string;
+  actionText?: string;
   onClick?: AlertProps['onClick'];
   closable?: AlertProps['closable'];
 }
@@ -34,14 +34,16 @@ interface LoadingErrorWrapperProps {
   loading?: boolean;
   errors?: WrapperError[];
   children: React.ReactNode;
-  hideChildren?: boolean;
+  hideOnLoading?: boolean;
+  hideOnError?: boolean;
 }
 
 const LoadingErrorWrapper = ({
   loading = false,
   errors = [],
   children,
-  hideChildren = false
+  hideOnLoading = false,
+  hideOnError = false
 }: LoadingErrorWrapperProps): JSX.Element => {
   if (loading) {
     return (
@@ -51,7 +53,7 @@ const LoadingErrorWrapper = ({
         data-testid="loading-error-wrapper__spinner"
         className="loading-error-wrapper__spinner"
       >
-        {hideChildren === false && children}
+        {hideOnLoading === false && children}
       </Spin>
     );
   }
@@ -60,17 +62,22 @@ const LoadingErrorWrapper = ({
   if (enabledErrors.length > 0) {
     return (
       <>
-        {enabledErrors.map(error => (
-          <Alert
-            key={error.message?.toString()}
-            type="error"
-            message={error.message}
-            description={error.description}
-            closable={error.closable}
-            onClick={error.onClick}
-            action={<BorderlessButton onClick={error.onClick}>{error.action}</BorderlessButton>}
-          />
-        ))}
+        <div className="loading-error-wrapper__errors">
+          {enabledErrors.map(error => (
+            <Alert
+              showIcon
+              key={error.message?.toString()}
+              type="error"
+              message={error.message}
+              description={error.description}
+              closable={error.closable}
+              action={
+                <BorderlessButton onClick={error.onClick}>{error.actionText}</BorderlessButton>
+              }
+            />
+          ))}
+        </div>
+        {hideOnError === false && children}
       </>
     );
   }

+ 3 - 2
desktop/core/src/desktop/js/reactComponents/PaginatedTable/PaginatedTable.tsx

@@ -50,7 +50,7 @@ export interface PaginatedTableProps<T> {
 }
 
 const TABLE_HEADER_HEIGHT = 47;
-const PAGINATION_HEIGHT = 49;
+const PAGINATION_HEIGHT = 50;
 
 const PaginatedTable = <T extends object>({
   loading = false,
@@ -101,7 +101,8 @@ const PaginatedTable = <T extends object>({
   const getColumnsFromConfig = (columnsConfig: ColumnProps<T>[]) => {
     return columnsConfig.map(col => ({
       ...col,
-      defaultSortOrder: sortByColumn === col.dataIndex ? sortOrder : undefined
+      defaultSortOrder: sortByColumn === col.dataIndex ? sortOrder : undefined,
+      showSorterTooltip: false
     }));
   };
 

+ 2 - 0
desktop/core/src/desktop/js/reactComponents/Pagination/Pagination.scss

@@ -13,6 +13,7 @@
 // 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.
+
 @use 'variables' as vars;
 @use 'mixins';
 
@@ -25,6 +26,7 @@
     justify-content: flex-end;
     color: vars.$fluidx-gray-700;
     line-height: 32px;
+    max-height: 50px;
   }
 
   .hue-pagination__page-size-control {

+ 22 - 24
desktop/core/src/desktop/js/utils/hooks/useLoadData/useLoadData.ts

@@ -14,48 +14,41 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import { ApiFetchOptions, get } from '../../../api/utils';
-import { AxiosError } from 'axios';
 import { convertKeysToCamelCase } from '../../../utils/string/changeCasing';
 
-export interface Options<T, U> {
+export interface Options<T, U, E> {
   params?: U;
-  fetchOptions?: ApiFetchOptions<T>;
+  fetchOptions?: ApiFetchOptions<T, E>;
   skip?: boolean;
   onSuccess?: (data: T) => void;
-  onError?: (error: AxiosError) => void;
+  onError?: (error: E) => void;
   pollInterval?: number;
   transformKeys?: 'camelCase' | 'none';
 }
 
-interface UseLoadDataProps<T> {
+interface UseLoadDataProps<T, E> {
   data?: T;
   loading: boolean;
-  error?: AxiosError;
+  error?: E;
   reloadData: () => Promise<T | undefined>;
 }
 
-const useLoadData = <T, U = unknown>(
+const useLoadData = <T, U = unknown, E = string>(
   url?: string,
-  options?: Options<T, U>
-): UseLoadDataProps<T> => {
-  const [localOptions, setLocalOptions] = useState<Options<T, U> | undefined>(options);
+  options?: Options<T, U, E>
+): UseLoadDataProps<T, E> => {
+  const [localOptions, setLocalOptions] = useState<Options<T, U, E> | undefined>(options);
   const [data, setData] = useState<T>();
   const [loading, setLoading] = useState<boolean>(false);
-  const [error, setError] = useState<AxiosError>();
+  const [error, setError] = useState<E>();
 
-  const fetchOptionsDefault: ApiFetchOptions<T> = {
+  const fetchOptionsDefault: ApiFetchOptions<T, E> = {
     silenceErrors: true,
-    ignoreSuccessErrors: true,
-    isRawError: true
+    ignoreSuccessErrors: true
   };
 
-  const fetchOptions = useMemo(
-    () => ({ ...fetchOptionsDefault, ...localOptions?.fetchOptions }),
-    [localOptions]
-  );
-
   const transformResponse = (data: T): T => {
     if (options?.transformKeys === 'none') {
       return data;
@@ -78,8 +71,13 @@ const useLoadData = <T, U = unknown>(
       setLoading(true);
       setError(undefined);
 
+      const fetchOptions = {
+        ...fetchOptionsDefault,
+        ...localOptions?.fetchOptions
+      };
+
       try {
-        const response = await get<T, U>(url, localOptions?.params, fetchOptions);
+        const response = await get<T, U, E>(url, localOptions?.params, fetchOptions);
         const transformedResponse = transformResponse(response);
         setData(transformedResponse);
         if (localOptions?.onSuccess) {
@@ -87,15 +85,15 @@ const useLoadData = <T, U = unknown>(
         }
         return transformedResponse;
       } catch (error) {
-        setError(error as AxiosError);
+        setError(error as E);
         if (localOptions?.onError) {
-          localOptions.onError(error as AxiosError);
+          localOptions.onError(error as E);
         }
       } finally {
         setLoading(false);
       }
     },
-    [url, localOptions, fetchOptions]
+    [url, localOptions]
   );
 
   useEffect(() => {

+ 17 - 15
desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.ts

@@ -19,38 +19,40 @@ import { AxiosError } from 'axios';
 import { ApiFetchOptions, post } from '../../../api/utils';
 import { isJSON } from '../../jsonUtils';
 
-interface saveOptions<T> {
+interface saveOptions<T, E> {
   url?: string;
   onSuccess?: (data: T) => void;
   onError?: (error: AxiosError) => void;
-  postOptions?: ApiFetchOptions<T>;
+  postOptions?: ApiFetchOptions<T, E>;
 }
 
-export interface Options<T> extends saveOptions<T> {
+export interface Options<T, E> extends saveOptions<T, E> {
   skip?: boolean;
 }
 
-interface UseSaveData<T, U> {
+interface UseSaveData<T, U, E> {
   data?: T;
   loading: boolean;
-  error?: AxiosError;
-  save: (body: U, saveOption?: saveOptions<T>) => void;
+  error?: E;
+  save: (body: U, saveOption?: saveOptions<T, E>) => void;
 }
 
-const useSaveData = <T, U = unknown>(url?: string, options?: Options<T>): UseSaveData<T, U> => {
-  const [localOptions, setLocalOptions] = useState<Options<T> | undefined>(options);
+const useSaveData = <T, U = unknown, E = string>(
+  url?: string,
+  options?: Options<T, E>
+): UseSaveData<T, U, E> => {
+  const [localOptions, setLocalOptions] = useState<Options<T, E> | undefined>(options);
   const [data, setData] = useState<T | undefined>();
   const [loading, setLoading] = useState<boolean>(false);
-  const [error, setError] = useState<AxiosError | undefined>();
+  const [error, setError] = useState<E | undefined>();
 
-  const postOptionsDefault: ApiFetchOptions<T> = {
-    silenceErrors: false,
-    ignoreSuccessErrors: true,
-    isRawError: true
+  const postOptionsDefault: ApiFetchOptions<T, E> = {
+    silenceErrors: true,
+    ignoreSuccessErrors: true
   };
 
   const saveData = useCallback(
-    async (body: U, saveOptions?: saveOptions<T>) => {
+    async (body: U, saveOptions?: saveOptions<T, E>) => {
       // Avoid Posting data if the skip option is true
       // or if the URL is not provided
       const apiUrl = saveOptions?.url ?? url;
@@ -68,7 +70,7 @@ const useSaveData = <T, U = unknown>(url?: string, options?: Options<T>): UseSav
       };
 
       try {
-        const response = await post<T, U>(apiUrl, body, postOptions);
+        const response = await post<T, U, E>(apiUrl, body, postOptions);
         setData(response);
         if (saveOptions?.onSuccess) {
           saveOptions.onSuccess(response);