Browse Source

[ui-storagebrowser] adds extract archive action (#3950)

* [ui-storagebrowser] adds extract action

* fixes the display message
Ram Prasad Agarwal 10 months ago
parent
commit
ab69060432
20 changed files with 409 additions and 150 deletions
  1. 0 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.scss
  2. 9 10
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.test.tsx
  3. 8 11
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.tsx
  4. 19 14
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/DeletionModal/DeletionModal.test.tsx
  5. 17 30
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/DeletionModal/DeletionModal.tsx
  6. 171 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ExtractionModal/ExtractionModal.test.tsx
  7. 77 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ExtractionModal/ExtractionModal.tsx
  8. 8 8
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopyModal/MoveCopyModal.test.tsx
  9. 7 10
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopyModal/MoveCopyModal.tsx
  10. 11 12
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/RenameModal/RenameModal.test.tsx
  11. 6 7
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/RenameModal/RenameModal.tsx
  12. 8 12
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ReplicationModal/ReplicationModal.test.tsx
  13. 6 7
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ReplicationModal/ReplicationModal.tsx
  14. 26 13
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.tsx
  15. 21 6
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.util.ts
  16. 0 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.scss
  17. 8 6
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.test.tsx
  18. 4 4
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.tsx
  19. 1 0
      desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts
  20. 2 0
      desktop/core/src/desktop/js/utils/constants/storageBrowser.ts

+ 0 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Compress/Compress.scss → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.scss


+ 9 - 10
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Compress/Compress.test.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.test.tsx

@@ -17,9 +17,8 @@
 import React from 'react';
 import { render, fireEvent, waitFor } from '@testing-library/react';
 import '@testing-library/jest-dom';
-import CompressAction from './Compress';
+import CompressionModal from './CompressionModal';
 import { StorageDirectoryTableData } from '../../../../../reactComponents/FileChooser/types';
-import { COMPRESS_API_URL } from '../../../../../reactComponents/FileChooser/api';
 
 const mockFiles: StorageDirectoryTableData[] = [
   {
@@ -55,7 +54,7 @@ jest.mock('../../../../../utils/hooks/useSaveData', () => ({
   }))
 }));
 
-describe('CompressAction Component', () => {
+describe('CompressionModal Component', () => {
   const mockOnSuccess = jest.fn();
   const mockOnError = jest.fn();
   const mockOnClose = jest.fn();
@@ -67,7 +66,7 @@ describe('CompressAction Component', () => {
 
   it('should render the Compress modal with the correct title and buttons', () => {
     const { getByText, getByRole } = render(
-      <CompressAction
+      <CompressionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -86,7 +85,7 @@ describe('CompressAction Component', () => {
 
   it('should display the correct list of files to be compressed', () => {
     const { getByText } = render(
-      <CompressAction
+      <CompressionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -104,7 +103,7 @@ describe('CompressAction Component', () => {
 
   it('should call handleCompress with the correct data when "Compress" is clicked', async () => {
     const { getByText } = render(
-      <CompressAction
+      <CompressionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -124,12 +123,12 @@ describe('CompressAction Component', () => {
 
     fireEvent.click(getByText('Compress'));
 
-    expect(mockSave).toHaveBeenCalledWith(formData, { url: COMPRESS_API_URL });
+    expect(mockSave).toHaveBeenCalledWith(formData);
   });
 
   it('should update the compressed file name when input value changes', () => {
     const { getByRole } = render(
-      <CompressAction
+      <CompressionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -148,7 +147,7 @@ describe('CompressAction Component', () => {
 
   it('should call onClose when the modal is closed', () => {
     const { getByText } = render(
-      <CompressAction
+      <CompressionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -169,7 +168,7 @@ describe('CompressAction Component', () => {
     });
 
     const { getByText } = render(
-      <CompressAction
+      <CompressionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}

+ 8 - 11
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Compress/Compress.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.tsx

@@ -22,9 +22,9 @@ import { StorageDirectoryTableData } from '../../../../../reactComponents/FileCh
 import { COMPRESS_API_URL } from '../../../../../reactComponents/FileChooser/api';
 import { Input } from 'antd';
 
-import './Compress.scss';
+import './CompressionModal.scss';
 
-interface CompressActionProps {
+interface CompressionModalProps {
   currentPath: string;
   isOpen?: boolean;
   files: StorageDirectoryTableData[];
@@ -34,7 +34,7 @@ interface CompressActionProps {
   onClose: () => void;
 }
 
-const CompressAction = ({
+const CompressionModal = ({
   currentPath,
   isOpen = true,
   files,
@@ -42,17 +42,14 @@ const CompressAction = ({
   onSuccess,
   onError,
   onClose
-}: CompressActionProps): JSX.Element => {
+}: CompressionModalProps): JSX.Element => {
   const initialName = currentPath.split('/').pop() + '.zip';
   const [value, setValue] = useState<string>(initialName);
   const { t } = i18nReact.useTranslation();
 
-  const { save: saveForm, loading } = useSaveData(undefined, {
+  const { save: saveForm, loading } = useSaveData(COMPRESS_API_URL, {
     postOptions: {
-      qsEncodeData: false,
-      headers: {
-        'Content-Type': 'multipart/form-data'
-      }
+      qsEncodeData: false
     },
     skip: !files.length,
     onSuccess,
@@ -69,7 +66,7 @@ const CompressAction = ({
     formData.append('upload_path', currentPath);
     formData.append('archive_name', value);
 
-    saveForm(formData, { url: COMPRESS_API_URL });
+    saveForm(formData);
   };
 
   return (
@@ -106,4 +103,4 @@ const CompressAction = ({
   );
 };
 
-export default CompressAction;
+export default CompressionModal;

+ 19 - 14
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Delete/Delete.test.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/DeletionModal/DeletionModal.test.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { render, fireEvent } from '@testing-library/react';
 import '@testing-library/jest-dom';
-import DeleteAction from './Delete';
+import DeletionModal from './DeletionModal';
 import { StorageDirectoryTableData } from '../../../../../reactComponents/FileChooser/types';
 import {
   BULK_DELETION_API_URL,
@@ -42,7 +42,7 @@ jest.mock('../../../../../utils/hooks/useSaveData', () => ({
   }))
 }));
 
-describe('DeleteAction Component', () => {
+describe('DeletionModal Component', () => {
   const mockOnSuccess = jest.fn();
   const mockOnError = jest.fn();
   const mockOnClose = jest.fn();
@@ -54,7 +54,7 @@ describe('DeleteAction Component', () => {
 
   it('should render the Delete modal with the correct title and buttons', () => {
     const { getByText, getByRole } = render(
-      <DeleteAction
+      <DeletionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -73,7 +73,7 @@ describe('DeleteAction Component', () => {
 
   it('should render the Delete modal with the correct title and buttons when trash is not enabled', () => {
     const { getByText, queryByText, getByRole } = render(
-      <DeleteAction
+      <DeletionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -92,7 +92,7 @@ describe('DeleteAction Component', () => {
 
   it('should call handleDeletion with the correct data for single delete when "Delete Permanently" is clicked', async () => {
     const { getByText } = render(
-      <DeleteAction
+      <DeletionModal
         isOpen={true}
         files={[mockFiles[0]]}
         setLoading={setLoading}
@@ -105,13 +105,16 @@ describe('DeleteAction Component', () => {
 
     fireEvent.click(getByText('Delete Permanently'));
 
-    const payload = { path: mockFiles[0].path, skip_trash: true };
-    expect(mockSave).toHaveBeenCalledWith(payload, { url: DELETION_API_URL });
+    const formData = new FormData();
+    formData.append('path', mockFiles[0].path);
+    formData.append('skip_trash', 'true');
+
+    expect(mockSave).toHaveBeenCalledWith(formData, { url: DELETION_API_URL });
   });
 
   it('should call handleDeletion with the correct data for bulk delete when "Delete Permanently" is clicked', async () => {
     const { getByText } = render(
-      <DeleteAction
+      <DeletionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -135,7 +138,7 @@ describe('DeleteAction Component', () => {
 
   it('should call handleDeletion with the correct data for trash delete when "Move to Trash" is clicked', async () => {
     const { getByText } = render(
-      <DeleteAction
+      <DeletionModal
         isOpen={true}
         files={[mockFiles[0]]}
         setLoading={setLoading}
@@ -148,13 +151,15 @@ describe('DeleteAction Component', () => {
 
     fireEvent.click(getByText('Move to Trash'));
 
-    const payload = { path: mockFiles[0].path };
-    expect(mockSave).toHaveBeenCalledWith(payload, { url: DELETION_API_URL });
+    const formData = new FormData();
+    formData.append('path', mockFiles[0].path);
+
+    expect(mockSave).toHaveBeenCalledWith(formData, { url: DELETION_API_URL });
   });
 
   it('should call handleDeletion with the correct data for bulk trash delete when "Move to Trash" is clicked', async () => {
     const { getByText } = render(
-      <DeleteAction
+      <DeletionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -180,7 +185,7 @@ describe('DeleteAction Component', () => {
       mockOnError(new Error());
     });
     const { getByText } = render(
-      <DeleteAction
+      <DeletionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}
@@ -198,7 +203,7 @@ describe('DeleteAction Component', () => {
 
   it('should call onClose when the modal is closed', () => {
     const { getByText } = render(
-      <DeleteAction
+      <DeletionModal
         isOpen={true}
         files={mockFiles}
         setLoading={setLoading}

+ 17 - 30
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Delete/Delete.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/DeletionModal/DeletionModal.tsx

@@ -24,7 +24,7 @@ import {
   DELETION_API_URL
 } from '../../../../../reactComponents/FileChooser/api';
 
-interface DeleteActionProps {
+interface DeletionModalProps {
   isOpen?: boolean;
   isTrashEnabled?: boolean;
   files: StorageDirectoryTableData[];
@@ -34,7 +34,7 @@ interface DeleteActionProps {
   onClose: () => void;
 }
 
-const DeleteAction = ({
+const DeletionModal = ({
   isOpen = true,
   isTrashEnabled = false,
   files,
@@ -42,21 +42,12 @@ const DeleteAction = ({
   onSuccess,
   onError,
   onClose
-}: DeleteActionProps): JSX.Element => {
+}: DeletionModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { save, loading: saveLoading } = useSaveData(undefined, {
-    skip: !files.length,
-    onSuccess,
-    onError
-  });
-
-  const { save: saveForm, loading: saveFormLoading } = useSaveData(undefined, {
+  const { save, loading } = useSaveData(undefined, {
     postOptions: {
-      qsEncodeData: false,
-      headers: {
-        'Content-Type': 'multipart/form-data'
-      }
+      qsEncodeData: false
     },
     skip: !files.length,
     onSuccess,
@@ -64,28 +55,24 @@ const DeleteAction = ({
   });
 
   const handleDeletion = (isForedSkipTrash: boolean = false) => {
-    const isSkipTrash = !isTrashEnabled || isForedSkipTrash;
     setLoading(true);
+    const isSkipTrash = !isTrashEnabled || isForedSkipTrash;
 
-    const isBulkDelete = files.length > 1;
-    if (isBulkDelete) {
-      const formData = new FormData();
-      files.forEach(selectedFile => {
-        formData.append('path', selectedFile.path);
-      });
-      if (isSkipTrash) {
-        formData.append('skip_trash', String(isSkipTrash));
-      }
+    const formData = new FormData();
+    files.forEach(selectedFile => {
+      formData.append('path', selectedFile.path);
+    });
+    if (isSkipTrash) {
+      formData.append('skip_trash', String(isSkipTrash));
+    }
 
-      saveForm(formData, { url: BULK_DELETION_API_URL });
+    if (files.length > 1) {
+      save(formData, { url: BULK_DELETION_API_URL });
     } else {
-      const payload = { path: files[0].path, skip_trash: isSkipTrash ? true : undefined };
-      save(payload, { url: DELETION_API_URL });
+      save(formData, { url: DELETION_API_URL });
     }
   };
 
-  const loading = saveFormLoading || saveLoading;
-
   return (
     <Modal
       cancelText={t('Cancel')}
@@ -112,4 +99,4 @@ const DeleteAction = ({
   );
 };
 
-export default DeleteAction;
+export default DeletionModal;

+ 171 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ExtractionModal/ExtractionModal.test.tsx

@@ -0,0 +1,171 @@
+// 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.
+
+import React from 'react';
+import { render, fireEvent, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import ExtractAction from './ExtractionModal';
+import { StorageDirectoryTableData } from '../../../../../reactComponents/FileChooser/types';
+
+const mockFile: StorageDirectoryTableData = {
+  name: 'archive.zip',
+  size: '50 MB',
+  type: 'file',
+  permission: 'rwxrwxrwx',
+  mtime: '2021-01-01 00:00:00',
+  path: 'test/path/archive.zip',
+  user: 'test',
+  group: 'test',
+  replication: 1
+};
+
+const mockSave = jest.fn();
+let mockLoading = false;
+jest.mock('../../../../../utils/hooks/useSaveData', () => ({
+  __esModule: true,
+  default: jest.fn(() => ({
+    save: mockSave,
+    loading: mockLoading
+  }))
+}));
+
+describe('ExtractAction Component', () => {
+  const mockOnSuccess = jest.fn();
+  const mockOnError = jest.fn();
+  const mockOnClose = jest.fn();
+  const setLoading = jest.fn();
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should render the Extract modal with the correct title and buttons', () => {
+    const { getByText, getByRole } = render(
+      <ExtractAction
+        isOpen={true}
+        file={mockFile}
+        setLoading={setLoading}
+        onSuccess={mockOnSuccess}
+        onError={mockOnError}
+        onClose={mockOnClose}
+        currentPath="test/path"
+      />
+    );
+
+    expect(getByText('Extract Archive')).toBeInTheDocument();
+    expect(getByText(`Are you sure you want to extract "{{fileName}}" file?`)).toBeInTheDocument();
+    expect(getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
+    expect(getByRole('button', { name: 'Extract' })).toBeInTheDocument();
+  });
+
+  it('should call handleExtract with the correct path and name when "Extract" is clicked', async () => {
+    const { getByText } = render(
+      <ExtractAction
+        isOpen={true}
+        file={mockFile}
+        setLoading={setLoading}
+        onSuccess={mockOnSuccess}
+        onError={mockOnError}
+        onClose={mockOnClose}
+        currentPath="test/path"
+      />
+    );
+
+    fireEvent.click(getByText('Extract'));
+
+    expect(mockSave).toHaveBeenCalledWith({
+      upload_path: 'test/path',
+      archive_name: mockFile.name
+    });
+  });
+
+  it('should call onSuccess when the extract request is successful', async () => {
+    mockSave.mockImplementationOnce(() => {
+      mockOnSuccess();
+    });
+
+    const { getByText } = render(
+      <ExtractAction
+        isOpen={true}
+        file={mockFile}
+        setLoading={setLoading}
+        onSuccess={mockOnSuccess}
+        onError={mockOnError}
+        onClose={mockOnClose}
+        currentPath="test/path"
+      />
+    );
+
+    fireEvent.click(getByText('Extract'));
+    await waitFor(() => expect(mockOnSuccess).toHaveBeenCalledTimes(1));
+  });
+
+  it('should call onError when the extract request fails', async () => {
+    mockSave.mockImplementationOnce(() => {
+      mockOnError(new Error('Extraction failed'));
+    });
+
+    const { getByText } = render(
+      <ExtractAction
+        isOpen={true}
+        file={mockFile}
+        setLoading={setLoading}
+        onSuccess={mockOnSuccess}
+        onError={mockOnError}
+        onClose={mockOnClose}
+        currentPath="test/path"
+      />
+    );
+
+    fireEvent.click(getByText('Extract'));
+    await waitFor(() => expect(mockOnError).toHaveBeenCalledWith(new Error('Extraction failed')));
+  });
+
+  it('should call onClose when the modal is closed', () => {
+    const { getByText } = render(
+      <ExtractAction
+        isOpen={true}
+        file={mockFile}
+        setLoading={setLoading}
+        onSuccess={mockOnSuccess}
+        onError={mockOnError}
+        onClose={mockOnClose}
+        currentPath="test/path"
+      />
+    );
+
+    fireEvent.click(getByText('Cancel'));
+    expect(mockOnClose).toHaveBeenCalledTimes(1);
+  });
+
+  it('should disable the "Extract" button while loading', () => {
+    mockLoading = true;
+
+    const { getByRole } = render(
+      <ExtractAction
+        isOpen={true}
+        file={mockFile}
+        setLoading={setLoading}
+        onSuccess={mockOnSuccess}
+        onError={mockOnError}
+        onClose={mockOnClose}
+        currentPath="test/path"
+      />
+    );
+
+    expect(getByRole('button', { name: 'Extract' })).toBeDisabled();
+  });
+});

+ 77 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ExtractionModal/ExtractionModal.tsx

@@ -0,0 +1,77 @@
+// 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.
+
+import React from 'react';
+import Modal from 'cuix/dist/components/Modal';
+import { i18nReact } from '../../../../../utils/i18nReact';
+import useSaveData from '../../../../../utils/hooks/useSaveData';
+import { StorageDirectoryTableData } from '../../../../../reactComponents/FileChooser/types';
+import { EXTRACT_API_URL } from '../../../../../reactComponents/FileChooser/api';
+
+interface ExtractActionProps {
+  currentPath: string;
+  isOpen?: boolean;
+  file: StorageDirectoryTableData;
+  setLoading: (value: boolean) => void;
+  onSuccess: () => void;
+  onError: (error: Error) => void;
+  onClose: () => void;
+}
+
+const ExtractionModal = ({
+  currentPath,
+  isOpen = true,
+  file,
+  setLoading,
+  onSuccess,
+  onError,
+  onClose
+}: ExtractActionProps): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  const { save, loading } = useSaveData(EXTRACT_API_URL, {
+    skip: !file,
+    onSuccess,
+    onError
+  });
+
+  const handleExtract = () => {
+    setLoading(true);
+
+    save({
+      upload_path: currentPath,
+      archive_name: file.name
+    });
+  };
+
+  return (
+    <Modal
+      cancelText={t('Cancel')}
+      className="cuix antd"
+      okText={t('Extract')}
+      onCancel={onClose}
+      onOk={handleExtract}
+      open={isOpen}
+      title={t('Extract Archive')}
+      okButtonProps={{ disabled: loading }}
+      cancelButtonProps={{ disabled: loading }}
+    >
+      {t('Are you sure you want to extract "{{fileName}}" file?', { fileName: file.name })}
+    </Modal>
+  );
+};
+
+export default ExtractionModal;

+ 8 - 8
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopy/MoveCopy.test.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopyModal/MoveCopyModal.test.tsx

@@ -28,7 +28,7 @@
 import React from 'react';
 import { render, fireEvent } from '@testing-library/react';
 import '@testing-library/jest-dom';
-import MoveCopyAction from './MoveCopy';
+import MoveCopyModal from './MoveCopyModal';
 import { ActionType } from '../StorageBrowserActions.util';
 import {
   BULK_COPY_API_URL,
@@ -92,7 +92,7 @@ describe('MoveCopy Action Component', () => {
   describe('Copy Actions', () => {
     it('should render correctly and open the modal', () => {
       const { getByText } = render(
-        <MoveCopyAction
+        <MoveCopyModal
           isOpen={true}
           action={ActionType.Copy}
           currentPath={currentPath}
@@ -112,7 +112,7 @@ describe('MoveCopy Action Component', () => {
       const newDestPath = 'test/path/folder1';
 
       const { getByText } = render(
-        <MoveCopyAction
+        <MoveCopyModal
           isOpen={true}
           action={ActionType.Copy}
           currentPath={currentPath}
@@ -143,7 +143,7 @@ describe('MoveCopy Action Component', () => {
     it('should call onSuccess when the request succeeds', async () => {
       mockSave.mockImplementationOnce(mockOnSuccess);
       const { getByText } = render(
-        <MoveCopyAction
+        <MoveCopyModal
           isOpen={true}
           action={ActionType.Copy}
           currentPath={currentPath}
@@ -167,7 +167,7 @@ describe('MoveCopy Action Component', () => {
         mockOnError(new Error());
       });
       const { getByText } = render(
-        <MoveCopyAction
+        <MoveCopyModal
           isOpen={true}
           action={ActionType.Copy}
           currentPath={currentPath}
@@ -188,7 +188,7 @@ describe('MoveCopy Action Component', () => {
 
     it('should call onClose when the modal is closed', () => {
       const { getByText } = render(
-        <MoveCopyAction
+        <MoveCopyModal
           isOpen={true}
           action={ActionType.Copy}
           currentPath={currentPath}
@@ -209,7 +209,7 @@ describe('MoveCopy Action Component', () => {
   describe('Move Actions', () => {
     it('should render correctly and open the modal', () => {
       const { getByText } = render(
-        <MoveCopyAction
+        <MoveCopyModal
           isOpen={true}
           action={ActionType.Move}
           currentPath={currentPath}
@@ -229,7 +229,7 @@ describe('MoveCopy Action Component', () => {
       const newDestPath = 'test/path/folder1';
 
       const { getByText } = render(
-        <MoveCopyAction
+        <MoveCopyModal
           isOpen={true}
           action={ActionType.Move}
           currentPath={currentPath}

+ 7 - 10
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopy/MoveCopy.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopyModal/MoveCopyModal.tsx

@@ -28,7 +28,7 @@ import {
   StorageDirectoryTableData
 } from '../../../../../reactComponents/FileChooser/types';
 
-interface MoveCopyActionProps {
+interface MoveCopyModalProps {
   isOpen?: boolean;
   action: ActionType.Copy | ActionType.Move;
   currentPath: FileStats['path'];
@@ -39,7 +39,7 @@ interface MoveCopyActionProps {
   onClose: () => void;
 }
 
-const MoveCopyAction = ({
+const MoveCopyModal = ({
   isOpen = true,
   action,
   currentPath,
@@ -48,15 +48,12 @@ const MoveCopyAction = ({
   onSuccess,
   onError,
   onClose
-}: MoveCopyActionProps): JSX.Element => {
+}: MoveCopyModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { save: saveForm } = useSaveData(undefined, {
+  const { save } = useSaveData(undefined, {
     postOptions: {
-      qsEncodeData: false,
-      headers: {
-        'Content-Type': 'multipart/form-data'
-      }
+      qsEncodeData: false
     },
     skip: !files.length,
     onSuccess: onSuccess,
@@ -80,7 +77,7 @@ const MoveCopyAction = ({
     formData.append('destination_path', destination_path);
 
     setLoadingFiles(true);
-    saveForm(formData, { url });
+    save(formData, { url });
   };
 
   return (
@@ -95,4 +92,4 @@ const MoveCopyAction = ({
   );
 };
 
-export default MoveCopyAction;
+export default MoveCopyModal;

+ 11 - 12
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Rename/Rename.test.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/RenameModal/RenameModal.test.tsx

@@ -17,9 +17,8 @@
 import React from 'react';
 import { render, fireEvent } from '@testing-library/react';
 import '@testing-library/jest-dom';
-import RenameAction from './Rename';
+import RenameModal from './RenameModal';
 import { StorageDirectoryTableData } from '../../../../../reactComponents/FileChooser/types';
-import { RENAME_API_URL } from '../../../../../reactComponents/FileChooser/api';
 
 const mockSave = jest.fn();
 jest.mock('../../../../../utils/hooks/useSaveData', () => ({
@@ -30,7 +29,7 @@ jest.mock('../../../../../utils/hooks/useSaveData', () => ({
   }))
 }));
 
-describe('RenameAction Component', () => {
+describe('RenameModal Component', () => {
   const mockOnSuccess = jest.fn();
   const mockOnError = jest.fn();
   const mockOnClose = jest.fn();
@@ -53,7 +52,7 @@ describe('RenameAction Component', () => {
 
   it('should render the Rename modal with the correct title and initial input', () => {
     const { getByText, getByRole } = render(
-      <RenameAction
+      <RenameModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}
@@ -71,7 +70,7 @@ describe('RenameAction Component', () => {
 
   it('should call handleRename with the correct data when the form is submitted', async () => {
     const { getByRole } = render(
-      <RenameAction
+      <RenameModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}
@@ -88,16 +87,16 @@ describe('RenameAction Component', () => {
 
     expect(mockSave).toHaveBeenCalledTimes(1);
 
-    expect(mockSave).toHaveBeenCalledWith(
-      { source_path: '/path/to/file1.txt', destination_path: 'file2.txt' },
-      { url: RENAME_API_URL }
-    );
+    expect(mockSave).toHaveBeenCalledWith({
+      source_path: '/path/to/file1.txt',
+      destination_path: 'file2.txt'
+    });
   });
 
   it('should call onSuccess when the rename request succeeds', async () => {
     mockSave.mockImplementationOnce(mockOnSuccess);
     const { getByRole } = render(
-      <RenameAction
+      <RenameModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}
@@ -120,7 +119,7 @@ describe('RenameAction Component', () => {
       mockOnError(new Error());
     });
     const { getByRole } = render(
-      <RenameAction
+      <RenameModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}
@@ -140,7 +139,7 @@ describe('RenameAction Component', () => {
 
   it('should call onClose when the modal is closed', () => {
     const { getByRole } = render(
-      <RenameAction
+      <RenameModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}

+ 6 - 7
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Rename/Rename.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/RenameModal/RenameModal.tsx

@@ -21,7 +21,7 @@ import useSaveData from '../../../../../utils/hooks/useSaveData';
 import { RENAME_API_URL } from '../../../../../reactComponents/FileChooser/api';
 import InputModal from '../../../InputModal/InputModal';
 
-interface RenameActionProps {
+interface RenameModalProps {
   isOpen?: boolean;
   file: StorageDirectoryTableData;
   onSuccess: () => void;
@@ -29,24 +29,23 @@ interface RenameActionProps {
   onClose: () => void;
 }
 
-const RenameAction = ({
+const RenameModal = ({
   isOpen = true,
   file,
   onSuccess,
   onError,
   onClose
-}: RenameActionProps): JSX.Element => {
+}: RenameModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { save, loading } = useSaveData(undefined, {
+  const { save, loading } = useSaveData(RENAME_API_URL, {
     skip: !file.path,
     onSuccess,
     onError
   });
 
   const handleRename = (value: string) => {
-    const payload = { source_path: file.path, destination_path: value };
-    save(payload, { url: RENAME_API_URL });
+    save({ source_path: file.path, destination_path: value });
   };
 
   return (
@@ -64,4 +63,4 @@ const RenameAction = ({
   );
 };
 
-export default RenameAction;
+export default RenameModal;

+ 8 - 12
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Replication/Replication.test.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ReplicationModal/ReplicationModal.test.tsx

@@ -17,9 +17,8 @@
 import React from 'react';
 import { render, fireEvent } from '@testing-library/react';
 import '@testing-library/jest-dom';
-import ReplicationAction from './Replication';
+import ReplicationModal from './ReplicationModal';
 import { StorageDirectoryTableData } from '../../../../../reactComponents/FileChooser/types';
-import { SET_REPLICATION_API_URL } from '../../../../../reactComponents/FileChooser/api';
 
 const mockSave = jest.fn();
 jest.mock('../../../../../utils/hooks/useSaveData', () => ({
@@ -30,7 +29,7 @@ jest.mock('../../../../../utils/hooks/useSaveData', () => ({
   }))
 }));
 
-describe('ReplicationAction Component', () => {
+describe('ReplicationModal Component', () => {
   const mockOnSuccess = jest.fn();
   const mockOnError = jest.fn();
   const mockOnClose = jest.fn();
@@ -53,7 +52,7 @@ describe('ReplicationAction Component', () => {
 
   it('should render the Replication modal with the correct title and initial input', () => {
     const { getByText, getByRole } = render(
-      <ReplicationAction
+      <ReplicationModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}
@@ -71,7 +70,7 @@ describe('ReplicationAction Component', () => {
 
   it('should call handleReplication with the correct data when the form is submitted', async () => {
     const { getByRole } = render(
-      <ReplicationAction
+      <ReplicationModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}
@@ -88,16 +87,13 @@ describe('ReplicationAction Component', () => {
 
     expect(mockSave).toHaveBeenCalledTimes(1);
 
-    expect(mockSave).toHaveBeenCalledWith(
-      { path: '/path/to/file1.txt', replication_factor: '2' },
-      { url: SET_REPLICATION_API_URL }
-    );
+    expect(mockSave).toHaveBeenCalledWith({ path: '/path/to/file1.txt', replication_factor: '2' });
   });
 
   it('should call onSuccess when the rename request succeeds', async () => {
     mockSave.mockImplementationOnce(mockOnSuccess);
     const { getByRole } = render(
-      <ReplicationAction
+      <ReplicationModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}
@@ -120,7 +116,7 @@ describe('ReplicationAction Component', () => {
       mockOnError(new Error());
     });
     const { getByRole } = render(
-      <ReplicationAction
+      <ReplicationModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}
@@ -140,7 +136,7 @@ describe('ReplicationAction Component', () => {
 
   it('should call onClose when the modal is closed', () => {
     const { getByRole } = render(
-      <ReplicationAction
+      <ReplicationModal
         isOpen={true}
         file={file}
         onSuccess={mockOnSuccess}

+ 6 - 7
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Replication/Replication.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ReplicationModal/ReplicationModal.tsx

@@ -21,7 +21,7 @@ import useSaveData from '../../../../../utils/hooks/useSaveData';
 import { SET_REPLICATION_API_URL } from '../../../../../reactComponents/FileChooser/api';
 import InputModal from '../../../InputModal/InputModal';
 
-interface ReplicationActionProps {
+interface ReplicationModalProps {
   isOpen?: boolean;
   file: StorageDirectoryTableData;
   onSuccess: () => void;
@@ -29,24 +29,23 @@ interface ReplicationActionProps {
   onClose: () => void;
 }
 
-const ReplicationAction = ({
+const ReplicationModal = ({
   isOpen = true,
   file,
   onSuccess,
   onError,
   onClose
-}: ReplicationActionProps): JSX.Element => {
+}: ReplicationModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { save, loading } = useSaveData(undefined, {
+  const { save, loading } = useSaveData(SET_REPLICATION_API_URL, {
     skip: !file.path,
     onSuccess,
     onError
   });
 
   const handleReplication = (replicationFactor: number) => {
-    const payload = { path: file.path, replication_factor: replicationFactor };
-    save(payload, { url: SET_REPLICATION_API_URL });
+    save({ path: file.path, replication_factor: replicationFactor });
   };
 
   return (
@@ -64,4 +63,4 @@ const ReplicationAction = ({
   );
 };
 
-export default ReplicationAction;
+export default ReplicationModal;

+ 26 - 13
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.tsx

@@ -27,6 +27,7 @@ import CopyClipboardIcon from '@cloudera/cuix-core/icons/react/CopyClipboardIcon
 import DataMovementIcon from '@cloudera/cuix-core/icons/react/DataMovementIcon';
 import DeleteIcon from '@cloudera/cuix-core/icons/react/DeleteIcon';
 import CollapseIcon from '@cloudera/cuix-core/icons/react/CollapseViewIcon';
+import ExpandIcon from '@cloudera/cuix-core/icons/react/ExpandViewIcon';
 
 import { i18nReact } from '../../../../utils/i18nReact';
 import huePubSub from '../../../../utils/huePubSub';
@@ -36,12 +37,13 @@ import {
   StorageDirectoryTableData
 } from '../../../../reactComponents/FileChooser/types';
 import { ActionType, getEnabledActions } from './StorageBrowserActions.util';
-import MoveCopyAction from './MoveCopy/MoveCopy';
-import RenameAction from './Rename/Rename';
-import ReplicationAction from './Replication/Replication';
-import ViewSummary from './ViewSummary/ViewSummary';
-import DeleteAction from './Delete/Delete';
-import CompressAction from './Compress/Compress';
+import MoveCopyModal from './MoveCopyModal/MoveCopyModal';
+import RenameModal from './RenameModal/RenameModal';
+import ReplicationModal from './ReplicationModal/ReplicationModal';
+import SummaryModal from './SummaryModal/SummaryModal';
+import DeletionModal from './DeletionModal/DeletionModal';
+import CompressionModal from './CompressionModal/CompressionModal';
+import ExtractionModal from './ExtractionModal/ExtractionModal';
 
 interface StorageBrowserRowActionsProps {
   isTrashEnabled?: boolean;
@@ -58,7 +60,8 @@ const iconsMap: Record<ActionType, JSX.Element> = {
   [ActionType.Replication]: <DuplicateIcon />,
   [ActionType.Delete]: <DeleteIcon />,
   [ActionType.Summary]: <SummaryIcon />,
-  [ActionType.Compress]: <CollapseIcon />
+  [ActionType.Compress]: <CollapseIcon />,
+  [ActionType.Extract]: <ExpandIcon />
 };
 
 const StorageBrowserActions = ({
@@ -114,10 +117,10 @@ const StorageBrowserActions = ({
         </Button>
       </Dropdown>
       {selectedAction === ActionType.Summary && (
-        <ViewSummary path={selectedFiles[0].path} onClose={closeModal} />
+        <SummaryModal path={selectedFiles[0].path} onClose={closeModal} />
       )}
       {selectedAction === ActionType.Rename && (
-        <RenameAction
+        <RenameModal
           file={selectedFiles[0]}
           onSuccess={onApiSuccess}
           onError={onApiError}
@@ -125,7 +128,7 @@ const StorageBrowserActions = ({
         />
       )}
       {selectedAction === ActionType.Replication && (
-        <ReplicationAction
+        <ReplicationModal
           file={selectedFiles[0]}
           onSuccess={onApiSuccess}
           onError={onApiError}
@@ -133,7 +136,7 @@ const StorageBrowserActions = ({
         />
       )}
       {(selectedAction === ActionType.Move || selectedAction === ActionType.Copy) && (
-        <MoveCopyAction
+        <MoveCopyModal
           action={selectedAction}
           files={selectedFiles}
           currentPath={currentPath}
@@ -144,7 +147,7 @@ const StorageBrowserActions = ({
         />
       )}
       {selectedAction === ActionType.Delete && (
-        <DeleteAction
+        <DeletionModal
           isTrashEnabled={isTrashEnabled}
           files={selectedFiles}
           onSuccess={onApiSuccess}
@@ -154,7 +157,7 @@ const StorageBrowserActions = ({
         />
       )}
       {selectedAction === ActionType.Compress && (
-        <CompressAction
+        <CompressionModal
           currentPath={currentPath}
           files={selectedFiles}
           onSuccess={onApiSuccess}
@@ -163,6 +166,16 @@ const StorageBrowserActions = ({
           setLoading={setLoadingFiles}
         />
       )}
+      {selectedAction === ActionType.Extract && (
+        <ExtractionModal
+          currentPath={currentPath}
+          file={selectedFiles[0]}
+          onSuccess={onApiSuccess}
+          onError={onApiError}
+          onClose={closeModal}
+          setLoading={setLoadingFiles}
+        />
+      )}
     </>
   );
 };

+ 21 - 6
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.util.ts

@@ -33,6 +33,7 @@ import {
   isOFSRoot,
   inTrash
 } from '../../../../utils/storageBrowserUtils';
+import { SUPPORTED_COMPRESSED_FILE_EXTENTION } from '../../../../utils/constants/storageBrowser';
 
 export enum ActionType {
   Copy = 'copy',
@@ -41,7 +42,8 @@ export enum ActionType {
   Rename = 'rename',
   Replication = 'replication',
   Delete = 'delete',
-  Compress = 'compress'
+  Compress = 'compress',
+  Extract = 'extract'
 }
 
 const isValidFileOrFolder = (filePath: string): boolean => {
@@ -54,7 +56,12 @@ const isValidFileOrFolder = (filePath: string): boolean => {
   );
 };
 
+const isFileCompressed = (filePath: string): boolean => {
+  return SUPPORTED_COMPRESSED_FILE_EXTENTION.some(ext => filePath.endsWith(ext));
+};
+
 const isActionEnabled = (file: StorageDirectoryTableData, action: ActionType): boolean => {
+  const config = getLastKnownConfig();
   switch (action) {
     case ActionType.Summary:
       return (isHDFS(file.path) || isOFS(file.path)) && file.type === BrowserViewType.file;
@@ -65,8 +72,14 @@ const isActionEnabled = (file: StorageDirectoryTableData, action: ActionType): b
     case ActionType.Delete:
     case ActionType.Move:
       return isValidFileOrFolder(file.path);
+    case ActionType.Extract:
+      return (
+        !!config?.storage_browser.enable_extract_uploaded_archive &&
+        isHDFS(file.path) &&
+        isFileCompressed(file.path)
+      );
     case ActionType.Compress:
-      return isHDFS(file.path) && isValidFileOrFolder(file.path);
+      return !!config?.storage_browser.enable_extract_uploaded_archive && isHDFS(file.path);
     default:
       return false;
   }
@@ -93,7 +106,6 @@ export const getEnabledActions = (
   type: ActionType;
   label: string;
 }[] => {
-  const config = getLastKnownConfig();
   const isAnyFileInTrash = files.some(file => inTrash(file.path));
   const isNoFileSelected = files && files.length === 0;
   if (isAnyFileInTrash || isNoFileSelected) {
@@ -133,11 +145,14 @@ export const getEnabledActions = (
       label: 'Set Replication'
     },
     {
-      enabled:
-        !!config?.storage_browser.enable_extract_uploaded_archive &&
-        isMultipleFileActionEnabled(files, ActionType.Compress),
+      enabled: isMultipleFileActionEnabled(files, ActionType.Compress),
       type: ActionType.Compress,
       label: 'Compress'
+    },
+    {
+      enabled: isSingleFileActionEnabled(files, ActionType.Extract),
+      type: ActionType.Extract,
+      label: 'Extract'
     }
   ].filter(e => e.enabled);
 

+ 0 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.scss → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.scss


+ 8 - 6
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.test.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.test.tsx

@@ -20,7 +20,7 @@ import '@testing-library/jest-dom';
 
 import { get } from '../../../../../api/utils';
 import formatBytes from '../../../../../utils/formatBytes';
-import ViewSummary from './ViewSummary';
+import SummaryModal from './SummaryModal';
 
 jest.mock('../../../../../api/utils', () => ({
   get: jest.fn()
@@ -28,7 +28,7 @@ jest.mock('../../../../../api/utils', () => ({
 
 const mockGet = get as jest.MockedFunction<typeof get>;
 
-describe('ViewSummary', () => {
+describe('SummaryModal', () => {
   beforeAll(() => {
     jest.clearAllMocks();
   });
@@ -54,14 +54,16 @@ describe('ViewSummary', () => {
   };
 
   it('should render path of file in title', async () => {
-    const { getByText } = render(<ViewSummary onClose={() => {}} path="some/path" />);
+    const { getByText } = render(<SummaryModal onClose={() => {}} path="some/path" />);
     await waitFor(async () => {
       expect(getByText('Summary for some/path')).toBeInTheDocument();
     });
   });
 
   it('should render summary content after successful data fetching', async () => {
-    const { getByText, getAllByText } = render(<ViewSummary onClose={() => {}} path="some/path" />);
+    const { getByText, getAllByText } = render(
+      <SummaryModal onClose={() => {}} path="some/path" />
+    );
     await waitFor(async () => {
       expect(getByText('Diskspace Consumed')).toBeInTheDocument();
       expect(getAllByText(formatBytes(mockSummary.spaceConsumed))[0]).toBeInTheDocument();
@@ -69,7 +71,7 @@ describe('ViewSummary', () => {
   });
 
   it('should render space consumed in Bytes after the values are formatted', async () => {
-    render(<ViewSummary path={'/user/demo'} onClose={() => {}} />);
+    render(<SummaryModal path={'/user/demo'} onClose={() => {}} />);
     const spaceConsumed = await screen.findAllByText('0 Byte');
     await waitFor(() => {
       expect(spaceConsumed[0]).toBeInTheDocument();
@@ -78,7 +80,7 @@ describe('ViewSummary', () => {
 
   it('should call onClose function when close button is clicked', async () => {
     const mockOnClose = jest.fn();
-    const { getByText } = render(<ViewSummary onClose={mockOnClose} path="some/path" />);
+    const { getByText } = render(<SummaryModal onClose={mockOnClose} path="some/path" />);
 
     const closeButton = getByText('Close');
     expect(mockOnClose).not.toHaveBeenCalled();

+ 4 - 4
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.tsx → desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.tsx

@@ -28,15 +28,15 @@ import {
   StorageDirectoryTableData
 } from '../../../../../reactComponents/FileChooser/types';
 
-import './ViewSummary.scss';
+import './SummaryModal.scss';
 
-interface ViewSummaryProps {
+interface SummaryModalProps {
   path: StorageDirectoryTableData['path'];
   isOpen?: boolean;
   onClose: () => void;
 }
 
-const ViewSummary = ({ isOpen = true, onClose, path }: ViewSummaryProps): JSX.Element => {
+const SummaryModal = ({ isOpen = true, onClose, path }: SummaryModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
   const { data: responseSummary, loading } = useLoadData<ContentSummary>(CONTENT_SUMMARY_API_URL, {
@@ -105,4 +105,4 @@ const ViewSummary = ({ isOpen = true, onClose, path }: ViewSummaryProps): JSX.El
   );
 };
 
-export default ViewSummary;
+export default SummaryModal;

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

@@ -31,6 +31,7 @@ export const SET_REPLICATION_API_URL = '/api/v1/storage/replication/';
 export const DELETION_API_URL = '/api/v1/storage/delete/';
 export const BULK_DELETION_API_URL = '/api/v1/storage/delete/bulk';
 export const COMPRESS_API_URL = '/api/v1/storage/compress';
+export const EXTRACT_API_URL = '/api/v1/storage/extract_archive';
 export const COPY_API_URL = '/api/v1/storage/copy/';
 export const BULK_COPY_API_URL = '/api/v1/storage/copy/bulk/';
 export const MOVE_API_URL = '/api/v1/storage/move/';

+ 2 - 0
desktop/core/src/desktop/js/utils/constants/storageBrowser.ts

@@ -63,3 +63,5 @@ export const SUPPORTED_FILE_EXTENSIONS: Record<string, SupportedFileTypes> = {
 };
 
 export const EDITABLE_FILE_FORMATS = new Set([SupportedFileTypes.TEXT]);
+
+export const SUPPORTED_COMPRESSED_FILE_EXTENTION = ['zip', 'tar.gz', 'tgz', 'bz2', 'bzip'];