diff --git a/public/app/core/services/impression_srv.ts b/public/app/core/services/impression_srv.ts index f26b85dc542..114468fddc7 100644 --- a/public/app/core/services/impression_srv.ts +++ b/public/app/core/services/impression_srv.ts @@ -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); diff --git a/public/app/features/search/components/SearchCard.tsx b/public/app/features/search/components/SearchCard.tsx index 862c3deefc8..f9b77ac4302 100644 --- a/public/app/features/search/components/SearchCard.tsx +++ b/public/app/features/search/components/SearchCard.tsx @@ -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 setHasImage(false)} /> ) : (
- + {item.icon ? ( + + ) : ( + + )}
)} diff --git a/public/app/features/search/components/SearchCardExpanded.tsx b/public/app/features/search/components/SearchCardExpanded.tsx index 548cce70706..4700fdd916d 100644 --- a/public/app/features/search/components/SearchCardExpanded.tsx +++ b/public/app/features/search/components/SearchCardExpanded.tsx @@ -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 /> ) : (
- + {item.icon ? ( + + ) : ( + + )}
)} diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index 2a83b0b11a2..167e18afcbe 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -79,7 +79,7 @@ export default function SearchPage() { // This gets the possible tags from within the query results const getTagOptions = (): Promise => { const q: SearchQuery = { - query: query.query ?? '*', + query: query.query?.length ? query.query : '*', tags: query.tag, ds_uid: query.datasource, }; diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx index 263cb7f320d..53a9dd2f272 100644 --- a/public/app/features/search/page/components/FolderSection.tsx +++ b/public/app/features/search/page/components/FolderSection.tsx @@ -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 = ({ 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 = ({ 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 = ({ section, selectionToggle icon = sectionExpanded ? 'folder-open' : 'folder'; } + const renderResults = () => { + if (!results.value?.length) { + return
No items found
; + } + + return results.value.map((v) => { + if (selection && selectionToggle) { + const type = v.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard'; + v = { + ...v, + checked: selection(type, v.uid!), + }; + } + return ( + + ); + }); + }; + return ( = ({ section, selectionToggle label={ <> {selectionToggle && selection && ( -
console.log(v)} className={styles.checkbox}> +
)} @@ -111,13 +175,7 @@ export const FolderSection: FC = ({ section, selectionToggle } > - {results.value && ( -
    - {results.value.map((v) => ( - - ))} -
- )} + {results.value &&
    {renderResults()}
} ); }; @@ -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; `, diff --git a/public/app/features/search/page/components/SearchResultsGrid.tsx b/public/app/features/search/page/components/SearchResultsGrid.tsx index 296e105ba35..5b4c7e24d54 100644 --- a/public/app/features/search/page/components/SearchResultsGrid.tsx +++ b/public/app/features/search/page/components/SearchResultsGrid.tsx @@ -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 ( {({ onItemsRendered, ref }) => ( { + 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 ? ( diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index 36666951f36..5df00f6cc75 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -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 ): 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 ( -
+
{txt}
diff --git a/public/app/features/search/service/bluge.ts b/public/app/features/search/service/bluge.ts index 0104d163e5f..59984d5314c 100644 --- a/public/app/features/search/service/bluge.ts +++ b/public/app/features/search/service/bluge.ts @@ -70,7 +70,21 @@ export async function doSearchQuery(query: SearchQuery): Promise 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(first); return { totalRows: meta.count ?? first.length, diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index ba0caf00c5d..4b1d16d9dc8 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -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;