2022-05-18 08:46:43 -05:00
|
|
|
import { css } from '@emotion/css';
|
2022-05-24 03:04:21 -05:00
|
|
|
import React, { useCallback, useMemo, useState } from 'react';
|
2022-06-03 10:22:17 -05:00
|
|
|
import { useAsync, useDebounce } from 'react-use';
|
2022-05-18 08:46:43 -05:00
|
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
|
|
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
|
|
|
import { useStyles2, Spinner, Button } from '@grafana/ui';
|
2022-06-03 10:22:17 -05:00
|
|
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
2022-05-18 08:46:43 -05:00
|
|
|
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
|
|
|
import { FolderDTO } from 'app/types';
|
|
|
|
|
|
|
|
import { PreviewsSystemRequirements } from '../../components/PreviewsSystemRequirements';
|
|
|
|
import { useSearchQuery } from '../../hooks/useSearchQuery';
|
|
|
|
import { getGrafanaSearcher, SearchQuery } from '../../service';
|
|
|
|
import { SearchLayout } from '../../types';
|
2022-06-03 10:22:17 -05:00
|
|
|
import { reportDashboardListViewed } from '../reporting';
|
2022-05-18 08:46:43 -05:00
|
|
|
import { newSearchSelection, updateSearchSelection } from '../selection';
|
|
|
|
|
|
|
|
import { ActionRow, getValidQueryLayout } from './ActionRow';
|
|
|
|
import { FolderSection } from './FolderSection';
|
|
|
|
import { FolderView } from './FolderView';
|
|
|
|
import { ManageActions } from './ManageActions';
|
|
|
|
import { SearchResultsGrid } from './SearchResultsGrid';
|
|
|
|
import { SearchResultsTable, SearchResultsProps } from './SearchResultsTable';
|
|
|
|
|
|
|
|
type SearchViewProps = {
|
|
|
|
queryText: string; // odd that it is not from query.query
|
|
|
|
showManage: boolean;
|
|
|
|
folderDTO?: FolderDTO;
|
2022-05-23 08:03:05 -05:00
|
|
|
hidePseudoFolders?: boolean; // Recent + starred
|
2022-06-07 02:57:23 -05:00
|
|
|
onQueryTextChange: (newQueryText: string) => void;
|
2022-06-01 11:05:53 -05:00
|
|
|
includePanels: boolean;
|
|
|
|
setIncludePanels: (v: boolean) => void;
|
2022-05-18 08:46:43 -05:00
|
|
|
};
|
|
|
|
|
2022-06-01 11:05:53 -05:00
|
|
|
export const SearchView = ({
|
|
|
|
showManage,
|
|
|
|
folderDTO,
|
|
|
|
queryText,
|
2022-06-07 02:57:23 -05:00
|
|
|
onQueryTextChange,
|
2022-06-01 11:05:53 -05:00
|
|
|
hidePseudoFolders,
|
|
|
|
includePanels,
|
|
|
|
setIncludePanels,
|
|
|
|
}: SearchViewProps) => {
|
2022-05-18 08:46:43 -05:00
|
|
|
const styles = useStyles2(getStyles);
|
|
|
|
|
2022-06-07 02:57:23 -05:00
|
|
|
const { query, onTagFilterChange, onTagAdd, onDatasourceChange, onSortChange, onLayoutChange } = useSearchQuery({});
|
2022-05-18 08:46:43 -05:00
|
|
|
query.query = queryText; // Use the query value passed in from parent rather than from URL
|
|
|
|
|
|
|
|
const [searchSelection, setSearchSelection] = useState(newSearchSelection());
|
|
|
|
const layout = getValidQueryLayout(query);
|
|
|
|
const isFolders = layout === SearchLayout.Folders;
|
|
|
|
|
2022-06-03 10:33:04 -05:00
|
|
|
const [listKey, setListKey] = useState(Date.now());
|
|
|
|
|
2022-05-24 03:04:21 -05:00
|
|
|
const searchQuery = useMemo(() => {
|
2022-05-18 08:46:43 -05:00
|
|
|
const q: SearchQuery = {
|
2022-05-23 12:05:15 -05:00
|
|
|
query: queryText,
|
2022-05-18 08:46:43 -05:00
|
|
|
tags: query.tag as string[],
|
|
|
|
ds_uid: query.datasource as string,
|
|
|
|
location: folderDTO?.uid, // This will scope all results to the prefix
|
2022-05-23 12:05:15 -05:00
|
|
|
sort: query.sort?.value,
|
2022-05-18 08:46:43 -05:00
|
|
|
};
|
2022-05-23 12:05:15 -05:00
|
|
|
|
|
|
|
// Only dashboards have additional properties
|
|
|
|
if (q.sort?.length && !q.sort.includes('name')) {
|
|
|
|
q.kind = ['dashboard', 'folder']; // skip panels
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!q.query?.length) {
|
|
|
|
q.query = '*';
|
|
|
|
if (!q.location) {
|
|
|
|
q.kind = ['dashboard', 'folder']; // skip panels
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-01 11:05:53 -05:00
|
|
|
if (!includePanels && !q.kind) {
|
|
|
|
q.kind = ['dashboard', 'folder']; // skip panels
|
|
|
|
}
|
|
|
|
|
2022-05-23 12:05:15 -05:00
|
|
|
if (q.query === '*' && !q.sort?.length) {
|
|
|
|
q.sort = 'name_sort';
|
|
|
|
}
|
2022-05-24 03:04:21 -05:00
|
|
|
return q;
|
2022-06-01 11:05:53 -05:00
|
|
|
}, [query, queryText, folderDTO, includePanels]);
|
2022-05-23 12:05:15 -05:00
|
|
|
|
2022-06-03 10:22:17 -05:00
|
|
|
// Search usage reporting
|
|
|
|
useDebounce(
|
|
|
|
() => {
|
|
|
|
reportDashboardListViewed(folderDTO ? 'manage_dashboards' : 'dashboard_search', {
|
|
|
|
layout: query.layout,
|
|
|
|
starred: query.starred,
|
|
|
|
sortValue: query.sort?.value,
|
|
|
|
query: query.query,
|
|
|
|
tagCount: query.tag?.length,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
1000,
|
|
|
|
[folderDTO, query.layout, query.starred, query.sort?.value, query.query?.length, query.tag?.length]
|
|
|
|
);
|
|
|
|
|
2022-05-24 03:04:21 -05:00
|
|
|
const results = useAsync(() => {
|
|
|
|
return getGrafanaSearcher().search(searchQuery);
|
|
|
|
}, [searchQuery]);
|
2022-05-18 08:46:43 -05:00
|
|
|
|
2022-05-23 12:01:18 -05:00
|
|
|
const clearSelection = useCallback(() => {
|
|
|
|
searchSelection.items.clear();
|
|
|
|
setSearchSelection({ ...searchSelection });
|
|
|
|
}, [searchSelection]);
|
|
|
|
|
2022-05-18 08:46:43 -05:00
|
|
|
const toggleSelection = useCallback(
|
|
|
|
(kind: string, uid: string) => {
|
|
|
|
const current = searchSelection.isSelected(kind, uid);
|
|
|
|
setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
|
|
|
|
},
|
|
|
|
[searchSelection]
|
|
|
|
);
|
|
|
|
|
|
|
|
// This gets the possible tags from within the query results
|
|
|
|
const getTagOptions = (): Promise<TermCount[]> => {
|
2022-05-24 03:04:21 -05:00
|
|
|
return getGrafanaSearcher().tags(searchQuery);
|
2022-05-18 08:46:43 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
// function to update items when dashboards or folders are moved or deleted
|
|
|
|
const onChangeItemsList = async () => {
|
|
|
|
// clean up search selection
|
2022-06-03 10:33:04 -05:00
|
|
|
clearSelection();
|
|
|
|
setListKey(Date.now());
|
2022-05-18 08:46:43 -05:00
|
|
|
// trigger again the search to the backend
|
2022-06-07 02:57:23 -05:00
|
|
|
onQueryTextChange(query.query);
|
2022-05-18 08:46:43 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
const renderResults = () => {
|
|
|
|
const value = results.value;
|
|
|
|
|
|
|
|
if ((!value || !value.totalRows) && !isFolders) {
|
|
|
|
if (results.loading && !value) {
|
|
|
|
return <Spinner />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={styles.noResults}>
|
|
|
|
<div>No results found for your query.</div>
|
|
|
|
<br />
|
|
|
|
<Button
|
|
|
|
variant="secondary"
|
|
|
|
onClick={() => {
|
|
|
|
if (query.query) {
|
2022-06-07 02:57:23 -05:00
|
|
|
onQueryTextChange('');
|
2022-05-18 08:46:43 -05:00
|
|
|
}
|
|
|
|
if (query.tag?.length) {
|
|
|
|
onTagFilterChange([]);
|
|
|
|
}
|
|
|
|
if (query.datasource) {
|
|
|
|
onDatasourceChange(undefined);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Remove search constraints
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const selection = showManage ? searchSelection.isSelected : undefined;
|
|
|
|
if (layout === SearchLayout.Folders) {
|
|
|
|
if (folderDTO) {
|
|
|
|
return (
|
|
|
|
<FolderSection
|
|
|
|
section={{ uid: folderDTO.uid, kind: 'folder', title: folderDTO.title }}
|
|
|
|
selection={selection}
|
|
|
|
selectionToggle={toggleSelection}
|
|
|
|
onTagSelected={onTagAdd}
|
|
|
|
renderStandaloneBody={true}
|
2022-05-24 03:04:21 -05:00
|
|
|
tags={query.tag}
|
2022-06-03 10:33:04 -05:00
|
|
|
key={listKey}
|
2022-05-18 08:46:43 -05:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2022-05-19 16:16:25 -05:00
|
|
|
return (
|
2022-05-23 08:03:05 -05:00
|
|
|
<FolderView
|
2022-06-03 10:33:04 -05:00
|
|
|
key={listKey}
|
2022-05-23 08:03:05 -05:00
|
|
|
selection={selection}
|
|
|
|
selectionToggle={toggleSelection}
|
|
|
|
tags={query.tag}
|
|
|
|
onTagSelected={onTagAdd}
|
|
|
|
hidePseudoFolders={hidePseudoFolders}
|
|
|
|
/>
|
2022-05-19 16:16:25 -05:00
|
|
|
);
|
2022-05-18 08:46:43 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div style={{ height: '100%', width: '100%' }}>
|
|
|
|
<AutoSizer>
|
|
|
|
{({ width, height }) => {
|
|
|
|
const props: SearchResultsProps = {
|
|
|
|
response: value!,
|
|
|
|
selection,
|
|
|
|
selectionToggle: toggleSelection,
|
2022-05-23 12:01:18 -05:00
|
|
|
clearSelection,
|
2022-05-18 08:46:43 -05:00
|
|
|
width: width,
|
|
|
|
height: height,
|
|
|
|
onTagSelected: onTagAdd,
|
|
|
|
onDatasourceChange: query.datasource ? onDatasourceChange : undefined,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (layout === SearchLayout.Grid) {
|
|
|
|
return <SearchResultsGrid {...props} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return <SearchResultsTable {...props} />;
|
|
|
|
}}
|
|
|
|
</AutoSizer>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-06-03 10:22:17 -05:00
|
|
|
if (folderDTO && !results.loading && !results.value?.totalRows && !queryText.length) {
|
|
|
|
return (
|
|
|
|
<EmptyListCTA
|
|
|
|
title="This folder doesn't have any dashboards yet"
|
|
|
|
buttonIcon="plus"
|
|
|
|
buttonTitle="Create Dashboard"
|
|
|
|
buttonLink={`dashboard/new?folderId=${folderDTO.id}`}
|
|
|
|
proTip="Add/move dashboards to your folder at ->"
|
|
|
|
proTipLink="dashboards"
|
|
|
|
proTipLinkTitle="Manage dashboards"
|
|
|
|
proTipTarget=""
|
|
|
|
/>
|
|
|
|
);
|
2022-05-18 08:46:43 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{Boolean(searchSelection.items.size > 0) ? (
|
2022-05-23 12:01:18 -05:00
|
|
|
<ManageActions items={searchSelection.items} onChange={onChangeItemsList} clearSelection={clearSelection} />
|
2022-05-18 08:46:43 -05:00
|
|
|
) : (
|
|
|
|
<ActionRow
|
|
|
|
onLayoutChange={(v) => {
|
|
|
|
if (v === SearchLayout.Folders) {
|
|
|
|
if (query.query) {
|
2022-06-07 02:57:23 -05:00
|
|
|
onQueryTextChange(''); // parent will clear the sort
|
2022-05-18 08:46:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
onLayoutChange(v);
|
|
|
|
}}
|
|
|
|
onSortChange={onSortChange}
|
|
|
|
onTagFilterChange={onTagFilterChange}
|
|
|
|
getTagOptions={getTagOptions}
|
2022-05-26 19:08:17 -05:00
|
|
|
getSortOptions={getGrafanaSearcher().getSortOptions}
|
2022-05-18 08:46:43 -05:00
|
|
|
onDatasourceChange={onDatasourceChange}
|
|
|
|
query={query}
|
2022-06-01 11:05:53 -05:00
|
|
|
includePanels={includePanels!}
|
|
|
|
setIncludePanels={setIncludePanels}
|
2022-05-18 08:46:43 -05:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{layout === SearchLayout.Grid && (
|
|
|
|
<PreviewsSystemRequirements
|
|
|
|
bottomSpacing={3}
|
|
|
|
showPreviews={true}
|
|
|
|
onRemove={() => onLayoutChange(SearchLayout.List)}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{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};
|
|
|
|
`,
|
|
|
|
});
|