From c6673f8daa010783d37e42efe68c604559681083 Mon Sep 17 00:00:00 2001 From: Nidhi Bhat G <44032758+nidhibhatg@users.noreply.github.com> Date: Tue, 14 May 2024 16:20:47 +0530 Subject: [PATCH] [ui-storageBrowser] Implement UI modal for content summary (#3681) --- .../InputModal/InputModal.test.tsx | 14 --- .../StorageBrowserRowActions.scss | 31 +++++ .../StorageBrowserRowActions.test.tsx | 97 ++++++++++++++++ .../StorageBrowserRowActions.tsx | 80 +++++++++++++ .../StorageBrowserTable.tsx | 26 ++++- .../SummaryModal/SummaryModal.scss | 24 ++++ .../SummaryModal/SummaryModal.test.tsx | 83 ++++++++++++++ .../SummaryModal/SummaryModal.tsx | 106 ++++++++++++++++++ desktop/core/src/desktop/js/jest/jest.init.js | 16 +++ .../js/jquery/plugins/jquery.filechooser.js | 2 +- .../js/reactComponents/FileChooser/api.ts | 6 +- .../js/reactComponents/FileChooser/types.ts | 14 +++ .../src/desktop/js/utils/formatBytes.test.ts | 55 +++++++++ .../core/src/desktop/js/utils/formatBytes.ts | 31 +++++ .../js/utils/storageBrowserUtils.test.ts | 48 ++++++++ .../desktop/js/utils/storageBrowserUtils.ts | 24 ++++ 16 files changed, 640 insertions(+), 17 deletions(-) create mode 100644 desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.scss create mode 100644 desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.test.tsx create mode 100644 desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.tsx create mode 100644 desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.scss create mode 100644 desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.test.tsx create mode 100644 desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.tsx create mode 100644 desktop/core/src/desktop/js/utils/formatBytes.test.ts create mode 100644 desktop/core/src/desktop/js/utils/formatBytes.ts create mode 100644 desktop/core/src/desktop/js/utils/storageBrowserUtils.test.ts create mode 100644 desktop/core/src/desktop/js/utils/storageBrowserUtils.ts diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/InputModal/InputModal.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/InputModal/InputModal.test.tsx index 5210926978..1323359056 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/InputModal/InputModal.test.tsx +++ b/desktop/core/src/desktop/js/apps/storageBrowser/InputModal/InputModal.test.tsx @@ -21,20 +21,6 @@ import '@testing-library/jest-dom'; import InputModal from './InputModal'; -Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn() - })) -}); - describe('InputModal', () => { test('renders custom modal title', () => { const inputModal = render( diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.scss b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.scss new file mode 100644 index 0000000000..57c5ab63a4 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.scss @@ -0,0 +1,31 @@ +// 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 'mixins'; +@use 'variables' as vars; +$action-dropdown-width: 214px; + +//TODO: Remove styling for cuix button +.hue-storage-browser__table-actions-btn { + box-shadow: none; + background-color: transparent; + border: none; +} + +.hue-storage-browser__table-actions-dropdown { + align-items: center; + width: $action-dropdown-width; + @include mixins.hue-svg-icon__d3-conflict; +} diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.test.tsx new file mode 100644 index 0000000000..4514e76801 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.test.tsx @@ -0,0 +1,97 @@ +// 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 } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; + +import StorageBrowserRowActions from './StorageBrowserRowActions'; +import { StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types'; + +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 = { + name: 'test', + size: '0\u00a0bytes', + user: 'demo', + group: 'demo', + permission: 'drwxr-xr-x', + mtime: 'May 12, 2024 10:37 PM', + type: '', + path: '' + }; + test('renders view summary option when record is a hdfs file', async () => { + const onViewSummary = jest.fn(); + const user = userEvent.setup(); + mockRecord.path = '/user/demo/test'; + mockRecord.type = 'file'; + const { getByRole, queryByRole } = render( + + ); + 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 onViewSummary = jest.fn(); + const user = userEvent.setup(); + mockRecord.path = 'ofs://demo/test'; + mockRecord.type = 'file'; + const { getByRole, queryByRole } = render( + + ); + await user.click(getByRole('button')); + expect(queryByRole('menuitem', { name: 'View Summary' })).not.toBeNull(); + }); + + test('does not render view summary option when record is a hdfs folder', async () => { + const onViewSummary = jest.fn(); + const user = userEvent.setup(); + mockRecord.path = '/user/demo/test'; + mockRecord.type = 'dir'; + const { getByRole, queryByRole } = render( + + ); + await user.click(getByRole('button')); + expect(queryByRole('menuitem', { name: 'View Summary' })).toBeNull(); + }); + + test('does not render view summary option when record is a an abfs file', async () => { + const onViewSummary = jest.fn(); + const user = userEvent.setup(); + mockRecord.path = 'abfs://demo/test'; + mockRecord.type = 'file'; + const { getByRole, queryByRole } = render( + + ); + await user.click(getByRole('button')); + expect(queryByRole('menuitem', { name: 'View Summary' })).toBeNull(); + }); + + test('calls onViewSummary after View summary menu option is clicked', async () => { + const onViewSummary = jest.fn(); + const user = userEvent.setup(); + mockRecord.path = '/user/demo/test'; + mockRecord.type = 'file'; + const { getByRole } = render( + + ); + await user.click(getByRole('button')); + expect(onViewSummary).not.toBeCalled(); + await user.click(getByRole('menuitem', { name: 'View Summary' })); + expect(onViewSummary).toBeCalled(); + }); +}); diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.tsx new file mode 100644 index 0000000000..f74cf664d4 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.tsx @@ -0,0 +1,80 @@ +// 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 { Dropdown } from 'antd'; +import { MenuItemType } from 'antd/lib/menu/hooks/useItems'; + +import { BorderlessButton } from 'cuix/dist/components/Button'; +import MoreVerticalIcon from '@cloudera/cuix-core/icons/react/MoreVerticalIcon'; +import InfoIcon from '@cloudera/cuix-core/icons/react/InfoIcon'; + +import { i18nReact } from '../../../../utils/i18nReact'; +import { StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types'; +import { isHDFS, isOFS } from '../../../../../js/utils/storageBrowserUtils'; + +import './StorageBrowserRowActions.scss'; + +interface StorageBrowserRowActionsProps { + rowData: StorageBrowserTableData; + onViewSummary: (selectedFilePath: string) => void; +} + +const StorageBrowserRowActions = ({ + rowData, + onViewSummary +}: StorageBrowserRowActionsProps): JSX.Element => { + const { t } = i18nReact.useTranslation(); + + //TODO: handle multiple file selection scenarios + const isSummaryEnabled = () => + (isHDFS(rowData.path) || isOFS(rowData.path)) && rowData.type === 'file'; + + const getActions = () => { + const actions: MenuItemType[] = []; + if (isSummaryEnabled()) { + actions.push({ + key: 'content_summary', + icon: , + label: t('View Summary'), + onClick: () => { + onViewSummary(rowData.path); + } + }); + } + return actions; + }; + + return ( + + e.stopPropagation()} + className="hue-storage-browser__table-actions-btn" + data-event="" + icon={} + /> + + ); +}; + +export default StorageBrowserRowActions; diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx index 64c5594a0a..388f458784 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx @@ -30,8 +30,10 @@ import { SortOrder } from '../../../../reactComponents/FileChooser/types'; import Pagination from '../../../../reactComponents/Pagination/Pagination'; +import StorageBrowserRowActions from '../StorageBrowserRowActions/StorageBrowserRowActions'; import './StorageBrowserTable.scss'; import Tooltip from 'antd/es/tooltip'; +import SummaryModal from '../../SummaryModal/SummaryModal'; interface StorageBrowserTableProps { className?: string; @@ -72,6 +74,9 @@ const StorageBrowserTable: React.FC = ({ ...restProps }): JSX.Element => { const [tableHeight, setTableHeight] = useState(); + const [showSummaryModal, setShowSummaryModal] = useState(false); + //TODO: accept multiple files and folder select + const [selectedFile, setSelectedFile] = useState(''); const { t } = i18nReact.useTranslation(); @@ -90,6 +95,11 @@ const StorageBrowserTable: React.FC = ({ } }; + const onViewSummary = (filePath: string) => { + setSelectedFile(filePath); + setShowSummaryModal(true); + }; + const getColumns = (file: StorageBrowserTableData) => { const columns: ColumnProps[] = []; for (const [key] of Object.entries(file)) { @@ -126,10 +136,19 @@ const StorageBrowserTable: React.FC = ({ ); } else { - column.width = key === 'mtime' ? '15%' : '10%'; + column.width = key === 'mtime' ? '15%' : '9%'; } columns.push(column); } + columns.push({ + dataIndex: 'actions', + title: '', + key: 'actions', + render: (_, record: StorageBrowserTableData) => ( + + ), + width: '4%' + }); return columns.filter(col => col.dataIndex !== 'type' && col.dataIndex !== 'path'); }; @@ -212,6 +231,11 @@ const StorageBrowserTable: React.FC = ({ pageSize={pageSize} pageStats={pageStats} /> + setShowSummaryModal(false)} + > ); } else { diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.scss b/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.scss new file mode 100644 index 0000000000..5ad18d84c6 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.scss @@ -0,0 +1,24 @@ +// 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; + +.hue-summary-modal__row.ant-row { + margin-bottom: vars.$fluidx-spacing-s; + + &:last-child { + margin-bottom: 0; + } +} diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.test.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.test.tsx new file mode 100644 index 0000000000..b15b466f30 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.test.tsx @@ -0,0 +1,83 @@ +// 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 userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; + +import SummaryModal from './SummaryModal'; +import * as StorageBrowserApi from '../../../reactComponents/FileChooser/api'; +import { ContentSummary } from '../../../reactComponents/FileChooser/types'; +import { CancellablePromise } from '../../../api/cancellablePromise'; + +describe('SummaryModal', () => { + let summaryApiMock; + + 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(mockSummaryData)); + }; + + afterEach(() => { + summaryApiMock?.mockClear(); + }); + + test('renders path of file in title', async () => { + const onCloseMock = jest.fn(); + setUpMock(); + render(); + const title = await screen.findByText('Summary for /user/demo'); + await waitFor(() => { + expect(title).toBeInTheDocument(); + }); + }); + + test('renders space consumed in Bytes after the values are formatted', async () => { + const onCloseMock = jest.fn(); + setUpMock(); + render(); + 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(); + const closeButton = await screen.findByText('Close'); + expect(onCloseMock).not.toHaveBeenCalled(); + await user.click(closeButton); + expect(onCloseMock).toHaveBeenCalled(); + }); +}); diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.tsx new file mode 100644 index 0000000000..aaa93ad170 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.tsx @@ -0,0 +1,106 @@ +// 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, { useEffect, useState } 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 { ContentSummary } from '../../../reactComponents/FileChooser/types'; + +interface SummaryModalProps { + path: string; + showModal: boolean; + onClose: () => void; +} + +const SummaryModal: React.FC = ({ showModal, onClose, path }): JSX.Element => { + const { t } = i18nReact.useTranslation(); + const [loadingSummary, setLoadingSummary] = useState(true); + const [summary, setSummary] = useState([]); + + const getSummary = () => { + const cols = summary.map((item, index) => ( + + {item[0]} + {item[1]} + + )); + + const rows = []; + for (let i = 0; i < cols.length - 1; i = i + 2) { + rows.push( + + {cols[i]} + {cols[i + 1]} + + ); + } + 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], + [,], + ['NUMBER OF DIRECTORIES', responseSummary.summary.directoryCount], + ['NUMBER OF FILES', responseSummary.summary.fileCount] + ]; + setSummary(summaryData); + }; + + useEffect(() => { + if (path === '') { + return; + } + setLoadingSummary(true); + fetchContentSummary(path) + .then(responseSummary => { + updateSummaryData(responseSummary); + }) + .catch(error => { + huePubSub.publish('hue.error', error); + onClose(); + }) + .finally(() => { + setLoadingSummary(false); + }); + }, [path]); + + //TODO:Handle long modal title + return ( + + {summary && getSummary()} + + ); +}; + +export default SummaryModal; diff --git a/desktop/core/src/desktop/js/jest/jest.init.js b/desktop/core/src/desktop/js/jest/jest.init.js index afc17282ed..baa0fe4e1c 100644 --- a/desktop/core/src/desktop/js/jest/jest.init.js +++ b/desktop/core/src/desktop/js/jest/jest.init.js @@ -126,3 +126,19 @@ process.on('unhandledRejection', err => { jest.mock('../utils/i18nReact'); jest.mock('../utils/hueAnalytics'); + +//Official workaround for TypeError: window.matchMedia is not a function +//learn more: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn() + })) +}); diff --git a/desktop/core/src/desktop/js/jquery/plugins/jquery.filechooser.js b/desktop/core/src/desktop/js/jquery/plugins/jquery.filechooser.js index d63fbb529f..3fb4d4dd72 100644 --- a/desktop/core/src/desktop/js/jquery/plugins/jquery.filechooser.js +++ b/desktop/core/src/desktop/js/jquery/plugins/jquery.filechooser.js @@ -874,7 +874,7 @@ function initUploader(path, _parent, el, labels) { } }, onSubmit: function (id, fileName) { - let newPath = + const newPath = '/filebrowser/upload/chunks/file?dest=' + encodeURIComponent(path.normalize('NFC')); this.setEndpoint(newPath); num_of_pending_uploads++; diff --git a/desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts b/desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts index 622ba8fd97..f092a26056 100644 --- a/desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts +++ b/desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts @@ -15,12 +15,13 @@ // limitations under the License. import { get, post } from '../../api/utils'; import { CancellablePromise } from '../../api/cancellablePromise'; -import { PathAndFileData, SortOrder } from './types'; +import { PathAndFileData, ContentSummary, SortOrder } from './types'; const FILESYSTEMS_API_URL = '/api/v1/storage/filesystems'; const VIEWFILES_API_URl = '/api/v1/storage/view='; 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='; export interface ApiFileSystem { file_system: string; @@ -74,3 +75,6 @@ export const mkdir = async (folderName: string, path: string): Promise => export const touch = async (fileName: string, path: string): Promise => { await post(TOUCH_API_URL, { name: fileName, path: path }); }; + +export const fetchContentSummary = (path: string): CancellablePromise => + get(CONTENT_SUMMARY_API_URL + path); diff --git a/desktop/core/src/desktop/js/reactComponents/FileChooser/types.ts b/desktop/core/src/desktop/js/reactComponents/FileChooser/types.ts index 7ddc58c99e..caa188038a 100644 --- a/desktop/core/src/desktop/js/reactComponents/FileChooser/types.ts +++ b/desktop/core/src/desktop/js/reactComponents/FileChooser/types.ts @@ -80,6 +80,20 @@ export interface PathAndFileData { pagesize: number; } +export interface ContentSummary { + summary: { + directoryCount: number; + ecPolicy: string; + fileCount: number; + length: number; + quota: number; + spaceConsumed: number; + spaceQuota: number; + typeQuota: number; + replication: number; + }; +} + export enum SortOrder { ASC = 'ascending', DSC = 'descending', diff --git a/desktop/core/src/desktop/js/utils/formatBytes.test.ts b/desktop/core/src/desktop/js/utils/formatBytes.test.ts new file mode 100644 index 0000000000..5fdebb4dd4 --- /dev/null +++ b/desktop/core/src/desktop/js/utils/formatBytes.test.ts @@ -0,0 +1,55 @@ +// 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 formatBytes from './formatBytes'; + +describe('formatBytes function', () => { + test('returns "Not available" when bytes is -1', () => { + expect(formatBytes(-1)).toBe('Not available'); + }); + + test('returns "0 Byte" when bytes is 0', () => { + expect(formatBytes(0)).toBe('0 Byte'); + }); + + test('correctly formats bytes to KB', () => { + expect(formatBytes(1024)).toBe('1.00 KB'); + expect(formatBytes(2048)).toBe('2.00 KB'); + }); + + test('correctly formats bytes to MB', () => { + expect(formatBytes(1024 * 1024)).toBe('1.00 MB'); + expect(formatBytes(2 * 1024 * 1024)).toBe('2.00 MB'); + }); + + test('correctly formats bytes to GB', () => { + expect(formatBytes(1024 * 1024 * 1024)).toBe('1.00 GB'); + expect(formatBytes(2 * 1024 * 1024 * 1024)).toBe('2.00 GB'); + }); + + test('correctly formats bytes to TB', () => { + expect(formatBytes(1024 * 1024 * 1024 * 1024)).toBe('1.00 TB'); + expect(formatBytes(2 * 1024 * 1024 * 1024 * 1024)).toBe('2.00 TB'); + }); + + test('correctly formats bytes to specified decimal points', () => { + expect(formatBytes(2000, 3)).toBe('1.953 KB'); + expect(formatBytes(20000, 1)).toBe('19.5 KB'); + }); + + test('correctly formats bytes to default 2 decimal points', () => { + expect(formatBytes(2000)).toBe('1.95 KB'); + }); +}); diff --git a/desktop/core/src/desktop/js/utils/formatBytes.ts b/desktop/core/src/desktop/js/utils/formatBytes.ts new file mode 100644 index 0000000000..d3bab5517d --- /dev/null +++ b/desktop/core/src/desktop/js/utils/formatBytes.ts @@ -0,0 +1,31 @@ +// 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. + +const formatBytes = (bytes: number, decimalPoints?: number): string => { + if (bytes == -1) { + return 'Not available'; + } + if (bytes == 0) { + return '0 Byte'; + } + const k = 1024; + const dm = decimalPoints ? decimalPoints : 2; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(dm) + ' ' + sizes[i]; +}; + +export default formatBytes; diff --git a/desktop/core/src/desktop/js/utils/storageBrowserUtils.test.ts b/desktop/core/src/desktop/js/utils/storageBrowserUtils.test.ts new file mode 100644 index 0000000000..415c0807bb --- /dev/null +++ b/desktop/core/src/desktop/js/utils/storageBrowserUtils.test.ts @@ -0,0 +1,48 @@ +// 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 { isHDFS, isOFS } from './storageBrowserUtils'; + +describe('isHDFS function', () => { + test('returns true for paths starting with "/"', () => { + expect(isHDFS('/path/to/file')).toBe(true); + expect(isHDFS('/')).toBe(true); + }); + + test('returns true for paths starting with "hdfs"', () => { + expect(isHDFS('hdfs://path/to/file')).toBe(true); + expect(isHDFS('hdfs://')).toBe(true); + }); + + test('returns false for other paths', () => { + expect(isHDFS('s3://path/to/file')).toBe(false); + expect(isHDFS('file://path/to/file')).toBe(false); + expect(isHDFS('')).toBe(false); + }); +}); + +describe('isOFS function', () => { + test('returns true for paths starting with "ofs://"', () => { + expect(isOFS('ofs://path/to/file')).toBe(true); + expect(isOFS('ofs://')).toBe(true); + }); + + test('returns false for other paths', () => { + expect(isOFS('/path/to/file')).toBe(false); + expect(isOFS('hdfs://path/to/file')).toBe(false); + expect(isOFS('s3://path/to/file')).toBe(false); + expect(isOFS('')).toBe(false); + }); +}); diff --git a/desktop/core/src/desktop/js/utils/storageBrowserUtils.ts b/desktop/core/src/desktop/js/utils/storageBrowserUtils.ts new file mode 100644 index 0000000000..e566ab945a --- /dev/null +++ b/desktop/core/src/desktop/js/utils/storageBrowserUtils.ts @@ -0,0 +1,24 @@ +// 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. + +export const isHDFS = (path: string): boolean => { + const currentPath = path.toLowerCase(); + return currentPath.indexOf('/') === 0 || currentPath.indexOf('hdfs') === 0; +}; + +export const isOFS = (path: string): boolean => { + return path.toLowerCase().indexOf('ofs://') === 0; +};