Skip to content

Commit

Permalink
[ui-storageBrowser] Implement UI for rename action (#3764)
Browse files Browse the repository at this point in the history
* [ui-storageBrowser] Implement UI for rename action
  • Loading branch information
nidhibhatg authored Jun 28, 2024
1 parent db70a86 commit 78d41a4
Show file tree
Hide file tree
Showing 7 changed files with 451 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';

import StorageBrowserActions from './StorageBrowserActions';
import { StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types';
import {
StorageBrowserTableData,
ContentSummary
} from '../../../../reactComponents/FileChooser/types';
import * as StorageBrowserApi from '../../../../reactComponents/FileChooser/api';
import { CancellablePromise } from '../../../../api/cancellablePromise';

describe('StorageBrowserRowActions', () => {
//View summary option is enabled and added to the actions menu when the row data is either hdfs/ofs and a single file
Expand All @@ -33,7 +38,7 @@ describe('StorageBrowserRowActions', () => {
type: '',
path: ''
};
const mockRecord2: StorageBrowserTableData[] = [
const mockTwoRecords: StorageBrowserTableData[] = [
{
name: 'test',
size: '0\u00a0bytes',
Expand All @@ -56,56 +61,137 @@ describe('StorageBrowserRowActions', () => {
}
];

test('does not render view summary option when there are multiple records', async () => {
const user = userEvent.setup();
const { getByRole, queryByRole } = render(
<StorageBrowserActions selectedFiles={mockRecord2} />
);
await user.click(getByRole('button'));
expect(queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
});
const setLoadingFiles = jest.fn();
const onSuccessfulAction = jest.fn();

test('renders view summary option when record is a hdfs file', async () => {
const setUpActionMenu = async (
records: StorageBrowserTableData[],
recordPath?: string,
recordType?: string
) => {
const user = userEvent.setup();
mockRecord.path = '/user/demo/test';
mockRecord.type = 'file';
const { getByRole, queryByRole } = render(
<StorageBrowserActions selectedFiles={[mockRecord]} />
if (recordPath) {
records[0].path = recordPath;
}
if (recordType) {
records[0].type = recordType;
}
const { getByRole } = render(
<StorageBrowserActions
setLoadingFiles={setLoadingFiles}
onSuccessfulAction={onSuccessfulAction}
selectedFiles={records}
/>
);
await user.click(getByRole('button'));
expect(queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
});
};

test('renders view summary option when record is a ofs file', async () => {
const user = userEvent.setup();
mockRecord.path = 'ofs://demo/test';
mockRecord.type = 'file';
const { getByRole, queryByRole } = render(
<StorageBrowserActions selectedFiles={[mockRecord]} />
);
await user.click(getByRole('button'));
expect(queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
});
describe('Summary option', () => {
let summaryApiMock;

test('does not render view summary option when record is a hdfs folder', async () => {
const user = userEvent.setup();
mockRecord.path = '/user/demo/test';
mockRecord.type = 'dir';
const { getByRole, queryByRole } = render(
<StorageBrowserActions selectedFiles={[mockRecord]} />
);
await user.click(getByRole('button'));
expect(queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
const mockSummaryData = {
summary: {
directoryCount: 0,
ecPolicy: 'Replicated',
fileCount: 1,
length: 0,
quota: -1,
spaceConsumed: 0,
spaceQuota: -1,
typeQuota: -1,
replication: 3
}
};

const setUpMock = () => {
summaryApiMock = jest
.spyOn(StorageBrowserApi, 'fetchContentSummary')
.mockReturnValue(CancellablePromise.resolve<ContentSummary>(mockSummaryData));
};

afterEach(() => {
summaryApiMock?.mockClear();
});

test('does not render view summary option when there are multiple records selected', async () => {
await setUpActionMenu(mockTwoRecords);
expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
});

test('renders view summary option when record is a hdfs file', async () => {
await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
expect(screen.queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
});

test('renders view summary option when record is a ofs file', async () => {
await setUpActionMenu([mockRecord], 'ofs://demo/test', 'file');
expect(screen.queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull();
});

test('does not render view summary option when record is a hdfs folder', async () => {
await setUpActionMenu([mockRecord], '/user/demo/test', 'dir');
expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
});

test('does not render view summary option when record is a an abfs file', async () => {
await setUpActionMenu([mockRecord], 'abfs://demo/test', 'file');
expect(screen.queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
});

test('renders summary modal when view summary option is clicked', async () => {
const user = userEvent.setup();
setUpMock();
await setUpActionMenu([mockRecord], '/user/demo/test', 'file');
await user.click(screen.queryByRole('menuitem', { name: 'View Summary' }));
expect(await screen.findByText('Summary for /user/demo/test')).toBeInTheDocument();
});
});

test('does not render view summary option when record is a an abfs file', async () => {
const user = userEvent.setup();
mockRecord.path = 'abfs://demo/test';
mockRecord.type = 'file';
const { getByRole, queryByRole } = render(
<StorageBrowserActions selectedFiles={[mockRecord]} />
);
await user.click(getByRole('button'));
expect(queryByRole('menuitem', { name: 'View Summary' })).toBeNull();
describe('Rename option', () => {
test('does not render view summary option when there are multiple records selected', async () => {
await setUpActionMenu(mockTwoRecords);
expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
});
test('does not render rename option when selected record is a abfs root folder', async () => {
await setUpActionMenu([mockRecord], 'abfs://', 'dir');
expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
});

test('does not render rename option when selected record is a gs root folder', async () => {
await setUpActionMenu([mockRecord], 'gs://', 'dir');
expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
});

test('does not render rename option when selected record is a s3 root folder', async () => {
await setUpActionMenu([mockRecord], 's3a://', 'dir');
expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
});

test('does not render rename option when selected record is a ofs root folder', async () => {
await setUpActionMenu([mockRecord], 'ofs://', 'dir');
expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
});

test('does not render rename option when selected record is a ofs service ID folder', async () => {
await setUpActionMenu([mockRecord], 'ofs://serviceID', 'dir');
expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
});

test('does not render rename option when selected record is a ofs volume folder', async () => {
await setUpActionMenu([mockRecord], 'ofs://serviceID/volume', 'dir');
expect(screen.queryByRole('menuitem', { name: 'Rename' })).toBeNull();
});

test('renders rename option when selected record is a file or a folder', async () => {
await setUpActionMenu([mockRecord], 'abfs://test', 'dir');
expect(screen.queryByRole('menuitem', { name: 'Rename' })).not.toBeNull();
});

test('renders rename modal when rename option is clicked', async () => {
const user = userEvent.setup();
await setUpActionMenu([mockRecord], 'abfs://test', 'dir');
await user.click(screen.queryByRole('menuitem', { name: 'Rename' }));
expect(await screen.findByText('Enter new name here')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,43 +24,110 @@ import InfoIcon from '@cloudera/cuix-core/icons/react/InfoIcon';

import { i18nReact } from '../../../../utils/i18nReact';
import { StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types';
import { isHDFS, isOFS } from '../../../../utils/storageBrowserUtils';
import {
isHDFS,
isOFS,
isABFSRoot,
isGSRoot,
isOFSServiceID,
isOFSVol,
isS3Root,
inTrash,
isABFS,
isGS,
isS3,
isOFSRoot
} from '../../../../utils/storageBrowserUtils';
import { rename } from '../../../../reactComponents/FileChooser/api';
import huePubSub from '../../../../utils/huePubSub';

import SummaryModal from '../../SummaryModal/SummaryModal';
import InputModal from '../../InputModal/InputModal';

import './StorageBrowserActions.scss';

interface StorageBrowserRowActionsProps {
selectedFiles: StorageBrowserTableData[];
onSuccessfulAction: () => void;
setLoadingFiles: (value: boolean) => void;
}

const StorageBrowserActions = ({ selectedFiles }: StorageBrowserRowActionsProps): JSX.Element => {
const StorageBrowserActions = ({
selectedFiles,
setLoadingFiles,
onSuccessfulAction
}: StorageBrowserRowActionsProps): JSX.Element => {
const [showSummaryModal, setShowSummaryModal] = useState<boolean>(false);
const [showRenameModal, setShowRenameModal] = useState<boolean>(false);
const [selectedFile, setSelectedFile] = useState<string>('');

const { t } = i18nReact.useTranslation();

const handleRename = (newName: string) => {
setLoadingFiles(true);
rename(selectedFile, newName)
.then(() => {
onSuccessfulAction();
})
.catch(error => {
huePubSub.publish('hue.error', error);
setShowRenameModal(false);
})
.finally(() => {
setLoadingFiles(false);
});
};

const isSummaryEnabled = () => {
if (selectedFiles.length !== 1) {
return false;
}
const selectedFile = selectedFiles[0];
return (isHDFS(selectedFile.path) || isOFS(selectedFile.path)) && selectedFile.type === 'file';
};

const isRenameEnabled = () => {
if (selectedFiles.length !== 1) {
return false;
}
const selectedFilePath = selectedFiles[0].path;
return (
selectedFiles.length == 1 &&
(isHDFS(selectedFile.path) || isOFS(selectedFile.path)) &&
selectedFile.type === 'file'
isHDFS(selectedFilePath) ||
(isS3(selectedFilePath) && !isS3Root(selectedFilePath)) ||
(isGS(selectedFilePath) && !isGSRoot(selectedFilePath)) ||
(isABFS(selectedFilePath) && !isABFSRoot(selectedFilePath)) ||
(isOFS(selectedFilePath) &&
!isOFSRoot(selectedFilePath) &&
!isOFSServiceID(selectedFilePath) &&
!isOFSVol(selectedFilePath))
);
};

const getActions = () => {
const actions: MenuItemType[] = [];
if (isSummaryEnabled()) {
actions.push({
key: 'content_summary',
icon: <InfoIcon />,
label: t('View Summary'),
onClick: () => {
setSelectedFile(selectedFiles[0].path);
setShowSummaryModal(true);
}
});
if (selectedFiles && selectedFiles.length > 0 && !inTrash(selectedFiles[0].path)) {
if (isSummaryEnabled()) {
actions.push({
key: 'content_summary',
icon: <InfoIcon />,
label: t('View Summary'),
onClick: () => {
setSelectedFile(selectedFiles[0].path);
setShowSummaryModal(true);
}
});
}
if (isRenameEnabled()) {
actions.push({
key: 'rename',
icon: <InfoIcon />,
label: t('Rename'),
onClick: () => {
setSelectedFile(selectedFiles[0].path);
setShowRenameModal(true);
}
});
}
}
return actions;
};
Expand All @@ -85,6 +152,14 @@ const StorageBrowserActions = ({ selectedFiles }: StorageBrowserRowActionsProps)
path={selectedFile}
onClose={() => setShowSummaryModal(false)}
/>
<InputModal
title={t('Rename')}
inputLabel={t('Enter new name here')}
submitText={t('Rename')}
showModal={showRenameModal}
onSubmit={handleRename}
onClose={() => setShowRenameModal(false)}
/>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,15 @@ const StorageBrowserTable = ({
onPageNumberChange(nextPageNumber === 0 ? numPages : nextPageNumber);
};

const reloadData = () => {
setRefreshKey(oldKey => oldKey + 1);
};

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

0 comments on commit 78d41a4

Please sign in to comment.