mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 04:34:23 -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));
|
||||
}
|
||||
|
||||
getDashboardOpened() {
|
||||
/** Returns an array of internal (numeric) dashboard IDs */
|
||||
getDashboardOpened(): number[] {
|
||||
let impressions = store.get(this.impressionKey()) || '[]';
|
||||
|
||||
impressions = JSON.parse(impressions);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
`,
|
||||
|
@ -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 ? (
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user