From ab690604326c8a6b84093375a227ea58fa61ec67 Mon Sep 17 00:00:00 2001 From: Ram Prasad Agarwal Date: Tue, 21 Jan 2025 01:01:53 +0530 Subject: [PATCH] [ui-storagebrowser] adds extract archive action (#3950) * [ui-storagebrowser] adds extract action * fixes the display message --- .../CompressionModal.scss} | 0 .../CompressionModal.test.tsx} | 19 +- .../CompressionModal.tsx} | 19 +- .../DeletionModal.test.tsx} | 33 ++-- .../DeletionModal.tsx} | 47 ++--- .../ExtractionModal/ExtractionModal.test.tsx | 171 ++++++++++++++++++ .../ExtractionModal/ExtractionModal.tsx | 77 ++++++++ .../MoveCopyModal.test.tsx} | 16 +- .../MoveCopyModal.tsx} | 17 +- .../RenameModal.test.tsx} | 23 ++- .../RenameModal.tsx} | 13 +- .../ReplicationModal.test.tsx} | 20 +- .../ReplicationModal.tsx} | 13 +- .../StorageBrowserActions.tsx | 39 ++-- .../StorageBrowserActions.util.ts | 27 ++- .../SummaryModal.scss} | 0 .../SummaryModal.test.tsx} | 14 +- .../SummaryModal.tsx} | 8 +- .../js/reactComponents/FileChooser/api.ts | 1 + .../js/utils/constants/storageBrowser.ts | 2 + 20 files changed, 409 insertions(+), 150 deletions(-) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Compress/Compress.scss => CompressionModal/CompressionModal.scss} (100%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Compress/Compress.test.tsx => CompressionModal/CompressionModal.test.tsx} (93%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Compress/Compress.tsx => CompressionModal/CompressionModal.tsx} (88%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Delete/Delete.test.tsx => DeletionModal/DeletionModal.test.tsx} (89%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Delete/Delete.tsx => DeletionModal/DeletionModal.tsx} (74%) create mode 100644 desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ExtractionModal/ExtractionModal.test.tsx create mode 100644 desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ExtractionModal/ExtractionModal.tsx rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{MoveCopy/MoveCopy.test.tsx => MoveCopyModal/MoveCopyModal.test.tsx} (97%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{MoveCopy/MoveCopy.tsx => MoveCopyModal/MoveCopyModal.tsx} (89%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Rename/Rename.test.tsx => RenameModal/RenameModal.test.tsx} (90%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Rename/Rename.tsx => RenameModal/RenameModal.tsx} (86%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Replication/Replication.test.tsx => ReplicationModal/ReplicationModal.test.tsx} (90%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{Replication/Replication.tsx => ReplicationModal/ReplicationModal.tsx} (85%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{ViewSummary/ViewSummary.scss => SummaryModal/SummaryModal.scss} (100%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{ViewSummary/ViewSummary.test.tsx => SummaryModal/SummaryModal.test.tsx} (85%) rename desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/{ViewSummary/ViewSummary.tsx => SummaryModal/SummaryModal.tsx} (95%) diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Compress/Compress.scss b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.scss similarity index 100% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Compress/Compress.scss rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.scss diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Compress/Compress.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.test.tsx similarity index 93% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Compress/Compress.test.tsx rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/CompressionModal/CompressionModal.test.tsx index 953ec2e5bda..fbdbb456002 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Compress/Compress.test.tsx +++ b/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( - { it('should display the correct list of files to be compressed', () => { const { getByText } = render( - { it('should call handleCompress with the correct data when "Compress" is clicked', async () => { const { getByText } = render( - { 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( - { it('should call onClose when the modal is closed', () => { const { getByText } = render( - { }); const { getByText } = render( - 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(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; diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Delete/Delete.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/DeletionModal/DeletionModal.test.tsx similarity index 89% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Delete/Delete.test.tsx rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/DeletionModal/DeletionModal.test.tsx index ec9af9deed4..7e06b43a2fb 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Delete/Delete.test.tsx +++ b/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( - { it('should render the Delete modal with the correct title and buttons when trash is not enabled', () => { const { getByText, queryByText, getByRole } = render( - { it('should call handleDeletion with the correct data for single delete when "Delete Permanently" is clicked', async () => { const { getByText } = render( - { 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( - { it('should call handleDeletion with the correct data for trash delete when "Move to Trash" is clicked', async () => { const { getByText } = render( - { 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( - { mockOnError(new Error()); }); const { getByText } = render( - { it('should call onClose when the modal is closed', () => { const { getByText } = render( - 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 ( ({ + __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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + fireEvent.click(getByText('Cancel')); + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('should disable the "Extract" button while loading', () => { + mockLoading = true; + + const { getByRole } = render( + + ); + + expect(getByRole('button', { name: 'Extract' })).toBeDisabled(); + }); +}); diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ExtractionModal/ExtractionModal.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ExtractionModal/ExtractionModal.tsx new file mode 100644 index 00000000000..8b2ed86670f --- /dev/null +++ b/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 ( + + {t('Are you sure you want to extract "{{fileName}}" file?', { fileName: file.name })} + + ); +}; + +export default ExtractionModal; diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopy/MoveCopy.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopyModal/MoveCopyModal.test.tsx similarity index 97% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopy/MoveCopy.test.tsx rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopyModal/MoveCopyModal.test.tsx index b6020f8ff23..90c092ef414 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/MoveCopy/MoveCopy.test.tsx +++ b/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( - { const newDestPath = 'test/path/folder1'; const { getByText } = render( - { it('should call onSuccess when the request succeeds', async () => { mockSave.mockImplementationOnce(mockOnSuccess); const { getByText } = render( - { mockOnError(new Error()); }); const { getByText } = render( - { it('should call onClose when the modal is closed', () => { const { getByText } = render( - { describe('Move Actions', () => { it('should render correctly and open the modal', () => { const { getByText } = render( - { const newDestPath = 'test/path/folder1'; const { getByText } = render( - 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; diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Rename/Rename.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/RenameModal/RenameModal.test.tsx similarity index 90% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Rename/Rename.test.tsx rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/RenameModal/RenameModal.test.tsx index a6a7ac6a3ab..b0d82f0d953 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Rename/Rename.test.tsx +++ b/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( - { it('should call handleRename with the correct data when the form is submitted', async () => { const { getByRole } = render( - { 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( - { mockOnError(new Error()); }); const { getByRole } = render( - { it('should call onClose when the modal is closed', () => { const { getByRole } = render( - 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; diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Replication/Replication.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ReplicationModal/ReplicationModal.test.tsx similarity index 90% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Replication/Replication.test.tsx rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ReplicationModal/ReplicationModal.test.tsx index d492c6b154f..851e51c1bc5 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/Replication/Replication.test.tsx +++ b/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( - { it('should call handleReplication with the correct data when the form is submitted', async () => { const { getByRole } = render( - { 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( - { mockOnError(new Error()); }); const { getByRole } = render( - { it('should call onClose when the modal is closed', () => { const { getByRole } = render( - 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; diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.tsx index fc0b11027ea..e59eabd236f 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.tsx +++ b/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.Replication]: , [ActionType.Delete]: , [ActionType.Summary]: , - [ActionType.Compress]: + [ActionType.Compress]: , + [ActionType.Extract]: }; const StorageBrowserActions = ({ @@ -114,10 +117,10 @@ const StorageBrowserActions = ({ {selectedAction === ActionType.Summary && ( - + )} {selectedAction === ActionType.Rename && ( - )} {selectedAction === ActionType.Replication && ( - )} {(selectedAction === ActionType.Move || selectedAction === ActionType.Copy) && ( - )} {selectedAction === ActionType.Delete && ( - )} {selectedAction === ActionType.Compress && ( - )} + {selectedAction === ActionType.Extract && ( + + )} ); }; diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.util.ts b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.util.ts index d46da3178a2..7b65af73e91 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.util.ts +++ b/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); diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.scss b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.scss similarity index 100% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.scss rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.scss diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.test.tsx similarity index 85% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.test.tsx rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.test.tsx index 273d533a5db..b3d0ff6865c 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.test.tsx +++ b/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; -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( {}} path="some/path" />); + const { getByText } = render( {}} 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( {}} path="some/path" />); + const { getByText, getAllByText } = render( + {}} 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( {}} />); + render( {}} />); 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(); + const { getByText } = render(); const closeButton = getByText('Close'); expect(mockOnClose).not.toHaveBeenCalled(); diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.tsx similarity index 95% rename from desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.tsx rename to desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/SummaryModal/SummaryModal.tsx index 522bb56a6b9..3f4c368f8ac 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/ViewSummary/ViewSummary.tsx +++ b/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(CONTENT_SUMMARY_API_URL, { @@ -105,4 +105,4 @@ const ViewSummary = ({ isOpen = true, onClose, path }: ViewSummaryProps): JSX.El ); }; -export default ViewSummary; +export default SummaryModal; diff --git a/desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts b/desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts index cbe0f7391d6..c26be786008 100644 --- a/desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts +++ b/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/'; diff --git a/desktop/core/src/desktop/js/utils/constants/storageBrowser.ts b/desktop/core/src/desktop/js/utils/constants/storageBrowser.ts index a19428f6e02..2079d6d2982 100644 --- a/desktop/core/src/desktop/js/utils/constants/storageBrowser.ts +++ b/desktop/core/src/desktop/js/utils/constants/storageBrowser.ts @@ -63,3 +63,5 @@ export const SUPPORTED_FILE_EXTENSIONS: Record = { }; export const EDITABLE_FILE_FORMATS = new Set([SupportedFileTypes.TEXT]); + +export const SUPPORTED_COMPRESSED_FILE_EXTENTION = ['zip', 'tar.gz', 'tgz', 'bz2', 'bzip'];