Browse Source

[ui-storageBrowser]Implement storage browser table search (#3781)

* [ui-storageBrowser]Implement storage browser table search
Nidhi Bhat G 1 year ago
parent
commit
376c3c4088

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

@@ -47,14 +47,13 @@ const StorageBrowserTabContent = ({
   const [pageNumber, setPageNumber] = useState<number>(1);
   const [sortByColumn, setSortByColumn] = useState<string>('');
   const [sortOrder, setSortOrder] = useState<SortOrder>(SortOrder.NONE);
-  //TODO: Add filter functionality
-  const [filterData] = useState<string>('');
+  const [searchTerm, setSearchTerm] = useState<string>('');
 
   const { t } = i18nReact.useTranslation();
 
   const getFiles = useCallback(async () => {
     setLoadingFiles(true);
-    fetchFiles(filePath, pageSize, pageNumber, filterData, sortByColumn, sortOrder)
+    fetchFiles(filePath, pageSize, pageNumber, searchTerm, sortByColumn, sortOrder)
       .then(responseFilesData => {
         setFilesData(responseFilesData);
         setPageSize(responseFilesData.pagesize);
@@ -66,11 +65,11 @@ const StorageBrowserTabContent = ({
       .finally(() => {
         setLoadingFiles(false);
       });
-  }, [filePath, pageSize, pageNumber, filterData, sortByColumn, sortOrder]);
+  }, [filePath, pageSize, pageNumber, searchTerm, sortByColumn, sortOrder]);
 
   useEffect(() => {
     getFiles();
-  }, [filePath, pageSize, pageNumber, sortByColumn, sortOrder]);
+  }, [getFiles]);
 
   return (
     <Spin spinning={loadingFiles}>
@@ -101,6 +100,7 @@ const StorageBrowserTabContent = ({
           onPageNumberChange={setPageNumber}
           onSortByColumnChange={setSortByColumn}
           onSortOrderChange={setSortOrder}
+          onSearch={setSearchTerm}
           sortByColumn={sortByColumn}
           sortOrder={sortOrder}
           refetchData={getFiles}

+ 20 - 2
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState, useCallback } from 'react';
 import { ColumnProps } from 'antd/lib/table';
 import { Dropdown, Input } from 'antd';
 import { MenuItemGroupType } from 'antd/lib/menu/hooks/useItems';
@@ -33,6 +33,8 @@ import Table from 'cuix/dist/components/Table';
 
 import { i18nReact } from '../../../../utils/i18nReact';
 import huePubSub from '../../../../utils/huePubSub';
+import useDebounce from '../../../../utils/useDebounce';
+
 import { mkdir, touch } from '../../../../reactComponents/FileChooser/api';
 import {
   StorageBrowserTableData,
@@ -55,6 +57,7 @@ interface StorageBrowserTableProps {
   onPageSizeChange: (pageSize: number) => void;
   onSortByColumnChange: (sortByColumn: string) => void;
   onSortOrderChange: (sortOrder: SortOrder) => void;
+  onSearch: (searchTerm: string) => void;
   pageSize: number;
   sortByColumn: string;
   sortOrder: SortOrder;
@@ -79,6 +82,7 @@ const StorageBrowserTable = ({
   onPageSizeChange,
   onSortByColumnChange,
   onSortOrderChange,
+  onSearch,
   sortByColumn,
   sortOrder,
   pageSize,
@@ -267,6 +271,13 @@ const StorageBrowserTable = ({
       });
   };
 
+  const handleSearch = useCallback(
+    useDebounce(searchTerm => {
+      onSearch(encodeURIComponent(searchTerm));
+    }),
+    [onSearch]
+  );
+
   useEffect(() => {
     //TODO: handle table resize
     const calculateTableHeight = () => {
@@ -305,7 +316,14 @@ const StorageBrowserTable = ({
   return (
     <>
       <div className="hue-storage-browser__actions-bar">
-        <Input className="hue-storage-browser__search" placeholder={t('Search')} />
+        <Input
+          className="hue-storage-browser__search"
+          placeholder={t('Search')}
+          allowClear={true}
+          onChange={event => {
+            handleSearch(event.target.value);
+          }}
+        />
         <div className="hue-storage-browser__actions-bar-right">
           {viewType === BrowserViewType.dir && (
             <>

+ 17 - 0
desktop/core/src/desktop/js/utils/constants/common.ts

@@ -0,0 +1,17 @@
+// 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 DEBOUNCE_DELAY = 300; // in milliseconds

+ 95 - 0
desktop/core/src/desktop/js/utils/useDebounce.test.ts

@@ -0,0 +1,95 @@
+// 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 { renderHook, act } from '@testing-library/react';
+import useDebounce, { SomeFunction } from './useDebounce';
+import { DEBOUNCE_DELAY } from './constants/common';
+
+const mockFunction: jest.Mock<SomeFunction> = jest.fn();
+jest.useFakeTimers();
+
+afterEach(() => {
+  jest.clearAllTimers();
+  mockFunction.mockReset();
+});
+
+describe('useDebounce', () => {
+  it('should return a debounced function', () => {
+    const { result } = renderHook(() => useDebounce(mockFunction));
+    expect(typeof result.current).toBe('function');
+  });
+
+  it('should not call the function immediately when called with arguments', () => {
+    const { result } = renderHook(() => useDebounce(mockFunction));
+    act(() => {
+      result.current('test');
+    });
+    expect(mockFunction).not.toHaveBeenCalled();
+  });
+
+  it('should call the function with the latest arguments after the delay', async () => {
+    const { result } = renderHook(() => useDebounce(mockFunction));
+    act(() => {
+      result.current('test1');
+      result.current('test2'); // Simulate multiple calls before delay
+    });
+    jest.advanceTimersByTime(DEBOUNCE_DELAY);
+    expect(mockFunction).toHaveBeenCalledTimes(1);
+    expect(mockFunction).toHaveBeenCalledWith('test2'); // Only the latest arguments should be passed
+  });
+
+  it('should cancel the previous timeout if called again before delay', () => {
+    const { result } = renderHook(() => useDebounce(mockFunction));
+    act(() => {
+      result.current('test1');
+      result.current('test2');
+    });
+    jest.advanceTimersByTime(DEBOUNCE_DELAY / 2); // Advance half the delay to simulate a race condition
+    result.current('test3');
+
+    jest.runAllTimers();
+
+    expect(mockFunction).toHaveBeenCalledTimes(1);
+    expect(mockFunction).toHaveBeenCalledWith('test3'); // Only the latest arguments after the race condition should be passed
+  });
+
+  it('should cleanup the timer on unmount', () => {
+    const { result, unmount } = renderHook(() => useDebounce(mockFunction));
+    act(() => {
+      result.current('test');
+    });
+    unmount();
+    jest.advanceTimersByTime(DEBOUNCE_DELAY);
+    expect(mockFunction).not.toHaveBeenCalled();
+  });
+
+  it('should not call the function with empty arguments', () => {
+    const { result } = renderHook(() => useDebounce(mockFunction));
+    act(() => {
+      result.current(); // Call with empty arguments
+    });
+    expect(mockFunction).not.toHaveBeenCalled();
+  });
+
+  it('should handle very short delays (0ms)', () => {
+    const { result } = renderHook(() => useDebounce(mockFunction, 0));
+    act(() => {
+      result.current('test');
+    });
+    jest.runAllTimers();
+    expect(mockFunction).toHaveBeenCalledTimes(1);
+    expect(mockFunction).toHaveBeenCalledWith('test');
+  });
+});

+ 48 - 0
desktop/core/src/desktop/js/utils/useDebounce.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 { useRef, useEffect } from 'react';
+import { DEBOUNCE_DELAY } from './constants/common';
+
+type Timer = ReturnType<typeof setTimeout>;
+export type SomeFunction = (arg: string) => void;
+
+const useDebounce = <Func extends SomeFunction>(func: Func, delay = DEBOUNCE_DELAY): Func => {
+  const timer = useRef<Timer>();
+
+  useEffect(() => {
+    return () => {
+      if (!timer.current) {
+        return;
+      }
+      clearTimeout(timer.current);
+    };
+  }, []);
+
+  const debouncedFunction = ((...args) => {
+    if (timer.current) {
+      clearTimeout(timer.current);
+    }
+    const newTimer = setTimeout(() => {
+      func(...args);
+    }, delay);
+
+    timer.current = newTimer;
+  }) as Func;
+
+  return debouncedFunction;
+};
+
+export default useDebounce;