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;
+};