Browse Source

[ui-storagebrowser] adds download action (#3964)

Ram Prasad Agarwal 10 months ago
parent
commit
83337edd77

+ 180 - 60
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.test.tsx

@@ -13,6 +13,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
+
 import React from 'react';
 import React from 'react';
 import { render, screen } from '@testing-library/react';
 import { render, screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import userEvent from '@testing-library/user-event';
@@ -21,50 +22,57 @@ import '@testing-library/jest-dom';
 import StorageBrowserActions from './StorageBrowserActions';
 import StorageBrowserActions from './StorageBrowserActions';
 import { StorageDirectoryTableData } from '../../../../reactComponents/FileChooser/types';
 import { StorageDirectoryTableData } from '../../../../reactComponents/FileChooser/types';
 import { get } from '../../../../api/utils';
 import { get } from '../../../../api/utils';
+import huePubSub from '../../../../utils/huePubSub';
 
 
 jest.mock('../../../../api/utils', () => ({
 jest.mock('../../../../api/utils', () => ({
   get: jest.fn()
   get: jest.fn()
 }));
 }));
 
 
+jest.mock('../../../../utils/huePubSub', () => ({
+  publish: jest.fn()
+}));
+
+const mockLastConfig = {
+  storage_browser: {
+    enable_file_download_button: true,
+    enable_extract_uploaded_archive: true
+  }
+};
+jest.mock('config/hueConfig', () => ({
+  getLastKnownConfig: jest.fn(() => mockLastConfig)
+}));
+
 const mockGet = get as jest.MockedFunction<typeof get>;
 const mockGet = get as jest.MockedFunction<typeof get>;
+
 describe('StorageBrowserRowActions', () => {
 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
   //View summary option is enabled and added to the actions menu when the row data is either hdfs/ofs and a single file
-  const mockRecord: StorageDirectoryTableData = {
-    name: 'test',
-    size: '0\u00a0bytes',
-    user: 'demo',
-    group: 'demo',
-    permission: 'drwxr-xr-x',
-    mtime: 'May 12, 2024 10:37 PM',
-    type: '',
-    path: '',
-    replication: 0
-  };
   const mockTwoRecords: StorageDirectoryTableData[] = [
   const mockTwoRecords: StorageDirectoryTableData[] = [
     {
     {
-      name: 'test',
-      size: '0\u00a0bytes',
+      name: 'test.txt',
+      size: '0 Bytes',
       user: 'demo',
       user: 'demo',
       group: 'demo',
       group: 'demo',
       permission: 'drwxr-xr-x',
       permission: 'drwxr-xr-x',
       mtime: 'May 12, 2024 10:37 PM',
       mtime: 'May 12, 2024 10:37 PM',
       type: 'file',
       type: 'file',
-      path: '',
+      path: '/path/to/folder/test.txt',
       replication: 0
       replication: 0
     },
     },
     {
     {
       name: 'testFolder',
       name: 'testFolder',
-      size: '0\u00a0bytes',
+      size: '0 Bytes',
       user: 'demo',
       user: 'demo',
       group: 'demo',
       group: 'demo',
       permission: 'drwxr-xr-x',
       permission: 'drwxr-xr-x',
       mtime: 'May 12, 2024 10:37 PM',
       mtime: 'May 12, 2024 10:37 PM',
       type: 'dir',
       type: 'dir',
-      path: '',
+      path: '/path/to/folder/testFolder',
       replication: 0
       replication: 0
     }
     }
   ];
   ];
 
 
+  const mockRecord: StorageDirectoryTableData = mockTwoRecords[0];
+
   const setLoadingFiles = jest.fn();
   const setLoadingFiles = jest.fn();
   const onSuccessfulAction = jest.fn();
   const onSuccessfulAction = jest.fn();
 
 
@@ -74,17 +82,21 @@ describe('StorageBrowserRowActions', () => {
     recordType?: string
     recordType?: string
   ) => {
   ) => {
     const user = userEvent.setup();
     const user = userEvent.setup();
-    if (recordPath) {
-      records[0].path = recordPath;
-    }
-    if (recordType) {
-      records[0].type = recordType;
-    }
+    const selectedFiles =
+      records.length === 1
+        ? [
+            {
+              ...records[0],
+              path: recordPath ?? records[0].path,
+              type: recordType ?? records[0].type
+            }
+          ]
+        : records;
     const { getByRole } = render(
     const { getByRole } = render(
       <StorageBrowserActions
       <StorageBrowserActions
         setLoadingFiles={setLoadingFiles}
         setLoadingFiles={setLoadingFiles}
         onSuccessfulAction={onSuccessfulAction}
         onSuccessfulAction={onSuccessfulAction}
-        selectedFiles={records}
+        selectedFiles={selectedFiles}
         currentPath="/path/to/folder"
         currentPath="/path/to/folder"
       />
       />
     );
     );
@@ -117,123 +129,231 @@ describe('StorageBrowserRowActions', () => {
         replication: 3
         replication: 3
       }
       }
     };
     };
-    test('does not render view summary option when there are multiple records selected', async () => {
+
+    it('should not render view summary option when there are multiple records selected', async () => {
       await setUpActionMenu(mockTwoRecords);
       await setUpActionMenu(mockTwoRecords);
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
     });
     });
 
 
-    test('renders view summary option when record is a hdfs file', async () => {
+    it('should render view summary option when record is a hdfs file', async () => {
       await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
       await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
     });
     });
 
 
-    test('renders view summary option when record is a ofs file', async () => {
+    it('should render view summary option when record is a ofs file', async () => {
       await setUpActionMenu([mockRecord], 'ofs://demo/test', 'file');
       await setUpActionMenu([mockRecord], 'ofs://demo/test', 'file');
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
     });
     });
 
 
-    test('does not render view summary option when record is a hdfs folder', async () => {
+    it('should not render view summary option when record is a hdfs folder', async () => {
       await setUpActionMenu([mockRecord], '/user/demo/test', 'dir');
       await setUpActionMenu([mockRecord], '/user/demo/test', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
     });
     });
 
 
-    test('does not render view summary option when record is a an abfs file', async () => {
+    it('should not render view summary option when record is a an abfs file', async () => {
       await setUpActionMenu([mockRecord], 'abfs://demo/test', 'file');
       await setUpActionMenu([mockRecord], 'abfs://demo/test', 'file');
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
     });
     });
-
-    test('renders summary modal when view summary option is clicked', async () => {
-      const user = userEvent.setup();
-      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();
-    });
   });
   });
 
 
   describe('Rename option', () => {
   describe('Rename option', () => {
-    test('does not render rename option when there are multiple records selected', async () => {
+    it('should not render rename option when there are multiple records selected', async () => {
       await setUpActionMenu(mockTwoRecords);
       await setUpActionMenu(mockTwoRecords);
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
     });
     });
-    test('does not render rename option when selected record is a abfs root folder', async () => {
+    it('should not render rename option when selected record is a abfs root folder', async () => {
       await setUpActionMenu([mockRecord], 'abfs://', 'dir');
       await setUpActionMenu([mockRecord], 'abfs://', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
     });
     });
 
 
-    test('does not render rename option when selected record is a gs root folder', async () => {
+    it('should not render rename option when selected record is a gs root folder', async () => {
       await setUpActionMenu([mockRecord], 'gs://', 'dir');
       await setUpActionMenu([mockRecord], 'gs://', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
     });
     });
 
 
-    test('does not render rename option when selected record is a s3 root folder', async () => {
+    it('should not render rename option when selected record is a s3 root folder', async () => {
       await setUpActionMenu([mockRecord], 's3a://', 'dir');
       await setUpActionMenu([mockRecord], 's3a://', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
     });
     });
 
 
-    test('does not render rename option when selected record is a ofs root folder', async () => {
+    it('should not render rename option when selected record is a ofs root folder', async () => {
       await setUpActionMenu([mockRecord], 'ofs://', 'dir');
       await setUpActionMenu([mockRecord], 'ofs://', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
     });
     });
 
 
-    test('does not render rename option when selected record is a ofs service ID folder', async () => {
+    it('should not render rename option when selected record is a ofs service ID folder', async () => {
       await setUpActionMenu([mockRecord], 'ofs://serviceID', 'dir');
       await setUpActionMenu([mockRecord], 'ofs://serviceID', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
     });
     });
 
 
-    test('does not render rename option when selected record is a ofs volume folder', async () => {
+    it('should not render rename option when selected record is a ofs volume folder', async () => {
       await setUpActionMenu([mockRecord], 'ofs://serviceID/volume', 'dir');
       await setUpActionMenu([mockRecord], 'ofs://serviceID/volume', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
     });
     });
 
 
-    test('renders rename option when selected record is a file or a folder', async () => {
+    it('should render rename option when selected record is a file or a folder', async () => {
       await setUpActionMenu([mockRecord], 'abfs://test', 'dir');
       await setUpActionMenu([mockRecord], 'abfs://test', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).not.toBeNull();
       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.getByRole('menuitem', { name: 'Rename' }));
-      expect(await screen.findByText('Enter new name')).toBeInTheDocument();
-    });
   });
   });
 
 
   describe('Set replication option', () => {
   describe('Set replication option', () => {
-    test('does not render set replication option when there are multiple records selected', async () => {
+    it('should not render set replication option when there are multiple records selected', async () => {
       await setUpActionMenu(mockTwoRecords);
       await setUpActionMenu(mockTwoRecords);
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
     });
     });
 
 
-    test('renders set replication option when selected record is a hdfs file', async () => {
+    it('should render set replication option when selected record is a hdfs file', async () => {
       await setUpActionMenu([mockRecord], 'hdfs://test', 'file');
       await setUpActionMenu([mockRecord], 'hdfs://test', 'file');
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).not.toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).not.toBeNull();
     });
     });
 
 
-    test('does not render set replication option when selected record is a hdfs folder', async () => {
+    it('should not render set replication option when selected record is a hdfs folder', async () => {
       await setUpActionMenu([mockRecord], 'hdfs://', 'dir');
       await setUpActionMenu([mockRecord], 'hdfs://', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
     });
     });
 
 
-    test('does not render set replication option when selected record is a gs file/folder', async () => {
+    it('should not render set replication option when selected record is a gs file/folder', async () => {
       await setUpActionMenu([mockRecord], 'gs://', 'dir');
       await setUpActionMenu([mockRecord], 'gs://', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
     });
     });
 
 
-    test('does not render set replication option when selected record is a s3 file/folder', async () => {
+    it('should not render set replication option when selected record is a s3 file/folder', async () => {
       await setUpActionMenu([mockRecord], 's3a://', 'dir');
       await setUpActionMenu([mockRecord], 's3a://', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
     });
     });
 
 
-    test('does not render set replication option when selected record is a ofs file/folder', async () => {
+    it('should not render set replication option when selected record is a ofs file/folder', async () => {
       await setUpActionMenu([mockRecord], 'ofs://', 'dir');
       await setUpActionMenu([mockRecord], 'ofs://', 'dir');
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
       expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
     });
     });
+  });
+
+  describe('Delete option', () => {
+    it('should render delete option for multiple selected records', async () => {
+      await setUpActionMenu(mockTwoRecords, mockRecord.path);
+      expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeNull();
+    });
+
+    it('should render delete option for a single selected file/folder', async () => {
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeNull();
+    });
+  });
+
+  describe('Download option', () => {
+    it('should not render download option for multiple selected records', async () => {
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'Download' })).toBeNull();
+    });
+
+    it('should render download option for a single selected file', async () => {
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Download' })).not.toBeNull();
+    });
+
+    it('should not render download option for a folder', async () => {
+      const mockFolder = { ...mockRecord, type: 'dir' };
+      await setUpActionMenu([mockFolder], '/user/demo/test', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Download' })).toBeNull();
+    });
+
+    it('should not render download option when enable_file_download_button is false', async () => {
+      mockLastConfig.storage_browser.enable_file_download_button = false;
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Download' })).toBeNull();
+      mockLastConfig.storage_browser.enable_file_download_button = true;
+    });
 
 
-    test('renders set replication modal when set replication option is clicked', async () => {
+    it('should trigger file download when download option is clicked for a file', async () => {
       const user = userEvent.setup();
       const user = userEvent.setup();
-      await setUpActionMenu([mockRecord], 'hdfs://test', 'file');
-      await user.click(screen.getByRole('menuitem', { name: 'Set Replication' }));
-      expect(await screen.findByText(/Setting Replication factor/i)).toBeInTheDocument();
+      await setUpActionMenu([mockRecord], mockRecord.path, 'file');
+      await user.click(screen.getByRole('menuitem', { name: 'Download' }));
+      expect(huePubSub.publish).toHaveBeenCalled();
+    });
+  });
+
+  describe('Copy Action', () => {
+    it('should render copy option when record is a file', async () => {
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Copy' })).not.toBeNull();
+    });
+
+    it('should render copy option when record is a folder', async () => {
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Copy' })).not.toBeNull();
+    });
+
+    it('should render copy option when multiple records are selected', async () => {
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'Copy' })).not.toBeNull();
+    });
+  });
+
+  describe('Move Action', () => {
+    it('should render move option when record is a file', async () => {
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Move' })).not.toBeNull();
+    });
+
+    it('should render move option when record is a folder', async () => {
+      await setUpActionMenu([mockRecord], '/user/demo/test', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Move' })).not.toBeNull();
+    });
+
+    it('should render move option when multiple records are selected', async () => {
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'Move' })).not.toBeNull();
+    });
+  });
+
+  describe('Compress Action', () => {
+    it('should render compress option when record is a hdfs file', async () => {
+      await setUpActionMenu([mockRecord], mockRecord.path, mockRecord.type);
+      expect(screen.queryByRole('menuitem', { name: 'Compress' })).not.toBeNull();
+    });
+
+    it('should render compress option when multiple records are hdfs file', async () => {
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'Compress' })).not.toBeNull();
+    });
+
+    it('should render compress option when record is not hdfs file', async () => {
+      await setUpActionMenu([mockRecord], 's3a://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Compress' })).toBeNull();
+    });
+
+    it('should not render compress option when enable_extract_uploaded_archive is false', async () => {
+      mockLastConfig.storage_browser.enable_extract_uploaded_archive = false;
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'Compress' })).toBeNull();
+      mockLastConfig.storage_browser.enable_extract_uploaded_archive = true;
+    });
+  });
+
+  describe('Extract Action', () => {
+    it('should render extract option when record is a compressed file', async () => {
+      mockRecord.path = '/user/demo/test.zip';
+      await setUpActionMenu([mockRecord], '/user/demo/test.zip', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Extract' })).not.toBeNull();
+    });
+
+    it('should not render extract option when multiple records are files', async () => {
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'Extract' })).toBeNull();
+    });
+
+    it('should not render extract option when file type is not supported', async () => {
+      mockRecord.path = '/user/demo/test.zip1';
+      await setUpActionMenu([mockRecord], '/user/demo/test.zip1', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Extract' })).toBeNull();
+    });
+
+    it('should not render extract option when enable_extract_uploaded_archive is false', async () => {
+      mockLastConfig.storage_browser.enable_extract_uploaded_archive = false;
+      await setUpActionMenu([mockRecord], '/user/demo/test.zip', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Extract' })).toBeNull();
+      mockLastConfig.storage_browser.enable_extract_uploaded_archive = true;
     });
     });
   });
   });
 });
 });

+ 17 - 2
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.tsx

@@ -28,6 +28,7 @@ import DataMovementIcon from '@cloudera/cuix-core/icons/react/DataMovementIcon';
 import DeleteIcon from '@cloudera/cuix-core/icons/react/DeleteIcon';
 import DeleteIcon from '@cloudera/cuix-core/icons/react/DeleteIcon';
 import CollapseIcon from '@cloudera/cuix-core/icons/react/CollapseViewIcon';
 import CollapseIcon from '@cloudera/cuix-core/icons/react/CollapseViewIcon';
 import ExpandIcon from '@cloudera/cuix-core/icons/react/ExpandViewIcon';
 import ExpandIcon from '@cloudera/cuix-core/icons/react/ExpandViewIcon';
+import DownloadIcon from '@cloudera/cuix-core/icons/react/DownloadIcon';
 
 
 import { i18nReact } from '../../../../utils/i18nReact';
 import { i18nReact } from '../../../../utils/i18nReact';
 import huePubSub from '../../../../utils/huePubSub';
 import huePubSub from '../../../../utils/huePubSub';
@@ -44,6 +45,7 @@ import SummaryModal from './SummaryModal/SummaryModal';
 import DeletionModal from './DeletionModal/DeletionModal';
 import DeletionModal from './DeletionModal/DeletionModal';
 import CompressionModal from './CompressionModal/CompressionModal';
 import CompressionModal from './CompressionModal/CompressionModal';
 import ExtractionModal from './ExtractionModal/ExtractionModal';
 import ExtractionModal from './ExtractionModal/ExtractionModal';
+import { DOWNLOAD_API_URL } from '../../../../reactComponents/FileChooser/api';
 
 
 interface StorageBrowserRowActionsProps {
 interface StorageBrowserRowActionsProps {
   isTrashEnabled?: boolean;
   isTrashEnabled?: boolean;
@@ -61,7 +63,8 @@ const iconsMap: Record<ActionType, JSX.Element> = {
   [ActionType.Delete]: <DeleteIcon />,
   [ActionType.Delete]: <DeleteIcon />,
   [ActionType.Summary]: <SummaryIcon />,
   [ActionType.Summary]: <SummaryIcon />,
   [ActionType.Compress]: <CollapseIcon />,
   [ActionType.Compress]: <CollapseIcon />,
-  [ActionType.Extract]: <ExpandIcon />
+  [ActionType.Extract]: <ExpandIcon />,
+  [ActionType.Download]: <DownloadIcon />
 };
 };
 
 
 const StorageBrowserActions = ({
 const StorageBrowserActions = ({
@@ -79,6 +82,18 @@ const StorageBrowserActions = ({
     setSelectedAction(undefined);
     setSelectedAction(undefined);
   };
   };
 
 
+  const downloadFile = () => {
+    huePubSub.publish('hue.global.info', { message: t('Downloading your file, Please wait...') });
+    location.href = `${DOWNLOAD_API_URL}${selectedFiles[0]?.path}`;
+  };
+
+  const onActionClick = (actionType: ActionType) => () => {
+    if (actionType === ActionType.Download) {
+      return downloadFile();
+    }
+    setSelectedAction(actionType);
+  };
+
   const onApiSuccess = () => {
   const onApiSuccess = () => {
     setLoadingFiles(false);
     setLoadingFiles(false);
     closeModal();
     closeModal();
@@ -96,7 +111,7 @@ const StorageBrowserActions = ({
       key: String(action.type),
       key: String(action.type),
       label: t(action.label),
       label: t(action.label),
       icon: iconsMap[action.type],
       icon: iconsMap[action.type],
-      onClick: () => setSelectedAction(action.type)
+      onClick: onActionClick(action.type)
     }));
     }));
   }, [selectedFiles]);
   }, [selectedFiles]);
 
 

+ 12 - 4
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageBrowserActions/StorageBrowserActions.util.ts

@@ -43,7 +43,8 @@ export enum ActionType {
   Replication = 'replication',
   Replication = 'replication',
   Delete = 'delete',
   Delete = 'delete',
   Compress = 'compress',
   Compress = 'compress',
-  Extract = 'extract'
+  Extract = 'extract',
+  Download = 'download'
 }
 }
 
 
 const isValidFileOrFolder = (filePath: string): boolean => {
 const isValidFileOrFolder = (filePath: string): boolean => {
@@ -61,7 +62,7 @@ const isFileCompressed = (filePath: string): boolean => {
 };
 };
 
 
 const isActionEnabled = (file: StorageDirectoryTableData, action: ActionType): boolean => {
 const isActionEnabled = (file: StorageDirectoryTableData, action: ActionType): boolean => {
-  const config = getLastKnownConfig();
+  const config = getLastKnownConfig()?.storage_browser;
   switch (action) {
   switch (action) {
     case ActionType.Summary:
     case ActionType.Summary:
       return (isHDFS(file.path) || isOFS(file.path)) && file.type === BrowserViewType.file;
       return (isHDFS(file.path) || isOFS(file.path)) && file.type === BrowserViewType.file;
@@ -74,12 +75,14 @@ const isActionEnabled = (file: StorageDirectoryTableData, action: ActionType): b
       return isValidFileOrFolder(file.path);
       return isValidFileOrFolder(file.path);
     case ActionType.Extract:
     case ActionType.Extract:
       return (
       return (
-        !!config?.storage_browser.enable_extract_uploaded_archive &&
+        !!config?.enable_extract_uploaded_archive &&
         isHDFS(file.path) &&
         isHDFS(file.path) &&
         isFileCompressed(file.path)
         isFileCompressed(file.path)
       );
       );
     case ActionType.Compress:
     case ActionType.Compress:
-      return !!config?.storage_browser.enable_extract_uploaded_archive && isHDFS(file.path);
+      return !!config?.enable_extract_uploaded_archive && isHDFS(file.path);
+    case ActionType.Download:
+      return !!config?.enable_file_download_button && file.type === BrowserViewType.file;
     default:
     default:
       return false;
       return false;
   }
   }
@@ -153,6 +156,11 @@ export const getEnabledActions = (
       enabled: isSingleFileActionEnabled(files, ActionType.Extract),
       enabled: isSingleFileActionEnabled(files, ActionType.Extract),
       type: ActionType.Extract,
       type: ActionType.Extract,
       label: 'Extract'
       label: 'Extract'
+    },
+    {
+      enabled: isSingleFileActionEnabled(files, ActionType.Download),
+      type: ActionType.Download,
+      label: 'Download'
     }
     }
   ].filter(e => e.enabled);
   ].filter(e => e.enabled);