From 0c36b247af44342a0107a776567006c0d48ab743 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 23 Feb 2023 16:52:54 +0100 Subject: [PATCH] Chore: Refactor Search/Folder view types into DashboardViewItem (#63162) * Chore: Refactor Search/Folder view types into DashboardViewItem * uid is not optional in api * rename queryResultToNestedFolderItem function * Fix error from locationInfo being empty * change queryResultToViewItem to take view instead * Fix sortMeta fields not showing on search cards * Show correct parent for panel search results --- .betterer.results | 10 +- .../components/FolderFilter/FolderFilter.tsx | 4 +- .../Select/ReadonlyFolderPicker/api.test.ts | 4 +- .../SubMenu/DashboardLinksDashboard.test.tsx | 1 - .../explore/AddToDashboard/index.test.tsx | 3 - .../features/search/components/SearchCard.tsx | 11 +- .../search/components/SearchCardExpanded.tsx | 8 +- .../search/components/SearchItem.test.tsx | 13 +-- .../features/search/components/SearchItem.tsx | 18 +-- .../page/components/FolderSection.test.tsx | 8 +- .../search/page/components/FolderSection.tsx | 105 ++++++------------ .../search/page/components/FolderView.tsx | 21 ++-- .../page/components/SearchResultsCards.tsx | 35 +----- .../page/components/SearchResultsGrid.tsx | 28 ++--- .../search/page/components/columns.tsx | 11 +- public/app/features/search/service/sql.ts | 4 +- public/app/features/search/service/types.ts | 3 + public/app/features/search/service/utils.ts | 56 +++++++++- public/app/features/search/types.ts | 82 ++++++++------ .../app/plugins/panel/dashlist/DashList.tsx | 2 +- 20 files changed, 199 insertions(+), 228 deletions(-) diff --git a/.betterer.results b/.betterer.results index ebad01992cc..0a7af2ec9ea 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4075,13 +4075,6 @@ exports[`better eslint`] = { "public/app/features/search/page/components/MoveToFolderModal.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/search/page/components/SearchResultsCards.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/features/search/page/components/SearchResultsGrid.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], "public/app/features/search/page/components/columns.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -4096,6 +4089,9 @@ exports[`better eslint`] = { "public/app/features/search/service/sql.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/features/search/service/utils.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/search/state/SearchStateManager.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/public/app/core/components/FolderFilter/FolderFilter.tsx b/public/app/core/components/FolderFilter/FolderFilter.tsx index 197b9eadc62..d1fb837e2da 100644 --- a/public/app/core/components/FolderFilter/FolderFilter.tsx +++ b/public/app/core/components/FolderFilter/FolderFilter.tsx @@ -5,7 +5,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { AsyncMultiSelect, Icon, Button, useStyles2 } from '@grafana/ui'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types'; +import { DashboardSearchItemType } from 'app/features/search/types'; import { FolderInfo, PermissionLevelString } from 'app/types'; export interface FolderFilterProps { @@ -72,7 +72,7 @@ async function getFoldersAsOptions( }; // FIXME: stop using id from search and use UID instead - const searchHits: DashboardSearchHit[] = await getBackendSrv().search(params); + const searchHits = await getBackendSrv().search(params); const options = searchHits.map((d) => ({ label: d.title, value: { uid: d.uid, title: d.title } })); if (!searchString || 'general'.includes(searchString.toLowerCase())) { options.unshift({ label: 'General', value: { uid: 'general', title: 'General' } }); diff --git a/public/app/core/components/Select/ReadonlyFolderPicker/api.test.ts b/public/app/core/components/Select/ReadonlyFolderPicker/api.test.ts index d0cab2a2861..46ab7d3adb8 100644 --- a/public/app/core/components/Select/ReadonlyFolderPicker/api.test.ts +++ b/public/app/core/components/Select/ReadonlyFolderPicker/api.test.ts @@ -1,13 +1,13 @@ import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput'; import * as api from '../../../../features/manage-dashboards/state/actions'; -import { DashboardSearchItem } from '../../../../features/search/types'; +import { DashboardSearchHit } from '../../../../features/search/types'; import { PermissionLevelString } from '../../../../types'; import { ALL_FOLDER, GENERAL_FOLDER } from './ReadonlyFolderPicker'; import { getFolderAsOption, getFoldersAsOptions } from './api'; function getTestContext( - searchHits: DashboardSearchItem[] = [], + searchHits: DashboardSearchHit[] = [], folderById: { id: number; title: string } = { id: 1, title: 'Folder 1' } ) { jest.clearAllMocks(); diff --git a/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.test.tsx b/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.test.tsx index dd7eeee731c..c05ce36eac1 100644 --- a/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.test.tsx +++ b/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.test.tsx @@ -59,7 +59,6 @@ describe('resolveLinks', () => { title: 'DashLinks', url: '/d/6ieouugGk/DashLinks', isStarred: false, - items: [], tags: [], uri: 'db/DashLinks', type: DashboardSearchItemType.DashDB, diff --git a/public/app/features/explore/AddToDashboard/index.test.tsx b/public/app/features/explore/AddToDashboard/index.test.tsx index a5bb2b2102b..b1a36dcb248 100644 --- a/public/app/features/explore/AddToDashboard/index.test.tsx +++ b/public/app/features/explore/AddToDashboard/index.test.tsx @@ -201,7 +201,6 @@ describe('AddToDashboardButton', () => { { uid: 'someUid', isStarred: false, - items: [], title: 'Dashboard Title', tags: [], type: DashboardSearchItemType.DashDB, @@ -243,7 +242,6 @@ describe('AddToDashboardButton', () => { { uid: 'someUid', isStarred: false, - items: [], title: 'Dashboard Title', tags: [], type: DashboardSearchItemType.DashDB, @@ -359,7 +357,6 @@ describe('AddToDashboardButton', () => { { uid: 'someUid', isStarred: false, - items: [], title: 'Dashboard Title', tags: [], type: DashboardSearchItemType.DashDB, diff --git a/public/app/features/search/components/SearchCard.tsx b/public/app/features/search/components/SearchCard.tsx index d7856ae6e8b..607cd6df887 100644 --- a/public/app/features/search/components/SearchCard.tsx +++ b/public/app/features/search/components/SearchCard.tsx @@ -9,7 +9,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { Icon, Portal, TagList, useTheme2 } from '@grafana/ui'; import { backendSrv } from 'app/core/services/backend_srv'; -import { DashboardSectionItem, OnToggleChecked } from '../types'; +import { DashboardViewItem, OnToggleChecked } from '../types'; import { SearchCardExpanded } from './SearchCardExpanded'; import { SearchCheckbox } from './SearchCheckbox'; @@ -18,7 +18,8 @@ const DELAY_BEFORE_EXPANDING = 500; export interface Props { editable?: boolean; - item: DashboardSectionItem; + item: DashboardViewItem; + isSelected?: boolean; onTagSelected?: (name: string) => any; onToggleChecked?: OnToggleChecked; onClick?: (event: React.MouseEvent) => void; @@ -28,7 +29,7 @@ export function getThumbnailURL(uid: string, isLight?: boolean) { return `/api/dashboards/uid/${uid}/img/thumb/${isLight ? 'light' : 'dark'}`; } -export function SearchCard({ editable, item, onTagSelected, onToggleChecked, onClick }: Props) { +export function SearchCard({ editable, item, isSelected, onTagSelected, onToggleChecked, onClick }: Props) { const [hasImage, setHasImage] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); const [showExpandedView, setShowExpandedView] = useState(false); @@ -130,7 +131,7 @@ export function SearchCard({ editable, item, onTagSelected, onToggleChecked, onC className={styles.checkbox} aria-label={`Select dashboard ${item.title}`} editable={editable} - checked={item.checked} + checked={isSelected} onClick={onCheckboxClick} /> {hasImage ? ( @@ -153,7 +154,7 @@ export function SearchCard({ editable, item, onTagSelected, onToggleChecked, onC
{item.title}
- +
{showExpandedView && ( diff --git a/public/app/features/search/components/SearchCardExpanded.tsx b/public/app/features/search/components/SearchCardExpanded.tsx index 51b72bd4f49..86028c590cf 100644 --- a/public/app/features/search/components/SearchCardExpanded.tsx +++ b/public/app/features/search/components/SearchCardExpanded.tsx @@ -6,7 +6,7 @@ import SVG from 'react-inlinesvg'; import { GrafanaTheme2 } from '@grafana/data'; import { Icon, Spinner, TagList, useTheme2 } from '@grafana/ui'; -import { DashboardSectionItem } from '../types'; +import { DashboardViewItem } from '../types'; import { getThumbnailURL } from './SearchCard'; @@ -14,7 +14,7 @@ export interface Props { className?: string; imageHeight: number; imageWidth: number; - item: DashboardSectionItem; + item: DashboardViewItem; lastUpdated?: string | null; onClick?: (event: React.MouseEvent) => void; } @@ -25,7 +25,7 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l const imageSrc = getThumbnailURL(item.uid!, theme.isLight); const styles = getStyles(theme, imageHeight, imageWidth); - const folderTitle = item.folderTitle || 'General'; + const folderTitle = item.parentTitle || 'General'; return ( @@ -66,7 +66,7 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l )}
- +
diff --git a/public/app/features/search/components/SearchItem.test.tsx b/public/app/features/search/components/SearchItem.test.tsx index 822b3c14c13..50c92b69f1a 100644 --- a/public/app/features/search/components/SearchItem.test.tsx +++ b/public/app/features/search/components/SearchItem.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { selectors } from '@grafana/e2e-selectors'; -import { DashboardSearchItemType } from '../types'; +import { DashboardViewItem } from '../types'; import { Props, SearchItem } from './SearchItem'; @@ -11,17 +11,12 @@ beforeEach(() => { jest.clearAllMocks(); }); -const data = { - id: 1, +const data: DashboardViewItem = { + kind: 'dashboard' as const, uid: 'lBdLINUWk', title: 'Test 1', - uri: 'db/test1', url: '/d/lBdLINUWk/test1', - slug: '', - type: DashboardSearchItemType.DashDB, tags: ['Tag1', 'Tag2'], - isStarred: false, - checked: false, }; const setup = (propOverrides?: Partial) => { @@ -54,7 +49,7 @@ describe('SearchItem', () => { }); it('should mark items as checked', () => { - setup({ editable: true, item: { ...data, checked: true } }); + setup({ editable: true, isSelected: true }); expect(screen.getByRole('checkbox')).toBeChecked(); }); diff --git a/public/app/features/search/components/SearchItem.tsx b/public/app/features/search/components/SearchItem.tsx index f831693df21..34e257570c1 100644 --- a/public/app/features/search/components/SearchItem.tsx +++ b/public/app/features/search/components/SearchItem.tsx @@ -6,12 +6,14 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Card, Icon, IconName, TagList, useStyles2 } from '@grafana/ui'; import { SEARCH_ITEM_HEIGHT } from '../constants'; -import { DashboardSectionItem, OnToggleChecked } from '../types'; +import { getIconForKind } from '../service/utils'; +import { DashboardViewItem, OnToggleChecked } from '../types'; import { SearchCheckbox } from './SearchCheckbox'; export interface Props { - item: DashboardSectionItem; + item: DashboardViewItem; + isSelected?: boolean; editable?: boolean; onTagSelected: (name: string) => any; onToggleChecked?: OnToggleChecked; @@ -30,7 +32,7 @@ const getIconFromMeta = (meta = ''): IconName => { }; /** @deprecated */ -export const SearchItem: FC = ({ item, editable, onToggleChecked, onTagSelected, onClickItem }) => { +export const SearchItem: FC = ({ item, isSelected, editable, onToggleChecked, onTagSelected, onClickItem }) => { const styles = useStyles2(getStyles); const tagSelected = useCallback( (tag: string, event: React.MouseEvent) => { @@ -53,7 +55,6 @@ export const SearchItem: FC = ({ item, editable, onToggleChecked, onTagSe [item, onToggleChecked] ); - const folderTitle = item.folderTitle || 'General'; return ( = ({ item, editable, onToggleChecked, onTagSe - - {folderTitle} + + {item.parentTitle || 'General'} + {item.sortMetaName && ( @@ -84,7 +86,7 @@ export const SearchItem: FC = ({ item, editable, onToggleChecked, onTagSe )} - `Filter by tag "${tag}"`} /> + `Filter by tag "${tag}"`} /> ); diff --git a/public/app/features/search/page/components/FolderSection.test.tsx b/public/app/features/search/page/components/FolderSection.test.tsx index 9359618cc8e..025e939d508 100644 --- a/public/app/features/search/page/components/FolderSection.test.tsx +++ b/public/app/features/search/page/components/FolderSection.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { ArrayVector, DataFrame, DataFrameView, FieldType } from '@grafana/data'; import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service'; -import { DashboardSearchItemType } from '../../types'; +import { DashboardSearchItemType, DashboardViewItem } from '../../types'; import { FolderSection } from './FolderSection'; @@ -14,8 +14,8 @@ describe('FolderSection', () => { const mockOnTagSelected = jest.fn(); const mockSelectionToggle = jest.fn(); const mockSelection = jest.fn(); - const mockSection = { - kind: 'folder', + const mockSection: DashboardViewItem = { + kind: 'folder' as const, uid: 'my-folder', title: 'My folder', }; @@ -218,7 +218,7 @@ describe('FolderSection', () => { }); describe('when in a pseudo-folder (i.e. Starred/Recent)', () => { - const mockRecentSection = { + const mockRecentSection: DashboardViewItem = { kind: 'folder', uid: '__recent', title: 'Recent', diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx index fce4dc6b022..f40efa93db8 100644 --- a/public/app/features/search/page/components/FolderSection.tsx +++ b/public/app/features/search/page/components/FolderSection.tsx @@ -1,35 +1,26 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import React, { useCallback } from 'react'; import { useAsync, useLocalStorage } from 'react-use'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, toIconName } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Card, Checkbox, CollapsableSection, Icon, IconName, Spinner, useStyles2 } from '@grafana/ui'; +import { Card, Checkbox, CollapsableSection, Icon, Spinner, useStyles2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { getSectionStorageKey } from 'app/features/search/utils'; import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId'; import { SearchItem } from '../..'; import { getGrafanaSearcher, SearchQuery } from '../../service'; -import { DashboardSearchItemType, DashboardSectionItem } from '../../types'; +import { queryResultToViewItem } from '../../service/utils'; +import { DashboardViewItem } from '../../types'; import { SelectionChecker, SelectionToggle } from '../selection'; -export interface DashboardSection { - kind: string; // folder | query! - uid: string; - title: string; - selected?: boolean; // not used ? keyboard - url?: string; - icon?: IconName; - itemsUIDs?: string[]; // for pseudo folders -} - interface SectionHeaderProps { selection?: SelectionChecker; selectionToggle?: SelectionToggle; onClickItem?: (e: React.MouseEvent) => void; onTagSelected: (tag: string) => void; - section: DashboardSection; + section: DashboardViewItem; renderStandaloneBody?: boolean; // render the body on its own tags?: string[]; } @@ -44,20 +35,13 @@ export const FolderSection = ({ tags, }: SectionHeaderProps) => { const editable = selectionToggle != null; - const styles = useStyles2( - useCallback( - (theme: GrafanaTheme2) => getSectionHeaderStyles(theme, section.selected, editable), - [section.selected, editable] - ) - ); + const styles = useStyles2(useCallback((theme: GrafanaTheme2) => getSectionHeaderStyles(theme, editable), [editable])); const [sectionExpanded, setSectionExpanded] = useLocalStorage(getSectionStorageKey(section.title), false); const results = useAsync(async () => { if (!sectionExpanded && !renderStandaloneBody) { return Promise.resolve([]); } - let folderUid: string | undefined = section.uid; - let folderTitle: string | undefined = section.title; let query: SearchQuery = { query: '*', kind: ['dashboard'], @@ -69,24 +53,11 @@ export const FolderSection = ({ query = { uid: section.itemsUIDs, // array of UIDs }; - folderUid = undefined; - folderTitle = undefined; } const raw = await getGrafanaSearcher().search({ ...query, tags }); - const v = raw.view.map((item) => ({ - uid: item.uid, - title: item.name, - url: item.url, - uri: item.url, - type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB, - id: 666, // do not use me! - isStarred: false, - tags: item.tags ?? [], - folderUid: folderUid || item.location, - folderTitle: folderTitle || raw.view.dataFrame.meta?.custom?.locationInfo[item.location].name, - })); - return v; + const items = raw.view.map((v) => queryResultToViewItem(v, raw.view)); + return items; }, [sectionExpanded, tags]); const onSectionExpand = () => { @@ -111,7 +82,7 @@ export const FolderSection = ({ const id = useUniqueId(); const labelId = `section-header-label-${id}`; - let icon = section.icon; + let icon = toIconName(section.icon ?? ''); if (!icon) { icon = sectionExpanded ? 'folder-open' : 'folder'; } @@ -127,18 +98,11 @@ export const FolderSection = ({ ); } - return results.value.map((v) => { - if (selection && selectionToggle) { - const type = v.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard'; - v = { - ...v, - checked: selection(type, v.uid!), - }; - } + return results.value.map((item) => { return ( { if (selectionToggle) { @@ -147,6 +111,7 @@ export const FolderSection = ({ }} editable={Boolean(selection != null)} onClickItem={onClickItem} + isSelected={selectionToggle && selection?.(item.kind, item.uid)} /> ); }); @@ -203,34 +168,30 @@ export const FolderSection = ({ ); }; -const getSectionHeaderStyles = (theme: GrafanaTheme2, selected = false, editable: boolean) => { +const getSectionHeaderStyles = (theme: GrafanaTheme2, editable: boolean) => { const sm = theme.spacing(1); return { - wrapper: cx( - css` - align-items: center; - font-size: ${theme.typography.size.base}; - padding: 12px; - border-bottom: none; - color: ${theme.colors.text.secondary}; - z-index: 1; + wrapper: css` + align-items: center; + font-size: ${theme.typography.size.base}; + padding: 12px; + border-bottom: none; + color: ${theme.colors.text.secondary}; + z-index: 1; - &:hover, - &.selected { - color: ${theme.colors.text}; - } + &:hover, + &.selected { + color: ${theme.colors.text}; + } - &:hover, - &:focus-visible, - &:focus-within { - a { - opacity: 1; - } + &:hover, + &:focus-visible, + &:focus-within { + a { + opacity: 1; } - `, - 'pointer', - { selected } - ), + } + `, sectionItems: css` margin: 0 24px 0 32px; `, diff --git a/public/app/features/search/page/components/FolderView.tsx b/public/app/features/search/page/components/FolderView.tsx index 0cce5f0ac62..b68bc0ab1a4 100644 --- a/public/app/features/search/page/components/FolderView.tsx +++ b/public/app/features/search/page/components/FolderView.tsx @@ -11,9 +11,11 @@ import { contextSrv } from '../../../../core/services/context_srv'; import impressionSrv from '../../../../core/services/impression_srv'; import { GENERAL_FOLDER_UID } from '../../constants'; import { getGrafanaSearcher } from '../../service'; +import { queryResultToViewItem } from '../../service/utils'; +import { DashboardViewItem } from '../../types'; import { SearchResultsProps } from '../components/SearchResultsTable'; -import { DashboardSection, FolderSection } from './FolderSection'; +import { FolderSection } from './FolderSection'; type Props = Pick & { tags?: string[]; @@ -31,38 +33,33 @@ export const FolderView = ({ const styles = useStyles2(getStyles); const results = useAsync(async () => { - const folders: DashboardSection[] = []; + const folders: DashboardViewItem[] = []; if (!hidePseudoFolders) { if (contextSrv.isSignedIn) { const stars = await getBackendSrv().get('api/user/stars'); if (stars.length > 0) { - folders.push({ title: 'Starred', icon: 'star', kind: 'query-star', uid: '__starred', itemsUIDs: stars }); + folders.push({ title: 'Starred', icon: 'star', kind: 'folder', uid: '__starred', itemsUIDs: stars }); } } const itemsUIDs = await impressionSrv.getDashboardOpened(); if (itemsUIDs.length) { - folders.push({ title: 'Recent', icon: 'clock-nine', kind: 'query-recent', uid: '__recent', itemsUIDs }); + folders.push({ title: 'Recent', icon: 'clock-nine', kind: 'folder', uid: '__recent', itemsUIDs }); } } folders.push({ title: 'General', url: '/dashboards', kind: 'folder', uid: GENERAL_FOLDER_UID }); const searcher = getGrafanaSearcher(); - const rsp = await searcher.search({ + const results = await searcher.search({ query: '*', kind: ['folder'], sort: searcher.getFolderViewSort(), limit: 1000, }); - for (const row of rsp.view) { - folders.push({ - title: row.name, - url: row.url, - uid: row.uid, - kind: row.kind, - }); + for (const row of results.view) { + folders.push(queryResultToViewItem(row, results.view)); } return folders; diff --git a/public/app/features/search/page/components/SearchResultsCards.tsx b/public/app/features/search/page/components/SearchResultsCards.tsx index e199b1e4618..3b6d9791fd4 100644 --- a/public/app/features/search/page/components/SearchResultsCards.tsx +++ b/public/app/features/search/page/components/SearchResultsCards.tsx @@ -9,8 +9,7 @@ import { useStyles2 } from '@grafana/ui'; import { SearchItem } from '../../components/SearchItem'; import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection'; -import { SearchResultMeta } from '../../service'; -import { DashboardSearchItemType, DashboardSectionItem } from '../../types'; +import { queryResultToViewItem } from '../../service/utils'; import { SearchResultsProps } from './SearchResultsTable'; @@ -42,45 +41,18 @@ export const SearchResultsCards = React.memo( const RenderRow = useCallback( ({ index: rowIndex, style }: { index: number; style: CSSProperties }) => { - const meta = response.view.dataFrame.meta?.custom as SearchResultMeta; - let className = ''; if (rowIndex === highlightIndex.y) { className += ' ' + styles.selectedRow; } const item = response.view.get(rowIndex); - let v: DashboardSectionItem = { - uid: item.uid, - title: item.name, - url: item.url, - uri: item.url, - type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB, - id: 666, // do not use me! - isStarred: false, - tags: item.tags ?? [], - }; + const searchItem = queryResultToViewItem(item, response.view); - if (item.location) { - const first = item.location.split('/')[0]; - const finfo = meta.locationInfo[first]; - if (finfo) { - v.folderUid = item.location; - v.folderTitle = finfo.name; - } - } - - if (selection && selectionToggle) { - const type = v.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard'; - v = { - ...v, - checked: selection(type, v.uid!), - }; - } return (
{ if (selectionToggle) { @@ -89,6 +61,7 @@ export const SearchResultsCards = React.memo( }} editable={Boolean(selection != null)} onClickItem={onClickItem} + isSelected={selectionToggle && selection?.(searchItem.kind, searchItem.uid)} />
); diff --git a/public/app/features/search/page/components/SearchResultsGrid.tsx b/public/app/features/search/page/components/SearchResultsGrid.tsx index d063789c960..037fe739886 100644 --- a/public/app/features/search/page/components/SearchResultsGrid.tsx +++ b/public/app/features/search/page/components/SearchResultsGrid.tsx @@ -9,7 +9,8 @@ import { useStyles2 } from '@grafana/ui'; import { SearchCard } from '../../components/SearchCard'; import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection'; -import { DashboardSearchItemType, DashboardSectionItem } from '../../types'; +import { queryResultToViewItem } from '../../service/utils'; +import { DashboardViewItem } from '../../types'; import { SearchResultsProps } from './SearchResultsTable'; @@ -28,11 +29,9 @@ export const SearchResultsGrid = ({ // Hacked to reuse existing SearchCard (and old DashboardSectionItem) const itemProps = { editable: selection != null, - onToggleChecked: (item: any) => { - const d = item as DashboardSectionItem; - const t = d.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard'; + onToggleChecked: (item: DashboardViewItem) => { if (selectionToggle) { - selectionToggle(t, d.uid!); + selectionToggle(item.kind, item.uid); } }, onTagSelected, @@ -77,17 +76,7 @@ export const SearchResultsGrid = ({ const item = view.get(index); const kind = item.kind ?? 'dashboard'; - const facade: DashboardSectionItem = { - uid: item.uid, - title: item.name, - url: item.url, - uri: item.url, - type: kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB, - id: 666, // do not use me! - isStarred: false, - tags: item.tags ?? [], - checked: selection ? selection(kind, item.uid) : false, - }; + const facade = queryResultToViewItem(item, view); if (kind === 'panel') { const type = item.panel_type; @@ -112,7 +101,12 @@ export const SearchResultsGrid = ({ // And without this wrapper there is no room for that margin return item ? (
  • - +
  • ) : null; }} diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index f9a709057e1..18eaacd3039 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -17,6 +17,7 @@ import { PluginIconName } from 'app/features/plugins/admin/types'; import { ShowModalReactEvent } from 'app/types/events'; import { QueryResponse, SearchResultMeta } from '../../service'; +import { getIconForKind } from '../../service/utils'; import { SelectionChecker, SelectionToggle } from '../selection'; import { ExplainScorePopup } from './ExplainScorePopup'; @@ -249,16 +250,6 @@ export const generateColumns = ( return columns; }; -function getIconForKind(v: string): IconName { - if (v === 'dashboard') { - return 'apps'; - } - if (v === 'folder') { - return 'folder'; - } - return 'question-circle'; -} - function hasValue(f: Field): boolean { for (let i = 0; i < f.values.length; i++) { if (f.values.get(i) != null) { diff --git a/public/app/features/search/service/sql.ts b/public/app/features/search/service/sql.ts index 259ba392c30..c5c57502eed 100644 --- a/public/app/features/search/service/sql.ts +++ b/public/app/features/search/service/sql.ts @@ -148,7 +148,7 @@ export class SQLSearcher implements GrafanaSearcher { const k = hit.type === 'dash-folder' ? 'folder' : 'dashboard'; kind.push(k); name.push(hit.title); - uid.push(hit.uid!); + uid.push(hit.uid); url.push(hit.url); tags.push(hit.tags); sortBy.push(hit.sortMeta!); @@ -171,7 +171,7 @@ export class SQLSearcher implements GrafanaSearcher { folderId: hit.folderId, }; } else if (k === 'folder') { - this.locationInfo[hit.uid!] = { + this.locationInfo[hit.uid] = { kind: k, name: hit.title!, url: hit.url, diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index 582ddf8cc9b..4b4fd0414d8 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -40,6 +40,9 @@ export interface DashboardQueryResult { // debugging fields score: number; explain: {}; + + // enterprise sends extra properties through for sorting (views, errors, etc) + [key: string]: unknown; } export interface LocationInfo { diff --git a/public/app/features/search/service/utils.ts b/public/app/features/search/service/utils.ts index 2d080d4f2a9..083ddc613e1 100644 --- a/public/app/features/search/service/utils.ts +++ b/public/app/features/search/service/utils.ts @@ -1,6 +1,9 @@ +import { DataFrameView, IconName } from '@grafana/data'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { SearchQuery } from './types'; +import { DashboardViewItem } from '../types'; + +import { DashboardQueryResult, SearchQuery, SearchResultMeta } from './types'; /** prepare the query replacing folder:current */ export async function replaceCurrentFolderQuery(query: SearchQuery): Promise { @@ -34,3 +37,54 @@ async function getCurrentFolderUID(): Promise { function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export function getIconForKind(kind: string): IconName { + if (kind === 'dashboard') { + return 'apps'; + } + + if (kind === 'folder') { + return 'folder'; + } + + return 'question-circle'; +} + +export function queryResultToViewItem( + item: DashboardQueryResult, + view?: DataFrameView, + index = -1 +): DashboardViewItem { + const meta = view?.dataFrame.meta?.custom as SearchResultMeta | undefined; + + const viewItem: DashboardViewItem = { + kind: 'dashboard', + uid: item.uid, + title: item.name, + url: item.url, + tags: item.tags ?? [], + }; + + // Set enterprise sort value property + const sortFieldName = meta?.sortBy; + if (sortFieldName) { + console.log('have sortFieldName', sortFieldName); + const sortFieldValue = item[sortFieldName]; + if (typeof sortFieldValue === 'string' || typeof sortFieldValue === 'number') { + viewItem.sortMetaName = sortFieldName; + viewItem.sortMeta = sortFieldValue; + } + } + + if (item.location) { + const ancestors = item.location.split('/'); + const parentUid = ancestors[ancestors.length - 1]; + const parentInfo = meta?.locationInfo[parentUid]; + if (parentInfo) { + viewItem.parentTitle = parentInfo.name; + viewItem.parentKind = parentInfo.kind; + } + } + + return viewItem; +} diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index ac8f930f094..e2d8e209977 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -11,59 +11,67 @@ export enum DashboardSearchItemType { } /** - * @deprecated + * @deprecated Use DashboardSearchItem and use UIDs instead of IDs + * DTO type for search API result items, but with deprecated IDs + * This type was previously also used heavily for views, so contains lots of + * extraneous properties */ -export interface DashboardSection { - id?: number; - uid?: string; - title: string; - expanded?: boolean; - url: string; - icon?: string; - score?: number; - checked?: boolean; - items: DashboardSectionItem[]; - toggle?: (section: DashboardSection) => Promise; - selected?: boolean; - type: DashboardSearchItemType; - slug?: string; - itemsFetching?: boolean; -} - -/** - * @deprecated - */ -export interface DashboardSectionItem { - checked?: boolean; +export interface DashboardSearchHit extends WithAccessControlMetadata { folderId?: number; folderTitle?: string; folderUid?: string; folderUrl?: string; id?: number; - isStarred: boolean; - selected?: boolean; tags: string[]; title: string; type: DashboardSearchItemType; - icon?: string; // used for grid view - uid?: string; - uri: string; + uid: string; url: string; sortMeta?: number; sortMetaName?: string; } /** - * @deprecated - It uses dashboard ID which is deprecated in favor of dashboard UID. Please, use DashboardSearchItem instead. + * DTO type for search API result items + * This should not be used directly - use GrafanaSearcher instead and get a DashboardQueryResult */ -export interface DashboardSearchHit extends DashboardSectionItem, DashboardSection, WithAccessControlMetadata {} - -export interface DashboardSearchItem - extends Omit< - DashboardSearchHit, - 'id' | 'uid' | 'expanded' | 'selected' | 'checked' | 'folderId' | 'icon' | 'sortMeta' | 'sortMetaName' - > { +export interface DashboardSearchItem { uid: string; + title: string; + uri: string; + url: string; + type: string; // dash-db, dash-home + tags: string[]; + isStarred: boolean; + + // Only on dashboards in folders results + folderUid?: string; + folderTitle?: string; + folderUrl?: string; +} + +/** + * Type used in the folder view components + */ +export interface DashboardViewItem { + kind: 'folder' | 'dashboard' | 'panel'; + uid: string; + title: string; + url?: string; + tags?: string[]; + + icon?: string; + + // Most commonly parent folder title, but can be dashboard if panelTitleSearch is enabled + parentTitle?: string; + parentKind?: string; + + // Used only for psuedo-folders, such as Starred or Recent + itemsUIDs?: string[]; + + // For enterprise sort options + sortMeta?: number | string; // value sorted by + sortMetaName?: string; // name of the value being sorted e.g. 'Views' } export interface SearchAction extends Action { @@ -88,7 +96,7 @@ export interface SearchState { eventTrackingNamespace: EventTrackingNamespace; } -export type OnToggleChecked = (item: DashboardSectionItem | DashboardSection) => void; +export type OnToggleChecked = (item: DashboardViewItem) => void; export enum SearchLayout { List = 'list', diff --git a/public/app/plugins/panel/dashlist/DashList.tsx b/public/app/plugins/panel/dashlist/DashList.tsx index 6c9515667c9..5b16d30b2c5 100644 --- a/public/app/plugins/panel/dashlist/DashList.tsx +++ b/public/app/plugins/panel/dashlist/DashList.tsx @@ -170,7 +170,7 @@ export function DashList(props: PanelProps) {
      {dashboards.map((dash) => (
    • - +
    • ))}