Просмотр исходного кода

[ui-storageBrowser] Implement UI for rename action (#3764)

* [ui-storageBrowser] Implement UI for rename action
Nidhi Bhat G 1 год назад
Родитель
Сommit
78d41a495a

+ 132 - 46
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserActions/StorageBrowserActions.test.tsx

@@ -14,12 +14,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 import React from 'react';
-import { render } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import '@testing-library/jest-dom';
 
 import StorageBrowserActions from './StorageBrowserActions';
-import { StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types';
+import {
+  StorageBrowserTableData,
+  ContentSummary
+} from '../../../../reactComponents/FileChooser/types';
+import * as StorageBrowserApi from '../../../../reactComponents/FileChooser/api';
+import { CancellablePromise } from '../../../../api/cancellablePromise';
 
 describe('StorageBrowserRowActions', () => {
   //View summary option is enabled and added to the actions menu when the row data is either hdfs/ofs and a single file
@@ -33,7 +38,7 @@ describe('StorageBrowserRowActions', () => {
     type: '',
     path: ''
   };
-  const mockRecord2: StorageBrowserTableData[] = [
+  const mockTwoRecords: StorageBrowserTableData[] = [
     {
       name: 'test',
       size: '0\u00a0bytes',
@@ -56,56 +61,137 @@ describe('StorageBrowserRowActions', () => {
     }
   ];
 
-  test('does not render view summary option when there are multiple records', async () => {
-    const user = userEvent.setup();
-    const { getByRole, queryByRole } = render(
-      <StorageBrowserActions selectedFiles={mockRecord2} />
-    );
-    await user.click(getByRole('button'));
-    expect(queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
-  });
+  const setLoadingFiles = jest.fn();
+  const onSuccessfulAction = jest.fn();
 
-  test('renders view summary option when record is a hdfs file', async () => {
+  const setUpActionMenu = async (
+    records: StorageBrowserTableData[],
+    recordPath?: string,
+    recordType?: string
+  ) => {
     const user = userEvent.setup();
-    mockRecord.path = '/user/demo/test';
-    mockRecord.type = 'file';
-    const { getByRole, queryByRole } = render(
-      <StorageBrowserActions selectedFiles={[mockRecord]} />
+    if (recordPath) {
+      records[0].path = recordPath;
+    }
+    if (recordType) {
+      records[0].type = recordType;
+    }
+    const { getByRole } = render(
+      <StorageBrowserActions
+        setLoadingFiles={setLoadingFiles}
+        onSuccessfulAction={onSuccessfulAction}
+        selectedFiles={records}
+      />
     );
     await user.click(getByRole('button'));
-    expect(queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
-  });
+  };
 
-  test('renders view summary option when record is a ofs file', async () => {
-    const user = userEvent.setup();
-    mockRecord.path = 'ofs://demo/test';
-    mockRecord.type = 'file';
-    const { getByRole, queryByRole } = render(
-      <StorageBrowserActions selectedFiles={[mockRecord]} />
-    );
-    await user.click(getByRole('button'));
-    expect(queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
-  });
+  describe('Summary option', () => {
+    let summaryApiMock;
 
-  test('does not render view summary option when record is a hdfs folder', async () => {
-    const user = userEvent.setup();
-    mockRecord.path = '/user/demo/test';
-    mockRecord.type = 'dir';
-    const { getByRole, queryByRole } = render(
-      <StorageBrowserActions selectedFiles={[mockRecord]} />
-    );
-    await user.click(getByRole('button'));
-    expect(queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
+    const mockSummaryData = {
+      summary: {
+        directoryCount: 0,
+        ecPolicy: 'Replicated',
+        fileCount: 1,
+        length: 0,
+        quota: -1,
+        spaceConsumed: 0,
+        spaceQuota: -1,
+        typeQuota: -1,
+        replication: 3
+      }
+    };
+
+    const setUpMock = () => {
+      summaryApiMock = jest
+        .spyOn(StorageBrowserApi, 'fetchContentSummary')
+        .mockReturnValue(CancellablePromise.resolve<ContentSummary>(mockSummaryData));
+    };
+
+    afterEach(() => {
+      summaryApiMock?.mockClear();
+    });
+
+    test('does not render view summary option when there are multiple records selected', async () => {
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
+    });
+
+    test('renders view summary option when record is a hdfs file', async () => {
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
+    });
+
+    test('renders view summary option when record is a ofs file', async () => {
+      await setUpActionMenu([mockRecord], 'ofs://demo/test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
+    });
+
+    test('does not render view summary option when record is a hdfs folder', async () => {
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
+    });
+
+    test('does not render view summary option when record is a an abfs file', async () => {
+      await setUpActionMenu([mockRecord], 'abfs://demo/test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
+    });
+
+    test('renders summary modal when view summary option is clicked', async () => {
+      const user = userEvent.setup();
+      setUpMock();
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
+      await user.click(screen.queryByRole('menuitem', { name: 'View Summary' }));
+      expect(await screen.findByText('Summary for /user/demo/test')).toBeInTheDocument();
+    });
   });
 
-  test('does not render view summary option when record is a an abfs file', async () => {
-    const user = userEvent.setup();
-    mockRecord.path = 'abfs://demo/test';
-    mockRecord.type = 'file';
-    const { getByRole, queryByRole } = render(
-      <StorageBrowserActions selectedFiles={[mockRecord]} />
-    );
-    await user.click(getByRole('button'));
-    expect(queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
+  describe('Rename option', () => {
+    test('does not render view summary option when there are multiple records selected', async () => {
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
+    });
+    test('does not render rename option when selected record is a abfs root folder', async () => {
+      await setUpActionMenu([mockRecord], 'abfs://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
+    });
+
+    test('does not render rename option when selected record is a gs root folder', async () => {
+      await setUpActionMenu([mockRecord], 'gs://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
+    });
+
+    test('does not render rename option when selected record is a s3 root folder', async () => {
+      await setUpActionMenu([mockRecord], 's3a://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
+    });
+
+    test('does not render rename option when selected record is a ofs root folder', async () => {
+      await setUpActionMenu([mockRecord], 'ofs://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
+    });
+
+    test('does not render rename option when selected record is a ofs service ID folder', async () => {
+      await setUpActionMenu([mockRecord], 'ofs://serviceID', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
+    });
+
+    test('does not render rename option when selected record is a ofs volume folder', async () => {
+      await setUpActionMenu([mockRecord], 'ofs://serviceID/volume', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
+    });
+
+    test('renders rename option when selected record is a file or a folder', async () => {
+      await setUpActionMenu([mockRecord], 'abfs://test', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Rename' })).not.toBeNull();
+    });
+
+    test('renders rename modal when rename option is clicked', async () => {
+      const user = userEvent.setup();
+      await setUpActionMenu([mockRecord], 'abfs://test', 'dir');
+      await user.click(screen.queryByRole('menuitem', { name: 'Rename' }));
+      expect(await screen.findByText('Enter new name here')).toBeInTheDocument();
+    });
   });
 });

+ 90 - 15
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserActions/StorageBrowserActions.tsx

@@ -24,43 +24,110 @@ import InfoIcon from '@cloudera/cuix-core/icons/react/InfoIcon';
 
 import { i18nReact } from '../../../../utils/i18nReact';
 import { StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types';
-import { isHDFS, isOFS } from '../../../../utils/storageBrowserUtils';
+import {
+  isHDFS,
+  isOFS,
+  isABFSRoot,
+  isGSRoot,
+  isOFSServiceID,
+  isOFSVol,
+  isS3Root,
+  inTrash,
+  isABFS,
+  isGS,
+  isS3,
+  isOFSRoot
+} from '../../../../utils/storageBrowserUtils';
+import { rename } from '../../../../reactComponents/FileChooser/api';
+import huePubSub from '../../../../utils/huePubSub';
 
 import SummaryModal from '../../SummaryModal/SummaryModal';
+import InputModal from '../../InputModal/InputModal';
 
 import './StorageBrowserActions.scss';
 
 interface StorageBrowserRowActionsProps {
   selectedFiles: StorageBrowserTableData[];
+  onSuccessfulAction: () => void;
+  setLoadingFiles: (value: boolean) => void;
 }
 
-const StorageBrowserActions = ({ selectedFiles }: StorageBrowserRowActionsProps): JSX.Element => {
+const StorageBrowserActions = ({
+  selectedFiles,
+  setLoadingFiles,
+  onSuccessfulAction
+}: StorageBrowserRowActionsProps): JSX.Element => {
   const [showSummaryModal, setShowSummaryModal] = useState<boolean>(false);
+  const [showRenameModal, setShowRenameModal] = useState<boolean>(false);
   const [selectedFile, setSelectedFile] = useState<string>('');
 
   const { t } = i18nReact.useTranslation();
 
+  const handleRename = (newName: string) => {
+    setLoadingFiles(true);
+    rename(selectedFile, newName)
+      .then(() => {
+        onSuccessfulAction();
+      })
+      .catch(error => {
+        huePubSub.publish('hue.error', error);
+        setShowRenameModal(false);
+      })
+      .finally(() => {
+        setLoadingFiles(false);
+      });
+  };
+
   const isSummaryEnabled = () => {
+    if (selectedFiles.length !== 1) {
+      return false;
+    }
     const selectedFile = selectedFiles[0];
+    return (isHDFS(selectedFile.path) || isOFS(selectedFile.path)) && selectedFile.type === 'file';
+  };
+
+  const isRenameEnabled = () => {
+    if (selectedFiles.length !== 1) {
+      return false;
+    }
+    const selectedFilePath = selectedFiles[0].path;
     return (
-      selectedFiles.length == 1 &&
-      (isHDFS(selectedFile.path) || isOFS(selectedFile.path)) &&
-      selectedFile.type === 'file'
+      isHDFS(selectedFilePath) ||
+      (isS3(selectedFilePath) && !isS3Root(selectedFilePath)) ||
+      (isGS(selectedFilePath) && !isGSRoot(selectedFilePath)) ||
+      (isABFS(selectedFilePath) && !isABFSRoot(selectedFilePath)) ||
+      (isOFS(selectedFilePath) &&
+        !isOFSRoot(selectedFilePath) &&
+        !isOFSServiceID(selectedFilePath) &&
+        !isOFSVol(selectedFilePath))
     );
   };
 
   const getActions = () => {
     const actions: MenuItemType[] = [];
-    if (isSummaryEnabled()) {
-      actions.push({
-        key: 'content_summary',
-        icon: <InfoIcon />,
-        label: t('View Summary'),
-        onClick: () => {
-          setSelectedFile(selectedFiles[0].path);
-          setShowSummaryModal(true);
-        }
-      });
+    if (selectedFiles && selectedFiles.length > 0 && !inTrash(selectedFiles[0].path)) {
+      if (isSummaryEnabled()) {
+        actions.push({
+          key: 'content_summary',
+          icon: <InfoIcon />,
+          label: t('View Summary'),
+          onClick: () => {
+            setSelectedFile(selectedFiles[0].path);
+            setShowSummaryModal(true);
+          }
+        });
+      }
+      if (isRenameEnabled()) {
+        actions.push({
+          key: 'rename',
+          icon: <InfoIcon />,
+          label: t('Rename'),
+          onClick: () => {
+            setSelectedFile(selectedFiles[0].path);
+            setShowRenameModal(true);
+          }
+        });
+      }
     }
     return actions;
   };
@@ -85,6 +152,14 @@ const StorageBrowserActions = ({ selectedFiles }: StorageBrowserRowActionsProps)
         path={selectedFile}
         onClose={() => setShowSummaryModal(false)}
       />
+      <InputModal
+        title={t('Rename')}
+        inputLabel={t('Enter new name here')}
+        submitText={t('Rename')}
+        showModal={showRenameModal}
+        onSubmit={handleRename}
+        onClose={() => setShowRenameModal(false)}
+      />
     </>
   );
 };

+ 11 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx

@@ -220,11 +220,15 @@ const StorageBrowserTable = ({
     onPageNumberChange(nextPageNumber === 0 ? numPages : nextPageNumber);
   };
 
+  const reloadData = () => {
+    setRefreshKey(oldKey => oldKey + 1);
+  };
+
   const handleCreateNewFolder = (folderName: string) => {
     setLoadingFiles(true);
     mkdir(folderName, filePath)
       .then(() => {
-        setRefreshKey(oldKey => oldKey + 1);
+        reloadData();
       })
       .catch(error => {
         huePubSub.publish('hue.error', error);
@@ -239,7 +243,7 @@ const StorageBrowserTable = ({
     setLoadingFiles(true);
     touch(fileName, filePath)
       .then(() => {
-        setRefreshKey(oldKey => oldKey + 1);
+        reloadData();
       })
       .catch(error => {
         huePubSub.publish('hue.error', error);
@@ -288,7 +292,11 @@ const StorageBrowserTable = ({
         <div className="hue-storage-browser__actions-bar">
           <Input className="hue-storage-browser__search" placeholder={t('Search')} />
           <div className="hue-storage-browser__actions-bar-right">
-            <StorageBrowserActions selectedFiles={selectedFiles} />
+            <StorageBrowserActions
+              selectedFiles={selectedFiles}
+              setLoadingFiles={setLoadingFiles}
+              onSuccessfulAction={reloadData}
+            />
             <Dropdown
               overlayClassName="hue-storage-browser__actions-dropdown"
               menu={{

+ 14 - 15
desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.tsx

@@ -70,22 +70,21 @@ const SummaryModal = ({ showModal, onClose, path }: SummaryModalProps): JSX.Elem
   };
 
   useEffect(() => {
-    if (path === '') {
-      return;
+    if (showModal) {
+      setLoadingSummary(true);
+      fetchContentSummary(path)
+        .then(responseSummary => {
+          updateSummaryData(responseSummary);
+        })
+        .catch(error => {
+          huePubSub.publish('hue.error', error);
+          onClose();
+        })
+        .finally(() => {
+          setLoadingSummary(false);
+        });
     }
-    setLoadingSummary(true);
-    fetchContentSummary(path)
-      .then(responseSummary => {
-        updateSummaryData(responseSummary);
-      })
-      .catch(error => {
-        huePubSub.publish('hue.error', error);
-        onClose();
-      })
-      .finally(() => {
-        setLoadingSummary(false);
-      });
-  }, [path]);
+  }, [path, showModal]);
 
   //TODO:Handle long modal title
   return (

+ 5 - 0
desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts

@@ -22,6 +22,7 @@ const VIEWFILES_API_URl = '/api/v1/storage/view=';
 const MAKE_DIRECTORY_API_URL = '/api/v1/storage/mkdir';
 const TOUCH_API_URL = '/api/v1/storage/touch';
 const CONTENT_SUMMARY_API_URL = '/api/v1/storage/content_summary=';
+const RENAME_API_URL = '/api/v1/storage/rename';
 
 export interface ApiFileSystem {
   file_system: string;
@@ -78,3 +79,7 @@ export const touch = async (fileName: string, path: string): Promise<void> => {
 
 export const fetchContentSummary = (path: string): CancellablePromise<ContentSummary> =>
   get(CONTENT_SUMMARY_API_URL + path);
+
+export const rename = async (src_path: string, dest_path: string): Promise<void> => {
+  await post(RENAME_API_URL, { src_path: src_path, dest_path: dest_path });
+};

+ 159 - 3
desktop/core/src/desktop/js/utils/storageBrowserUtils.test.ts

@@ -13,7 +13,20 @@
 // 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.
-import { isHDFS, isOFS } from './storageBrowserUtils';
+import {
+  isHDFS,
+  isS3,
+  isS3Root,
+  isABFS,
+  isABFSRoot,
+  isGS,
+  isGSRoot,
+  isOFS,
+  isOFSRoot,
+  isOFSServiceID,
+  isOFSVol,
+  inTrash
+} from './storageBrowserUtils';
 
 describe('isHDFS function', () => {
   test('returns true for paths starting with "/"', () => {
@@ -24,11 +37,13 @@ describe('isHDFS function', () => {
   test('returns true for paths starting with "hdfs"', () => {
     expect(isHDFS('hdfs://path/to/file')).toBe(true);
     expect(isHDFS('hdfs://')).toBe(true);
+    expect(isHDFS('HDFS://')).toBe(true);
   });
 
   test('returns false for other paths', () => {
-    expect(isHDFS('s3://path/to/file')).toBe(false);
+    expect(isHDFS('s3a://path/to/file')).toBe(false);
     expect(isHDFS('file://path/to/file')).toBe(false);
+
     expect(isHDFS('')).toBe(false);
   });
 });
@@ -37,12 +52,153 @@ describe('isOFS function', () => {
   test('returns true for paths starting with "ofs://"', () => {
     expect(isOFS('ofs://path/to/file')).toBe(true);
     expect(isOFS('ofs://')).toBe(true);
+    expect(isOFS('OFS://')).toBe(true);
   });
 
   test('returns false for other paths', () => {
     expect(isOFS('/path/to/file')).toBe(false);
     expect(isOFS('hdfs://path/to/file')).toBe(false);
-    expect(isOFS('s3://path/to/file')).toBe(false);
+    expect(isOFS('gs://path/to/file')).toBe(false);
+    expect(isOFS('s3a://path/to/file')).toBe(false);
     expect(isOFS('')).toBe(false);
   });
 });
+
+describe('isS3 function', () => {
+  test('returns true for paths starting with "s3a://"', () => {
+    expect(isS3('s3a://path/to/file')).toBe(true);
+    expect(isS3('s3a://')).toBe(true);
+    expect(isS3('S3A://')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isS3('/path/to/file')).toBe(false);
+    expect(isS3('hdfs://path/to/file')).toBe(false);
+    expect(isS3('ofs://path/to/file')).toBe(false);
+    expect(isS3('gs://path/to/file')).toBe(false);
+    expect(isS3('')).toBe(false);
+  });
+});
+
+describe('isGS function', () => {
+  test('returns true for paths starting with "gs://"', () => {
+    expect(isGS('gs://path/to/file')).toBe(true);
+    expect(isGS('gs://')).toBe(true);
+    expect(isGS('GS://')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isGS('/path/to/file')).toBe(false);
+    expect(isGS('hdfs://path/to/file')).toBe(false);
+    expect(isGS('ofs://path/to/file')).toBe(false);
+    expect(isGS('s3a://path/to/file')).toBe(false);
+    expect(isGS('')).toBe(false);
+  });
+});
+
+describe('isABFS function', () => {
+  test('returns true for paths starting with "abfs://"', () => {
+    expect(isABFS('abfs://path/to/file')).toBe(true);
+    expect(isABFS('abfs://')).toBe(true);
+    expect(isABFS('ABFS://')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isABFS('/path/to/file')).toBe(false);
+    expect(isABFS('hdfs://path/to/file')).toBe(false);
+    expect(isABFS('ofs://path/to/file')).toBe(false);
+    expect(isABFS('s3a://path/to/file')).toBe(false);
+    expect(isABFS('')).toBe(false);
+  });
+});
+
+describe('isS3Root function', () => {
+  test('returns true if path equals to "s3a://"', () => {
+    expect(isS3Root('s3a://')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isS3Root('s3a://path/to/file')).toBe(false);
+    expect(isS3Root('/path/to/file')).toBe(false);
+    expect(isS3Root('hdfs://path/to/file')).toBe(false);
+    expect(isS3Root('ofs://path/to/file')).toBe(false);
+  });
+});
+
+describe('isGSRoot function', () => {
+  test('returns true if path equals to "gs://"', () => {
+    expect(isGSRoot('gs://')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isGSRoot('s3a://path/to/file')).toBe(false);
+    expect(isGSRoot('/path/to/file')).toBe(false);
+    expect(isGSRoot('hdfs://path/to/file')).toBe(false);
+    expect(isGSRoot('gs://path/to/file')).toBe(false);
+  });
+});
+
+describe('isABFSRoot function', () => {
+  test('returns true if path equals to "abfs://"', () => {
+    expect(isABFSRoot('abfs://')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isABFSRoot('s3a://path/to/file')).toBe(false);
+    expect(isABFSRoot('/path/to/file')).toBe(false);
+    expect(isABFSRoot('hdfs://path/to/file')).toBe(false);
+    expect(isABFSRoot('abfs://path/to/file')).toBe(false);
+  });
+});
+
+describe('isOFSRoot function', () => {
+  test('returns true if path equals to "ofs://"', () => {
+    expect(isOFSRoot('ofs://')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isOFSRoot('s3a://path/to/file')).toBe(false);
+    expect(isOFSRoot('/path/to/file')).toBe(false);
+    expect(isOFSRoot('hdfs://path/to/file')).toBe(false);
+    expect(isOFSRoot('ofs://path/to/file')).toBe(false);
+  });
+});
+
+describe('isOFSServiceID function', () => {
+  test('returns true if path equals to ofs serviceID', () => {
+    expect(isOFSServiceID('ofs://serviceID')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isOFSServiceID('s3a://path/to/file')).toBe(false);
+    expect(isOFSServiceID('ofs://serviceID/volume')).toBe(false);
+    expect(isOFSServiceID('hdfs://path/to/file')).toBe(false);
+    expect(isOFSServiceID('ofs://')).toBe(false);
+  });
+});
+
+describe('isOFSVol function', () => {
+  test('returns true if path equals to ofs volume', () => {
+    expect(isOFSVol('ofs://serviceID/volume')).toBe(true);
+  });
+
+  test('returns false for other paths', () => {
+    expect(isOFSVol('ofs://serviceID/volume/file')).toBe(false);
+    expect(isOFSVol('/path/to/file')).toBe(false);
+    expect(isOFSVol('hdfs://path/to/file')).toBe(false);
+    expect(isOFSVol('ofs://')).toBe(false);
+  });
+});
+
+describe('inTrash function', () => {
+  test('returns true if path is in trash folder"', () => {
+    expect(inTrash('/user/path/.Trash')).toBe(true);
+  });
+
+  test('returns false if path not in trash', () => {
+    expect(inTrash('/user/trash')).toBe(false);
+    expect(inTrash('/path/to/.Trash')).toBe(false);
+    expect(inTrash('hdfs://path/to/file')).toBe(false);
+    expect(inTrash('ofs://path/to/file')).toBe(false);
+  });
+});

+ 40 - 0
desktop/core/src/desktop/js/utils/storageBrowserUtils.ts

@@ -22,3 +22,43 @@ export const isHDFS = (path: string): boolean => {
 export const isOFS = (path: string): boolean => {
   return path.toLowerCase().indexOf('ofs://') === 0;
 };
+
+export const isS3 = (path: string): boolean => {
+  return path.toLowerCase().indexOf('s3a://') === 0;
+};
+
+export const isGS = (path: string): boolean => {
+  return path.toLowerCase().indexOf('gs://') === 0;
+};
+
+export const isABFS = (path: string): boolean => {
+  return path.toLowerCase().indexOf('abfs://') === 0;
+};
+
+export const isS3Root = (path: string): boolean => {
+  return isS3(path) && path.toLowerCase() === 's3a://';
+};
+
+export const isGSRoot = (path: string): boolean => {
+  return isGS(path) && path.toLowerCase() === 'gs://';
+};
+
+export const isABFSRoot = (path: string): boolean => {
+  return isABFS(path) && path.toLowerCase() === 'abfs://';
+};
+
+export const isOFSRoot = (path: string): boolean => {
+  return isOFS(path) && path.toLowerCase() === 'ofs://';
+};
+
+export const isOFSServiceID = (path: string): boolean => {
+  return isOFS(path) && path.split('/').length === 3 && path.split('/')[2] !== '';
+};
+
+export const isOFSVol = (path: string): boolean => {
+  return isOFS(path) && path.split('/').length === 4 && path.split('/')[3] !== '';
+};
+
+export const inTrash = (path: string): boolean => {
+  return path.match(/^\/user\/.+?\/\.Trash/) !== null;
+};