NestedFolderPicker: Debounce search correctly (#80956)

* debounce nested folder picker search

* readd logic when no search string
This commit is contained in:
Ashley Harrison 2024-01-24 11:41:25 +00:00 committed by GitHub
parent 5b4a984b78
commit e84ee33c8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 39 additions and 20 deletions

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { autoUpdate, flip, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { autoUpdate, flip, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react';
import React, { useCallback, useId, useMemo, useState } from 'react'; import debounce from 'debounce-promise';
import { useAsync } from 'react-use'; import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
@ -19,7 +19,7 @@ import {
} from 'app/features/browse-dashboards/state'; } from 'app/features/browse-dashboards/state';
import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils'; import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils';
import { DashboardViewItemCollection } from 'app/features/browse-dashboards/types'; import { DashboardViewItemCollection } from 'app/features/browse-dashboards/types';
import { getGrafanaSearcher } from 'app/features/search/service'; import { QueryResponse, getGrafanaSearcher } from 'app/features/search/service';
import { queryResultToViewItem } from 'app/features/search/service/utils'; import { queryResultToViewItem } from 'app/features/search/service/utils';
import { DashboardViewItem } from 'app/features/search/types'; import { DashboardViewItem } from 'app/features/search/types';
import { useDispatch, useSelector } from 'app/types/store'; import { useDispatch, useSelector } from 'app/types/store';
@ -47,6 +47,19 @@ export interface NestedFolderPickerProps {
const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const]; const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const];
const debouncedSearch = debounce(getSearchResults, 300);
async function getSearchResults(searchQuery: string) {
const queryResponse = await getGrafanaSearcher().search({
query: searchQuery,
kind: ['folder'],
limit: 100,
});
const items = queryResponse.view.map((v) => queryResultToViewItem(v, queryResponse.view));
return { ...queryResponse, items };
}
export function NestedFolderPicker({ export function NestedFolderPicker({
value, value,
invalid, invalid,
@ -62,26 +75,34 @@ export function NestedFolderPicker({
const nestedFoldersEnabled = Boolean(config.featureToggles.nestedFolders); const nestedFoldersEnabled = Boolean(config.featureToggles.nestedFolders);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<(QueryResponse & { items: DashboardViewItem[] }) | null>(null);
const [isFetchingSearchResults, setIsFetchingSearchResults] = useState(false);
const [autoFocusButton, setAutoFocusButton] = useState(false); const [autoFocusButton, setAutoFocusButton] = useState(false);
const [overlayOpen, setOverlayOpen] = useState(false); const [overlayOpen, setOverlayOpen] = useState(false);
const [folderOpenState, setFolderOpenState] = useState<Record<string, boolean>>({}); const [folderOpenState, setFolderOpenState] = useState<Record<string, boolean>>({});
const overlayId = useId(); const overlayId = useId();
const [error] = useState<Error | undefined>(undefined); // TODO: error not populated anymore const [error] = useState<Error | undefined>(undefined); // TODO: error not populated anymore
const lastSearchTimestamp = useRef<number>(0);
const searchState = useAsync(async () => { useEffect(() => {
if (!search) { if (!search) {
return undefined; setSearchResults(null);
return;
} }
const searcher = getGrafanaSearcher(); const timestamp = Date.now();
const queryResponse = await searcher.search({ setIsFetchingSearchResults(true);
query: search, debouncedSearch(search).then((queryResponse) => {
kind: ['folder'], // Only keep the results if it's was issued after the most recently resolved search.
limit: 100, // This prevents results showing out of order if first request is slower than later ones.
}); // We don't need to worry about clearing the isFetching state either - if there's a later
// request in progress, this will clear it for us
if (timestamp > lastSearchTimestamp.current) {
const items = queryResponse.view.map((v) => queryResultToViewItem(v, queryResponse.view)); const items = queryResponse.view.map((v) => queryResultToViewItem(v, queryResponse.view));
setSearchResults({ ...queryResponse, items });
return { ...queryResponse, items }; setIsFetchingSearchResults(false);
lastSearchTimestamp.current = timestamp;
}
});
}, [search]); }, [search]);
const rootCollection = useSelector(rootItemsSelector); const rootCollection = useSelector(rootItemsSelector);
@ -152,9 +173,7 @@ export function NestedFolderPicker({
); );
const flatTree = useMemo(() => { const flatTree = useMemo(() => {
const searchResults = search && searchState.value; if (search && searchResults) {
if (searchResults) {
const searchCollection: DashboardViewItemCollection = { const searchCollection: DashboardViewItemCollection = {
isFullyLoaded: true, //searchResults.items.length === searchResults.totalRows, isFullyLoaded: true, //searchResults.items.length === searchResults.totalRows,
lastKindHasMoreItems: false, // TODO: paginate search lastKindHasMoreItems: false, // TODO: paginate search
@ -203,7 +222,7 @@ export function NestedFolderPicker({
} }
return flatTree; return flatTree;
}, [search, searchState.value, rootCollection, childrenCollections, folderOpenState, excludeUIDs, showRootFolder]); }, [search, searchResults, rootCollection, childrenCollections, folderOpenState, excludeUIDs, showRootFolder]);
const isItemLoaded = useCallback( const isItemLoaded = useCallback(
(itemIndex: number) => { (itemIndex: number) => {
@ -219,7 +238,7 @@ export function NestedFolderPicker({
[flatTree] [flatTree]
); );
const isLoading = rootStatus === 'pending' || searchState.loading; const isLoading = rootStatus === 'pending' || isFetchingSearchResults;
const { focusedItemIndex, handleKeyDown } = useTreeInteractions({ const { focusedItemIndex, handleKeyDown } = useTreeInteractions({
tree: flatTree, tree: flatTree,
@ -311,7 +330,7 @@ export function NestedFolderPicker({
onFolderExpand={handleFolderExpand} onFolderExpand={handleFolderExpand}
onFolderSelect={handleFolderSelect} onFolderSelect={handleFolderSelect}
idPrefix={overlayId} idPrefix={overlayId}
foldersAreOpenable={nestedFoldersEnabled && !(search && searchState.value)} foldersAreOpenable={nestedFoldersEnabled && !(search && searchResults)}
isItemLoaded={isItemLoaded} isItemLoaded={isItemLoaded}
requestLoadMore={handleLoadMore} requestLoadMore={handleLoadMore}
/> />