Search: add starred filter and swap button order (#52184)

Co-authored-by: Alexandra Vargas <alexa1866@gmail.com>
Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>
This commit is contained in:
Ryan McKinley
2022-07-29 07:34:17 -07:00
committed by GitHub
parent 085ae014cd
commit fbd289c19c
7 changed files with 133 additions and 35 deletions

View File

@@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { HorizontalGroup, LinkButton } from '@grafana/ui';
import { Menu, Dropdown, Button, Icon } from '@grafana/ui';
export interface Props {
folderId?: number;
@@ -19,13 +19,24 @@ export const DashboardActions: FC<Props> = ({ folderId, canCreateFolders = false
return url;
};
const MenuActions = () => {
return (
<Menu>
{canCreateDashboards && <Menu.Item url={actionUrl('new')} label="New Dashboard" />}
{!folderId && canCreateFolders && <Menu.Item url="dashboards/folder/new" label="New Folder" />}
{canCreateDashboards && <Menu.Item url={actionUrl('import')} label="Import" />}
</Menu>
);
};
return (
<div>
<HorizontalGroup spacing="md" align="center">
{canCreateDashboards && <LinkButton href={actionUrl('new')}>New Dashboard</LinkButton>}
{!folderId && canCreateFolders && <LinkButton href="dashboards/folder/new">New Folder</LinkButton>}
{canCreateDashboards && <LinkButton href={actionUrl('import')}>Import</LinkButton>}
</HorizontalGroup>
<Dropdown overlay={MenuActions} placement="bottom-start">
<Button variant="primary">
New
<Icon name="angle-down" />
</Button>
</Dropdown>
</div>
);
};

View File

@@ -64,6 +64,11 @@ export const useSearchQuery = (defaults: Partial<DashboardQuery>) => {
updateLocation({ starred: starred || null });
}, []);
const onClearStarred = useCallback(() => {
dispatch({ type: TOGGLE_STARRED, payload: false });
updateLocation({ starred: null });
}, []);
const onSortChange = useCallback((sort: SelectableValue | null) => {
dispatch({ type: TOGGLE_SORT, payload: sort });
updateLocation({ sort: sort?.value, layout: SearchLayout.List });
@@ -85,6 +90,7 @@ export const useSearchQuery = (defaults: Partial<DashboardQuery>) => {
onClearFilters,
onTagFilterChange,
onStarredFilterChange,
onClearStarred,
onTagAdd,
onSortChange,
onLayoutChange,

View File

@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import React, { FC, FormEvent } from 'react';
import React, { FC, FormEvent, useEffect } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
@@ -39,7 +39,7 @@ export function getValidQueryLayout(q: DashboardQuery): SearchLayout {
// Folders is not valid when a query exists
if (layout === SearchLayout.Folders) {
if (q.query || q.sort) {
if (q.query || q.sort || q.starred) {
return SearchLayout.List;
}
}
@@ -75,8 +75,37 @@ export const ActionRow: FC<Props> = ({
onLayoutChange(layout);
};
useEffect(() => {
if (includePanels && layout === SearchLayout.Folders) {
setIncludePanels(false);
}
}, [layout, includePanels, setIncludePanels]);
return (
<div className={styles.actionRow}>
<HorizontalGroup spacing="md" width="auto">
<TagFilter isClearable={false} tags={query.tag} tagOptions={getTagOptions} onChange={onTagFilterChange} />
{config.featureToggles.panelTitleSearch && (
<Checkbox
data-testid="include-panels"
disabled={layout === SearchLayout.Folders}
value={includePanels}
onChange={() => setIncludePanels(!includePanels)}
label="Include panels"
/>
)}
{showStarredFilter && (
<div className={styles.checkboxWrapper}>
<Checkbox label="Starred" onChange={onStarredFilterChange} value={query.starred} />
</div>
)}
{query.datasource && (
<Button icon="times" variant="secondary" onClick={() => onDatasourceChange(undefined)}>
Datasource: {query.datasource}
</Button>
)}
</HorizontalGroup>
<div className={styles.rowContainer}>
<HorizontalGroup spacing="md" width="auto">
{!hideLayout && (
@@ -90,29 +119,6 @@ export const ActionRow: FC<Props> = ({
<SortPicker onChange={onSortChange} value={query.sort?.value} getSortOptions={getSortOptions} isClearable />
</HorizontalGroup>
</div>
<HorizontalGroup spacing="md" width="auto">
{showStarredFilter && (
<div className={styles.checkboxWrapper}>
<Checkbox label="Filter by starred" onChange={onStarredFilterChange} value={query.starred} />
</div>
)}
{query.datasource && (
<Button icon="times" variant="secondary" onClick={() => onDatasourceChange(undefined)}>
Datasource: {query.datasource}
</Button>
)}
{config.featureToggles.panelTitleSearch && (
<Checkbox
data-testid="include-panels"
disabled={layout === SearchLayout.Folders}
value={includePanels}
onChange={() => setIncludePanels(!includePanels)}
label="Include panels"
/>
)}
<TagFilter isClearable tags={query.tag} tagOptions={getTagOptions} onChange={onTagFilterChange} />
</HorizontalGroup>
</div>
);
};

View File

@@ -48,7 +48,16 @@ export const SearchView = ({
}: SearchViewProps) => {
const styles = useStyles2(getStyles);
const { query, onTagFilterChange, onTagAdd, onDatasourceChange, onSortChange, onLayoutChange } = useSearchQuery({});
const {
query,
onTagFilterChange,
onStarredFilterChange,
onTagAdd,
onDatasourceChange,
onSortChange,
onLayoutChange,
onClearStarred,
} = useSearchQuery({});
query.query = queryText; // Use the query value passed in from parent rather than from URL
const [searchSelection, setSearchSelection] = useState(newSearchSelection());
@@ -66,6 +75,7 @@ export const SearchView = ({
sort: query.sort?.value,
explain: query.explain,
withAllowedActions: query.explain, // allowedActions are currently not used for anything on the UI and added only in `explain` mode
starred: query.starred,
};
// Only dashboards have additional properties
@@ -106,6 +116,9 @@ export const SearchView = ({
);
const results = useAsync(() => {
if (searchQuery.starred) {
return getGrafanaSearcher().starred(searchQuery);
}
return getGrafanaSearcher().search(searchQuery);
}, [searchQuery]);
@@ -136,6 +149,13 @@ export const SearchView = ({
onQueryTextChange(query.query);
};
const getStarredItems = useCallback(
(e) => {
onStarredFilterChange(e);
},
[onStarredFilterChange]
);
const renderResults = () => {
const value = results.value;
@@ -252,9 +272,14 @@ export const SearchView = ({
if (query.query) {
onQueryTextChange(''); // parent will clear the sort
}
if (query.starred) {
onClearStarred();
}
}
onLayoutChange(v);
}}
showStarredFilter={hidePseudoFolders}
onStarredFilterChange={!hidePseudoFolders ? undefined : getStarredItems}
onSortChange={onSortChange}
onTagFilterChange={onTagFilterChange}
getTagOptions={getTagOptions}

View File

@@ -1,7 +1,7 @@
import { lastValueFrom } from 'rxjs';
import { ArrayVector, DataFrame, DataFrameView, getDisplayProcessor, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { config, getBackendSrv } from '@grafana/runtime';
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
import { getGrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
@@ -13,11 +13,24 @@ import { DashboardQueryResult, GrafanaSearcher, QueryResponse, SearchQuery, Sear
export class BlugeSearcher implements GrafanaSearcher {
async search(query: SearchQuery): Promise<QueryResponse> {
if (query.facet?.length) {
throw 'facets not supported!';
throw new Error('facets not supported!');
}
return doSearchQuery(query);
}
async starred(query: SearchQuery): Promise<QueryResponse> {
if (query.facet?.length) {
throw new Error('facets not supported!');
}
// get the starred dashboards
const starsUIDS = await getBackendSrv().get('api/user/stars');
const starredQuery = {
uid: starsUIDS,
query: query.query ?? '*',
};
return doSearchQuery(starredQuery);
}
async tags(query: SearchQuery): Promise<TermCount[]> {
const ds = await getGrafanaDatasource();
const target = {

View File

@@ -20,6 +20,7 @@ interface APIQuery {
dashboardUID?: string[];
folderIds?: number[];
sort?: string;
starred?: boolean;
}
// Internal object to hold folderId
@@ -39,7 +40,7 @@ export class SQLSearcher implements GrafanaSearcher {
async search(query: SearchQuery): Promise<QueryResponse> {
if (query.facet?.length) {
throw 'facets not supported!';
throw new Error('facets not supported!');
}
const q: APIQuery = {
limit: query.limit ?? 1000, // default 1k max values
@@ -70,6 +71,40 @@ export class SQLSearcher implements GrafanaSearcher {
return this.doAPIQuery(q);
}
async starred(query: SearchQuery): Promise<QueryResponse> {
if (query.facet?.length) {
throw new Error('facets not supported!');
}
const q: APIQuery = {
limit: query.limit ?? 1000, // default 1k max values
tag: query.tags,
sort: query.sort,
starred: query.starred,
};
query = await replaceCurrentFolderQuery(query);
if (query.query === '*') {
if (query.kind?.length === 1 && query.kind[0] === 'folder') {
q.type = 'dash-folder';
}
} else if (query.query?.length) {
q.query = query.query;
}
if (query.uid) {
q.dashboardUID = query.uid;
} else if (query.location?.length) {
let info = this.locationInfo[query.location];
if (!info) {
// This will load all folder folders
await this.doAPIQuery({ type: 'dash-folder', limit: 999 });
info = this.locationInfo[query.location];
}
q.folderIds = [info.folderId ?? 0];
}
return this.doAPIQuery(q);
}
// returns the appropriate sorting options
async getSortOptions(): Promise<SelectableValue[]> {
// {

View File

@@ -23,6 +23,7 @@ export interface SearchQuery {
hasPreview?: string; // theme
limit?: number;
from?: number;
starred?: boolean;
}
export interface DashboardQueryResult {
@@ -68,6 +69,7 @@ export interface QueryResponse {
export interface GrafanaSearcher {
search: (query: SearchQuery) => Promise<QueryResponse>;
starred: (query: SearchQuery) => Promise<QueryResponse>;
tags: (query: SearchQuery) => Promise<TermCount[]>;
getSortOptions: () => Promise<SelectableValue[]>;
}