mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search: a few minor improvements (#48989)
This commit is contained in:
parent
2691872c7a
commit
68757cfa73
@ -28,7 +28,8 @@ export class ImpressionSrv {
|
|||||||
store.set(impressionsKey, JSON.stringify(impressions));
|
store.set(impressionsKey, JSON.stringify(impressions));
|
||||||
}
|
}
|
||||||
|
|
||||||
getDashboardOpened() {
|
/** Returns an array of internal (numeric) dashboard IDs */
|
||||||
|
getDashboardOpened(): number[] {
|
||||||
let impressions = store.get(this.impressionKey()) || '[]';
|
let impressions = store.get(this.impressionKey()) || '[]';
|
||||||
|
|
||||||
impressions = JSON.parse(impressions);
|
impressions = JSON.parse(impressions);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
|
import SVG from 'react-inlinesvg';
|
||||||
import { usePopper } from 'react-popper';
|
import { usePopper } from 'react-popper';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
@ -130,7 +131,11 @@ export function SearchCard({ editable, item, onTagSelected, onToggleChecked }: P
|
|||||||
<img loading="lazy" className={styles.image} src={imageSrc} onError={() => setHasImage(false)} />
|
<img loading="lazy" className={styles.image} src={imageSrc} onError={() => setHasImage(false)} />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.imagePlaceholder}>
|
<div className={styles.imagePlaceholder}>
|
||||||
<Icon name="apps" size="xl" />
|
{item.icon ? (
|
||||||
|
<SVG src={item.icon} width={36} height={36} title={item.title} />
|
||||||
|
) : (
|
||||||
|
<Icon name="apps" size="xl" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import SVG from 'react-inlinesvg';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Icon, Spinner, TagList, useTheme2 } from '@grafana/ui';
|
import { Icon, Spinner, TagList, useTheme2 } from '@grafana/ui';
|
||||||
@ -38,7 +39,11 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.imagePlaceholder}>
|
<div className={styles.imagePlaceholder}>
|
||||||
<Icon name="apps" size="xl" />
|
{item.icon ? (
|
||||||
|
<SVG src={item.icon} width={36} height={36} title={item.title} />
|
||||||
|
) : (
|
||||||
|
<Icon name="apps" size="xl" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,7 +79,7 @@ export default function SearchPage() {
|
|||||||
// This gets the possible tags from within the query results
|
// This gets the possible tags from within the query results
|
||||||
const getTagOptions = (): Promise<TermCount[]> => {
|
const getTagOptions = (): Promise<TermCount[]> => {
|
||||||
const q: SearchQuery = {
|
const q: SearchQuery = {
|
||||||
query: query.query ?? '*',
|
query: query.query?.length ? query.query : '*',
|
||||||
tags: query.tag,
|
tags: query.tag,
|
||||||
ds_uid: query.datasource,
|
ds_uid: query.datasource,
|
||||||
};
|
};
|
||||||
|
@ -3,12 +3,14 @@ import React, { FC } from 'react';
|
|||||||
import { useAsync, useLocalStorage } from 'react-use';
|
import { useAsync, useLocalStorage } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { Checkbox, CollapsableSection, Icon, stylesFactory, useTheme } from '@grafana/ui';
|
import { Checkbox, CollapsableSection, Icon, stylesFactory, useTheme } from '@grafana/ui';
|
||||||
|
import impressionSrv from 'app/core/services/impression_srv';
|
||||||
import { getSectionStorageKey } from 'app/features/search/utils';
|
import { getSectionStorageKey } from 'app/features/search/utils';
|
||||||
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
||||||
|
|
||||||
import { SearchItem } from '../..';
|
import { SearchItem } from '../..';
|
||||||
import { getGrafanaSearcher } from '../../service';
|
import { getGrafanaSearcher, SearchQuery } from '../../service';
|
||||||
import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
|
import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
|
||||||
import { SelectionChecker, SelectionToggle } from '../selection';
|
import { SelectionChecker, SelectionToggle } from '../selection';
|
||||||
|
|
||||||
@ -38,15 +40,32 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
|
|||||||
if (!sectionExpanded) {
|
if (!sectionExpanded) {
|
||||||
return Promise.resolve([] as DashboardSectionItem[]);
|
return Promise.resolve([] as DashboardSectionItem[]);
|
||||||
}
|
}
|
||||||
let query = {
|
let folderUid: string | undefined = section.uid;
|
||||||
|
let folderTitle: string | undefined = section.title;
|
||||||
|
let query: SearchQuery = {
|
||||||
query: '*',
|
query: '*',
|
||||||
kind: ['dashboard'],
|
kind: ['dashboard'],
|
||||||
location: section.uid,
|
location: section.uid,
|
||||||
};
|
};
|
||||||
if (section.title === 'Starred') {
|
if (section.title === 'Starred') {
|
||||||
// TODO
|
const stars = await getBackendSrv().get('api/user/stars');
|
||||||
|
if (stars.length > 0) {
|
||||||
|
query = {
|
||||||
|
uid: stars, // array of UIDs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
folderUid = undefined;
|
||||||
|
folderTitle = undefined;
|
||||||
} else if (section.title === 'Recent') {
|
} else if (section.title === 'Recent') {
|
||||||
// TODO
|
const ids = impressionSrv.getDashboardOpened();
|
||||||
|
const uids = await getBackendSrv().get(`/api/dashboards/ids/${ids.slice(0, 30).join(',')}`);
|
||||||
|
if (uids?.length) {
|
||||||
|
query = {
|
||||||
|
uid: uids,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
folderUid = undefined;
|
||||||
|
folderTitle = undefined;
|
||||||
}
|
}
|
||||||
const raw = await getGrafanaSearcher().search(query);
|
const raw = await getGrafanaSearcher().search(query);
|
||||||
const v = raw.view.map(
|
const v = raw.view.map(
|
||||||
@ -60,16 +79,36 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
|
|||||||
id: 666, // do not use me!
|
id: 666, // do not use me!
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
tags: item.tags ?? [],
|
tags: item.tags ?? [],
|
||||||
checked: selection ? selection(item.kind, item.uid) : false,
|
folderUid,
|
||||||
|
folderTitle,
|
||||||
} as DashboardSectionItem)
|
} as DashboardSectionItem)
|
||||||
);
|
);
|
||||||
console.log('HERE!');
|
|
||||||
return v;
|
return v;
|
||||||
}, [sectionExpanded, section]);
|
}, [sectionExpanded, section]);
|
||||||
|
|
||||||
const onSectionExpand = () => {
|
const onSectionExpand = () => {
|
||||||
setSectionExpanded(!sectionExpanded);
|
setSectionExpanded(!sectionExpanded);
|
||||||
console.log('TODO!! section', section.title, section);
|
};
|
||||||
|
|
||||||
|
const onToggleFolder = (evt: React.FormEvent) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
if (selectionToggle && selection) {
|
||||||
|
const checked = !selection(section.kind, section.uid);
|
||||||
|
selectionToggle(section.kind, section.uid);
|
||||||
|
const sub = results.value ?? [];
|
||||||
|
for (const item of sub) {
|
||||||
|
if (selection('dashboard', item.uid!) !== checked) {
|
||||||
|
selectionToggle('dashboard', item.uid!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleChecked = (item: DashboardSectionItem) => {
|
||||||
|
if (selectionToggle) {
|
||||||
|
selectionToggle('dashboard', item.uid!);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const id = useUniqueId();
|
const id = useUniqueId();
|
||||||
@ -80,6 +119,31 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
|
|||||||
icon = sectionExpanded ? 'folder-open' : 'folder';
|
icon = sectionExpanded ? 'folder-open' : 'folder';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderResults = () => {
|
||||||
|
if (!results.value?.length) {
|
||||||
|
return <div>No items found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.value.map((v) => {
|
||||||
|
if (selection && selectionToggle) {
|
||||||
|
const type = v.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard';
|
||||||
|
v = {
|
||||||
|
...v,
|
||||||
|
checked: selection(type, v.uid!),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SearchItem
|
||||||
|
key={v.uid}
|
||||||
|
item={v}
|
||||||
|
onTagSelected={onTagSelected}
|
||||||
|
onToggleChecked={onToggleChecked as any}
|
||||||
|
editable={Boolean(selection != null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsableSection
|
<CollapsableSection
|
||||||
isOpen={sectionExpanded ?? false}
|
isOpen={sectionExpanded ?? false}
|
||||||
@ -91,7 +155,7 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
|
|||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
{selectionToggle && selection && (
|
{selectionToggle && selection && (
|
||||||
<div onClick={(v) => console.log(v)} className={styles.checkbox}>
|
<div className={styles.checkbox} onClick={onToggleFolder}>
|
||||||
<Checkbox value={selection(section.kind, section.uid)} aria-label="Select folder" />
|
<Checkbox value={selection(section.kind, section.uid)} aria-label="Select folder" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -111,13 +175,7 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{results.value && (
|
{results.value && <ul className={styles.sectionItems}>{renderResults()}</ul>}
|
||||||
<ul>
|
|
||||||
{results.value.map((v) => (
|
|
||||||
<SearchItem key={v.uid} item={v} onTagSelected={onTagSelected} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</CollapsableSection>
|
</CollapsableSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -150,6 +208,9 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa
|
|||||||
'pointer',
|
'pointer',
|
||||||
{ selected }
|
{ selected }
|
||||||
),
|
),
|
||||||
|
sectionItems: css`
|
||||||
|
margin: 0 24px 0 32px;
|
||||||
|
`,
|
||||||
checkbox: css`
|
checkbox: css`
|
||||||
padding: 0 ${sm} 0 0;
|
padding: 0 ${sm} 0 0;
|
||||||
`,
|
`,
|
||||||
|
@ -4,6 +4,7 @@ import { FixedSizeGrid } from 'react-window';
|
|||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { SearchCard } from '../../components/SearchCard';
|
import { SearchCard } from '../../components/SearchCard';
|
||||||
@ -42,10 +43,20 @@ export const SearchResultsGrid = ({
|
|||||||
const cellWidth = width / numColumns;
|
const cellWidth = width / numColumns;
|
||||||
const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8;
|
const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8;
|
||||||
const numRows = Math.ceil(itemCount / numColumns);
|
const numRows = Math.ceil(itemCount / numColumns);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteLoader isItemLoaded={response.isItemLoaded} itemCount={itemCount} loadMoreItems={response.loadMoreItems}>
|
<InfiniteLoader isItemLoaded={response.isItemLoaded} itemCount={itemCount} loadMoreItems={response.loadMoreItems}>
|
||||||
{({ onItemsRendered, ref }) => (
|
{({ onItemsRendered, ref }) => (
|
||||||
<FixedSizeGrid
|
<FixedSizeGrid
|
||||||
|
ref={ref}
|
||||||
|
onItemsRendered={(v) => {
|
||||||
|
onItemsRendered({
|
||||||
|
visibleStartIndex: v.visibleRowStartIndex * numColumns,
|
||||||
|
visibleStopIndex: v.visibleRowStopIndex * numColumns,
|
||||||
|
overscanStartIndex: v.overscanRowStartIndex * numColumns,
|
||||||
|
overscanStopIndex: v.overscanColumnStopIndex * numColumns,
|
||||||
|
});
|
||||||
|
}}
|
||||||
columnCount={numColumns}
|
columnCount={numColumns}
|
||||||
columnWidth={cellWidth}
|
columnWidth={cellWidth}
|
||||||
rowCount={numRows}
|
rowCount={numRows}
|
||||||
@ -60,9 +71,9 @@ export const SearchResultsGrid = ({
|
|||||||
if (index >= view.length) {
|
if (index >= view.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = view.get(index);
|
const item = view.get(index);
|
||||||
const kind = item.kind ?? 'dashboard';
|
const kind = item.kind ?? 'dashboard';
|
||||||
|
|
||||||
const facade: DashboardSectionItem = {
|
const facade: DashboardSectionItem = {
|
||||||
uid: item.uid,
|
uid: item.uid,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
@ -75,6 +86,20 @@ export const SearchResultsGrid = ({
|
|||||||
checked: selection ? selection(kind, item.uid) : false,
|
checked: selection ? selection(kind, item.uid) : false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (kind === 'panel') {
|
||||||
|
const type = item.panel_type;
|
||||||
|
facade.icon = 'public/img/icons/unicons/graph-bar.svg';
|
||||||
|
if (type) {
|
||||||
|
const info = config.panels[type];
|
||||||
|
if (info?.name) {
|
||||||
|
const v = info.info?.logos.small;
|
||||||
|
if (v && v.endsWith('.svg')) {
|
||||||
|
facade.icon = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The wrapper div is needed as the inner SearchItem has margin-bottom spacing
|
// The wrapper div is needed as the inner SearchItem has margin-bottom spacing
|
||||||
// And without this wrapper there is no room for that margin
|
// And without this wrapper there is no room for that margin
|
||||||
return item ? (
|
return item ? (
|
||||||
|
@ -5,7 +5,6 @@ import SVG from 'react-inlinesvg';
|
|||||||
import { Field } from '@grafana/data';
|
import { Field } from '@grafana/data';
|
||||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { Checkbox, Icon, IconName, TagList } from '@grafana/ui';
|
import { Checkbox, Icon, IconName, TagList } from '@grafana/ui';
|
||||||
import { DefaultCell } from '@grafana/ui/src/components/Table/DefaultCell';
|
|
||||||
|
|
||||||
import { QueryResponse, SearchResultMeta } from '../../service';
|
import { QueryResponse, SearchResultMeta } from '../../service';
|
||||||
import { SelectionChecker, SelectionToggle } from '../selection';
|
import { SelectionChecker, SelectionToggle } from '../selection';
|
||||||
@ -221,11 +220,11 @@ function makeTypeColumn(
|
|||||||
styles: Record<string, string>
|
styles: Record<string, string>
|
||||||
): TableColumn {
|
): TableColumn {
|
||||||
return {
|
return {
|
||||||
Cell: DefaultCell,
|
|
||||||
id: `column-type`,
|
id: `column-type`,
|
||||||
field: kindField ?? typeField,
|
field: kindField ?? typeField,
|
||||||
Header: 'Type',
|
Header: 'Type',
|
||||||
accessor: (row: any, i: number) => {
|
Cell: (p) => {
|
||||||
|
const i = p.row.index;
|
||||||
const kind = kindField?.values.get(i) ?? 'dashboard';
|
const kind = kindField?.values.get(i) ?? 'dashboard';
|
||||||
let icon = 'public/img/icons/unicons/apps.svg';
|
let icon = 'public/img/icons/unicons/apps.svg';
|
||||||
let txt = 'Dashboard';
|
let txt = 'Dashboard';
|
||||||
@ -253,13 +252,15 @@ function makeTypeColumn(
|
|||||||
icon = v;
|
icon = v;
|
||||||
}
|
}
|
||||||
txt = info.name;
|
txt = info.name;
|
||||||
|
} else {
|
||||||
|
icon = `public/img/icons/unicons/question.svg`; // plugin not found
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={styles.typeText}>
|
<div {...p.cellProps} className={styles.typeText}>
|
||||||
<SVG src={icon} width={14} height={14} title={txt} className={styles.typeIcon} />
|
<SVG src={icon} width={14} height={14} title={txt} className={styles.typeIcon} />
|
||||||
{txt}
|
{txt}
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +70,21 @@ export async function doSearchQuery(query: SearchQuery): Promise<QueryResponse>
|
|||||||
field.display = getDisplayProcessor({ field, theme: config.theme2 });
|
field.display = getDisplayProcessor({ field, theme: config.theme2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = first.meta?.custom as SearchResultMeta;
|
// Make sure the object exists
|
||||||
|
if (!first.meta?.custom) {
|
||||||
|
first.meta = {
|
||||||
|
...first.meta,
|
||||||
|
custom: {
|
||||||
|
count: first.length,
|
||||||
|
max_score: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = first.meta.custom as SearchResultMeta;
|
||||||
|
if (!meta.locationInfo) {
|
||||||
|
meta.locationInfo = {};
|
||||||
|
}
|
||||||
const view = new DataFrameView<DashboardQueryResult>(first);
|
const view = new DataFrameView<DashboardQueryResult>(first);
|
||||||
return {
|
return {
|
||||||
totalRows: meta.count ?? first.length,
|
totalRows: meta.count ?? first.length,
|
||||||
|
@ -40,6 +40,7 @@ export interface DashboardSectionItem {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
title: string;
|
title: string;
|
||||||
type: DashboardSearchItemType;
|
type: DashboardSearchItemType;
|
||||||
|
icon?: string; // used for grid view
|
||||||
uid?: string;
|
uid?: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user