Pārlūkot izejas kodu

[ui-storageBrowser] Implement UI modal for content summary (#3681)

Nidhi Bhat G 1 gadu atpakaļ
vecāks
revīzija
c6673f8daa
16 mainītis faili ar 640 papildinājumiem un 17 dzēšanām
  1. 0 14
      desktop/core/src/desktop/js/apps/storageBrowser/InputModal/InputModal.test.tsx
  2. 31 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.scss
  3. 97 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.test.tsx
  4. 80 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserRowActions/StorageBrowserRowActions.tsx
  5. 25 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx
  6. 24 0
      desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.scss
  7. 83 0
      desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.test.tsx
  8. 106 0
      desktop/core/src/desktop/js/apps/storageBrowser/SummaryModal/SummaryModal.tsx
  9. 16 0
      desktop/core/src/desktop/js/jest/jest.init.js
  10. 1 1
      desktop/core/src/desktop/js/jquery/plugins/jquery.filechooser.js
  11. 5 1
      desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts
  12. 14 0
      desktop/core/src/desktop/js/reactComponents/FileChooser/types.ts
  13. 55 0
      desktop/core/src/desktop/js/utils/formatBytes.test.ts
  14. 31 0
      desktop/core/src/desktop/js/utils/formatBytes.ts
  15. 48 0
      desktop/core/src/desktop/js/utils/storageBrowserUtils.test.ts
  16. 24 0
      desktop/core/src/desktop/js/utils/storageBrowserUtils.ts

+ 0 - 14
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(

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

+ 97 - 0
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(
+      <StorageBrowserRowActions rowData={mockRecord} onViewSummary={onViewSummary} />
+    );
+    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(
+      <StorageBrowserRowActions rowData={mockRecord} onViewSummary={onViewSummary} />
+    );
+    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(
+      <StorageBrowserRowActions rowData={mockRecord} onViewSummary={onViewSummary} />
+    );
+    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(
+      <StorageBrowserRowActions rowData={mockRecord} onViewSummary={onViewSummary} />
+    );
+    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(
+      <StorageBrowserRowActions rowData={mockRecord} onViewSummary={onViewSummary} />
+    );
+    await user.click(getByRole('button'));
+    expect(onViewSummary).not.toBeCalled();
+    await user.click(getByRole('menuitem', { name: 'View Summary' }));
+    expect(onViewSummary).toBeCalled();
+  });
+});

+ 80 - 0
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: <InfoIcon />,
+        label: t('View Summary'),
+        onClick: () => {
+          onViewSummary(rowData.path);
+        }
+      });
+    }
+    return actions;
+  };
+
+  return (
+    <Dropdown
+      overlayClassName="hue-storage-browser__table-actions-dropdown"
+      menu={{
+        items: getActions(),
+        className: 'hue-storage-browser__table-actions-menu'
+      }}
+      trigger={['click']}
+    >
+      <BorderlessButton
+        onClick={e => e.stopPropagation()}
+        className="hue-storage-browser__table-actions-btn"
+        data-event=""
+        icon={<MoreVerticalIcon />}
+      />
+    </Dropdown>
+  );
+};
+
+export default StorageBrowserRowActions;

+ 25 - 1
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<StorageBrowserTableProps> = ({
   ...restProps
 }): JSX.Element => {
   const [tableHeight, setTableHeight] = useState<number>();
+  const [showSummaryModal, setShowSummaryModal] = useState<boolean>(false);
+  //TODO: accept multiple files and folder select
+  const [selectedFile, setSelectedFile] = useState<string>('');
 
   const { t } = i18nReact.useTranslation();
 
@@ -90,6 +95,11 @@ const StorageBrowserTable: React.FC<StorageBrowserTableProps> = ({
     }
   };
 
+  const onViewSummary = (filePath: string) => {
+    setSelectedFile(filePath);
+    setShowSummaryModal(true);
+  };
+
   const getColumns = (file: StorageBrowserTableData) => {
     const columns: ColumnProps<StorageBrowserTableData>[] = [];
     for (const [key] of Object.entries(file)) {
@@ -126,10 +136,19 @@ const StorageBrowserTable: React.FC<StorageBrowserTableProps> = ({
           </Tooltip>
         );
       } 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) => (
+        <StorageBrowserRowActions onViewSummary={onViewSummary} rowData={record} />
+      ),
+      width: '4%'
+    });
     return columns.filter(col => col.dataIndex !== 'type' && col.dataIndex !== 'path');
   };
 
@@ -212,6 +231,11 @@ const StorageBrowserTable: React.FC<StorageBrowserTableProps> = ({
           pageSize={pageSize}
           pageStats={pageStats}
         />
+        <SummaryModal
+          showModal={showSummaryModal}
+          path={selectedFile}
+          onClose={() => setShowSummaryModal(false)}
+        ></SummaryModal>
       </>
     );
   } else {

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

+ 83 - 0
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<ContentSummary>(mockSummaryData));
+  };
+
+  afterEach(() => {
+    summaryApiMock?.mockClear();
+  });
+
+  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 space consumed in Bytes after the values are formatted', async () => {
+    const onCloseMock = jest.fn();
+    setUpMock();
+    render(<SummaryModal path={'/user/demo'} showModal={true} onClose={onCloseMock} />);
+    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();
+  });
+});

+ 106 - 0
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<SummaryModalProps> = ({ 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) => (
+      <Col key={'summaryItem' + index} span={12}>
+        <Row key={'summaryItem' + index + 'key'}>{item[0]}</Row>
+        <Row key={'summaryItem' + index + 'value'}>{item[1]}</Row>
+      </Col>
+    ));
+
+    const rows = [];
+    for (let i = 0; i < cols.length - 1; i = i + 2) {
+      rows.push(
+        <Row key={'summaryRow' + i} className="hue-summary-modal__row">
+          {cols[i]}
+          {cols[i + 1]}
+        </Row>
+      );
+    }
+    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 (
+    <Modal
+      className="hue-summary-modal"
+      okText={t('Close')}
+      onOk={onClose}
+      open={showModal}
+      title={t('Summary for ') + path}
+      cancellable={false}
+      onCancel={onClose}
+    >
+      <Spin spinning={loadingSummary}>{summary && getSummary()}</Spin>
+    </Modal>
+  );
+};
+
+export default SummaryModal;

+ 16 - 0
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()
+  }))
+});

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

+ 5 - 1
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<void> =>
 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);

+ 14 - 0
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',

+ 55 - 0
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');
+  });
+});

+ 31 - 0
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;

+ 48 - 0
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);
+  });
+});

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