瀏覽代碼

[ui-storagebrowser] add useWindowSize hook to get window size (#3930)

Ram Prasad Agarwal 8 月之前
父節點
當前提交
cce0baea2d

+ 63 - 51
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.scss

@@ -13,82 +13,94 @@
 // 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;
 $table-placeholder-height: 100px;
-$action-dropdown-width: 214px;
 $icon-margin: 5px;
 
 .antd.cuix {
-  .hue-storage-browser__actions-bar {
+  .hue-storage-browser-directory {
     display: flex;
-    margin: vars.$fluidx-spacing-s 0 vars.$fluidx-spacing-s vars.$fluidx-spacing-xxs;
-    justify-content: space-between;
-  }
-
-  .hue-storage-browser__search {
-    width: 30%;
+    flex-direction: column;
+    flex: 1;
+
+    &__actions-bar {
+      display: flex;
+      margin: vars.$fluidx-spacing-s 0 vars.$fluidx-spacing-s vars.$fluidx-spacing-xxs;
+      justify-content: space-between;
+
+      &__search {
+        width: min(300px, calc(100% - 200px));
+
+        .ant-input {
+          box-shadow: none;
+        }
+      }
+
+      &__actions {
+        display: flex;
+        gap: vars.$fluidx-spacing-xs;
+
+        .cdp-icon-dropdown {
+          margin-left: $icon-margin;
+        }
+      }
+    }
 
-    .ant-input {
-      box-shadow: none;
+    &__table-container {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
     }
-  }
 
-  .hue-storage-browser__actions-bar-right {
-    display: flex;
-    gap: vars.$fluidx-spacing-xs;
+    .hue-storage-browser__table {
+      .ant-table-placeholder {
+        height: $table-placeholder-height;
+        text-align: center;
+      }
 
-    .cdp-icon-dropdown {
-      margin-left: $icon-margin;
+      th.ant-table-cell {
+        height: $cell-height;
+        background-color: vars.$fluidx-gray-040;
+      }
     }
-  }
 
-  .hue-storage-browser__table {
-    .ant-table-placeholder {
-      height: $table-placeholder-height;
-      text-align: center;
+    .hue-storage-browser__table-cell-icon {
+      color: vars.$fluidx-blue-700;
+      margin-right: 6px;
+      vertical-align: middle;
     }
 
-    th.ant-table-cell {
-      height: $cell-height;
-      background-color: vars.$fluidx-gray-040;
-    }
-  }
+    .hue-storage-browser__table-row {
+      border-bottom: 1px solid vars.$fluidx-gray-040;
 
-  .hue-storage-browser__table-cell-icon {
-    color: vars.$fluidx-blue-700;
-    margin-right: 6px;
-    vertical-align: middle;
-  }
+      :hover {
+        cursor: pointer;
+      }
 
-  .hue-storage-browser__table-row {
-    border-bottom: 1px solid vars.$fluidx-gray-040;
+      td.ant-table-cell {
+        height: $cell-height;
+        @include mixins.nowrap-ellipsis;
+      }
 
-    :hover {
-      cursor: pointer;
+      td.ant-table-cell:first-child {
+        text-overflow: initial;
+      }
     }
 
-    td.ant-table-cell {
-      height: $cell-height;
-      @include mixins.nowrap-ellipsis;
+    .hue-storage-browser__table-cell-name {
+      color: vars.$fluidx-blue-700;
     }
 
-    td.ant-table-cell:first-child {
-      text-overflow: initial;
+    .hue-storage-browser__table-column-header {
+      display: flex;
     }
-  }
-
-  .hue-storage-browser__table-cell-name {
-    color: vars.$fluidx-blue-700;
-  }
 
-  .hue-storage-browser__table-column-header {
-    display: flex;
-  }
-
-  .hue-storage-browser__table-column-title {
-    text-transform: capitalize;
+    .hue-storage-browser__table-column-title {
+      text-transform: capitalize;
+    }
   }
 }

+ 46 - 63
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import React, { useMemo, useState, useCallback, useEffect } from 'react';
+import React, { useMemo, useState, useCallback } from 'react';
 import { ColumnProps } from 'antd/lib/table';
 import { Input, Tooltip } from 'antd';
 
@@ -52,6 +52,7 @@ import DragAndDrop from '../../../reactComponents/DragAndDrop/DragAndDrop';
 import UUID from '../../../utils/string/UUID';
 import { UploadItem } from '../../../utils/hooks/useFileUpload/util';
 import FileUploadQueue from '../../../reactComponents/FileUploadQueue/FileUploadQueue';
+import { useWindowSize } from '../../../utils/hooks/useWindowSize/useWindowSize';
 import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 import StorageDirectoryActions from './StorageDirectoryActions/StorageDirectoryActions';
 
@@ -82,7 +83,6 @@ const StorageDirectoryPage = ({
   ...restProps
 }: StorageDirectoryPageProps): JSX.Element => {
   const [loadingFiles, setLoadingFiles] = useState<boolean>(false);
-  const [tableHeight, setTableHeight] = useState<number>(100);
   const [selectedFiles, setSelectedFiles] = useState<StorageDirectoryTableData[]>([]);
   const [filesToUpload, setFilesToUpload] = useState<UploadItem[]>([]);
   const [polling, setPolling] = useState<boolean>(false);
@@ -237,28 +237,9 @@ const StorageDirectoryPage = ({
     setFilesToUpload(prevFiles => [...prevFiles, ...newUploadItems]);
   };
 
-  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);
-    };
-  }, []);
+  const [tableRef, rect] = useWindowSize();
+  // 40px for table header, 50px for pagination
+  const tableBodyHeight = Math.max(rect.height - 90, 100);
 
   const locale = {
     emptyText: t('Folder is empty')
@@ -274,10 +255,10 @@ const StorageDirectoryPage = ({
   ];
 
   return (
-    <>
-      <div className="hue-storage-browser__actions-bar">
+    <div className="hue-storage-browser-directory">
+      <div className="hue-storage-browser-directory__actions-bar">
         <Input
-          className="hue-storage-browser__search"
+          className="hue-storage-browser-directory__actions-bar__search"
           placeholder={t('Search')}
           allowClear={true}
           onChange={event => {
@@ -285,7 +266,7 @@ const StorageDirectoryPage = ({
           }}
           disabled={!tableData.length && !searchTerm.length}
         />
-        <div className="hue-storage-browser__actions-bar-right">
+        <div className="hue-storage-browser-directory__actions-bar__actions">
           <StorageDirectoryActions
             fileStats={fileStats}
             fileSystem={fileSystem}
@@ -301,41 +282,43 @@ const StorageDirectoryPage = ({
         </div>
       </div>
 
-      <DragAndDrop onDrop={onFilesDrop}>
-        <LoadingErrorWrapper
-          loading={(loadingFiles || listDirectoryLoading) && !polling}
-          errors={errorConfig}
-        >
-          <Table
-            className={className}
-            columns={getColumns(tableData[0] ?? {})}
-            dataSource={tableData}
-            onRow={onRowClicked}
-            pagination={false}
-            rowClassName={rowClassName}
-            rowKey={r => `${r.path}_${r.type}_${r.mtime}`}
-            rowSelection={{
-              hideSelectAll: !tableData.length,
-              columnWidth: 36,
-              type: 'checkbox',
-              ...rowSelection
-            }}
-            scroll={{ y: tableHeight }}
-            data-testid={testId}
-            locale={locale}
-            {...restProps}
-          />
-
-          {filesData?.page && filesData?.page?.total_pages > 0 && (
-            <Pagination
-              setPageSize={setPageSize}
-              pageSize={pageSize}
-              setPageNumber={setPageNumber}
-              pageStats={filesData?.page}
+      <div ref={tableRef} className="hue-storage-browser-directory__table-container">
+        <DragAndDrop onDrop={onFilesDrop}>
+          <LoadingErrorWrapper
+            loading={(loadingFiles || listDirectoryLoading) && !polling}
+            errors={errorConfig}
+          >
+            <Table
+              className={className}
+              columns={getColumns(tableData[0] ?? {})}
+              dataSource={tableData}
+              onRow={onRowClicked}
+              pagination={false}
+              rowClassName={rowClassName}
+              rowKey={r => `${r.path}_${r.type}_${r.mtime}`}
+              rowSelection={{
+                hideSelectAll: !tableData.length,
+                columnWidth: 36,
+                type: 'checkbox',
+                ...rowSelection
+              }}
+              scroll={{ y: tableBodyHeight }}
+              data-testid={`${testId}`}
+              locale={locale}
+              {...restProps}
             />
-          )}
-        </LoadingErrorWrapper>
-      </DragAndDrop>
+
+            {filesData?.page && filesData?.page?.total_pages > 0 && (
+              <Pagination
+                setPageSize={setPageSize}
+                pageSize={pageSize}
+                setPageNumber={setPageNumber}
+                pageStats={filesData?.page}
+              />
+            )}
+          </LoadingErrorWrapper>
+        </DragAndDrop>
+      </div>
       {filesToUpload.length > 0 && (
         <FileUploadQueue
           filesQueue={filesToUpload}
@@ -346,7 +329,7 @@ const StorageDirectoryPage = ({
           }}
         />
       )}
-    </>
+    </div>
   );
 };
 

+ 82 - 0
desktop/core/src/desktop/js/utils/hooks/useWindowSize/useWindowSize.test.tsx

@@ -0,0 +1,82 @@
+// 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 { act, renderHook } from '@testing-library/react';
+import { useWindowSize } from './useWindowSize';
+
+describe('useWindowSize', () => {
+  const mockCallback = jest.fn();
+  const mockObserver = {
+    observe: jest.fn(),
+    disconnect: jest.fn()
+  };
+
+  beforeAll(() => {
+    global.ResizeObserver = jest.fn().mockImplementation(callback => {
+      mockCallback.mockImplementation(callback);
+      return mockObserver;
+    });
+  });
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should initialize with default dimensions', () => {
+    const { result } = renderHook(() => useWindowSize());
+
+    expect(result.current[1]).toEqual({
+      x: 0,
+      y: 0,
+      width: 0,
+      height: 0,
+      top: 0,
+      right: 0,
+      bottom: 0,
+      left: 0
+    });
+  });
+
+  it('should update dimensions when ResizeObserver fires', () => {
+    const { result } = renderHook(() => useWindowSize());
+    const div = document.createElement('div');
+    result.current[0].current = div;
+
+    const newDimensions = {
+      x: 10,
+      y: 20,
+      width: 100,
+      height: 200,
+      top: 20,
+      right: 110,
+      bottom: 220,
+      left: 10
+    };
+
+    act(() => {
+      mockCallback([{ target: div, contentRect: newDimensions }]);
+    });
+
+    expect(result.current[1]).toEqual(newDimensions);
+  });
+
+  it('should disconnect observer on unmount', () => {
+    const { unmount } = renderHook(() => useWindowSize());
+
+    unmount();
+    expect(mockObserver.disconnect).toHaveBeenCalled();
+  });
+});

+ 51 - 0
desktop/core/src/desktop/js/utils/hooks/useWindowSize/useWindowSize.ts

@@ -0,0 +1,51 @@
+// 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 { useLayoutEffect, useRef, useState } from 'react';
+
+type ObserverRect = Omit<DOMRectReadOnly, 'toJSON'>;
+
+export const useWindowSize = (): [React.RefObject<HTMLDivElement>, ObserverRect] => {
+  const ref = useRef<HTMLDivElement>(null);
+  const [rect, setRect] = useState<ObserverRect>({
+    x: 0,
+    y: 0,
+    width: 0,
+    height: 0,
+    top: 0,
+    right: 0,
+    bottom: 0,
+    left: 0
+  });
+
+  const handleResize = (entries: ResizeObserverEntry[]) => {
+    const boundingRect = entries[0].contentRect;
+    setRect(boundingRect);
+  };
+
+  useLayoutEffect(() => {
+    const observer = new ResizeObserver(handleResize);
+    if (ref.current) {
+      observer.observe(ref.current);
+    }
+
+    return () => {
+      observer.disconnect();
+    };
+  }, [ref]);
+
+  return [ref, rect];
+};