SearchView.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import { css } from '@emotion/css';
  2. import React, { useCallback, useMemo, useState } from 'react';
  3. import { useAsync } from 'react-use';
  4. import AutoSizer from 'react-virtualized-auto-sizer';
  5. import { Observable } from 'rxjs';
  6. import { GrafanaTheme2 } from '@grafana/data';
  7. import { config } from '@grafana/runtime';
  8. import { useStyles2, Spinner, Button } from '@grafana/ui';
  9. import { TermCount } from 'app/core/components/TagFilter/TagFilter';
  10. import { FolderDTO } from 'app/types';
  11. import { PreviewsSystemRequirements } from '../../components/PreviewsSystemRequirements';
  12. import { useSearchQuery } from '../../hooks/useSearchQuery';
  13. import { getGrafanaSearcher, SearchQuery } from '../../service';
  14. import { SearchLayout } from '../../types';
  15. import { newSearchSelection, updateSearchSelection } from '../selection';
  16. import { ActionRow, getValidQueryLayout } from './ActionRow';
  17. import { FolderSection } from './FolderSection';
  18. import { FolderView } from './FolderView';
  19. import { ManageActions } from './ManageActions';
  20. import { SearchResultsGrid } from './SearchResultsGrid';
  21. import { SearchResultsTable, SearchResultsProps } from './SearchResultsTable';
  22. type SearchViewProps = {
  23. queryText: string; // odd that it is not from query.query
  24. showManage: boolean;
  25. folderDTO?: FolderDTO;
  26. hidePseudoFolders?: boolean; // Recent + starred
  27. onQueryTextChange: (newQueryText: string) => void;
  28. includePanels: boolean;
  29. setIncludePanels: (v: boolean) => void;
  30. keyboardEvents: Observable<React.KeyboardEvent>;
  31. };
  32. export const SearchView = ({
  33. showManage,
  34. folderDTO,
  35. queryText,
  36. hidePseudoFolders,
  37. onQueryTextChange,
  38. includePanels,
  39. setIncludePanels,
  40. keyboardEvents,
  41. }: SearchViewProps) => {
  42. const styles = useStyles2(getStyles);
  43. const { query, onTagFilterChange, onTagAdd, onDatasourceChange, onSortChange, onLayoutChange } = useSearchQuery({});
  44. query.query = queryText; // Use the query value passed in from parent rather than from URL
  45. const [searchSelection, setSearchSelection] = useState(newSearchSelection());
  46. const layout = getValidQueryLayout(query);
  47. const isFolders = layout === SearchLayout.Folders;
  48. const [listKey, setListKey] = useState(Date.now());
  49. const searchQuery = useMemo(() => {
  50. const q: SearchQuery = {
  51. query: queryText,
  52. tags: query.tag as string[],
  53. ds_uid: query.datasource as string,
  54. location: folderDTO?.uid, // This will scope all results to the prefix
  55. sort: query.sort?.value,
  56. };
  57. // Only dashboards have additional properties
  58. if (q.sort?.length && !q.sort.includes('name')) {
  59. q.kind = ['dashboard', 'folder']; // skip panels
  60. }
  61. if (!q.query?.length) {
  62. q.query = '*';
  63. if (!q.location) {
  64. q.kind = ['dashboard', 'folder']; // skip panels
  65. }
  66. }
  67. if (!includePanels && !q.kind) {
  68. q.kind = ['dashboard', 'folder']; // skip panels
  69. }
  70. if (q.query === '*' && !q.sort?.length) {
  71. q.sort = 'name_sort';
  72. }
  73. return q;
  74. }, [query, queryText, folderDTO, includePanels]);
  75. const results = useAsync(() => {
  76. return getGrafanaSearcher().search(searchQuery);
  77. }, [searchQuery]);
  78. const clearSelection = useCallback(() => {
  79. searchSelection.items.clear();
  80. setSearchSelection({ ...searchSelection });
  81. }, [searchSelection]);
  82. const toggleSelection = useCallback(
  83. (kind: string, uid: string) => {
  84. const current = searchSelection.isSelected(kind, uid);
  85. setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
  86. },
  87. [searchSelection]
  88. );
  89. if (!config.featureToggles.panelTitleSearch) {
  90. return <div className={styles.unsupported}>Unsupported</div>;
  91. }
  92. // This gets the possible tags from within the query results
  93. const getTagOptions = (): Promise<TermCount[]> => {
  94. return getGrafanaSearcher().tags(searchQuery);
  95. };
  96. // function to update items when dashboards or folders are moved or deleted
  97. const onChangeItemsList = async () => {
  98. // clean up search selection
  99. clearSelection();
  100. setListKey(Date.now());
  101. // trigger again the search to the backend
  102. onQueryTextChange(query.query);
  103. };
  104. const renderResults = () => {
  105. const value = results.value;
  106. if ((!value || !value.totalRows) && !isFolders) {
  107. if (results.loading && !value) {
  108. return <Spinner />;
  109. }
  110. return (
  111. <div className={styles.noResults}>
  112. <div>No results found for your query.</div>
  113. <br />
  114. <Button
  115. variant="secondary"
  116. onClick={() => {
  117. if (query.query) {
  118. onQueryTextChange('');
  119. }
  120. if (query.tag?.length) {
  121. onTagFilterChange([]);
  122. }
  123. if (query.datasource) {
  124. onDatasourceChange(undefined);
  125. }
  126. }}
  127. >
  128. Remove search constraints
  129. </Button>
  130. </div>
  131. );
  132. }
  133. const selection = showManage ? searchSelection.isSelected : undefined;
  134. if (layout === SearchLayout.Folders) {
  135. if (folderDTO) {
  136. return (
  137. <FolderSection
  138. section={{ uid: folderDTO.uid, kind: 'folder', title: folderDTO.title }}
  139. selection={selection}
  140. selectionToggle={toggleSelection}
  141. onTagSelected={onTagAdd}
  142. renderStandaloneBody={true}
  143. tags={query.tag}
  144. key={listKey}
  145. />
  146. );
  147. }
  148. return (
  149. <FolderView
  150. key={listKey}
  151. selection={selection}
  152. selectionToggle={toggleSelection}
  153. tags={query.tag}
  154. onTagSelected={onTagAdd}
  155. hidePseudoFolders={hidePseudoFolders}
  156. />
  157. );
  158. }
  159. return (
  160. <div style={{ height: '100%', width: '100%' }}>
  161. <AutoSizer>
  162. {({ width, height }) => {
  163. const props: SearchResultsProps = {
  164. response: value!,
  165. selection,
  166. selectionToggle: toggleSelection,
  167. clearSelection,
  168. width: width,
  169. height: height,
  170. onTagSelected: onTagAdd,
  171. keyboardEvents,
  172. onDatasourceChange: query.datasource ? onDatasourceChange : undefined,
  173. };
  174. if (layout === SearchLayout.Grid) {
  175. return <SearchResultsGrid {...props} />;
  176. }
  177. return <SearchResultsTable {...props} />;
  178. }}
  179. </AutoSizer>
  180. </div>
  181. );
  182. };
  183. if (!config.featureToggles.panelTitleSearch) {
  184. return <div className={styles.unsupported}>Unsupported</div>;
  185. }
  186. return (
  187. <>
  188. {Boolean(searchSelection.items.size > 0) ? (
  189. <ManageActions items={searchSelection.items} onChange={onChangeItemsList} clearSelection={clearSelection} />
  190. ) : (
  191. <ActionRow
  192. onLayoutChange={(v) => {
  193. if (v === SearchLayout.Folders) {
  194. if (query.query) {
  195. onQueryTextChange(''); // parent will clear the sort
  196. }
  197. }
  198. onLayoutChange(v);
  199. }}
  200. onSortChange={onSortChange}
  201. onTagFilterChange={onTagFilterChange}
  202. getTagOptions={getTagOptions}
  203. getSortOptions={getGrafanaSearcher().getSortOptions}
  204. onDatasourceChange={onDatasourceChange}
  205. query={query}
  206. includePanels={includePanels!}
  207. setIncludePanels={setIncludePanels}
  208. />
  209. )}
  210. {layout === SearchLayout.Grid && (
  211. <PreviewsSystemRequirements
  212. bottomSpacing={3}
  213. showPreviews={true}
  214. onRemove={() => onLayoutChange(SearchLayout.List)}
  215. />
  216. )}
  217. {renderResults()}
  218. </>
  219. );
  220. };
  221. const getStyles = (theme: GrafanaTheme2) => ({
  222. searchInput: css`
  223. margin-bottom: 6px;
  224. min-height: ${theme.spacing(4)};
  225. `,
  226. unsupported: css`
  227. padding: 10px;
  228. display: flex;
  229. align-items: center;
  230. justify-content: center;
  231. height: 100%;
  232. font-size: 18px;
  233. `,
  234. noResults: css`
  235. padding: ${theme.v1.spacing.md};
  236. background: ${theme.v1.colors.bg2};
  237. font-style: italic;
  238. margin-top: ${theme.v1.spacing.md};
  239. `,
  240. });