Search: show dashboards form query (#47085)

This commit is contained in:
Ryan McKinley 2022-03-31 14:38:46 -07:00 committed by GitHub
parent 00ec99a8a1
commit 38dc34359b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 112 additions and 26 deletions

View File

@ -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<DashboardQuery>) => {
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<DashboardQuery>) => {
onTagAdd,
onSortChange,
onLayoutChange,
onDatasourceChange,
};
};

View File

@ -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 && (
<div>
<TagFilter isClearable tags={query.tag} tagOptions={getTagOptions} onChange={onTagChange} /> <br />
{query.datasource && (
<Button
icon="times"
variant="secondary"
onClick={() => onDatasourceChange(undefined)}
className={styles.clearClick}
>
Datasource: {query.datasource}
</Button>
)}
<AutoSizer style={{ width: '100%', height: '2000px' }}>
{({ width }) => {
return (
<>
<Table data={results.value!.body} width={width} tags={query.tag} onTagFilterChange={onTagChange} />
<Table
data={results.value!.body}
width={width}
tags={query.tag}
onTagFilterChange={onTagChange}
onDatasourceChange={onDatasourceChange}
/>
</>
);
}}
@ -88,4 +105,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
height: 100%;
font-size: 18px;
`,
clearClick: css`
&:hover {
text-decoration: line-through;
}
margin-bottom: 20px;
`,
});

View File

@ -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<FieldAccess>(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 (
<div {...row.getRowProps({ style })} className={styles.rowContainer}>
{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 (
<div key={index} className={styles.cellWrapper}>
<TableCell
@ -174,7 +179,6 @@ const getStyles = (theme: GrafanaTheme2) => {
`,
cellWrapper: css`
display: flex;
pointer-events: none;
`,
headerCell: css`
padding-top: 2px;

View File

@ -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<DataSourceRef[]>, width: number, iconClass: string): TableColumn {
function makeDataSourceColumn(
field: Field<DataSourceRef[]>,
width: number,
iconClass: string,
onDatasourceChange: (datasource?: string) => void
): TableColumn {
return {
Cell: DefaultCell,
id: `column-datasource`,
@ -188,10 +194,18 @@ function makeDataSourceColumn(field: Field<DataSourceRef[]>, width: number, icon
const icon = settings?.meta?.info?.logos?.small;
if (icon) {
return (
<span key={i}>
<a
key={i}
href={`datasources/edit/${settings.uid}`}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onDatasourceChange(settings.uid);
}}
>
<SVG src={icon} width={14} height={14} title={settings.type} className={iconClass} />
{settings.name}
</span>
</a>
);
}
return <span key={i}>{v.type}</span>;

View File

@ -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';

View File

@ -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:

View File

@ -154,23 +154,40 @@ export function filterFrame(frame: DataFrame, filter?: QueryFilters): DataFrame
const view = new DataFrameView<QueryResult>(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);
}

View File

@ -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 {

View File

@ -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;