Browse Source

[frontend] Initial Implementation of Storage browser table (#3525)

* [frontend] Displays table data in storage browser table

* [frontend] Added icons to rows and type of row(file/folder)

* [frontend] Added onClick functionality to sb rows to move between folders

* [frontend] Enabled table scroll using table height

* [frontend] Implemented pagination for storage browser table

* [frontend] Seperated Pagination component and added improvements
Nidhi Bhat G 1 year ago
parent
commit
651a090250

+ 4 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserPage.scss

@@ -28,6 +28,10 @@
     padding: 0 16px;
     //TODO: Find fix so the flex is applied
     flex: 0 0 100%;
+
+    .ant-tabs-content-holder {
+      height: 100vh;
+    }
   }
 }
 

+ 4 - 4
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.scss

@@ -22,12 +22,12 @@ $action-dropdown-width: 214px;
 .hue-storage-browser-tabContent {
   background-color: vars.$fluidx-white;
   margin: vars.$fluidx-spacing-s 0;
-  padding: 16px;
-  //TODO: Set height to content
 
   .hue-storage-browser__title-bar {
     display: flex;
     align-items: center;
+    margin: 0 vars.$fluidx-spacing-s;
+    padding-top: vars.$fluidx-spacing-s;
 
     .hue-storage-browser__icon {
       margin-right: vars.$fluidx-spacing-xs;
@@ -52,14 +52,14 @@ $action-dropdown-width: 214px;
       flex: 0 0 auto;
       color: vars.$text-color;
       font-weight: 600;
-      margin-right: vars.$fluidx-spacing-xs;
+      margin: 0 vars.$fluidx-spacing-xs 0 vars.$fluidx-spacing-s;
     }
   }
 }
 
 .hue-storage-browser__actions-bar {
   display: flex;
-  margin: vars.$fluidx-spacing-s 0;
+  margin: vars.$fluidx-spacing-s;
   justify-content: space-between;
   @include mixins.hue-svg-icon__d3-conflict;
 }

+ 35 - 5
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.tsx

@@ -28,12 +28,17 @@ import DropDownIcon from '@cloudera/cuix-core/icons/react/DropdownIcon';
 import FolderIcon from '@cloudera/cuix-core/icons/react/ProjectIcon';
 import ImportIcon from '@cloudera/cuix-core/icons/react/ImportIcon';
 import PlusCircleIcon from '@cloudera/cuix-core/icons/react/PlusCircleIcon';
-//Todo: Use cuix icon (Currently fileIcon does not exist in cuix)
+//TODO: Use cuix icon (Currently fileIcon does not exist in cuix)
 import { FileOutlined } from '@ant-design/icons';
 
 import PathBrowser from '../../../../reactComponents/FileChooser/PathBrowser/PathBrowser';
+import StorageBrowserTable from '../StorageBrowserTable/StorageBrowserTable';
 import { fetchFiles } from '../../../../reactComponents/FileChooser/api';
-import { PathAndFileData } from '../../../../reactComponents/FileChooser/types';
+import {
+  PathAndFileData,
+  StorageBrowserTableData,
+  PageStats
+} from '../../../../reactComponents/FileChooser/types';
 
 import './StorageBrowserTabContent.scss';
 
@@ -51,8 +56,12 @@ const StorageBrowserTabContent: React.FC<StorageBrowserTabContentProps> = ({
   testId
 }): JSX.Element => {
   const [filePath, setFilePath] = useState<string>(user_home_dir);
-  const [filesData, setFilesData] = useState<PathAndFileData | undefined>();
+  const [filesData, setFilesData] = useState<PathAndFileData>();
+  const [files, setFiles] = useState<StorageBrowserTableData[]>();
   const [loadingFiles, setloadingFiles] = useState(true);
+  const [pageStats, setPageStats] = useState<PageStats>();
+  const [pageSize, setPageSize] = useState<number>();
+  const [pageNumber, setPageNumber] = useState<number>(1);
 
   const { t } = i18nReact.useTranslation();
 
@@ -123,9 +132,22 @@ const StorageBrowserTabContent: React.FC<StorageBrowserTabContentProps> = ({
 
   useEffect(() => {
     setloadingFiles(true);
-    fetchFiles(filePath)
+    fetchFiles(filePath, pageSize, pageNumber)
       .then(responseFilesData => {
         setFilesData(responseFilesData);
+        const tableData: StorageBrowserTableData[] = responseFilesData.files.map(file => ({
+          name: file.name,
+          size: file.humansize,
+          user: file.stats.user,
+          groups: file.stats.group,
+          permission: file.rwx,
+          lastUpdated: file.mtime,
+          type: file.type,
+          path: file.path
+        }));
+        setFiles(tableData);
+        setPageStats(responseFilesData.page);
+        setPageSize(responseFilesData.pagesize);
       })
       .catch(error => {
         //TODO: handle errors
@@ -134,7 +156,7 @@ const StorageBrowserTabContent: React.FC<StorageBrowserTabContentProps> = ({
       .finally(() => {
         setloadingFiles(false);
       });
-  }, [filePath]);
+  }, [filePath, pageSize, pageNumber]);
 
   return (
     <Spin spinning={loadingFiles}>
@@ -188,6 +210,14 @@ const StorageBrowserTabContent: React.FC<StorageBrowserTabContentProps> = ({
             </Dropdown>
           </div>
         </div>
+        <StorageBrowserTable
+          dataSource={files}
+          pageStats={pageStats}
+          pageSize={pageSize}
+          onFilepathChange={setFilePath}
+          onPageSizeChange={setPageSize}
+          onPageNumberChange={setPageNumber}
+        ></StorageBrowserTable>
       </div>
     </Spin>
   );

+ 57 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.scss

@@ -0,0 +1,57 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+@use 'variables' as vars;
+@use 'mixins';
+
+$cell-height: 40px;
+
+.hue-storage-browser__table {
+  @include mixins.hue-svg-icon__d3-conflict;
+
+  margin: 0 vars.$fluidx-spacing-s;
+
+  .hue-storage-browser__table-cell-icon {
+    color: vars.$fluidx-blue-700;
+    margin-right: 6px;
+  }
+
+  thead {
+    //TODO:Check why height isnt applied in safari browser
+    height: $cell-height;
+    background-color: vars.$fluidx-gray-040;
+  }
+
+  th.ant-table-cell {
+    text-align: left;
+  }
+}
+
+.hue-storage-browser__table-row {
+  height: $cell-height;
+  border-bottom: solid 1px vars.$fluidx-gray-300;
+
+  td.ant-table-cell {
+    white-space: nowrap;
+    overflow-x: hidden;
+    text-overflow: ellipsis;
+    //ellipsis does not work without display as block or specifying max-width. Display as block not possible in table
+    max-width: 400px;
+  }
+}
+
+.hue-storage-browser__table-cell-name {
+  color: vars.$fluidx-blue-700;
+}

+ 165 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx

@@ -0,0 +1,165 @@
+// 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 { i18nReact } from '../../../../utils/i18nReact';
+import Table from 'cuix/dist/components/Table';
+import { ColumnProps } from 'antd/lib/table';
+import FolderIcon from '@cloudera/cuix-core/icons/react/ProjectIcon';
+//TODO: Use cuix icon (Currently fileIcon does not exist in cuix)
+import { FileOutlined } from '@ant-design/icons';
+
+import { PageStats, StorageBrowserTableData } from '../../../../reactComponents/FileChooser/types';
+import Pagination from '../../../../reactComponents/Pagination/Pagination';
+import './StorageBrowserTable.scss';
+import Tooltip from 'antd/es/tooltip';
+
+interface StorageBrowserTableProps {
+  className?: string;
+  dataSource: StorageBrowserTableData[];
+  onFilepathChange: (path: string) => void;
+  onPageNumberChange: (pageNumber: number) => void;
+  onPageSizeChange: (pageSize: number) => void;
+  pageSize: number;
+  pageStats: PageStats;
+  rowClassName?: string;
+  testId?: string;
+}
+
+const defaultProps = {
+  className: 'hue-storage-browser__table',
+  rowClassName: 'hue-storage-browser__table-row',
+  testId: 'hue-storage-browser__table'
+};
+
+const StorageBrowserTable: React.FC<StorageBrowserTableProps> = ({
+  className,
+  dataSource,
+  onFilepathChange,
+  onPageNumberChange,
+  onPageSizeChange,
+  pageSize,
+  pageStats,
+  rowClassName,
+  testId,
+  ...restProps
+}): JSX.Element => {
+  const [tableHeight, setTableHeight] = useState<number>();
+
+  const { t } = i18nReact.useTranslation();
+
+  const getColumns = file => {
+    const columns: ColumnProps<unknown>[] = [];
+    for (const [key] of Object.entries(file)) {
+      const column: ColumnProps<unknown> = {
+        dataIndex: `${key}`,
+        title: t(`${key}`.charAt(0).toUpperCase() + `${key}`.slice(1)),
+        key: `${key}`
+      };
+      if (key === 'name') {
+        column.width = '45%';
+        //TODO: Apply tooltip only for truncated values
+        column.render = (_, record: any) => (
+          <Tooltip title={record.name}>
+            <span className="hue-storage-browser__table-cell-icon">
+              {record.type === 'dir' ? <FolderIcon /> : <FileOutlined />}
+            </span>
+            <span className="hue-storage-browser__table-cell-name">{record.name}</span>
+          </Tooltip>
+        );
+      } else {
+        column.width = key === 'lastUpdated' ? '15%' : '10%';
+      }
+      columns.push(column);
+    }
+    return columns.filter(col => col.dataIndex !== 'type' && col.dataIndex !== 'path');
+  };
+
+  const onRowClicked = record => {
+    return {
+      onClick: e => {
+        if (record.type === 'dir') {
+          onFilepathChange(record.path);
+        }
+        //TODO: handle onclick file
+      }
+    };
+  };
+
+  //pagination related functions handled by parent
+  const onPreviousPageButtonClicked = previousPageNumber => {
+    //If previous page does not exists api returns 0
+    onPageNumberChange(previousPageNumber === 0 ? 1 : previousPageNumber);
+  };
+
+  const onNextPageButtonClicked = (nextPageNumber, numPages) => {
+    //If next page does not exists api returns 0
+    onPageNumberChange(nextPageNumber === 0 ? numPages : nextPageNumber);
+  };
+
+  useEffect(() => {
+    //TODO: handle table resize
+    const calculateTableHeight = () => {
+      const windowHeight = window.innerHeight;
+      // TODO: move 450 to dynamic based on  table header height, tab nav and some header.
+      const tableHeightFix = windowHeight - 450;
+      return tableHeightFix;
+    };
+
+    const handleWindowResize = () => {
+      const tableHeight = calculateTableHeight();
+      setTableHeight(tableHeight);
+    };
+
+    handleWindowResize(); // Calculate initial scroll height
+
+    window.addEventListener('resize', handleWindowResize);
+
+    return () => {
+      window.removeEventListener('resize', handleWindowResize);
+    };
+  }, []);
+
+  if (dataSource) {
+    return (
+      <>
+        <Table
+          className={className}
+          columns={getColumns(dataSource[0])}
+          dataSource={dataSource}
+          onRow={onRowClicked}
+          pagination={false}
+          rowClassName={rowClassName}
+          rowKey={(record, index) => record.path + '' + index}
+          scroll={{ y: tableHeight }}
+          data-testid={`${testId}`}
+          {...restProps}
+        ></Table>
+        <Pagination
+          onNextPageButtonClicked={onNextPageButtonClicked}
+          onPageNumberChange={onPageNumberChange}
+          onPageSizeChange={onPageSizeChange}
+          onPreviousPageButtonClicked={onPreviousPageButtonClicked}
+          pageSize={pageSize}
+          pageStats={pageStats}
+        />
+      </>
+    );
+  }
+};
+
+StorageBrowserTable.defaultProps = defaultProps;
+export default StorageBrowserTable;

+ 31 - 2
desktop/core/src/desktop/js/reactComponents/FileChooser/api.ts

@@ -20,6 +20,7 @@ import { PathAndFileData } from './types';
 
 const FILESYSTEMS_API_URL = '/api/v1/storage/filesystems';
 const VIEWFILES_API_URl = '/api/v1/storage/view=';
+
 export interface ApiFileSystem {
   file_system: string;
   user_home_directory: string;
@@ -27,5 +28,33 @@ export interface ApiFileSystem {
 
 export const fetchFileSystems = (): CancellablePromise<ApiFileSystem[]> => get(FILESYSTEMS_API_URL);
 
-export const fetchFiles = (filePath: string): CancellablePromise<PathAndFileData> =>
-  get(VIEWFILES_API_URl + filePath);
+//TODO: Use object as parameter instead
+export const fetchFiles = (
+  filePath: string,
+  pagesize?: number,
+  pagenum?: number,
+  filter?: string,
+  sortby?: string,
+  descending?: boolean
+): CancellablePromise<PathAndFileData> => {
+  //If value is undefined default value is assigned.
+  pagesize = pagesize || 10;
+  pagenum = pagenum || 1;
+  filter = filter || '';
+  sortby = sortby || '';
+  descending = descending || false;
+  return get(
+    VIEWFILES_API_URl +
+      filePath +
+      '?pagesize=' +
+      pagesize +
+      '&pagenum=' +
+      pagenum +
+      '&filter=' +
+      filter +
+      '&sortby=' +
+      sortby +
+      '&descending=' +
+      descending
+  );
+};

+ 26 - 1
desktop/core/src/desktop/js/reactComponents/FileChooser/types.ts

@@ -33,7 +33,7 @@ interface Stats {
   user: string;
 }
 
-interface File {
+export interface File {
   humansize: string;
   is_sentry_managed: boolean;
   mode: string;
@@ -45,12 +45,37 @@ interface File {
   type: string;
   url: string;
 }
+
+export interface StorageBrowserTableData {
+  name: string;
+  size: string;
+  user: string;
+  groups: string;
+  permission: string;
+  lastUpdated: string;
+  type: string;
+  path: string;
+}
+
+export interface PageStats {
+  number: number;
+  num_pages: number;
+  previous_page_number: number;
+  next_page_number: number;
+  start_index: number;
+  end_index: number;
+  total_count: number;
+}
+
 export interface BreadcrumbData {
   label: string;
   url: string;
 }
+
 export interface PathAndFileData {
   path: string;
   breadcrumbs: BreadcrumbData[];
   files: File[];
+  page: PageStats;
+  pagesize: number;
 }

+ 75 - 0
desktop/core/src/desktop/js/reactComponents/Pagination/Pagination.scss

@@ -0,0 +1,75 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+@use 'variables' as vars;
+@use 'mixins';
+
+.hue-pagination {
+  background-color: vars.$fluidx-gray-100;
+  height: 20px;
+  margin: 0;
+  padding: vars.$fluidx-spacing-s;
+  display: flex;
+  gap: vars.$fluidx-spacing-xxl;
+  justify-content: flex-end;
+  color: vars.$fluidx-gray-700;
+}
+
+.hue-pagination__page-size-control {
+  display: flex;
+  flex: 0 0 auto;
+}
+
+.hue-pagination__page-size-menu-btn {
+  display: flex;
+  border: none;
+  box-shadow: none;
+  background-color: transparent;
+  color: vars.$fluidx-gray-700;
+  @include mixins.hue-svg-icon__d3-conflict;
+}
+
+.hue-pagination__page-size-menu-item-btn {
+  border: none;
+  box-shadow: none;
+  background-color: transparent;
+}
+
+.hue-pagination__rows-stats-display {
+  flex: 0 0 auto;
+}
+
+.hue-pagination__control-buttons-panel {
+  display: flex;
+  width: 128px;
+  //If margin not specified the last button moves out of screen
+  margin-right: 60px;
+  flex: 0 0 auto;
+}
+
+.hue-pagination__control-button {
+  border: none;
+  box-shadow: none;
+  background-color: transparent;
+  margin: 0 vars.$fluidx-spacing-s;
+  padding: 0;
+  @include mixins.hue-svg-icon__d3-conflict;
+
+  svg {
+    height: 20px;
+    width: 20px;
+    color: vars.$fluidx-gray-700;
+  }
+}

+ 118 - 0
desktop/core/src/desktop/js/reactComponents/Pagination/Pagination.tsx

@@ -0,0 +1,118 @@
+// 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 { Button, Dropdown } from 'antd';
+import type { MenuProps } from 'antd';
+import { i18nReact } from '../../utils/i18nReact';
+
+import PageFirstIcon from '@cloudera/cuix-core/icons/react/PageFirstIcon';
+import PagePreviousIcon from '@cloudera/cuix-core/icons/react/PagePreviousIcon';
+import PageNextIcon from '@cloudera/cuix-core/icons/react/PageNextIcon';
+import PageLastIcon from '@cloudera/cuix-core/icons/react/PageLastIcon';
+import DropdownIcon from '@cloudera/cuix-core/icons/react/DropdownIcon';
+
+import { PageStats } from '../FileChooser/types';
+import './Pagination.scss';
+
+interface PaginationProps {
+  onNextPageButtonClicked: (nextPageNumber: number, numPages: number) => void;
+  onPageNumberChange: (pageNumber: number) => void;
+  onPageSizeChange: (pageSize: number) => void;
+  onPreviousPageButtonClicked: (previousPageNumber: number) => void;
+  pageSize: number;
+  pageSizeOptions?: number[];
+  pageStats: PageStats;
+}
+
+const defaultProps = {
+  pageSizeOptions: [10, 50, 500, 1000]
+};
+
+const Pagination: React.FC<PaginationProps> = ({
+  onNextPageButtonClicked,
+  onPageNumberChange,
+  onPageSizeChange,
+  onPreviousPageButtonClicked,
+  pageSize,
+  pageSizeOptions,
+  pageStats
+}): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  const currentPageSize = pageSize;
+
+  const pageSizeOptionsMenu: MenuProps['items'] = pageSizeOptions.map(option => {
+    return {
+      key: option,
+      label: (
+        <Button
+          onClick={() => {
+            onPageSizeChange(option);
+            onPageNumberChange(1);
+          }}
+          className="hue-pagination__page-size-menu-item-btn"
+        >
+          {option}
+        </Button>
+      )
+    };
+  });
+
+  return (
+    <div className="hue-pagination">
+      <div className="hue-pagination__page-size-control">
+        {t('Rows per page: ')}
+        <Dropdown menu={{ items: pageSizeOptionsMenu }}>
+          <Button className="hue-pagination__page-size-menu-btn">
+            <div>
+              <span>{currentPageSize}</span>
+              <DropdownIcon />
+            </div>
+          </Button>
+        </Dropdown>
+      </div>
+      <div className="hue-pagination__rows-stats-display">
+        {pageStats.start_index} - {pageStats.end_index} of {pageStats.total_count}
+      </div>
+      <div className="hue-pagination__control-buttons-panel">
+        <Button onClick={() => onPageNumberChange(1)} className="hue-pagination__control-button">
+          <PageFirstIcon />
+        </Button>
+        <Button
+          onClick={() => onPreviousPageButtonClicked(pageStats.previous_page_number)}
+          className="hue-pagination__control-button"
+        >
+          <PagePreviousIcon />
+        </Button>
+        <Button
+          onClick={() => onNextPageButtonClicked(pageStats.next_page_number, pageStats.num_pages)}
+          className="hue-pagination__control-button"
+        >
+          <PageNextIcon />
+        </Button>
+        <Button
+          onClick={() => onPageNumberChange(pageStats.num_pages)}
+          className="hue-pagination__control-button"
+        >
+          <PageLastIcon />
+        </Button>
+      </div>
+    </div>
+  );
+};
+
+Pagination.defaultProps = defaultProps;
+export default Pagination;