Преглед изворни кода

[ui-storagebrowser] adds pagination in file preview page (#3949)

* fixes linting
* fixes the search bar disabled edge case
Ram Prasad Agarwal пре 10 месеци
родитељ
комит
e67d150f97

+ 10 - 2
apps/filebrowser/src/filebrowser/api.py

@@ -58,7 +58,6 @@ from filebrowser.views import (
   MAX_CHUNK_SIZE_BYTES,
   _can_inline_display,
   _is_hdfs_superuser,
-  _massage_page,
   _normalize_path,
   extract_upload_data,
   perform_upload_task,
@@ -209,6 +208,15 @@ def download(request):
   return response
 
 
+def _massage_page(page, paginator):
+  return {
+      'page_number': page.number,
+      'page_size': paginator.per_page,
+      'total_pages': paginator.num_pages,
+      'total_size': paginator.count
+  }
+
+
 @api_error_handler
 def listdir_paged(request):
   """
@@ -280,7 +288,7 @@ def listdir_paged(request):
   response = {
     'is_trash_enabled': is_trash_enabled,
     'files': page.object_list if page else [],
-    'page': _massage_page(page, paginator) if page else {},  # TODO: Check if we need to clean response of _massage_page
+    'page': _massage_page(page, paginator) if page else {},
     # TODO: Check what to keep or what to remove? or move some fields to /get_config?
     'is_fs_superuser': is_fs_superuser,
     'groups': is_fs_superuser and [str(x) for x in Group.objects.values_list('name', flat=True)] or [],

+ 4 - 16
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx

@@ -213,17 +213,6 @@ const StorageDirectoryPage = ({
     }
   };
 
-  //pagination related functions handled by parent
-  const onPreviousPageButtonClicked = (previousPageNumber: number) => {
-    //If previous page does not exists api returns 0
-    setPageNumber(previousPageNumber === 0 ? 1 : previousPageNumber);
-  };
-
-  const onNextPageButtonClicked = (nextPageNumber: number, numPages: number) => {
-    //If next page does not exists api returns 0
-    setPageNumber(nextPageNumber === 0 ? numPages : nextPageNumber);
-  };
-
   const handleSearch = useCallback(
     useDebounce(searchTerm => {
       setSearchTerm(encodeURIComponent(searchTerm));
@@ -290,6 +279,7 @@ const StorageDirectoryPage = ({
           onChange={event => {
             handleSearch(event.target.value);
           }}
+          disabled={!tableData.length && !searchTerm.length}
         />
         <div className="hue-storage-browser__actions-bar-right">
           <StorageBrowserActions
@@ -333,13 +323,11 @@ const StorageDirectoryPage = ({
             {...restProps}
           />
 
-          {filesData?.page && filesData?.page?.total_count > 0 && (
+          {filesData?.page && filesData?.page?.total_pages > 0 && (
             <Pagination
-              onNextPageButtonClicked={onNextPageButtonClicked}
-              onPageNumberChange={setPageNumber}
-              onPageSizeChange={setPageSize}
-              onPreviousPageButtonClicked={onPreviousPageButtonClicked}
+              setPageSize={setPageSize}
               pageSize={pageSize}
+              setPageNumber={setPageNumber}
               pageStats={filesData?.page}
             />
           )}

+ 16 - 9
desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.scss

@@ -22,13 +22,13 @@
     flex: 1;
     height: 100%;
     flex-direction: column;
-    gap: vars.$fluidx-spacing-s;
-    padding: vars.$fluidx-spacing-s 0;
+    gap: vars.$fluidx-spacing-xs;
+    padding: vars.$fluidx-spacing-xs 0;
 
     .meta-data {
       display: flex;
       gap: vars.$fluidx-spacing-s;
-      padding: vars.$fluidx-spacing-s;
+      padding: vars.$fluidx-spacing-xs;
       background-color: vars.$fluidx-gray-040;
       border-radius: vars.$border-radius-base;
     }
@@ -47,7 +47,6 @@
     .meta-data__column {
       display: flex;
       flex-direction: column;
-
     }
 
     .meta-data__column-label {
@@ -64,14 +63,14 @@
     .preview {
       display: flex;
       flex-direction: column;
-      flex: 1;
+      height: 100%;
     }
 
     .preview__title-bar {
       display: flex;
       justify-content: space-between;
       gap: vars.$fluidx-spacing-s;
-      padding: vars.$fluidx-spacing-s;
+      padding: vars.$fluidx-spacing-xs vars.$fluidx-spacing-s;
       font-size: vars.$font-size-lg;
       background-color: vars.$fluidx-gray-040;
       color: vars.$fluidx-gray-700;
@@ -82,7 +81,7 @@
 
     .preview__action-group {
       display: flex;
-      gap: vars.$fluidx-spacing-s;
+      gap: vars.$fluidx-spacing-xs;
     }
 
     .preview__content {
@@ -98,7 +97,7 @@
         height: 100%;
         border: none;
         border-radius: 0;
-        padding: vars.$fluidx-spacing-s;
+        padding: vars.$fluidx-spacing-xs vars.$fluidx-spacing-s;
         box-shadow: none;
       }
 
@@ -108,6 +107,14 @@
         background-color: vars.$fluidx-white;
       }
 
+      .preview__editable-file {
+        display: flex;
+        flex: 1;
+        height: 100%;
+        flex-direction: column;
+        justify-content: flex-end;
+      }
+
       .preview__document {
         display: flex;
         align-items: center;
@@ -128,4 +135,4 @@
       }
     }
   }
-}
+}

+ 30 - 10
desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx

@@ -31,7 +31,9 @@ import {
 } from '../../../reactComponents/FileChooser/api';
 import huePubSub from '../../../utils/huePubSub';
 import useSaveData from '../../../utils/hooks/useSaveData';
+import Pagination from '../../../reactComponents/Pagination/Pagination';
 import {
+  DEFAULT_PREVIEW_PAGE_SIZE,
   EDITABLE_FILE_FORMATS,
   SUPPORTED_FILE_EXTENSIONS,
   SupportedFileTypes
@@ -54,6 +56,9 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
 
   const [isEditing, setIsEditing] = useState<boolean>(false);
   const [fileContent, setFileContent] = useState<FilePreview['contents']>();
+  const [pageNumber, setPageNumber] = useState<number>(1);
+  const pageSize = DEFAULT_PREVIEW_PAGE_SIZE;
+  const pageOffset = (pageNumber - 1) * pageSize;
 
   const { loading: isSaving, save } = useSaveData(SAVE_FILE_API_URL);
 
@@ -62,13 +67,17 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
     loading: loadingPreview,
     error: errorPreview
   } = useLoadData<FilePreview>(FILE_PREVIEW_API_URL, {
-    params: { path: fileStats.path },
+    params: {
+      path: fileStats.path,
+      offset: pageOffset,
+      length: pageSize
+    },
     onSuccess: d => setFileContent(d.contents),
     skip:
       fileStats.path === '' ||
       fileStats.path === undefined ||
       fileStats?.type !== BrowserViewType.file ||
-      EDITABLE_FILE_FORMATS[fileType] === undefined
+      !EDITABLE_FILE_FORMATS.has(fileType)
   });
 
   const fileMetaData = useMemo(() => getFileMetaData(t, fileStats), [t, fileStats]);
@@ -112,8 +121,14 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
     !isEditing &&
     config?.storage_browser.max_file_editor_size &&
     config?.storage_browser.max_file_editor_size > fileStats.size &&
-    EDITABLE_FILE_FORMATS[fileType] &&
-    fileData?.compression?.toLocaleLowerCase() === 'none';
+    EDITABLE_FILE_FORMATS.has(fileType);
+
+  const pageStats = {
+    page_number: pageNumber,
+    total_pages: Math.ceil(fileStats.size / pageSize),
+    page_size: 0,
+    total_size: 0
+  };
 
   const errorConfig = [
     {
@@ -192,12 +207,17 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
 
             <div className="preview__content">
               {fileType === SupportedFileTypes.TEXT && (
-                <textarea
-                  value={fileContent}
-                  onChange={e => setFileContent(e.target.value)}
-                  readOnly={!isEditing}
-                  className="preview__textarea"
-                />
+                <div className="preview__editable-file">
+                  <textarea
+                    value={fileContent}
+                    onChange={e => setFileContent(e.target.value)}
+                    readOnly={!isEditing}
+                    className="preview__textarea"
+                  />
+                  {!loadingPreview && pageStats.total_pages > 1 && (
+                    <Pagination setPageNumber={setPageNumber} pageStats={pageStats} />
+                  )}
+                </div>
               )}
 
               {fileType === SupportedFileTypes.IMAGE && <img src={filePreviewUrl} alt={fileName} />}

+ 4 - 8
desktop/core/src/desktop/js/reactComponents/FileChooser/types.ts

@@ -45,18 +45,14 @@ export interface StorageDirectoryTableData
 }
 
 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;
+  page_number: number;
+  total_pages: number;
+  page_size: number;
+  total_size: number;
 }
 
 export interface FilePreview {
   contents: string;
-  compression?: string;
   end: number;
   length: number;
   mode: string;

+ 1 - 1
desktop/core/src/desktop/js/reactComponents/Pagination/Pagination.scss

@@ -19,7 +19,7 @@
 .antd.cuix {
   .hue-pagination {
     background-color: vars.$fluidx-gray-100;
-    padding: vars.$fluidx-spacing-s;
+    padding: vars.$fluidx-spacing-xs;
     display: flex;
     gap: vars.$fluidx-spacing-xxl;
     justify-content: flex-end;

+ 42 - 32
desktop/core/src/desktop/js/reactComponents/Pagination/Pagination.tsx

@@ -28,13 +28,12 @@ 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;
+  setPageNumber: (pageNumber: number) => void;
+  setPageSize?: (pageSize: number) => void;
+  pageSize?: number;
   pageSizeOptions?: number[];
   pageStats: PageStats;
+  showIndexes?: boolean;
 }
 
 const defaultProps = {
@@ -42,17 +41,16 @@ const defaultProps = {
 };
 
 const Pagination = ({
-  onNextPageButtonClicked,
-  onPageNumberChange,
-  onPageSizeChange,
-  onPreviousPageButtonClicked,
-  pageSize,
+  setPageNumber,
+  setPageSize,
   pageSizeOptions = [],
-  pageStats
+  pageStats,
+  showIndexes = false
 }: PaginationProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const currentPageSize = pageSize;
+  const startIndex = pageStats.page_size * (pageStats.page_number - 1) + 1;
+  const endIndex = Math.min(pageStats.page_size * pageStats.page_number, pageStats.total_size);
 
   const pageSizeOptionsMenu: MenuProps['items'] = pageSizeOptions.map(option => {
     return {
@@ -60,8 +58,8 @@ const Pagination = ({
       label: (
         <BorderlessButton
           onClick={() => {
-            onPageSizeChange(option);
-            onPageNumberChange(1);
+            setPageSize && setPageSize(option);
+            setPageNumber(1);
           }}
           className="hue-pagination__page-size-menu-item-btn"
           data-event={''}
@@ -74,45 +72,57 @@ const Pagination = ({
 
   return (
     <div className="hue-pagination">
-      <div className="hue-pagination__page-size-control">
-        {t('Rows per page: ')}
-        <Dropdown menu={{ items: pageSizeOptionsMenu }}>
-          <BorderlessButton
-            className="hue-pagination__page-size-menu-btn"
-            data-event={''}
-            icon={<DropdownIcon />}
-            iconPosition="right"
-          >
-            {currentPageSize}
-          </BorderlessButton>
-        </Dropdown>
-      </div>
+      {pageStats.page_size > 0 && (
+        <div className="hue-pagination__page-size-control">
+          {t('Rows per page: ')}
+          <Dropdown menu={{ items: pageSizeOptionsMenu }}>
+            <BorderlessButton
+              className="hue-pagination__page-size-menu-btn"
+              data-event={''}
+              icon={<DropdownIcon />}
+              iconPosition="right"
+            >
+              {pageStats.page_size}
+            </BorderlessButton>
+          </Dropdown>
+        </div>
+      )}
       <div className="hue-pagination__rows-stats-display">
-        {pageStats.start_index} - {pageStats.end_index} of {pageStats.total_count}
+        {showIndexes
+          ? `${startIndex} - ${endIndex} of ${pageStats.total_size}`
+          : `${pageStats.page_number} of ${pageStats.total_pages}`}
       </div>
       <div className="hue-pagination__control-buttons-panel">
         <BorderlessButton
-          onClick={() => onPageNumberChange(1)}
+          onClick={() => setPageNumber(1)}
           className="hue-pagination__control-button"
           data-event={''}
+          disabled={pageStats.page_number === 1}
+          title={t('First Page')}
           icon={<PageFirstIcon />}
         />
         <BorderlessButton
-          onClick={() => onPreviousPageButtonClicked(pageStats.previous_page_number)}
+          onClick={() => setPageNumber(pageStats.page_number - 1)}
           className="hue-pagination__control-button"
           data-event={''}
+          disabled={pageStats.page_number === 1}
+          title={t('First Page')}
           icon={<PagePreviousIcon />}
         />
         <BorderlessButton
-          onClick={() => onNextPageButtonClicked(pageStats.next_page_number, pageStats.num_pages)}
+          onClick={() => setPageNumber(pageStats.page_number + 1)}
           className="hue-pagination__control-button"
           data-event={''}
+          disabled={pageStats.page_number === pageStats.total_pages}
+          title={t('Next Page')}
           icon={<PageNextIcon />}
         />
         <BorderlessButton
-          onClick={() => onPageNumberChange(pageStats.num_pages)}
+          onClick={() => setPageNumber(pageStats.total_pages)}
           className="hue-pagination__control-button"
           data-event={''}
+          disabled={pageStats.page_number === pageStats.total_pages}
+          title={t('Last Page')}
           icon={<PageLastIcon />}
         />
       </div>

+ 2 - 3
desktop/core/src/desktop/js/utils/constants/storageBrowser.ts

@@ -18,6 +18,7 @@ export const DEFAULT_PAGE_SIZE = 50;
 export const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024; // 5 MiB
 export const DEFAULT_CONCURRENT_MAX_CONNECTIONS = 3;
 export const DEFAULT_ENABLE_CHUNK_UPLOAD = false;
+export const DEFAULT_PREVIEW_PAGE_SIZE = 1024 * 512; // 0.5 MiB
 export const DEFAULT_POLLING_TIME = 10 * 1000; // 10 seconds
 
 export enum SupportedFileTypes {
@@ -61,6 +62,4 @@ export const SUPPORTED_FILE_EXTENSIONS: Record<string, SupportedFileTypes> = {
   mp4: SupportedFileTypes.VIDEO
 };
 
-export const EDITABLE_FILE_FORMATS = {
-  [SupportedFileTypes.TEXT]: 1
-};
+export const EDITABLE_FILE_FORMATS = new Set([SupportedFileTypes.TEXT]);