Pārlūkot izejas kodu

[ui-storageBrowser] Implement storage browser actions (#3877)

* [ui-storageBrowser] Improvements on storage browser content summary

* [ui-storageBrowser] Update create new files based on new public api

* [ui-storageBrowser] Update create new folders based on new public api

* [ui-storageBrowser] Update rename to use useSaveData hook

* [ui-storageBrowser] Implement UI for set replication

* [ui-storageBrowser] Added tests for set replication action

* [ui-storageBrowser] Improve storage browser actions using useMemo

* [ui-storageBrowser] Implement UI for copy action in storage browser

* [ui-storageBrowser] Implement UI for move action in storage browser
Nidhi Bhat G 11 mēneši atpakaļ
vecāks
revīzija
fec209e120

+ 88 - 0
desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.scss

@@ -0,0 +1,88 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+@use 'variables' as vars;
+@use 'mixins';
+
+$cell-height: 20px;
+$table-placeholder-height: 100px;
+$icon-margin: 5px;
+
+.antd.cuix {
+  .hue-filechooser-modal__body{
+    height:350px;
+  }
+
+  .hue-filechooser-modal__path-browser-panel {
+    display: flex;
+    align-items: center;
+    gap: vars.$fluidx-spacing-xs;
+  }
+
+  .hue-filechooser-modal__destPath {
+    flex: 0 0 auto;
+     font-weight: 600;
+  }
+
+  .hue-filechooser-modal__search {
+    margin: vars.$fluidx-spacing-xs 0;
+    width: 50%;
+
+    .ant-input {
+      box-shadow: none;
+    }
+  }
+
+  .hue-filechooser-modal__table {
+    .ant-table-placeholder {
+      height: $table-placeholder-height;
+      text-align: center;
+    }
+
+    th.ant-table-cell {
+      height: $cell-height;
+      border: none;
+    }
+  }
+
+  .hue-filechooser-modal__table-cell-icon {
+    color: vars.$fluidx-blue-700;
+    margin-right: 6px;
+  }
+
+  .hue-filechooser-modal__table-row {
+    :hover {
+      cursor: pointer;
+    }
+
+    td.ant-table-cell {
+      height: $cell-height;
+      padding: 2px;
+      @include mixins.nowrap-ellipsis;
+    }  
+  }
+
+  .disabled-row {
+    pointer-events: none;
+  }  
+
+  .ant-table-tbody > tr > td {
+    border: none
+  }
+
+  .hue-filechooser-modal__table-cell-name {
+    color: vars.$fluidx-blue-700;
+  }
+}

+ 107 - 0
desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.test.tsx

@@ -0,0 +1,107 @@
+// 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, screen, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+import FileChooserModal from './FileChooserModal';
+import { get } from '../../../api/utils';
+
+// Mock the `get` function
+jest.mock('../../../api/utils', () => ({
+  get: jest.fn()
+}));
+
+const mockGet = get as jest.MockedFunction<typeof get>;
+
+const mockData = [
+  {
+    name: 'test',
+    path: 'user/test',
+    type: 'file'
+  },
+  {
+    name: 'testFolder',
+    path: 'user/testFolder',
+    type: 'dir'
+  }
+];
+
+describe('FileChooserModal', () => {
+  beforeAll(() => {
+    jest.clearAllMocks();
+  });
+
+  beforeEach(() => {
+    mockGet.mockResolvedValue(mockData);
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  test('renders the modal with basic props', async () => {
+    render(
+      <FileChooserModal
+        showModal={true}
+        onClose={() => {}}
+        onSubmit={() => {}}
+        title="Select File"
+        sourcePath="/path/to/source"
+      />
+    );
+    await waitFor(() => {
+      expect(screen.getByText('Select File')).toBeInTheDocument();
+      expect(screen.getByText('Destination Path:')).toBeInTheDocument();
+      expect(screen.getByText('Cancel')).toBeInTheDocument();
+      expect(screen.getByText('Submit')).toBeInTheDocument();
+    });
+  });
+
+  test('displays empty message if there are no files in the directory', async () => {
+    mockGet.mockResolvedValue([]);
+    render(
+      <FileChooserModal
+        showModal={true}
+        onClose={() => {}}
+        onSubmit={() => {}}
+        title="Select File"
+        sourcePath="/path/to/source"
+      />
+    );
+    await waitFor(() => {
+      expect(screen.getByText('Folder is empty')).toBeInTheDocument();
+    });
+  });
+
+  test('Submit button is disabled if destination path is same as source path', async () => {
+    const mockOnSubmit = jest.fn();
+    render(
+      <FileChooserModal
+        showModal={true}
+        onClose={() => {}}
+        onSubmit={mockOnSubmit}
+        title="Select File"
+        sourcePath="/path/to/source"
+      />
+    );
+
+    const submitButton = screen.getByRole('button', { name: 'Submit' });
+    await waitFor(() => {
+      expect(submitButton).toBeDisabled();
+    });
+  });
+});

+ 191 - 0
desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.tsx

@@ -0,0 +1,191 @@
+// 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, { useCallback, useEffect, useMemo, useState } from 'react';
+import Modal from 'cuix/dist/components/Modal';
+import { Input, Spin, Tooltip } from 'antd';
+import Table, { ColumnProps } from 'cuix/dist/components/Table';
+import classNames from 'classnames';
+
+import FolderIcon from '@cloudera/cuix-core/icons/react/ProjectIcon';
+import { FileOutlined } from '@ant-design/icons';
+
+import { i18nReact } from '../../../utils/i18nReact';
+import useDebounce from '../../../utils/useDebounce';
+import useLoadData from '../../../utils/hooks/useLoadData';
+
+import { PathAndFileData } from '../../../reactComponents/FileChooser/types';
+import { VIEWFILES_API_URl } from '../../../reactComponents/FileChooser/api';
+import PathBrowser from '../../../reactComponents/FileChooser/PathBrowser/PathBrowser';
+
+import './FileChooserModal.scss';
+
+interface FileChooserModalProps {
+  onClose: () => void;
+  onSubmit: (destination_path: string) => void;
+  showModal: boolean;
+  title: string;
+  sourcePath: string;
+  submitText?: string;
+  cancelText?: string;
+}
+
+interface FileChooserTableData {
+  name: string;
+  path: string;
+  type: string;
+}
+
+const FileChooserModal = ({
+  showModal,
+  onClose,
+  onSubmit,
+  title,
+  sourcePath,
+  ...i18n
+}: FileChooserModalProps): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+  const { cancelText = t('Cancel'), submitText = t('Submit') } = i18n;
+  const [destPath, setDestPath] = useState<string>(sourcePath);
+  const [searchTerm, setSearchTerm] = useState<string>('');
+
+  useEffect(() => {
+    setDestPath(sourcePath);
+  }, [sourcePath]);
+
+  const { data: filesData, loading } = useLoadData<PathAndFileData>(
+    `${VIEWFILES_API_URl}${destPath}`,
+    {
+      params: {
+        pagesize: '1000',
+        filter: searchTerm
+      },
+      skip: destPath === '' || destPath === undefined
+    }
+  );
+
+  const tableData: FileChooserTableData[] = useMemo(() => {
+    if (!filesData?.files) {
+      return [];
+    }
+
+    return filesData?.files?.map(file => ({
+      name: file.name,
+      path: file.path,
+      type: file.type
+    }));
+  }, [filesData]);
+
+  const handleSearch = useCallback(
+    useDebounce(searchTerm => {
+      setSearchTerm(encodeURIComponent(searchTerm));
+    }),
+    [setSearchTerm]
+  );
+
+  const getColumns = (file: FileChooserTableData) => {
+    const columns: ColumnProps<FileChooserTableData>[] = [];
+    for (const key of Object.keys(file)) {
+      const column: ColumnProps<FileChooserTableData> = {
+        dataIndex: key,
+        key: `${key}`
+      };
+      if (key === 'name') {
+        column.render = (_, record: FileChooserTableData) => (
+          <Tooltip title={record.name} mouseEnterDelay={1.5}>
+            <span className="hue-filechooser-modal__table-cell-icon">
+              {record.type === 'dir' ? <FolderIcon /> : <FileOutlined />}
+            </span>
+            <span className="hue-filechooser-modal__table-cell-name">{record.name}</span>
+          </Tooltip>
+        );
+      }
+      columns.push(column);
+    }
+    return columns.filter(col => col.dataIndex !== 'type' && col.dataIndex !== 'path');
+  };
+
+  const onRowClicked = (record: FileChooserTableData) => {
+    return {
+      onClick: () => {
+        if (record.type === 'dir') {
+          setDestPath(record.path);
+        }
+      }
+    };
+  };
+
+  const locale = {
+    emptyText: t('Folder is empty')
+  };
+
+  return (
+    <Modal
+      cancelText={cancelText}
+      className="hue-filechooser-modal cuix antd"
+      okText={submitText}
+      title={title}
+      open={showModal}
+      onCancel={() => {
+        onClose();
+      }}
+      onOk={() => {
+        onSubmit(destPath);
+        onClose();
+      }}
+      okButtonProps={{ disabled: sourcePath === destPath }}
+    >
+      <div className="hue-filechooser-modal__body">
+        <div className="hue-filechooser-modal__path-browser-panel">
+          <span className="hue-filechooser-modal__destPath">{t('Destination Path:')}</span>
+          <PathBrowser
+            breadcrumbs={filesData?.breadcrumbs}
+            onFilepathChange={setDestPath}
+            seperator={'/'}
+            showIcon={false}
+          />
+        </div>
+        <Input
+          className="hue-filechooser-modal__search"
+          placeholder={t('Search')}
+          allowClear={true}
+          onChange={event => {
+            handleSearch(event.target.value);
+          }}
+        />
+        <Spin spinning={loading}>
+          <Table
+            className="hue-filechooser-modal__table"
+            dataSource={tableData?.slice(2)}
+            pagination={false}
+            columns={getColumns(tableData[0] ?? {})}
+            rowKey={(record, index) => record.path + index}
+            scroll={{ y: '250px' }}
+            rowClassName={record =>
+              record.type === 'file'
+                ? classNames('hue-filechooser-modal__table-row', 'disabled-row')
+                : 'hue-filechooser-modal__table-row'
+            }
+            onRow={onRowClicked}
+            locale={locale}
+            showHeader={false}
+          />
+        </Spin>
+      </div>
+    </Modal>
+  );
+};
+
+export default FileChooserModal;

+ 67 - 1
desktop/core/src/desktop/js/apps/storageBrowser/InputModal/InputModal.test.tsx

@@ -31,6 +31,8 @@ describe('InputModal', () => {
         showModal={true}
         onSubmit={jest.fn()}
         onClose={jest.fn()}
+        initialValue={''}
+        inputType={'text'}
       />
     );
     expect(inputModal.getByText('Custom title')).toBeVisible();
@@ -45,6 +47,8 @@ describe('InputModal', () => {
         showModal={true}
         onSubmit={jest.fn()}
         onClose={jest.fn()}
+        initialValue={''}
+        inputType={'text'}
       />
     );
     expect(inputModal.getByText('Custom input label')).toBeVisible();
@@ -62,6 +66,8 @@ describe('InputModal', () => {
         showModal={true}
         onSubmit={onSubmitMock}
         onClose={onCloseMock}
+        initialValue={''}
+        inputType={'text'}
       />
     );
     const submitButton = screen.getByRole('button', { name: 'Create' });
@@ -83,6 +89,8 @@ describe('InputModal', () => {
         showModal={true}
         onSubmit={onSubmitMock}
         onClose={onCloseMock}
+        initialValue={''}
+        inputType={'text'}
       />
     );
     const submitButton = screen.getByRole('button', { name: 'Create' });
@@ -104,6 +112,8 @@ describe('InputModal', () => {
         showModal={true}
         onSubmit={onSubmitMock}
         onClose={onCloseMock}
+        initialValue={''}
+        inputType={'text'}
       />
     );
     const closeButton = screen.getByRole('button', { name: 'Cancel' });
@@ -122,6 +132,8 @@ describe('InputModal', () => {
         showModal={true}
         onSubmit={jest.fn()}
         onClose={jest.fn()}
+        initialValue={''}
+        inputType={'text'}
       />
     );
     const input = screen.getByRole('textbox');
@@ -129,7 +141,25 @@ describe('InputModal', () => {
     expect(input).toBeVisible();
   });
 
-  test('renders modal with empty input value', () => {
+  test('renders modal with number input when input type is number', () => {
+    render(
+      <InputModal
+        title={'Set replication'}
+        inputLabel={'Custom input label'}
+        submitText={'Submit'}
+        showModal={true}
+        onSubmit={jest.fn()}
+        onClose={jest.fn()}
+        initialValue={2}
+        inputType={'number'}
+      />
+    );
+    const input = screen.getByRole('spinbutton');
+    expect(input).toHaveClass('hue-input-modal__input');
+    expect(input).toBeVisible();
+  });
+
+  test('renders modal with empty input value when intial value is empty', () => {
     render(
       <InputModal
         title={'Create New File'}
@@ -138,11 +168,45 @@ describe('InputModal', () => {
         showModal={true}
         onSubmit={jest.fn()}
         onClose={jest.fn()}
+        initialValue={''}
+        inputType={'text'}
       />
     );
     expect(screen.getByRole('textbox')).toHaveValue('');
   });
 
+  test('renders modal with intial value in input while input type is text', () => {
+    render(
+      <InputModal
+        title={'Create New File'}
+        inputLabel={'Custom input label'}
+        submitText={'Create'}
+        showModal={true}
+        onSubmit={jest.fn()}
+        onClose={jest.fn()}
+        initialValue={'hello'}
+        inputType={'text'}
+      />
+    );
+    expect(screen.getByRole('textbox')).toHaveValue('hello');
+  });
+
+  test('renders modal with intial value in input while input type is number', () => {
+    render(
+      <InputModal
+        title={'Create New File'}
+        inputLabel={'Custom input label'}
+        submitText={'Create'}
+        showModal={true}
+        onSubmit={jest.fn()}
+        onClose={jest.fn()}
+        initialValue={2}
+        inputType={'number'}
+      />
+    );
+    expect(screen.getByRole('spinbutton')).toHaveValue(2);
+  });
+
   test('accepts tab focus on input elements placed in the drawer', async () => {
     const user = userEvent.setup();
     render(
@@ -153,6 +217,8 @@ describe('InputModal', () => {
         showModal={true}
         onSubmit={jest.fn()}
         onClose={jest.fn()}
+        initialValue={''}
+        inputType={'text'}
       />
     );
     const inputModal = screen.getByRole('dialog', { name: 'Create New File' });

+ 15 - 9
desktop/core/src/desktop/js/apps/storageBrowser/InputModal/InputModal.tsx

@@ -13,7 +13,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import Modal from 'cuix/dist/components/Modal';
 import { Input } from 'antd';
 
@@ -21,41 +21,46 @@ import { i18nReact } from '../../../utils/i18nReact';
 
 import './InputModal.scss';
 
-//TODO: Add unit tests
 interface InputModalProps {
   cancelText?: string;
+  initialValue: string | number;
   inputLabel: string;
-  submitText?: string;
+  inputType: 'text' | 'number';
   onClose: () => void;
-  onSubmit: (value: string) => void;
+  onSubmit: (value: string | number) => void;
+  submitText?: string;
   showModal: boolean;
   title: string;
 }
 
 const InputModal = ({
   inputLabel,
+  inputType,
+  initialValue,
   onClose,
   onSubmit,
   showModal,
   title,
   ...i18n
 }: InputModalProps): JSX.Element => {
-  const [value, setValue] = useState<string>('');
+  const [value, setValue] = useState<string | number>(initialValue);
   const { t } = i18nReact.useTranslation();
   const { cancelText = t('Cancel'), submitText = t('Submit') } = i18n;
 
+  useEffect(() => {
+    setValue(initialValue);
+  }, [initialValue]);
+
   return (
     <Modal
       cancelText={cancelText}
       className="hue-input-modal cuix antd"
       okText={submitText}
       onCancel={() => {
-        setValue('');
         onClose();
       }}
       onOk={() => {
         onSubmit(value);
-        setValue('');
         onClose();
       }}
       open={showModal}
@@ -65,8 +70,9 @@ const InputModal = ({
       <Input
         className="hue-input-modal__input"
         value={value}
-        onInput={e => {
-          setValue((e.target as HTMLInputElement).value);
+        type={inputType}
+        onChange={e => {
+          setValue(e.target.value);
         }}
       />
     </Modal>

+ 66 - 25
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserActions/StorageBrowserActions.test.tsx

@@ -19,13 +19,14 @@ import userEvent from '@testing-library/user-event';
 import '@testing-library/jest-dom';
 
 import StorageBrowserActions from './StorageBrowserActions';
-import {
-  StorageBrowserTableData,
-  ContentSummary
-} from '../../../../reactComponents/FileChooser/types';
-import * as StorageBrowserApi from '../../../../reactComponents/FileChooser/api';
-import { CancellablePromise } from '../../../../api/cancellablePromise';
+import { StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types';
+import { get } from '../../../../api/utils';
 
+jest.mock('../../../../api/utils', () => ({
+  get: jest.fn()
+}));
+
+const mockGet = get as jest.MockedFunction<typeof get>;
 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
   const mockRecord: StorageBrowserTableData = {
@@ -36,7 +37,8 @@ describe('StorageBrowserRowActions', () => {
     permission: 'drwxr-xr-x',
     mtime: 'May 12, 2024 10:37 PM',
     type: '',
-    path: ''
+    path: '',
+    replication: 0
   };
   const mockTwoRecords: StorageBrowserTableData[] = [
     {
@@ -47,7 +49,8 @@ describe('StorageBrowserRowActions', () => {
       permission: 'drwxr-xr-x',
       mtime: 'May 12, 2024 10:37 PM',
       type: 'file',
-      path: ''
+      path: '',
+      replication: 0
     },
     {
       name: 'testFolder',
@@ -57,7 +60,8 @@ describe('StorageBrowserRowActions', () => {
       permission: 'drwxr-xr-x',
       mtime: 'May 12, 2024 10:37 PM',
       type: 'dir',
-      path: ''
+      path: '',
+      replication: 0
     }
   ];
 
@@ -87,9 +91,19 @@ describe('StorageBrowserRowActions', () => {
   };
 
   describe('Summary option', () => {
-    let summaryApiMock;
+    beforeAll(() => {
+      jest.clearAllMocks();
+    });
 
-    const mockSummaryData = {
+    beforeEach(() => {
+      mockGet.mockResolvedValue(mockSummary);
+    });
+
+    afterEach(() => {
+      jest.clearAllMocks();
+    });
+
+    const mockSummary = {
       summary: {
         directoryCount: 0,
         ecPolicy: 'Replicated',
@@ -102,17 +116,6 @@ describe('StorageBrowserRowActions', () => {
         replication: 3
       }
     };
-
-    const setUpMock = () => {
-      summaryApiMock = jest
-        .spyOn(StorageBrowserApi, 'fetchContentSummary')
-        .mockReturnValue(CancellablePromise.resolve<ContentSummary>(mockSummaryData));
-    };
-
-    afterEach(() => {
-      summaryApiMock?.mockClear();
-    });
-
     test('does not render view summary option when there are multiple records selected', async () => {
       await setUpActionMenu(mockTwoRecords);
       expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
@@ -140,15 +143,14 @@ describe('StorageBrowserRowActions', () => {
 
     test('renders summary modal when view summary option is clicked', async () => {
       const user = userEvent.setup();
-      setUpMock();
       await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
-      await user.click(screen.queryByRole('menuitem', { name: 'View Summary' }));
+      await user.click(screen.queryByRole('menuitem', { name: 'View Summary' })!);
       expect(await screen.findByText('Summary for /user/demo/test')).toBeInTheDocument();
     });
   });
 
   describe('Rename option', () => {
-    test('does not render view summary option when there are multiple records selected', async () => {
+    test('does not render rename option when there are multiple records selected', async () => {
       await setUpActionMenu(mockTwoRecords);
       expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
     });
@@ -194,4 +196,43 @@ describe('StorageBrowserRowActions', () => {
       expect(await screen.findByText('Enter new name here')).toBeInTheDocument();
     });
   });
+
+  describe('Set replication option', () => {
+    test('does not render set replication option when there are multiple records selected', async () => {
+      await setUpActionMenu(mockTwoRecords);
+      expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
+    });
+
+    test('renders set replication option when selected record is a hdfs file', async () => {
+      await setUpActionMenu([mockRecord], 'hdfs://test', 'file');
+      expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).not.toBeNull();
+    });
+
+    test('does not render set replication option when selected record is a hdfs folder', async () => {
+      await setUpActionMenu([mockRecord], 'hdfs://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
+    });
+
+    test('does not render set replication option when selected record is a gs file/folder', async () => {
+      await setUpActionMenu([mockRecord], 'gs://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
+    });
+
+    test('does not render set replication option when selected record is a s3 file/folder', async () => {
+      await setUpActionMenu([mockRecord], 's3a://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
+    });
+
+    test('does not render set replication option when selected record is a ofs file/folder', async () => {
+      await setUpActionMenu([mockRecord], 'ofs://', 'dir');
+      expect(screen.queryByRole('menuitem', { name: 'Set Replication' })).toBeNull();
+    });
+
+    test('renders set replication modal when set replication option is clicked', async () => {
+      const user = userEvent.setup();
+      await setUpActionMenu([mockRecord], 'hdfs://test', 'file');
+      await user.click(screen.queryByRole('menuitem', { name: 'Set Replication' }));
+      expect(await screen.findByText(/Setting Replication factor/i)).toBeInTheDocument();
+    });
+  });
 });

+ 213 - 29
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserActions/StorageBrowserActions.tsx

@@ -14,13 +14,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import React, { useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
 import { Dropdown } from 'antd';
 import { MenuItemType } from 'antd/lib/menu/hooks/useItems';
 
 import Button from 'cuix/dist/components/Button';
 import DropDownIcon from '@cloudera/cuix-core/icons/react/DropdownIcon';
 import InfoIcon from '@cloudera/cuix-core/icons/react/InfoIcon';
+import DuplicateIcon from '@cloudera/cuix-core/icons/react/DuplicateIcon';
+import CopyClipboardIcon from '@cloudera/cuix-core/icons/react/CopyClipboardIcon';
 
 import { i18nReact } from '../../../../utils/i18nReact';
 import { StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types';
@@ -38,59 +40,132 @@ import {
   isS3,
   isOFSRoot
 } from '../../../../utils/storageBrowserUtils';
-import { rename } from '../../../../reactComponents/FileChooser/api';
+import {
+  RENAME_API_URL,
+  SET_REPLICATION_API_URL,
+  BULK_COPY_API_URL,
+  BULK_MOVE_API_URL
+} from '../../../../reactComponents/FileChooser/api';
 import huePubSub from '../../../../utils/huePubSub';
+import useSaveData from '../../../../utils/hooks/useSaveData';
 
 import SummaryModal from '../../SummaryModal/SummaryModal';
 import InputModal from '../../InputModal/InputModal';
+import FileChooserModal from '../../FileChooserModal/FileChooserModal';
 
 import './StorageBrowserActions.scss';
 
 interface StorageBrowserRowActionsProps {
+  currentPath: string;
   selectedFiles: StorageBrowserTableData[];
   onSuccessfulAction: () => void;
   setLoadingFiles: (value: boolean) => void;
 }
 
 const StorageBrowserActions = ({
+  currentPath,
   selectedFiles,
-  setLoadingFiles,
-  onSuccessfulAction
+  onSuccessfulAction,
+  setLoadingFiles
 }: StorageBrowserRowActionsProps): JSX.Element => {
   const [showSummaryModal, setShowSummaryModal] = useState<boolean>(false);
   const [showRenameModal, setShowRenameModal] = useState<boolean>(false);
   const [selectedFile, setSelectedFile] = useState<string>('');
+  const [showReplicationModal, setShowReplicationModal] = useState<boolean>(false);
+  const [showCopyModal, setShowCopyModal] = useState<boolean>(false);
+  const [showMoveModal, setShowMoveModal] = useState<boolean>(false);
 
   const { t } = i18nReact.useTranslation();
 
-  const handleRename = (newName: string) => {
+  const { error: renameError, save: saveRename } = useSaveData(RENAME_API_URL);
+
+  const handleRename = (value: string) => {
     setLoadingFiles(true);
-    rename(selectedFile, newName)
-      .then(() => {
+    saveRename(
+      { source_path: selectedFile, destination_path: value },
+      {
+        onSuccess: () => {
+          setLoadingFiles(false);
+          onSuccessfulAction();
+        },
+        onError: () => {
+          huePubSub.publish('hue.error', renameError);
+          setLoadingFiles(false);
+        }
+      }
+    );
+  };
+
+  const { error: replicationError, save: saveReplication } = useSaveData(SET_REPLICATION_API_URL);
+  const handleReplication = (replicationFactor: number) => {
+    saveReplication(
+      { path: selectedFile, replication_factor: replicationFactor },
+      {
+        onSuccess: () => onSuccessfulAction(),
+        onError: () => {
+          huePubSub.publish('hue.error', replicationError);
+        }
+      }
+    );
+  };
+
+  const { error: bulkCopyError, save: saveBulkCopy } = useSaveData(BULK_COPY_API_URL, {
+    postOptions: {
+      qsEncodeData: false,
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    }
+  });
+
+  const handleCopy = (destination_path: string) => {
+    setLoadingFiles(true);
+    const formData = new FormData();
+    selectedFiles.map(selectedFile => {
+      formData.append('source_path', selectedFile.path);
+    });
+    formData.append('destination_path', destination_path);
+    saveBulkCopy(formData, {
+      onSuccess: () => {
+        setLoadingFiles(false);
         onSuccessfulAction();
-      })
-      .catch(error => {
-        huePubSub.publish('hue.error', error);
-        setShowRenameModal(false);
-      })
-      .finally(() => {
+      },
+      onError: () => {
+        huePubSub.publish('hue.error', bulkCopyError);
         setLoadingFiles(false);
-      });
+      }
+    });
   };
 
-  const isSummaryEnabled = () => {
-    if (selectedFiles.length !== 1) {
-      return false;
+  const { error: bulkMoveError, save: saveBulkMove } = useSaveData(BULK_MOVE_API_URL, {
+    postOptions: {
+      qsEncodeData: false,
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
     }
-    const selectedFile = selectedFiles[0];
-    return (isHDFS(selectedFile.path) || isOFS(selectedFile.path)) && selectedFile.type === 'file';
+  });
+
+  const handleMove = (destination_path: string) => {
+    setLoadingFiles(true);
+    const formData = new FormData();
+    selectedFiles.map(selectedFile => {
+      formData.append('source_path', selectedFile.path);
+    });
+    formData.append('destination_path', destination_path);
+    saveBulkMove(formData, {
+      onSuccess: () => {
+        setLoadingFiles(false);
+        onSuccessfulAction();
+      },
+      onError: () => {
+        huePubSub.publish('hue.error', bulkMoveError);
+        setLoadingFiles(false);
+      }
+    });
   };
 
-  const isRenameEnabled = () => {
-    if (selectedFiles.length !== 1) {
-      return false;
-    }
-    const selectedFilePath = selectedFiles[0].path;
+  const isValidFileOrFolder = (selectedFilePath: string) => {
     return (
       isHDFS(selectedFilePath) ||
       (isS3(selectedFilePath) && !isS3Root(selectedFilePath)) ||
@@ -103,10 +178,72 @@ const StorageBrowserActions = ({
     );
   };
 
-  const getActions = () => {
+  const isSummaryEnabled = useMemo(() => {
+    if (selectedFiles.length !== 1) {
+      return false;
+    }
+    const selectedFile = selectedFiles[0];
+    return (isHDFS(selectedFile.path) || isOFS(selectedFile.path)) && selectedFile.type === 'file';
+  }, [selectedFiles]);
+
+  const isRenameEnabled = useMemo(() => {
+    if (selectedFiles.length !== 1) {
+      return false;
+    }
+    const selectedFilePath = selectedFiles[0].path;
+    return isValidFileOrFolder(selectedFilePath);
+  }, [selectedFiles]);
+
+  const isReplicationEnabled = useMemo(() => {
+    if (selectedFiles.length !== 1) {
+      return false;
+    }
+    const selectedFile = selectedFiles[0];
+    return isHDFS(selectedFile.path) && selectedFile.type === 'file';
+  }, [selectedFiles]);
+
+  const isCopyEnabled = useMemo(() => {
+    if (selectedFiles.length > 0) {
+      const selectedFilePath = selectedFiles[0].path;
+      return isValidFileOrFolder(selectedFilePath);
+    }
+    return false;
+  }, [selectedFiles]);
+
+  const isMoveEnabled = useMemo(() => {
+    if (selectedFiles.length > 0) {
+      const selectedFilePath = selectedFiles[0].path;
+      return isValidFileOrFolder(selectedFilePath);
+    }
+    return false;
+  }, [selectedFiles]);
+
+  const getActions = useCallback(() => {
     const actions: MenuItemType[] = [];
     if (selectedFiles && selectedFiles.length > 0 && !inTrash(selectedFiles[0].path)) {
-      if (isSummaryEnabled()) {
+      if (isCopyEnabled) {
+        actions.push({
+          key: 'copy',
+          icon: <CopyClipboardIcon />,
+          label: t('Copy'),
+          onClick: () => {
+            setSelectedFile(selectedFiles[0].path);
+            setShowCopyModal(true);
+          }
+        });
+      }
+      if (isMoveEnabled) {
+        actions.push({
+          key: 'move',
+          icon: <CopyClipboardIcon />,
+          label: t('Move'),
+          onClick: () => {
+            setSelectedFile(selectedFiles[0].path);
+            setShowMoveModal(true);
+          }
+        });
+      }
+      if (isSummaryEnabled) {
         actions.push({
           key: 'content_summary',
           icon: <InfoIcon />,
@@ -117,7 +254,7 @@ const StorageBrowserActions = ({
           }
         });
       }
-      if (isRenameEnabled()) {
+      if (isRenameEnabled) {
         actions.push({
           key: 'rename',
           icon: <InfoIcon />,
@@ -128,9 +265,27 @@ const StorageBrowserActions = ({
           }
         });
       }
+      if (isReplicationEnabled) {
+        actions.push({
+          key: 'setReplication',
+          icon: <DuplicateIcon />,
+          label: t('Set Replication'),
+          onClick: () => {
+            setSelectedFile(selectedFiles[0].path);
+            setShowReplicationModal(true);
+          }
+        });
+      }
     }
     return actions;
-  };
+  }, [
+    selectedFiles,
+    isSummaryEnabled,
+    isRenameEnabled,
+    isReplicationEnabled,
+    isCopyEnabled,
+    currentPath
+  ]);
 
   return (
     <>
@@ -140,7 +295,8 @@ const StorageBrowserActions = ({
           items: getActions(),
           className: 'hue-storage-browser__table-actions-menu'
         }}
-        trigger={['click', 'hover']}
+        trigger={['click']}
+        disabled={getActions().length === 0 ? true : false}
       >
         <Button data-event="">
           {t('Actions')}
@@ -159,6 +315,34 @@ const StorageBrowserActions = ({
         showModal={showRenameModal}
         onSubmit={handleRename}
         onClose={() => setShowRenameModal(false)}
+        inputType="text"
+        initialValue={selectedFiles[0]?.name}
+      />
+      <InputModal
+        title={'Setting Replication factor for: ' + selectedFile}
+        inputLabel={t('Replication factor:')}
+        submitText={t('Submit')}
+        showModal={showReplicationModal}
+        onSubmit={handleReplication}
+        onClose={() => setShowReplicationModal(false)}
+        inputType="number"
+        initialValue={selectedFiles[0]?.replication}
+      />
+      <FileChooserModal
+        onClose={() => setShowCopyModal(false)}
+        onSubmit={handleCopy}
+        showModal={showCopyModal}
+        title="Copy to"
+        sourcePath={currentPath}
+        submitText={t('Copy')}
+      />
+      <FileChooserModal
+        onClose={() => setShowMoveModal(false)}
+        onSubmit={handleMove}
+        showModal={showMoveModal}
+        title="Move to"
+        sourcePath={currentPath}
+        submitText={t('Move')}
       />
     </>
   );

+ 36 - 29
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx

@@ -34,7 +34,10 @@ import { i18nReact } from '../../../../utils/i18nReact';
 import huePubSub from '../../../../utils/huePubSub';
 import useDebounce from '../../../../utils/useDebounce';
 
-import { mkdir, touch } from '../../../../reactComponents/FileChooser/api';
+import {
+  CREATE_DIRECTORY_API_URL,
+  CREATE_FILE_API_URL
+} from '../../../../reactComponents/FileChooser/api';
 import {
   StorageBrowserTableData,
   SortOrder,
@@ -44,6 +47,7 @@ import Pagination from '../../../../reactComponents/Pagination/Pagination';
 import StorageBrowserActions from '../StorageBrowserActions/StorageBrowserActions';
 import InputModal from '../../InputModal/InputModal';
 import formatBytes from '../../../../utils/formatBytes';
+import useSaveData from '../../../../utils/hooks/useSaveData';
 
 import './StorageBrowserTable.scss';
 import { formatTimestamp } from '../../../../utils/dateTimeUtils';
@@ -111,7 +115,8 @@ const StorageBrowserTable = ({
       permission: file.rwx,
       mtime: file.stats?.mtime ? formatTimestamp(new Date(Number(file.stats.mtime) * 1000)) : '-',
       type: file.type,
-      path: file.path
+      path: file.path,
+      replication: file.stats?.replication
     }));
   }, [filesData]);
 
@@ -209,7 +214,9 @@ const StorageBrowserTable = ({
       }
       columns.push(column);
     }
-    return columns.filter(col => col.dataIndex !== 'type' && col.dataIndex !== 'path');
+    return columns.filter(
+      col => col.dataIndex !== 'type' && col.dataIndex !== 'path' && col.dataIndex !== 'replication'
+    );
   };
 
   const onRowClicked = (record: StorageBrowserTableData) => {
@@ -240,34 +247,33 @@ const StorageBrowserTable = ({
     onPageNumberChange(nextPageNumber === 0 ? numPages : nextPageNumber);
   };
 
+  const { error: createFolderError, save: saveCreateFolder } =
+    useSaveData(CREATE_DIRECTORY_API_URL);
+
   const handleCreateNewFolder = (folderName: string) => {
-    setLoadingFiles(true);
-    mkdir(folderName, filePath)
-      .then(() => {
-        refetchData();
-      })
-      .catch(error => {
-        huePubSub.publish('hue.error', error);
-        setShowNewFolderModal(false);
-      })
-      .finally(() => {
-        setLoadingFiles(false);
-      });
+    saveCreateFolder(
+      { path: filePath, name: folderName },
+      {
+        onSuccess: () => refetchData(),
+        onError: () => {
+          huePubSub.publish('hue.error', createFolderError);
+        }
+      }
+    );
   };
 
+  const { error: createFileError, save: saveCreateFile } = useSaveData(CREATE_FILE_API_URL);
+
   const handleCreateNewFile = (fileName: string) => {
-    setLoadingFiles(true);
-    touch(fileName, filePath)
-      .then(() => {
-        refetchData();
-      })
-      .catch(error => {
-        huePubSub.publish('hue.error', error);
-        setShowNewFileModal(false);
-      })
-      .finally(() => {
-        setLoadingFiles(false);
-      });
+    saveCreateFile(
+      { path: filePath, name: fileName },
+      {
+        onSuccess: () => refetchData(),
+        onError: () => {
+          huePubSub.publish('hue.error', createFileError);
+        }
+      }
+    );
   };
 
   const handleSearch = useCallback(
@@ -317,6 +323,7 @@ const StorageBrowserTable = ({
         />
         <div className="hue-storage-browser__actions-bar-right">
           <StorageBrowserActions
+            currentPath={filePath}
             selectedFiles={selectedFiles}
             setLoadingFiles={setLoadingFiles}
             onSuccessfulAction={refetchData}
@@ -327,7 +334,7 @@ const StorageBrowserTable = ({
               items: newActionsMenuItems,
               className: 'hue-storage-browser__action-menu'
             }}
-            trigger={['hover', 'click']}
+            trigger={['click']}
           >
             <PrimaryButton data-event="">
               {t('New')}
@@ -345,7 +352,7 @@ const StorageBrowserTable = ({
           onRow={onRowClicked}
           pagination={false}
           rowClassName={rowClassName}
-          rowKey={(record, index) => record.path + '' + index}
+          rowKey={(record, index) => record.path + index}
           rowSelection={{
             type: 'checkbox',
             ...rowSelection

+ 49 - 35
desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.test.tsx

@@ -14,19 +14,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 import React from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
+import { waitFor, screen, fireEvent, render } from '@testing-library/react';
 import '@testing-library/jest-dom';
 
+import { get } from '../../../api/utils';
+import formatBytes from '../../../utils/formatBytes';
 import SummaryModal from './SummaryModal';
-import * as StorageBrowserApi from '../../../reactComponents/FileChooser/api';
-import { ContentSummary } from '../../../reactComponents/FileChooser/types';
-import { CancellablePromise } from '../../../api/cancellablePromise';
+
+// Mock the `get` function
+jest.mock('../../../api/utils', () => ({
+  get: jest.fn()
+}));
+
+const mockGet = get as jest.MockedFunction<typeof get>;
 
 describe('SummaryModal', () => {
-  let summaryApiMock;
+  beforeAll(() => {
+    jest.clearAllMocks();
+  });
+
+  beforeEach(() => {
+    mockGet.mockResolvedValue(mockSummary);
+  });
 
-  const mockSummaryData = {
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  const mockSummary = {
     summary: {
       directoryCount: 0,
       ecPolicy: 'Replicated',
@@ -39,45 +54,44 @@ describe('SummaryModal', () => {
       replication: 3
     }
   };
-
-  const setUpMock = () => {
-    summaryApiMock = jest
-      .spyOn(StorageBrowserApi, 'fetchContentSummary')
-      .mockReturnValue(CancellablePromise.resolve<ContentSummary>(mockSummaryData));
-  };
-
-  afterEach(() => {
-    summaryApiMock?.mockClear();
+  test('renders path of file in title', async () => {
+    const { getByText } = render(
+      <SummaryModal showModal={true} onClose={() => {}} path="some/path" />
+    );
+    await waitFor(async () => {
+      expect(getByText('Summary for some/path')).toBeInTheDocument();
+    });
   });
 
-  test('renders path of file in title', async () => {
-    const onCloseMock = jest.fn();
-    setUpMock();
-    render(<SummaryModal path={'/user/demo'} showModal={true} onClose={onCloseMock} />);
-    const title = await screen.findByText('Summary for /user/demo');
-    await waitFor(() => {
-      expect(title).toBeInTheDocument();
+  test('renders summary content after successful data fetching', async () => {
+    const { getByText, getAllByText } = render(
+      <SummaryModal showModal={true} onClose={() => {}} path="some/path" />
+    );
+    await waitFor(async () => {
+      expect(getByText('DISKSPACE CONSUMED')).toBeInTheDocument();
+      expect(getAllByText(formatBytes(mockSummary.summary.spaceConsumed))[0]).toBeInTheDocument();
     });
   });
 
   test('renders space consumed in Bytes after the values are formatted', async () => {
-    const onCloseMock = jest.fn();
-    setUpMock();
-    render(<SummaryModal path={'/user/demo'} showModal={true} onClose={onCloseMock} />);
+    render(<SummaryModal path={'/user/demo'} showModal={true} onClose={() => {}} />);
     const spaceConsumed = await screen.findAllByText('0 Byte');
     await waitFor(() => {
       expect(spaceConsumed[0]).toBeInTheDocument();
     });
   });
 
-  test('calls onClose when close button is clicked', async () => {
-    const onCloseMock = jest.fn();
-    setUpMock();
-    const user = userEvent.setup();
-    render(<SummaryModal path={'/user/demo'} showModal={true} onClose={onCloseMock} />);
-    const closeButton = await screen.findByText('Close');
-    expect(onCloseMock).not.toHaveBeenCalled();
-    await user.click(closeButton);
-    expect(onCloseMock).toHaveBeenCalled();
+  test('should call onClose function when close button is clicked', async () => {
+    const mockOnClose = jest.fn();
+    const { getByText } = render(
+      <SummaryModal showModal={true} onClose={mockOnClose} path="some/path" />
+    );
+
+    const closeButton = getByText('Close');
+    expect(mockOnClose).not.toHaveBeenCalled();
+    fireEvent.click(closeButton);
+    await waitFor(() => {
+      expect(mockOnClose).toHaveBeenCalledTimes(1);
+    });
   });
 });

+ 29 - 34
desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.tsx

@@ -13,17 +13,18 @@
 // 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, { useEffect, useState } from 'react';
+import React, { useMemo } from 'react';
 import Modal from 'cuix/dist/components/Modal';
 import { Row, Col, Spin } from 'antd';
 
 import huePubSub from '../../../utils/huePubSub';
 import { i18nReact } from '../../../utils/i18nReact';
 import formatBytes from '../../../utils/formatBytes';
-import { fetchContentSummary } from '../../../reactComponents/FileChooser/api';
-import './SummaryModal.scss';
+import useLoadData from '../../../utils/hooks/useLoadData';
+import { CONTENT_SUMMARY_API_URL } from '../../../reactComponents/FileChooser/api';
 import { ContentSummary } from '../../../reactComponents/FileChooser/types';
 
+import './SummaryModal.scss';
 interface SummaryModalProps {
   path: string;
   showModal: boolean;
@@ -32,8 +33,6 @@ interface SummaryModalProps {
 
 const SummaryModal = ({ showModal, onClose, path }: SummaryModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
-  const [loadingSummary, setLoadingSummary] = useState(true);
-  const [summary, setSummary] = useState([]);
 
   const getSummary = () => {
     const cols = summary.map((item, index) => (
@@ -43,7 +42,7 @@ const SummaryModal = ({ showModal, onClose, path }: SummaryModalProps): JSX.Elem
       </Col>
     ));
 
-    const rows = [];
+    const rows: JSX.Element[] = [];
     for (let i = 0; i < cols.length - 1; i = i + 2) {
       rows.push(
         <Row key={'summaryRow' + i} className="hue-summary-modal__row">
@@ -55,36 +54,32 @@ const SummaryModal = ({ showModal, onClose, path }: SummaryModalProps): JSX.Elem
     return rows;
   };
 
-  const updateSummaryData = (responseSummary: ContentSummary) => {
-    const summaryData = [
-      ['DISKSPACE CONSUMED', formatBytes(responseSummary.summary.spaceConsumed)],
-      ['BYTES USED', formatBytes(responseSummary.summary.length)],
-      ['NAMESPACE QUOTA', formatBytes(responseSummary.summary.quota)],
-      ['DISKSPACE QUOTA', formatBytes(responseSummary.summary.spaceQuota)],
-      ['REPLICATION FACTOR', responseSummary.summary.replication],
+  const {
+    data: responseSummary,
+    loading: loadingSummary,
+    error
+  } = useLoadData<ContentSummary>(CONTENT_SUMMARY_API_URL, {
+    params: {
+      path: path
+    },
+    onError: () => {
+      huePubSub.publish('hue.error', error);
+    },
+    skip: path === '' || path === undefined || !showModal
+  });
+
+  const summary = useMemo(() => {
+    return [
+      ['DISKSPACE CONSUMED', formatBytes(responseSummary?.summary.spaceConsumed)],
+      ['BYTES USED', formatBytes(responseSummary?.summary.length)],
+      ['NAMESPACE QUOTA', formatBytes(responseSummary?.summary.quota)],
+      ['DISKSPACE QUOTA', formatBytes(responseSummary?.summary.spaceQuota)],
+      ['REPLICATION FACTOR', responseSummary?.summary.replication],
       [,],
-      ['NUMBER OF DIRECTORIES', responseSummary.summary.directoryCount],
-      ['NUMBER OF FILES', responseSummary.summary.fileCount]
+      ['NUMBER OF DIRECTORIES', responseSummary?.summary.directoryCount],
+      ['NUMBER OF FILES', responseSummary?.summary.fileCount]
     ];
-    setSummary(summaryData);
-  };
-
-  useEffect(() => {
-    if (showModal) {
-      setLoadingSummary(true);
-      fetchContentSummary(path)
-        .then(responseSummary => {
-          updateSummaryData(responseSummary);
-        })
-        .catch(error => {
-          huePubSub.publish('hue.error', error);
-          onClose();
-        })
-        .finally(() => {
-          setLoadingSummary(false);
-        });
-    }
-  }, [path, showModal]);
+  }, [responseSummary]);
 
   //TODO:Handle long modal title
   return (

+ 12 - 25
desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts

@@ -13,35 +13,22 @@
 // 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 { get, post } from '../../api/utils';
-import { CancellablePromise } from '../../api/cancellablePromise';
-import { ContentSummary } from './types';
-
-export const FILESYSTEMS_API_URL = '/api/v1/storage/filesystems';
-export const VIEWFILES_API_URl = '/api/v1/storage/view=';
+export const CONTENT_SUMMARY_API_URL = '/api/v1/storage/content_summary';
 export const DOWNLOAD_API_URL = '/filebrowser/download=';
+export const FILESYSTEMS_API_URL = '/api/v1/storage/filesystems';
 export const SAVE_FILE_API_URL = '/filebrowser/save';
-const MAKE_DIRECTORY_API_URL = '/api/v1/storage/mkdir';
-const TOUCH_API_URL = '/api/v1/storage/touch';
-const CONTENT_SUMMARY_API_URL = '/api/v1/storage/content_summary=';
-const RENAME_API_URL = '/api/v1/storage/rename';
+export const VIEWFILES_API_URl = '/api/v1/storage/view=';
+
+export const CREATE_FILE_API_URL = '/api/v1/storage/create/file/';
+export const CREATE_DIRECTORY_API_URL = '/api/v1/storage/create/directory/';
+export const RENAME_API_URL = '/api/v1/storage/rename/';
+export const SET_REPLICATION_API_URL = '/api/v1/storage/replication/';
+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/';
+export const BULK_MOVE_API_URL = '/api/v1/storage/move/bulk/';
 
 export interface ApiFileSystem {
   file_system: string;
   user_home_directory: string;
 }
-
-export const mkdir = async (folderName: string, path: string): Promise<void> => {
-  await post(MAKE_DIRECTORY_API_URL, { name: folderName, path: path });
-};
-
-export const touch = async (fileName: string, path: string): Promise<void> => {
-  await post(TOUCH_API_URL, { name: fileName, path: path });
-};
-
-export const fetchContentSummary = (path: string): CancellablePromise<ContentSummary> =>
-  get(CONTENT_SUMMARY_API_URL + path);
-
-export const rename = async (src_path: string, dest_path: string): Promise<void> => {
-  await post(RENAME_API_URL, { src_path: src_path, dest_path: dest_path });
-};

+ 2 - 0
desktop/core/src/desktop/js/reactComponents/FileChooser/types.ts

@@ -31,6 +31,7 @@ interface Stats {
   path: string;
   size: number;
   user: string;
+  replication: number;
 }
 
 export interface File {
@@ -55,6 +56,7 @@ export interface StorageBrowserTableData {
   mtime: string;
   type: string;
   path: string;
+  replication: number;
 }
 
 export interface PageStats {