Command palette: Enable folder searching (#62663)

search folders as well as dashboards in the command palette
This commit is contained in:
Ashley Harrison 2023-02-09 16:31:05 +00:00 committed by GitHub
parent d3bc39be8f
commit 36c090416a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 53 additions and 31 deletions

View File

@ -21,7 +21,7 @@ import { t } from 'app/core/internationalization';
import { KBarResults } from './KBarResults'; import { KBarResults } from './KBarResults';
import { ResultItem } from './ResultItem'; import { ResultItem } from './ResultItem';
import { useDashboardResults } from './actions/dashboardActions'; import { useSearchResults } from './actions/dashboardActions';
import useActions from './actions/useActions'; import useActions from './actions/useActions';
import { CommandPaletteAction } from './types'; import { CommandPaletteAction } from './types';
import { useMatches } from './useMatches'; import { useMatches } from './useMatches';
@ -36,7 +36,7 @@ export function CommandPalette() {
const actions = useActions(searchQuery); const actions = useActions(searchQuery);
useRegisterActions(actions, [actions]); useRegisterActions(actions, [actions]);
const { dashboardResults, isFetchingDashboardResults } = useDashboardResults(searchQuery, showing); const { searchResults, isFetchingSearchResults } = useSearchResults(searchQuery, showing);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const { overlayProps } = useOverlay( const { overlayProps } = useOverlay(
@ -58,14 +58,14 @@ export function CommandPalette() {
<FocusScope contain autoFocus restoreFocus> <FocusScope contain autoFocus restoreFocus>
<div {...overlayProps} {...dialogProps}> <div {...overlayProps} {...dialogProps}>
<div className={styles.searchContainer}> <div className={styles.searchContainer}>
{isFetchingDashboardResults ? <Spinner className={styles.spinner} /> : <Icon name="search" size="md" />} {isFetchingSearchResults ? <Spinner className={styles.spinner} /> : <Icon name="search" size="md" />}
<KBarSearch <KBarSearch
defaultPlaceholder={t('command-palette.search-box.placeholder', 'Search or jump to...')} defaultPlaceholder={t('command-palette.search-box.placeholder', 'Search or jump to...')}
className={styles.search} className={styles.search}
/> />
</div> </div>
<div className={styles.resultsContainer}> <div className={styles.resultsContainer}>
<RenderResults dashboardResults={dashboardResults} /> <RenderResults searchResults={searchResults} />
</div> </div>
</div> </div>
</FocusScope> </FocusScope>
@ -76,24 +76,43 @@ export function CommandPalette() {
} }
interface RenderResultsProps { interface RenderResultsProps {
dashboardResults: CommandPaletteAction[]; searchResults: CommandPaletteAction[];
} }
const RenderResults = ({ dashboardResults }: RenderResultsProps) => { const RenderResults = ({ searchResults }: RenderResultsProps) => {
const { results, rootActionId } = useMatches(); const { results: kbarResults, rootActionId } = useMatches();
const styles = useStyles2(getSearchStyles); const styles = useStyles2(getSearchStyles);
const dashboardsSectionTitle = t('command-palette.section.dashboard-search-results', 'Dashboards'); const dashboardsSectionTitle = t('command-palette.section.dashboard-search-results', 'Dashboards');
const foldersSectionTitle = t('command-palette.section.folder-search-results', 'Folders');
// because dashboard search results aren't registered as actions, we need to manually // because dashboard search results aren't registered as actions, we need to manually
// convert them to ActionImpls before passing them as items to KBarResults // convert them to ActionImpls before passing them as items to KBarResults
const dashboardResultItems = useMemo( const dashboardResultItems = useMemo(
() => dashboardResults.map((dashboard) => new ActionImpl(dashboard, { store: {} })), () =>
[dashboardResults] searchResults
.filter((item) => item.id.startsWith('go/dashboard'))
.map((dashboard) => new ActionImpl(dashboard, { store: {} })),
[searchResults]
);
const folderResultItems = useMemo(
() =>
searchResults
.filter((item) => item.id.startsWith('go/folder'))
.map((folder) => new ActionImpl(folder, { store: {} })),
[searchResults]
); );
const items = useMemo( const items = useMemo(() => {
() => (dashboardResultItems.length > 0 ? [...results, dashboardsSectionTitle, ...dashboardResultItems] : results), const results = [...kbarResults];
[results, dashboardsSectionTitle, dashboardResultItems] if (folderResultItems.length > 0) {
); results.push(foldersSectionTitle);
results.push(...folderResultItems);
}
if (dashboardResultItems.length > 0) {
results.push(dashboardsSectionTitle);
results.push(...dashboardResultItems);
}
return results;
}, [kbarResults, dashboardsSectionTitle, dashboardResultItems, foldersSectionTitle, folderResultItems]);
return ( return (
<KBarResults <KBarResults

View File

@ -12,7 +12,7 @@ import { RECENT_DASHBOARDS_PRORITY, SEARCH_RESULTS_PRORITY } from '../values';
const MAX_SEARCH_RESULTS = 100; const MAX_SEARCH_RESULTS = 100;
const MAX_RECENT_DASHBOARDS = 5; const MAX_RECENT_DASHBOARDS = 5;
const debouncedDashboardSearch = debounce(getDashboardSearchResultActions, 200); const debouncedSearch = debounce(getSearchResultActions, 200);
export async function getRecentDashboardActions(): Promise<CommandPaletteAction[]> { export async function getRecentDashboardActions(): Promise<CommandPaletteAction[]> {
const recentUids = (await impressionSrv.getDashboardOpened()).slice(0, MAX_RECENT_DASHBOARDS); const recentUids = (await impressionSrv.getDashboardOpened()).slice(0, MAX_RECENT_DASHBOARDS);
@ -44,51 +44,54 @@ export async function getRecentDashboardActions(): Promise<CommandPaletteAction[
return recentDashboardActions; return recentDashboardActions;
} }
export async function getDashboardSearchResultActions(searchQuery: string): Promise<CommandPaletteAction[]> { export async function getSearchResultActions(searchQuery: string): Promise<CommandPaletteAction[]> {
// Empty strings should not come through to here // Empty strings should not come through to here
if (searchQuery.length === 0) { if (searchQuery.length === 0) {
return []; return [];
} }
const data = await getGrafanaSearcher().search({ const data = await getGrafanaSearcher().search({
kind: ['dashboard'], kind: ['dashboard', 'folder'],
query: searchQuery, query: searchQuery,
limit: MAX_SEARCH_RESULTS, limit: MAX_SEARCH_RESULTS,
}); });
const goToDashboardActions: CommandPaletteAction[] = data.view.map((item) => { const goToSearchResultActions: CommandPaletteAction[] = data.view.map((item) => {
const { url, name } = item; // items are backed by DataFrameView, so must hold the url in a closure const { url, name, kind } = item; // items are backed by DataFrameView, so must hold the url in a closure
return { return {
id: `go/dashboard${url}`, id: `go/${kind}${url}`,
name: `${name}`, name: `${name}`,
section: t('command-palette.section.dashboard-search-results', 'Dashboards'), section:
kind === 'dashboard'
? t('command-palette.section.dashboard-search-results', 'Dashboards')
: t('command-palette.section.folder-search-results', 'Folders'),
priority: SEARCH_RESULTS_PRORITY, priority: SEARCH_RESULTS_PRORITY,
url: locationUtil.stripBaseFromUrl(url), url: locationUtil.stripBaseFromUrl(url),
}; };
}); });
return goToDashboardActions; return goToSearchResultActions;
} }
export function useDashboardResults(searchQuery: string, isShowing: boolean) { export function useSearchResults(searchQuery: string, isShowing: boolean) {
const [dashboardResults, setDashboardResults] = useState<CommandPaletteAction[]>([]); const [searchResults, setSearchResults] = useState<CommandPaletteAction[]>([]);
const [isFetchingDashboardResults, setIsFetchingDashboardResults] = useState(false); const [isFetchingSearchResults, setIsFetchingSearchResults] = useState(false);
// Hit dashboards API // Hit dashboards API
useEffect(() => { useEffect(() => {
if (isShowing && searchQuery.length > 0) { if (isShowing && searchQuery.length > 0) {
setIsFetchingDashboardResults(true); setIsFetchingSearchResults(true);
debouncedDashboardSearch(searchQuery).then((resultActions) => { debouncedSearch(searchQuery).then((resultActions) => {
setDashboardResults(resultActions); setSearchResults(resultActions);
setIsFetchingDashboardResults(false); setIsFetchingSearchResults(false);
}); });
} else { } else {
setDashboardResults([]); setSearchResults([]);
} }
}, [isShowing, searchQuery]); }, [isShowing, searchQuery]);
return { return {
dashboardResults, searchResults,
isFetchingDashboardResults, isFetchingSearchResults,
}; };
} }