grafana/public/app/features/search/page/components/SearchView.tsx
Josh Hunt 9796b6b000
NestedFolders: Connect Search input fields to state manager (#67193)
* NestedFolders: Connect Search input fields to state manager

* Fix tag list not loading

* Clear includePanels checkbox when leaving search

* fix test

* Fix extra right margin

* fix missing style

* cleanup

* fix placeholder

* fix test
2023-04-26 15:42:25 +01:00

219 lines
7.0 KiB
TypeScript

import { css } from '@emotion/css';
import React, { useCallback, useState } from 'react';
import { useDebounce } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Observable } from 'rxjs';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useStyles2, Spinner, Button } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { FolderDTO } from 'app/types';
import { getGrafanaSearcher } from '../../service';
import { getSearchStateManager } from '../../state/SearchStateManager';
import { SearchLayout, DashboardViewItem } from '../../types';
import { newSearchSelection, updateSearchSelection } from '../selection';
import { ActionRow, getValidQueryLayout } from './ActionRow';
import { FolderSection } from './FolderSection';
import { ManageActions } from './ManageActions';
import { RootFolderView } from './RootFolderView';
import { SearchResultsCards } from './SearchResultsCards';
import { SearchResultsTable, SearchResultsProps } from './SearchResultsTable';
export type SearchViewProps = {
showManage: boolean;
folderDTO?: FolderDTO;
hidePseudoFolders?: boolean; // Recent + starred
keyboardEvents: Observable<React.KeyboardEvent>;
};
export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardEvents }: SearchViewProps) => {
const styles = useStyles2(getStyles);
const stateManager = getSearchStateManager(); // State is initialized from URL by parent component
const state = stateManager.useState();
const [searchSelection, setSearchSelection] = useState(newSearchSelection());
const layout = getValidQueryLayout(state);
const isFolders = layout === SearchLayout.Folders;
const [listKey, setListKey] = useState(Date.now());
// Search usage reporting
useDebounce(stateManager.onReportSearchUsage, 1000, []);
const clearSelection = useCallback(() => {
searchSelection.items.clear();
setSearchSelection({ ...searchSelection });
}, [searchSelection]);
const toggleSelection = useCallback(
(kind: string, uid: string) => {
const current = searchSelection.isSelected(kind, uid);
setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
},
[searchSelection]
);
// function to update items when dashboards or folders are moved or deleted
const onChangeItemsList = async () => {
// clean up search selection
clearSelection();
setListKey(Date.now());
// trigger again the search to the backend
stateManager.onQueryChange(state.query);
};
const renderResults = () => {
const value = state.result;
if ((!value || !value.totalRows) && !isFolders) {
if (state.loading && !value) {
return <Spinner />;
}
return (
<div className={styles.noResults}>
<div>No results found for your query.</div>
<br />
<Button variant="secondary" onClick={stateManager.onClearSearchAndFilters}>
Clear search and filters
</Button>
</div>
);
}
const selection = showManage ? searchSelection.isSelected : undefined;
if (layout === SearchLayout.Folders) {
if (folderDTO) {
return (
<FolderSection
section={sectionForFolderView(folderDTO)}
selection={selection}
selectionToggle={toggleSelection}
onTagSelected={stateManager.onAddTag}
renderStandaloneBody={true}
tags={state.tag}
key={listKey}
onClickItem={stateManager.onSearchItemClicked}
/>
);
}
return (
<RootFolderView
key={listKey}
selection={selection}
selectionToggle={toggleSelection}
tags={state.tag}
onTagSelected={stateManager.onAddTag}
hidePseudoFolders={hidePseudoFolders}
onClickItem={stateManager.onSearchItemClicked}
/>
);
}
return (
<div style={{ height: '100%', width: '100%' }}>
<AutoSizer>
{({ width, height }) => {
const props: SearchResultsProps = {
response: value!,
selection,
selectionToggle: toggleSelection,
clearSelection,
width: width,
height: height,
onTagSelected: stateManager.onAddTag,
keyboardEvents,
onDatasourceChange: state.datasource ? stateManager.onDatasourceChange : undefined,
onClickItem: stateManager.onSearchItemClicked,
};
if (width < 800) {
return <SearchResultsCards {...props} />;
}
return <SearchResultsTable {...props} />;
}}
</AutoSizer>
</div>
);
};
if (
folderDTO &&
// With nested folders, SearchView doesn't know if it's fetched all children
// of a folder so don't show empty state here.
!config.featureToggles.nestedFolders &&
!state.loading &&
!state.result?.totalRows &&
!stateManager.hasSearchFilters()
) {
return (
<EmptyListCTA
title="This folder doesn't have any dashboards yet"
buttonIcon="plus"
buttonTitle="Create Dashboard"
buttonLink={`dashboard/new?folderUid=${folderDTO.uid}`}
proTip="Add/move dashboards to your folder at ->"
proTipLink="dashboards"
proTipLinkTitle="Manage dashboards"
proTipTarget=""
/>
);
}
return (
<>
{Boolean(searchSelection.items.size > 0) ? (
<ManageActions items={searchSelection.items} onChange={onChangeItemsList} clearSelection={clearSelection} />
) : (
<ActionRow
onLayoutChange={stateManager.onLayoutChange}
showStarredFilter={hidePseudoFolders}
onStarredFilterChange={!hidePseudoFolders ? undefined : stateManager.onStarredFilterChange}
onSortChange={stateManager.onSortChange}
onTagFilterChange={stateManager.onTagFilterChange}
getTagOptions={stateManager.getTagOptions}
getSortOptions={getGrafanaSearcher().getSortOptions}
sortPlaceholder={getGrafanaSearcher().sortPlaceholder}
onDatasourceChange={stateManager.onDatasourceChange}
onPanelTypeChange={stateManager.onPanelTypeChange}
state={state}
includePanels={state.includePanels!}
onSetIncludePanels={stateManager.onSetIncludePanels}
/>
)}
{renderResults()}
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
searchInput: css`
margin-bottom: 6px;
min-height: ${theme.spacing(4)};
`,
unsupported: css`
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 18px;
`,
noResults: css`
padding: ${theme.v1.spacing.md};
background: ${theme.v1.colors.bg2};
font-style: italic;
margin-top: ${theme.v1.spacing.md};
`,
});
function sectionForFolderView(folderDTO: FolderDTO): DashboardViewItem {
return { uid: folderDTO.uid, kind: 'folder', title: folderDTO.title };
}