diff --git a/public/app/features/search/hooks/useSearchQuery.ts b/public/app/features/search/hooks/useSearchQuery.ts index 91862b5093e..64ec9e8b075 100644 --- a/public/app/features/search/hooks/useSearchQuery.ts +++ b/public/app/features/search/hooks/useSearchQuery.ts @@ -11,6 +11,7 @@ import { SET_TAGS, TOGGLE_SORT, TOGGLE_STARRED, + DATASOURCE_CHANGE, } from '../reducers/actionTypes'; import { DashboardQuery, SearchLayout } from '../types'; import { hasFilters, parseRouteParams } from '../utils'; @@ -32,6 +33,11 @@ export const useSearchQuery = (defaults: Partial) => { updateLocation({ tag: tags }); }; + const onDatasourceChange = (datasource?: string) => { + dispatch({ type: DATASOURCE_CHANGE, payload: datasource }); + updateLocation({ datasource }); + }; + const onTagAdd = useCallback( (tag: string) => { dispatch({ type: ADD_TAG, payload: tag }); @@ -75,5 +81,6 @@ export const useSearchQuery = (defaults: Partial) => { onTagAdd, onSortChange, onLayoutChange, + onDatasourceChange, }; }; diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index 1b24513e81e..a9e8ced3802 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { Input, useStyles2, Spinner } from '@grafana/ui'; +import { Input, useStyles2, Spinner, Button } from '@grafana/ui'; import { config } from '@grafana/runtime'; import AutoSizer from 'react-virtualized-auto-sizer'; import { css } from '@emotion/css'; @@ -22,15 +22,16 @@ const node: NavModelItem = { export default function SearchPage() { const styles = useStyles2(getStyles); - const { query, onQueryChange, onTagFilterChange } = useSearchQuery({}); + const { query, onQueryChange, onTagFilterChange, onDatasourceChange } = useSearchQuery({}); const results = useAsync(() => { - const { query: searchQuery, tag: tags } = query; + const { query: searchQuery, tag: tags, datasource } = query; const filters: QueryFilters = { tags, + datasource, }; - return getGrafanaSearcher().search(searchQuery, tags.length ? filters : undefined); + return getGrafanaSearcher().search(searchQuery, tags.length || datasource ? filters : undefined); }, [query]); if (!config.featureToggles.panelTitleSearch) { @@ -63,11 +64,27 @@ export default function SearchPage() { {results.value?.body && (

+ {query.datasource && ( + + )} {({ width }) => { return ( <> - +
); }} @@ -88,4 +105,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ height: 100%; font-size: 18px; `, + + clearClick: css` + &:hover { + text-decoration: line-through; + } + margin-bottom: 20px; + `, }); diff --git a/public/app/features/search/page/table/Table.tsx b/public/app/features/search/page/table/Table.tsx index 8fac162d65d..b91e96f67d2 100644 --- a/public/app/features/search/page/table/Table.tsx +++ b/public/app/features/search/page/table/Table.tsx @@ -15,6 +15,7 @@ type Props = { width: number; tags: string[]; onTagFilterChange: (tags: string[]) => void; + onDatasourceChange: (datasource?: string) => void; }; export type TableColumn = Column & { @@ -36,7 +37,7 @@ export interface FieldAccess { datasource: DataSourceRef[]; } -export const Table = ({ data, width, tags, onTagFilterChange }: Props) => { +export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange }: Props) => { const styles = useStyles2(getStyles); const tableStyles = useStyles2(getTableStyles); @@ -54,8 +55,8 @@ export const Table = ({ data, width, tags, onTagFilterChange }: Props) => { const access = useMemo(() => new DataFrameView(data), [data]); const memoizedColumns = useMemo(() => { const isDashboardList = data.meta?.type === DataFrameType.DirectoryListing; - return generateColumns(access, isDashboardList, width, styles, tags, onTagFilterChange); - }, [data.meta?.type, access, width, styles, tags, onTagFilterChange]); + return generateColumns(access, isDashboardList, width, styles, tags, onTagFilterChange, onDatasourceChange); + }, [data.meta?.type, access, width, styles, tags, onTagFilterChange, onDatasourceChange]); const options: TableOptions<{}> = useMemo( () => ({ @@ -77,7 +78,11 @@ export const Table = ({ data, width, tags, onTagFilterChange }: Props) => { return (
{row.cells.map((cell: Cell, index: number) => { - if (cell.column.id === 'column-checkbox' || cell.column.id === 'column-tags') { + if ( + cell.column.id === 'column-checkbox' || + cell.column.id === 'column-tags' || + cell.column.id === 'column-datasource' + ) { return (
{ `, cellWrapper: css` display: flex; - pointer-events: none; `, headerCell: css` padding-top: 2px; diff --git a/public/app/features/search/page/table/columns.tsx b/public/app/features/search/page/table/columns.tsx index 06164219e38..a88634fdf91 100644 --- a/public/app/features/search/page/table/columns.tsx +++ b/public/app/features/search/page/table/columns.tsx @@ -14,7 +14,8 @@ export const generateColumns = ( availableWidth: number, styles: { [key: string]: string }, tags: string[], - onTagFilterChange: (tags: string[]) => void + onTagFilterChange: (tags: string[]) => void, + onDatasourceChange: (datasource?: string) => void ): TableColumn[] => { const columns: TableColumn[] = []; const urlField = data.fields.url!; @@ -91,7 +92,7 @@ export const generateColumns = ( // Show datasources if we have any if (access.datasource && hasFieldValue(access.datasource)) { width = DATASOURCE_COLUMN_WIDTH; - columns.push(makeDataSourceColumn(access.datasource, width, styles.typeIcon)); + columns.push(makeDataSourceColumn(access.datasource, width, styles.typeIcon, onDatasourceChange)); availableWidth -= width; } @@ -171,7 +172,12 @@ function getIconForKind(v: string): IconName { return 'question-circle'; } -function makeDataSourceColumn(field: Field, width: number, iconClass: string): TableColumn { +function makeDataSourceColumn( + field: Field, + width: number, + iconClass: string, + onDatasourceChange: (datasource?: string) => void +): TableColumn { return { Cell: DefaultCell, id: `column-datasource`, @@ -188,10 +194,18 @@ function makeDataSourceColumn(field: Field, width: number, icon const icon = settings?.meta?.info?.logos?.small; if (icon) { return ( - + { + e.stopPropagation(); + e.preventDefault(); + onDatasourceChange(settings.uid); + }} + > {settings.name} - + ); } return {v.type}; diff --git a/public/app/features/search/reducers/actionTypes.ts b/public/app/features/search/reducers/actionTypes.ts index 3e2d92fa35a..c843c23f73a 100644 --- a/public/app/features/search/reducers/actionTypes.ts +++ b/public/app/features/search/reducers/actionTypes.ts @@ -16,6 +16,7 @@ export const DELETE_ITEMS = 'DELETE_ITEMS'; export const TOGGLE_STARRED = 'TOGGLE_STARRED'; export const REMOVE_STARRED = 'REMOVE_STARRED'; export const QUERY_CHANGE = 'QUERY_CHANGE'; +export const DATASOURCE_CHANGE = 'DATASOURCE_CHANGE'; export const REMOVE_TAG = 'REMOVE_TAG'; export const CLEAR_FILTERS = 'CLEAR_FILTERS'; export const SET_TAGS = 'SET_TAGS'; diff --git a/public/app/features/search/reducers/searchQueryReducer.ts b/public/app/features/search/reducers/searchQueryReducer.ts index f2eeb2767d6..6ba096ae266 100644 --- a/public/app/features/search/reducers/searchQueryReducer.ts +++ b/public/app/features/search/reducers/searchQueryReducer.ts @@ -7,6 +7,7 @@ import { REMOVE_STARRED, REMOVE_TAG, SET_TAGS, + DATASOURCE_CHANGE, TOGGLE_SORT, TOGGLE_STARRED, } from './actionTypes'; @@ -43,6 +44,8 @@ export const queryReducer = (state: DashboardQuery, action: SearchAction) => { const tag = action.payload; return tag && !state.tag.includes(tag) ? { ...state, tag: [...state.tag, tag] } : state; } + case DATASOURCE_CHANGE: + return { ...state, datasource: action.payload }; case TOGGLE_STARRED: return { ...state, starred: action.payload }; case REMOVE_STARRED: diff --git a/public/app/features/search/service/backend.ts b/public/app/features/search/service/backend.ts index e6137f3de3f..34e4222f350 100644 --- a/public/app/features/search/service/backend.ts +++ b/public/app/features/search/service/backend.ts @@ -154,23 +154,40 @@ export function filterFrame(frame: DataFrame, filter?: QueryFilters): DataFrame const view = new DataFrameView(frame); const keep: number[] = []; + const ds = filter.datasource ? view.fields.datasource : undefined; + const tags = filter.tags?.length ? view.fields.tags : undefined; + let ok = true; for (let i = 0; i < view.length; i++) { ok = true; - const row = view.get(i); - if (filter.tags) { - const tags = row.tags; - if (!tags) { + + if (tags) { + const v = tags.values.get(i); + if (!v) { ok = false; - continue; - } - for (const t of filter.tags) { - if (!tags.includes(t)) { - ok = false; - break; + } else { + for (const t of filter.tags!) { + if (!v.includes(t)) { + ok = false; + break; + } } } } + + if (ok && ds && filter.datasource) { + ok = false; + const v = ds.values.get(i); + if (v) { + for (const d of v) { + if (d.uid === filter.datasource) { + ok = true; + break; + } + } + } + } + if (ok) { keep.push(i); } diff --git a/public/app/features/search/service/minisearcher.ts b/public/app/features/search/service/minisearcher.ts index 432bf702e58..68db6a10b72 100644 --- a/public/app/features/search/service/minisearcher.ts +++ b/public/app/features/search/service/minisearcher.ts @@ -243,7 +243,22 @@ function shouldKeep(filter: QueryFilters, doc: InputDoc, index: number): boolean } } } - return true; + + let keep = true; + // Any is OK + if (filter.datasource) { + keep = false; + const dss = doc.datasource?.get(index); + if (dss) { + for (const ds of dss) { + if (ds.uid === filter.datasource) { + keep = true; + break; + } + } + } + } + return keep; } function getInputDoc(kind: SearchResultKind, frame: DataFrame): InputDoc { diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index d553c88a098..d7bcebd32da 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -68,6 +68,7 @@ export interface DashboardQuery { skipRecent: boolean; skipStarred: boolean; folderIds: number[]; + datasource?: string; sort: SelectableValue | null; // Save sorting data between layouts prevSort: SelectableValue | null;