Skip to content

Commit

Permalink
[ui-storageBrowser]Implement storage browser table search (#3781)
Browse files Browse the repository at this point in the history
* [ui-storageBrowser]Implement storage browser table search
  • Loading branch information
nidhibhatg authored Jul 11, 2024
1 parent df0e7f9 commit 376c3c4
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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}>
Expand Down Expand Up @@ -101,6 +100,7 @@ const StorageBrowserTabContent = ({
onPageNumberChange={setPageNumber}
onSortByColumnChange={setSortByColumn}
onSortOrderChange={setSortOrder}
onSearch={setSearchTerm}
sortByColumn={sortByColumn}
sortOrder={sortOrder}
refetchData={getFiles}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -79,6 +82,7 @@ const StorageBrowserTable = ({
onPageSizeChange,
onSortByColumnChange,
onSortOrderChange,
onSearch,
sortByColumn,
sortOrder,
pageSize,
Expand Down Expand Up @@ -267,6 +271,13 @@ const StorageBrowserTable = ({
});
};

const handleSearch = useCallback(
useDebounce(searchTerm => {
onSearch(encodeURIComponent(searchTerm));
}),
[onSearch]
);

useEffect(() => {
//TODO: handle table resize
const calculateTableHeight = () => {
Expand Down Expand Up @@ -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 && (
<>
Expand Down
17 changes: 17 additions & 0 deletions desktop/core/src/desktop/js/utils/constants/common.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 95 additions & 0 deletions desktop/core/src/desktop/js/utils/useDebounce.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 48 additions & 0 deletions desktop/core/src/desktop/js/utils/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 376c3c4

Please sign in to comment.