diff --git a/public/app/features/search/components/DashboardSearch.test.tsx b/public/app/features/search/components/DashboardSearch.test.tsx index 0ead5d5cc4c..a9383546e56 100644 --- a/public/app/features/search/components/DashboardSearch.test.tsx +++ b/public/app/features/search/components/DashboardSearch.test.tsx @@ -11,7 +11,7 @@ import * as SearchSrv from 'app/core/services/search_srv'; import { searchResults } from '../testData'; import { SearchLayout } from '../types'; -import { DashboardSearch, Props } from './DashboardSearch'; +import { DashboardSearchOLD as DashboardSearch, Props } from './DashboardSearch'; jest.mock('app/core/services/search_srv'); // Typecast the mock search so the mock import is correctly recognised by TS diff --git a/public/app/features/search/components/DashboardSearch.tsx b/public/app/features/search/components/DashboardSearch.tsx index da60871d749..def77ac758f 100644 --- a/public/app/features/search/components/DashboardSearch.tsx +++ b/public/app/features/search/components/DashboardSearch.tsx @@ -1,11 +1,14 @@ 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 { 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 { useSearchQuery } from '../hooks/useSearchQuery'; +import { SearchView } from '../page/components/SearchView'; import { ActionRow } from './ActionRow'; import { PreviewsSystemRequirements } from './PreviewsSystemRequirements'; @@ -16,7 +19,55 @@ export interface Props { onCloseSearch: () => void; } -export const DashboardSearch: FC = memo(({ onCloseSearch }) => { +export default function DashboardSearch({ onCloseSearch }: Props) { + if (false && config.featureToggles.panelTitleSearch) { + // TODO: "folder:current" ???? + return ; + } + return ; +} + +function DashbaordSearchNEW({ onCloseSearch }: Props) { + const styles = useStyles2(getStyles); + const { query, onQueryChange } = useSearchQuery({}); + + const [inputValue, setInputValue] = useState(query.query ?? ''); + const onSearchQueryChange = (e: React.ChangeEvent) => { + e.preventDefault(); + setInputValue(e.currentTarget.value); + }; + useDebounce(() => onQueryChange(inputValue), 200, [inputValue]); + + return ( +
+
+
+
+ +
+ +
+ +
+
+
+ +
+
+
+ ); +} + +export const DashboardSearchOLD: FC = memo(({ onCloseSearch }) => { const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange, onLayoutChange } = useSearchQuery({}); const { results, loading, onToggleSection, onKeyDown, showPreviews, setShowPreviews } = useDashboardSearch( query, @@ -67,9 +118,7 @@ export const DashboardSearch: FC = memo(({ onCloseSearch }) => { ); }); -DashboardSearch.displayName = 'DashboardSearch'; - -export default DashboardSearch; +DashboardSearchOLD.displayName = 'DashboardSearchOLD'; const getStyles = stylesFactory((theme: GrafanaTheme2) => { return { @@ -113,5 +162,19 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { height: 100%; 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}; + } + `, }; }); diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index aea7f0c6922..ba0509e0ab8 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -1,26 +1,17 @@ import { css } from '@emotion/css'; -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { useAsync, useDebounce } from 'react-use'; -import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField, Button, Select } from '@grafana/ui'; +import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField, Select } from '@grafana/ui'; 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 { getGrafanaSearcher, SearchQuery } from '../service'; -import { SearchLayout } from '../types'; +import { getGrafanaSearcher } from '../service'; -import { ActionRow, getValidQueryLayout } from './components/ActionRow'; -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'; +import { SearchView } from './components/SearchView'; const node: NavModelItem = { id: 'search', @@ -32,11 +23,9 @@ const node: NavModelItem = { export default function SearchPage() { const styles = useStyles2(getStyles); - const { query, onQueryChange, onTagFilterChange, onTagAdd, onDatasourceChange, onSortChange, onLayoutChange } = - useSearchQuery({}); const [showManage, setShowManage] = useState(false); // grid vs list view - const [folder, setFolder] = useState(); // grid vs list view + const [folderDTO, setFolderDTO] = useState(); // grid vs list view const folders = useAsync(async () => { const rsp = await getGrafanaSearcher().search({ query: '*', @@ -44,139 +33,25 @@ export default function SearchPage() { }); return rsp.view.map((v) => ({ value: v.uid, label: v.name })); }, []); - - const [searchSelection, setSearchSelection] = useState(newSearchSelection()); - const layout = getValidQueryLayout(query); - const isFolders = layout === SearchLayout.Folders; - - const results = useAsync(() => { - let qstr = query.query as string; - if (!qstr?.length) { - qstr = '*'; + const setFolder = async (uid?: string) => { + if (uid?.length) { + const dto = await backendSrv.getFolderByUid(uid); + setFolderDTO(dto); + } else { + setFolderDTO(undefined); } - 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) => { e.preventDefault(); setInputValue(e.currentTarget.value); }; - 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
Unsupported
; - } - - // This gets the possible tags from within the query results - const getTagOptions = (): Promise => { - 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 ; - } - - return ( -
-
No results found for your query.
-
- -
- ); - } - - const selection = showManage ? searchSelection.isSelected : undefined; - if (layout === SearchLayout.Folders) { - if (folder) { - return ( - - ); - } - return ; - } - - return ( -
- - {({ 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 ; - } - - return ; - }} - -
- ); - }; - return ( : null} + suffix={false ? : null} /> @@ -209,34 +84,7 @@ export default function SearchPage() { - {Boolean(searchSelection.items.size > 0) ? ( - - ) : ( - { - 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 && ( - onLayoutChange(SearchLayout.List)} - /> - )} - {renderResults()} + ); diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx index 92a7cc63e6c..aab6465dac7 100644 --- a/public/app/features/search/page/components/ActionRow.tsx +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -58,13 +58,22 @@ export const ActionRow: FC = ({ hideLayout, }) => { 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 (
{!hideLayout && ( - + )} diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx index 21c12946719..259c5229246 100644 --- a/public/app/features/search/page/components/FolderSection.tsx +++ b/public/app/features/search/page/components/FolderSection.tsx @@ -4,7 +4,7 @@ import { useAsync, useLocalStorage } from 'react-use'; import { GrafanaTheme } from '@grafana/data'; 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 { getSectionStorageKey } from 'app/features/search/utils'; import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId'; @@ -128,7 +128,15 @@ export const FolderSection: FC = ({ const renderResults = () => { if (!results.value?.length) { - return
No items found
; + if (results.loading) { + return ; + } + + return ( + + No results found + + ); } return results.value.map((v) => { diff --git a/public/app/features/search/page/components/FolderView.tsx b/public/app/features/search/page/components/FolderView.tsx index 4f5e58b8feb..6b8fe3c8a35 100644 --- a/public/app/features/search/page/components/FolderView.tsx +++ b/public/app/features/search/page/components/FolderView.tsx @@ -83,6 +83,8 @@ const getStyles = (theme: GrafanaTheme2) => { > ul { list-style: none; } + + border: solid 1px ${theme.v1.colors.border2}; `, section: css` display: flex; diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx new file mode 100644 index 00000000000..9a20af7bdf8 --- /dev/null +++ b/public/app/features/search/page/components/SearchView.tsx @@ -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
Unsupported
; + } + + // This gets the possible tags from within the query results + const getTagOptions = (): Promise => { + 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 ; + } + + return ( +
+
No results found for your query.
+
+ +
+ ); + } + + const selection = showManage ? searchSelection.isSelected : undefined; + if (layout === SearchLayout.Folders) { + if (folderDTO) { + return ( + + ); + } + return ; + } + + return ( +
+ + {({ 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 ; + } + + return ; + }} + +
+ ); + }; + + if (!config.featureToggles.panelTitleSearch) { + return
Unsupported
; + } + + return ( + <> + {Boolean(searchSelection.items.size > 0) ? ( + + ) : ( + { + 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 && ( + 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}; + `, +});