Search: a few minor improvements (#48989)

This commit is contained in:
Ryan McKinley 2022-05-16 15:38:27 -07:00 committed by GitHub
parent 2691872c7a
commit 68757cfa73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 138 additions and 25 deletions

View File

@ -28,7 +28,8 @@ export class ImpressionSrv {
store.set(impressionsKey, JSON.stringify(impressions));
}
getDashboardOpened() {
/** Returns an array of internal (numeric) dashboard IDs */
getDashboardOpened(): number[] {
let impressions = store.get(this.impressionKey()) || '[]';
impressions = JSON.parse(impressions);

View File

@ -1,5 +1,6 @@
import { css } from '@emotion/css';
import React, { useCallback, useRef, useState } from 'react';
import SVG from 'react-inlinesvg';
import { usePopper } from 'react-popper';
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)} />
) : (
<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>

View File

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import classNames from 'classnames';
import React, { useState } from 'react';
import SVG from 'react-inlinesvg';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Spinner, TagList, useTheme2 } from '@grafana/ui';
@ -38,7 +39,11 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l
/>
) : (
<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>

View File

@ -79,7 +79,7 @@ export default function SearchPage() {
// This gets the possible tags from within the query results
const getTagOptions = (): Promise<TermCount[]> => {
const q: SearchQuery = {
query: query.query ?? '*',
query: query.query?.length ? query.query : '*',
tags: query.tag,
ds_uid: query.datasource,
};

View File

@ -3,12 +3,14 @@ import React, { FC } from 'react';
import { useAsync, useLocalStorage } from 'react-use';
import { GrafanaTheme } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
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 { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
import { SearchItem } from '../..';
import { getGrafanaSearcher } from '../../service';
import { getGrafanaSearcher, SearchQuery } from '../../service';
import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
import { SelectionChecker, SelectionToggle } from '../selection';
@ -38,15 +40,32 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
if (!sectionExpanded) {
return Promise.resolve([] as DashboardSectionItem[]);
}
let query = {
let folderUid: string | undefined = section.uid;
let folderTitle: string | undefined = section.title;
let query: SearchQuery = {
query: '*',
kind: ['dashboard'],
location: section.uid,
};
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') {
// 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 v = raw.view.map(
@ -60,16 +79,36 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
id: 666, // do not use me!
isStarred: false,
tags: item.tags ?? [],
checked: selection ? selection(item.kind, item.uid) : false,
folderUid,
folderTitle,
} as DashboardSectionItem)
);
console.log('HERE!');
return v;
}, [sectionExpanded, section]);
const onSectionExpand = () => {
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();
@ -80,6 +119,31 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
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 (
<CollapsableSection
isOpen={sectionExpanded ?? false}
@ -91,7 +155,7 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
label={
<>
{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" />
</div>
)}
@ -111,13 +175,7 @@ export const FolderSection: FC<SectionHeaderProps> = ({ section, selectionToggle
</>
}
>
{results.value && (
<ul>
{results.value.map((v) => (
<SearchItem key={v.uid} item={v} onTagSelected={onTagSelected} />
))}
</ul>
)}
{results.value && <ul className={styles.sectionItems}>{renderResults()}</ul>}
</CollapsableSection>
);
};
@ -150,6 +208,9 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa
'pointer',
{ selected }
),
sectionItems: css`
margin: 0 24px 0 32px;
`,
checkbox: css`
padding: 0 ${sm} 0 0;
`,

View File

@ -4,6 +4,7 @@ import { FixedSizeGrid } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { SearchCard } from '../../components/SearchCard';
@ -42,10 +43,20 @@ export const SearchResultsGrid = ({
const cellWidth = width / numColumns;
const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8;
const numRows = Math.ceil(itemCount / numColumns);
return (
<InfiniteLoader isItemLoaded={response.isItemLoaded} itemCount={itemCount} loadMoreItems={response.loadMoreItems}>
{({ onItemsRendered, ref }) => (
<FixedSizeGrid
ref={ref}
onItemsRendered={(v) => {
onItemsRendered({
visibleStartIndex: v.visibleRowStartIndex * numColumns,
visibleStopIndex: v.visibleRowStopIndex * numColumns,
overscanStartIndex: v.overscanRowStartIndex * numColumns,
overscanStopIndex: v.overscanColumnStopIndex * numColumns,
});
}}
columnCount={numColumns}
columnWidth={cellWidth}
rowCount={numRows}
@ -60,9 +71,9 @@ export const SearchResultsGrid = ({
if (index >= view.length) {
return null;
}
const item = view.get(index);
const kind = item.kind ?? 'dashboard';
const facade: DashboardSectionItem = {
uid: item.uid,
title: item.name,
@ -75,6 +86,20 @@ export const SearchResultsGrid = ({
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
// And without this wrapper there is no room for that margin
return item ? (

View File

@ -5,7 +5,6 @@ import SVG from 'react-inlinesvg';
import { Field } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { Checkbox, Icon, IconName, TagList } from '@grafana/ui';
import { DefaultCell } from '@grafana/ui/src/components/Table/DefaultCell';
import { QueryResponse, SearchResultMeta } from '../../service';
import { SelectionChecker, SelectionToggle } from '../selection';
@ -221,11 +220,11 @@ function makeTypeColumn(
styles: Record<string, string>
): TableColumn {
return {
Cell: DefaultCell,
id: `column-type`,
field: kindField ?? typeField,
Header: 'Type',
accessor: (row: any, i: number) => {
Cell: (p) => {
const i = p.row.index;
const kind = kindField?.values.get(i) ?? 'dashboard';
let icon = 'public/img/icons/unicons/apps.svg';
let txt = 'Dashboard';
@ -253,13 +252,15 @@ function makeTypeColumn(
icon = v;
}
txt = info.name;
} else {
icon = `public/img/icons/unicons/question.svg`; // plugin not found
}
}
break;
}
}
return (
<div className={styles.typeText}>
<div {...p.cellProps} className={styles.typeText}>
<SVG src={icon} width={14} height={14} title={txt} className={styles.typeIcon} />
{txt}
</div>

View File

@ -70,7 +70,21 @@ export async function doSearchQuery(query: SearchQuery): Promise<QueryResponse>
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);
return {
totalRows: meta.count ?? first.length,

View File

@ -40,6 +40,7 @@ export interface DashboardSectionItem {
tags: string[];
title: string;
type: DashboardSearchItemType;
icon?: string; // used for grid view
uid?: string;
uri: string;
url: string;