mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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[]> {
|
||||
// {
|
||||
|
||||
@@ -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[]>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user