mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search: extract a reusable view from the search playground (#49132)
Co-authored-by: Alexandra Vargas <alexa1866@gmail.com>
This commit is contained in:
parent
7251115457
commit
f0496955e3
@ -11,7 +11,7 @@ import * as SearchSrv from 'app/core/services/search_srv';
|
|||||||
import { searchResults } from '../testData';
|
import { searchResults } from '../testData';
|
||||||
import { SearchLayout } from '../types';
|
import { SearchLayout } from '../types';
|
||||||
|
|
||||||
import { DashboardSearch, Props } from './DashboardSearch';
|
import { DashboardSearchOLD as DashboardSearch, Props } from './DashboardSearch';
|
||||||
|
|
||||||
jest.mock('app/core/services/search_srv');
|
jest.mock('app/core/services/search_srv');
|
||||||
// Typecast the mock search so the mock import is correctly recognised by TS
|
// Typecast the mock search so the mock import is correctly recognised by TS
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { FC, memo } from 'react';
|
import React, { FC, memo, useState } from 'react';
|
||||||
|
import { useDebounce } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { CustomScrollbar, IconButton, stylesFactory, useTheme2 } from '@grafana/ui';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { CustomScrollbar, IconButton, stylesFactory, useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { useDashboardSearch } from '../hooks/useDashboardSearch';
|
import { useDashboardSearch } from '../hooks/useDashboardSearch';
|
||||||
import { useSearchQuery } from '../hooks/useSearchQuery';
|
import { useSearchQuery } from '../hooks/useSearchQuery';
|
||||||
|
import { SearchView } from '../page/components/SearchView';
|
||||||
|
|
||||||
import { ActionRow } from './ActionRow';
|
import { ActionRow } from './ActionRow';
|
||||||
import { PreviewsSystemRequirements } from './PreviewsSystemRequirements';
|
import { PreviewsSystemRequirements } from './PreviewsSystemRequirements';
|
||||||
@ -16,7 +19,55 @@ export interface Props {
|
|||||||
onCloseSearch: () => void;
|
onCloseSearch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardSearch: FC<Props> = memo(({ onCloseSearch }) => {
|
export default function DashboardSearch({ onCloseSearch }: Props) {
|
||||||
|
if (false && config.featureToggles.panelTitleSearch) {
|
||||||
|
// TODO: "folder:current" ????
|
||||||
|
return <DashbaordSearchNEW onCloseSearch={onCloseSearch} />;
|
||||||
|
}
|
||||||
|
return <DashboardSearchOLD onCloseSearch={onCloseSearch} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DashbaordSearchNEW({ onCloseSearch }: Props) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const { query, onQueryChange } = useSearchQuery({});
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState(query.query ?? '');
|
||||||
|
const onSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setInputValue(e.currentTarget.value);
|
||||||
|
};
|
||||||
|
useDebounce(() => onQueryChange(inputValue), 200, [inputValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div tabIndex={0} className={styles.overlay}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.searchField}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search dashboards by name"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={onSearchQueryChange}
|
||||||
|
tabIndex={0}
|
||||||
|
spellCheck={false}
|
||||||
|
className={styles.input}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.closeBtn}>
|
||||||
|
<IconButton name="times" surface="panel" onClick={onCloseSearch} size="xxl" tooltip="Close search" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.search}>
|
||||||
|
<SearchView showManage={false} queryText={query.query} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardSearchOLD: FC<Props> = memo(({ onCloseSearch }) => {
|
||||||
const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange, onLayoutChange } = useSearchQuery({});
|
const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange, onLayoutChange } = useSearchQuery({});
|
||||||
const { results, loading, onToggleSection, onKeyDown, showPreviews, setShowPreviews } = useDashboardSearch(
|
const { results, loading, onToggleSection, onKeyDown, showPreviews, setShowPreviews } = useDashboardSearch(
|
||||||
query,
|
query,
|
||||||
@ -67,9 +118,7 @@ export const DashboardSearch: FC<Props> = memo(({ onCloseSearch }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
DashboardSearch.displayName = 'DashboardSearch';
|
DashboardSearchOLD.displayName = 'DashboardSearchOLD';
|
||||||
|
|
||||||
export default DashboardSearch;
|
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
@ -113,5 +162,19 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
padding-bottom: ${theme.spacing(3)};
|
padding-bottom: ${theme.spacing(3)};
|
||||||
`,
|
`,
|
||||||
|
input: css`
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: 2px solid ${theme.v1.colors.border1};
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 38px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: ${theme.v1.colors.textWeak};
|
||||||
|
}
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1,26 +1,17 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useAsync, useDebounce } from 'react-use';
|
import { useAsync, useDebounce } from 'react-use';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField, Select } from '@grafana/ui';
|
||||||
import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField, Button, Select } from '@grafana/ui';
|
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
|
import { FolderDTO } from 'app/types';
|
||||||
|
|
||||||
import { PreviewsSystemRequirements } from '../components/PreviewsSystemRequirements';
|
|
||||||
import { useSearchQuery } from '../hooks/useSearchQuery';
|
import { useSearchQuery } from '../hooks/useSearchQuery';
|
||||||
import { getGrafanaSearcher, SearchQuery } from '../service';
|
import { getGrafanaSearcher } from '../service';
|
||||||
import { SearchLayout } from '../types';
|
|
||||||
|
|
||||||
import { ActionRow, getValidQueryLayout } from './components/ActionRow';
|
import { SearchView } from './components/SearchView';
|
||||||
import { FolderSection } from './components/FolderSection';
|
|
||||||
import { FolderView } from './components/FolderView';
|
|
||||||
import { ManageActions } from './components/ManageActions';
|
|
||||||
import { SearchResultsGrid } from './components/SearchResultsGrid';
|
|
||||||
import { SearchResultsTable, SearchResultsProps } from './components/SearchResultsTable';
|
|
||||||
import { newSearchSelection, updateSearchSelection } from './selection';
|
|
||||||
|
|
||||||
const node: NavModelItem = {
|
const node: NavModelItem = {
|
||||||
id: 'search',
|
id: 'search',
|
||||||
@ -32,11 +23,9 @@ const node: NavModelItem = {
|
|||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { query, onQueryChange, onTagFilterChange, onTagAdd, onDatasourceChange, onSortChange, onLayoutChange } =
|
|
||||||
useSearchQuery({});
|
|
||||||
|
|
||||||
const [showManage, setShowManage] = useState(false); // grid vs list view
|
const [showManage, setShowManage] = useState(false); // grid vs list view
|
||||||
const [folder, setFolder] = useState<string>(); // grid vs list view
|
const [folderDTO, setFolderDTO] = useState<FolderDTO>(); // grid vs list view
|
||||||
const folders = useAsync(async () => {
|
const folders = useAsync(async () => {
|
||||||
const rsp = await getGrafanaSearcher().search({
|
const rsp = await getGrafanaSearcher().search({
|
||||||
query: '*',
|
query: '*',
|
||||||
@ -44,139 +33,25 @@ export default function SearchPage() {
|
|||||||
});
|
});
|
||||||
return rsp.view.map((v) => ({ value: v.uid, label: v.name }));
|
return rsp.view.map((v) => ({ value: v.uid, label: v.name }));
|
||||||
}, []);
|
}, []);
|
||||||
|
const setFolder = async (uid?: string) => {
|
||||||
const [searchSelection, setSearchSelection] = useState(newSearchSelection());
|
if (uid?.length) {
|
||||||
const layout = getValidQueryLayout(query);
|
const dto = await backendSrv.getFolderByUid(uid);
|
||||||
const isFolders = layout === SearchLayout.Folders;
|
setFolderDTO(dto);
|
||||||
|
} else {
|
||||||
const results = useAsync(() => {
|
setFolderDTO(undefined);
|
||||||
let qstr = query.query as string;
|
|
||||||
if (!qstr?.length) {
|
|
||||||
qstr = '*';
|
|
||||||
}
|
}
|
||||||
const q: SearchQuery = {
|
};
|
||||||
query: qstr,
|
|
||||||
tags: query.tag as string[],
|
|
||||||
ds_uid: query.datasource as string,
|
|
||||||
location: folder, // This will scope all results to the prefix
|
|
||||||
};
|
|
||||||
return getGrafanaSearcher().search(q);
|
|
||||||
}, [query, layout, folder]);
|
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
// since we don't use "query" from use search... it is not actually loaded from the URL!
|
||||||
|
const { query, onQueryChange } = useSearchQuery({});
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState(query.query ?? '');
|
||||||
const onSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const onSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setInputValue(e.currentTarget.value);
|
setInputValue(e.currentTarget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useDebounce(() => onQueryChange(inputValue), 200, [inputValue]);
|
useDebounce(() => onQueryChange(inputValue), 200, [inputValue]);
|
||||||
|
|
||||||
const toggleSelection = useCallback(
|
|
||||||
(kind: string, uid: string) => {
|
|
||||||
const current = searchSelection.isSelected(kind, uid);
|
|
||||||
if (kind === 'folder') {
|
|
||||||
// ??? also select all children?
|
|
||||||
}
|
|
||||||
setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
|
|
||||||
},
|
|
||||||
[searchSelection]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!config.featureToggles.panelTitleSearch) {
|
|
||||||
return <div className={styles.unsupported}>Unsupported</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This gets the possible tags from within the query results
|
|
||||||
const getTagOptions = (): Promise<TermCount[]> => {
|
|
||||||
const q: SearchQuery = {
|
|
||||||
query: query.query?.length ? query.query : '*',
|
|
||||||
tags: query.tag,
|
|
||||||
ds_uid: query.datasource,
|
|
||||||
};
|
|
||||||
return getGrafanaSearcher().tags(q);
|
|
||||||
};
|
|
||||||
|
|
||||||
// function to update items when dashboards or folders are moved or deleted
|
|
||||||
const onChangeItemsList = async () => {
|
|
||||||
// clean up search selection
|
|
||||||
setSearchSelection(newSearchSelection());
|
|
||||||
// trigger again the search to the backend
|
|
||||||
onQueryChange(inputValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
|
||||||
onQueryChange('');
|
|
||||||
}
|
|
||||||
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 (folder) {
|
|
||||||
return (
|
|
||||||
<FolderSection
|
|
||||||
section={{ uid: folder, kind: 'folder', title: folder }}
|
|
||||||
selection={selection}
|
|
||||||
selectionToggle={toggleSelection}
|
|
||||||
onTagSelected={onTagAdd}
|
|
||||||
renderStandaloneBody={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <FolderView selection={selection} selectionToggle={toggleSelection} onTagSelected={onTagAdd} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ height: '100%', width: '100%' }}>
|
|
||||||
<AutoSizer>
|
|
||||||
{({ width, height }) => {
|
|
||||||
const props: SearchResultsProps = {
|
|
||||||
response: value!,
|
|
||||||
selection,
|
|
||||||
selectionToggle: toggleSelection,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
onTagSelected: onTagAdd,
|
|
||||||
onDatasourceChange: query.datasource ? onDatasourceChange : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (layout === SearchLayout.Grid) {
|
|
||||||
return <SearchResultsGrid {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SearchResultsTable {...props} />;
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={{ node: node, main: node }}>
|
<Page navModel={{ node: node, main: node }}>
|
||||||
<Page.Contents
|
<Page.Contents
|
||||||
@ -193,7 +68,7 @@ export default function SearchPage() {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder="Search for dashboards and panels"
|
placeholder="Search for dashboards and panels"
|
||||||
className={styles.searchInput}
|
className={styles.searchInput}
|
||||||
suffix={results.loading ? <Spinner /> : null}
|
suffix={false ? <Spinner /> : null}
|
||||||
/>
|
/>
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Show manage options">
|
<InlineField label="Show manage options">
|
||||||
@ -209,34 +84,7 @@ export default function SearchPage() {
|
|||||||
</InlineField>
|
</InlineField>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
|
|
||||||
{Boolean(searchSelection.items.size > 0) ? (
|
<SearchView showManage={showManage} folderDTO={folderDTO} queryText={query.query} />
|
||||||
<ManageActions items={searchSelection.items} onChange={onChangeItemsList} />
|
|
||||||
) : (
|
|
||||||
<ActionRow
|
|
||||||
onLayoutChange={(v) => {
|
|
||||||
if (v === SearchLayout.Folders) {
|
|
||||||
if (query.query) {
|
|
||||||
onQueryChange(''); // parent will clear the sort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onLayoutChange(v);
|
|
||||||
}}
|
|
||||||
onSortChange={onSortChange}
|
|
||||||
onTagFilterChange={onTagFilterChange}
|
|
||||||
getTagOptions={getTagOptions}
|
|
||||||
onDatasourceChange={onDatasourceChange}
|
|
||||||
query={query}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{layout === SearchLayout.Grid && (
|
|
||||||
<PreviewsSystemRequirements
|
|
||||||
bottomSpacing={3}
|
|
||||||
showPreviews={true}
|
|
||||||
onRemove={() => onLayoutChange(SearchLayout.List)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{renderResults()}
|
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
@ -58,13 +58,22 @@ export const ActionRow: FC<Props> = ({
|
|||||||
hideLayout,
|
hideLayout,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const layout = getValidQueryLayout(query);
|
||||||
|
|
||||||
|
// Disabled folder layout option when query is present
|
||||||
|
const disabledOptions = query.sort || query.query ? [SearchLayout.Folders] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.actionRow}>
|
<div className={styles.actionRow}>
|
||||||
<div className={styles.rowContainer}>
|
<div className={styles.rowContainer}>
|
||||||
<HorizontalGroup spacing="md" width="auto">
|
<HorizontalGroup spacing="md" width="auto">
|
||||||
{!hideLayout && (
|
{!hideLayout && (
|
||||||
<RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={getValidQueryLayout(query)} />
|
<RadioButtonGroup
|
||||||
|
options={layoutOptions}
|
||||||
|
disabledOptions={disabledOptions}
|
||||||
|
onChange={onLayoutChange}
|
||||||
|
value={layout}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<SortPicker onChange={onSortChange} value={query.sort?.value} />
|
<SortPicker onChange={onSortChange} value={query.sort?.value} />
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
|
@ -4,7 +4,7 @@ import { useAsync, useLocalStorage } from 'react-use';
|
|||||||
|
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { Checkbox, CollapsableSection, Icon, stylesFactory, useTheme } from '@grafana/ui';
|
import { Card, Checkbox, CollapsableSection, Icon, Spinner, stylesFactory, useTheme } from '@grafana/ui';
|
||||||
import impressionSrv from 'app/core/services/impression_srv';
|
import impressionSrv from 'app/core/services/impression_srv';
|
||||||
import { getSectionStorageKey } from 'app/features/search/utils';
|
import { getSectionStorageKey } from 'app/features/search/utils';
|
||||||
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
||||||
@ -128,7 +128,15 @@ export const FolderSection: FC<SectionHeaderProps> = ({
|
|||||||
|
|
||||||
const renderResults = () => {
|
const renderResults = () => {
|
||||||
if (!results.value?.length) {
|
if (!results.value?.length) {
|
||||||
return <div>No items found</div>;
|
if (results.loading) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Card.Heading>No results found</Card.Heading>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.value.map((v) => {
|
return results.value.map((v) => {
|
||||||
|
@ -83,6 +83,8 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
> ul {
|
> ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
border: solid 1px ${theme.v1.colors.border2};
|
||||||
`,
|
`,
|
||||||
section: css`
|
section: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
219
public/app/features/search/page/components/SearchView.tsx
Normal file
219
public/app/features/search/page/components/SearchView.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { useStyles2, Spinner, Button } from '@grafana/ui';
|
||||||
|
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';
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchView = ({ showManage, folderDTO, queryText }: SearchViewProps) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const { query, onQueryChange, onTagFilterChange, onTagAdd, onDatasourceChange, onSortChange, onLayoutChange } =
|
||||||
|
useSearchQuery({});
|
||||||
|
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;
|
||||||
|
|
||||||
|
const results = useAsync(() => {
|
||||||
|
let qstr = queryText;
|
||||||
|
if (!qstr?.length) {
|
||||||
|
qstr = '*';
|
||||||
|
}
|
||||||
|
const q: SearchQuery = {
|
||||||
|
query: qstr,
|
||||||
|
tags: query.tag as string[],
|
||||||
|
ds_uid: query.datasource as string,
|
||||||
|
location: folderDTO?.uid, // This will scope all results to the prefix
|
||||||
|
};
|
||||||
|
return getGrafanaSearcher().search(q);
|
||||||
|
}, [query, layout, queryText, folderDTO]);
|
||||||
|
|
||||||
|
const toggleSelection = useCallback(
|
||||||
|
(kind: string, uid: string) => {
|
||||||
|
const current = searchSelection.isSelected(kind, uid);
|
||||||
|
if (kind === 'folder') {
|
||||||
|
// ??? also select all children?
|
||||||
|
}
|
||||||
|
setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
|
||||||
|
},
|
||||||
|
[searchSelection]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!config.featureToggles.panelTitleSearch) {
|
||||||
|
return <div className={styles.unsupported}>Unsupported</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This gets the possible tags from within the query results
|
||||||
|
const getTagOptions = (): Promise<TermCount[]> => {
|
||||||
|
const q: SearchQuery = {
|
||||||
|
query: query.query?.length ? query.query : '*',
|
||||||
|
tags: query.tag,
|
||||||
|
ds_uid: query.datasource,
|
||||||
|
};
|
||||||
|
return getGrafanaSearcher().tags(q);
|
||||||
|
};
|
||||||
|
|
||||||
|
// function to update items when dashboards or folders are moved or deleted
|
||||||
|
const onChangeItemsList = async () => {
|
||||||
|
// clean up search selection
|
||||||
|
setSearchSelection(newSearchSelection());
|
||||||
|
// trigger again the search to the backend
|
||||||
|
onQueryChange(query.query);
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
onQueryChange('');
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <FolderView selection={selection} selectionToggle={toggleSelection} onTagSelected={onTagAdd} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100%', width: '100%' }}>
|
||||||
|
<AutoSizer>
|
||||||
|
{({ width, height }) => {
|
||||||
|
const props: SearchResultsProps = {
|
||||||
|
response: value!,
|
||||||
|
selection,
|
||||||
|
selectionToggle: toggleSelection,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
onTagSelected: onTagAdd,
|
||||||
|
onDatasourceChange: query.datasource ? onDatasourceChange : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layout === SearchLayout.Grid) {
|
||||||
|
return <SearchResultsGrid {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SearchResultsTable {...props} />;
|
||||||
|
}}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config.featureToggles.panelTitleSearch) {
|
||||||
|
return <div className={styles.unsupported}>Unsupported</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Boolean(searchSelection.items.size > 0) ? (
|
||||||
|
<ManageActions items={searchSelection.items} onChange={onChangeItemsList} />
|
||||||
|
) : (
|
||||||
|
<ActionRow
|
||||||
|
onLayoutChange={(v) => {
|
||||||
|
if (v === SearchLayout.Folders) {
|
||||||
|
if (query.query) {
|
||||||
|
onQueryChange(''); // parent will clear the sort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onLayoutChange(v);
|
||||||
|
}}
|
||||||
|
onSortChange={onSortChange}
|
||||||
|
onTagFilterChange={onTagFilterChange}
|
||||||
|
getTagOptions={getTagOptions}
|
||||||
|
onDatasourceChange={onDatasourceChange}
|
||||||
|
query={query}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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};
|
||||||
|
`,
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user