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
This commit is contained in:
Josh Hunt 2023-02-23 16:52:54 +01:00 committed by GitHub
parent a48793b542
commit 0c36b247af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 199 additions and 228 deletions

View File

@ -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"],

View File

@ -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' } });

View File

@ -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();

View File

@ -59,7 +59,6 @@ describe('resolveLinks', () => {
title: 'DashLinks',
url: '/d/6ieouugGk/DashLinks',
isStarred: false,
items: [],
tags: [],
uri: 'db/DashLinks',
type: DashboardSearchItemType.DashDB,

View File

@ -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,

View File

@ -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<HTMLAnchorElement>) => 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<string | null>(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
</div>
<div className={styles.info}>
<div className={styles.title}>{item.title}</div>
<TagList displayMax={1} tags={item.tags} onClick={onTagClick} />
<TagList displayMax={1} tags={item.tags ?? []} onClick={onTagClick} />
</div>
{showExpandedView && (
<Portal className={styles.portal}>

View File

@ -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<HTMLAnchorElement>) => 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 (
<a className={classNames(className, styles.card)} key={item.uid} href={item.url} onClick={onClick}>
@ -66,7 +66,7 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l
)}
</div>
<div>
<TagList className={styles.tagList} tags={item.tags} />
<TagList className={styles.tagList} tags={item.tags ?? []} />
</div>
</div>
</a>

View File

@ -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<Props>) => {
@ -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();
});

View File

@ -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<Props> = ({ item, editable, onToggleChecked, onTagSelected, onClickItem }) => {
export const SearchItem: FC<Props> = ({ item, isSelected, editable, onToggleChecked, onTagSelected, onClickItem }) => {
const styles = useStyles2(getStyles);
const tagSelected = useCallback(
(tag: string, event: React.MouseEvent<HTMLElement>) => {
@ -53,7 +55,6 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
[item, onToggleChecked]
);
const folderTitle = item.folderTitle || 'General';
return (
<Card
data-testid={selectors.dashboardItem(item.title)}
@ -67,15 +68,16 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
<SearchCheckbox
aria-label="Select dashboard"
editable={editable}
checked={item.checked}
checked={isSelected}
onClick={handleCheckboxClick}
/>
</Card.Figure>
<Card.Meta separator={''}>
<span className={styles.metaContainer}>
<Icon name={'folder'} aria-hidden />
{folderTitle}
<Icon name={getIconForKind(item.parentKind ?? 'folder')} aria-hidden />
{item.parentTitle || 'General'}
</span>
{item.sortMetaName && (
<span className={styles.metaContainer}>
<Icon name={getIconFromMeta(item.sortMetaName)} />
@ -84,7 +86,7 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
)}
</Card.Meta>
<Card.Tags>
<TagList tags={item.tags} onClick={tagSelected} getAriaLabel={(tag) => `Filter by tag "${tag}"`} />
<TagList tags={item.tags ?? []} onClick={tagSelected} getAriaLabel={(tag) => `Filter by tag "${tag}"`} />
</Card.Tags>
</Card>
);

View File

@ -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',

View File

@ -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<HTMLElement>) => 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<DashboardSectionItem>((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 (
<SearchItem
key={v.uid}
item={v}
key={item.uid}
item={item}
onTagSelected={onTagSelected}
onToggleChecked={(item) => {
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;
`,

View File

@ -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<SearchResultsProps, 'selection' | 'selectionToggle' | 'onTagSelected' | 'onClickItem'> & {
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;

View File

@ -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 (
<div style={style} key={item.uid} className={className} role="row">
<SearchItem
item={v}
item={searchItem}
onTagSelected={onTagSelected}
onToggleChecked={(item) => {
if (selectionToggle) {
@ -89,6 +61,7 @@ export const SearchResultsCards = React.memo(
}}
editable={Boolean(selection != null)}
onClickItem={onClickItem}
isSelected={selectionToggle && selection?.(searchItem.kind, searchItem.uid)}
/>
</div>
);

View File

@ -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 ? (
<li style={style} className={className}>
<SearchCard key={item.uid} {...itemProps} item={facade} />
<SearchCard
key={item.uid}
{...itemProps}
item={facade}
isSelected={selection ? selection(facade.kind, facade.uid) : false}
/>
</li>
) : null;
}}

View File

@ -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) {

View File

@ -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,

View File

@ -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 {

View File

@ -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<SearchQuery> {
@ -34,3 +37,54 @@ async function getCurrentFolderUID(): Promise<string | undefined> {
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<DashboardQueryResult>,
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;
}

View File

@ -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<DashboardSection>;
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',

View File

@ -170,7 +170,7 @@ export function DashList(props: PanelProps<PanelOptions>) {
<ul className={css.gridContainer}>
{dashboards.map((dash) => (
<li key={dash.uid}>
<SearchCard item={dash} />
<SearchCard item={{ ...dash, kind: 'folder' }} />
</li>
))}
</ul>