NestedFolders: Basic item selection (#66843)

* NestedFolders: Item selection state

* Select children in state

* Unselect parents when children are unselected

* remove unneeded comment

* tests

* tidy test a little bit

* tidy test a little bit

* tidy test a little bit

* tidy test a little bit

* tidy test a little bit
This commit is contained in:
Josh Hunt 2023-04-19 20:44:07 +01:00 committed by GitHub
parent 3634079b8f
commit e0c5b4f0e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 462 additions and 42 deletions

View File

@ -269,6 +269,12 @@ export const Pages = {
interval: 'Playlist interval',
itemDelete: 'Delete playlist item',
},
BrowseDashbards: {
table: {
row: (uid: string) => `data-testid ${uid} row`,
checkbox: (uid: string) => `data-testid ${uid} checkbox`,
},
},
Search: {
url: '/?search=openn',
FolderView: {

View File

@ -0,0 +1,139 @@
import { getByLabelText, render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Router } from 'react-router-dom';
import { selectors } from '@grafana/e2e-selectors';
import { locationService } from '@grafana/runtime';
import { wellFormedTree } from '../fixtures/dashboardsTreeItem.fixture';
import { BrowseView } from './BrowseView';
const [mockTree, { folderA, folderA_folderA, folderA_folderB, folderA_folderB_dashbdB, dashbdD }] = wellFormedTree();
function render(...args: Parameters<typeof rtlRender>) {
const [ui, options] = args;
rtlRender(<Router history={locationService.getHistory()}>{ui}</Router>, options);
}
jest.mock('app/features/search/service/folders', () => {
return {
getFolderChildren(parentUID?: string) {
const childrenForUID = mockTree
.filter((v) => v.item.kind !== 'ui-empty-folder' && v.item.parentUID === parentUID)
.map((v) => v.item);
return Promise.resolve(childrenForUID);
},
};
});
describe('browse-dashboards BrowseView', () => {
const WIDTH = 800;
const HEIGHT = 600;
it('expands and collapses a folder', async () => {
render(<BrowseView folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
await expandFolder(folderA.item.uid);
expect(screen.queryByText(folderA_folderA.item.title)).toBeInTheDocument();
await collapseFolder(folderA.item.uid);
expect(screen.queryByText(folderA_folderA.item.title)).not.toBeInTheDocument();
});
it('checks items when selected', async () => {
render(<BrowseView folderUID={undefined} width={WIDTH} height={HEIGHT} />);
const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashbdD.item.uid));
expect(checkbox).not.toBeChecked();
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
});
it('checks all descendants when a folder is selected', async () => {
render(<BrowseView folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
// First expand then click folderA
await expandFolder(folderA.item.uid);
await clickCheckbox(folderA.item.uid);
// All the visible items in it should be checked now
const directChildren = mockTree.filter(
(v) => v.item.kind !== 'ui-empty-folder' && v.item.parentUID === folderA.item.uid
);
for (const child of directChildren) {
const childCheckbox = screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(child.item.uid));
expect(childCheckbox).toBeChecked();
}
});
it('checks descendants loaded after a folder is selected', async () => {
render(<BrowseView folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
// First expand then click folderA
await expandFolder(folderA.item.uid);
await clickCheckbox(folderA.item.uid);
// When additional children are loaded (by expanding a folder), those items
// should also be selected
await expandFolder(folderA_folderB.item.uid);
const grandchildren = mockTree.filter(
(v) => v.item.kind !== 'ui-empty-folder' && v.item.parentUID === folderA_folderB.item.uid
);
for (const child of grandchildren) {
const childCheckbox = screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(child.item.uid));
expect(childCheckbox).toBeChecked();
}
});
it('unchecks ancestors when unselecting an item', async () => {
render(<BrowseView folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
await expandFolder(folderA.item.uid);
await expandFolder(folderA_folderB.item.uid);
await clickCheckbox(folderA.item.uid);
await clickCheckbox(folderA_folderB_dashbdB.item.uid);
const itemCheckbox = screen.queryByTestId(
selectors.pages.BrowseDashbards.table.checkbox(folderA_folderB_dashbdB.item.uid)
);
expect(itemCheckbox).not.toBeChecked();
const parentCheckbox = screen.queryByTestId(
selectors.pages.BrowseDashbards.table.checkbox(folderA_folderB.item.uid)
);
expect(parentCheckbox).not.toBeChecked();
const grandparentCheckbox = screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(folderA.item.uid));
expect(grandparentCheckbox).not.toBeChecked();
});
});
async function expandFolder(uid: string) {
const row = screen.getByTestId(selectors.pages.BrowseDashbards.table.row(uid));
const expandButton = getByLabelText(row, 'Expand folder');
await userEvent.click(expandButton);
}
async function collapseFolder(uid: string) {
const row = screen.getByTestId(selectors.pages.BrowseDashbards.table.row(uid));
const expandButton = getByLabelText(row, 'Collapse folder');
await userEvent.click(expandButton);
}
async function clickCheckbox(uid: string) {
const checkbox = screen.getByTestId(selectors.pages.BrowseDashbards.table.checkbox(uid));
await userEvent.click(checkbox);
}

View File

@ -1,7 +1,8 @@
import produce from 'immer';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { getFolderChildren } from 'app/features/search/service/folders';
import { DashboardViewItem } from 'app/features/search/types';
import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
import { DashboardsTreeItem } from '../types';
@ -16,19 +17,45 @@ interface BrowseViewProps {
export function BrowseView({ folderUID, width, height }: BrowseViewProps) {
const [openFolders, setOpenFolders] = useState<Record<string, boolean>>({ [folderUID ?? '$$root']: true });
const [selectedItems, setSelectedItems] = useState<
Record<DashboardViewItemKind, Record<string, boolean | undefined>>
>({
folder: {},
dashboard: {},
panel: {},
});
// Rather than storing an actual tree structure (requiring traversing the tree to update children), instead
// we keep track of children for each UID and then later combine them in the format required to display them
const [childrenByUID, setChildrenByUID] = useState<Record<string, DashboardViewItem[] | undefined>>({});
async function loadChildrenForUID(uid: string | undefined) {
const folderKey = uid ?? '$$root';
const loadChildrenForUID = useCallback(
async (uid: string | undefined) => {
const folderKey = uid ?? '$$root';
const childItems = await getFolderChildren(uid, undefined, true);
setChildrenByUID((v) => ({ ...v, [folderKey]: childItems }));
}
const childItems = await getFolderChildren(uid, undefined, true);
setChildrenByUID((v) => ({ ...v, [folderKey]: childItems }));
// If the parent is already selected, mark these items as selected also
const parentIsSelected = selectedItems.folder[folderKey];
if (parentIsSelected) {
setSelectedItems((currentState) =>
produce(currentState, (draft) => {
for (const child of childItems) {
draft[child.kind][child.uid] = true;
}
})
);
}
},
[selectedItems]
);
useEffect(() => {
loadChildrenForUID(folderUID);
// No need to depend on loadChildrenForUID - we only want this to run
// when folderUID changes (initial page view)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [folderUID]);
const flatTree = useMemo(
@ -36,15 +63,66 @@ export function BrowseView({ folderUID, width, height }: BrowseViewProps) {
[folderUID, childrenByUID, openFolders]
);
const handleFolderClick = useCallback((uid: string, folderIsOpen: boolean) => {
if (folderIsOpen) {
loadChildrenForUID(uid);
}
const handleFolderClick = useCallback(
(uid: string, newState: boolean) => {
if (newState) {
loadChildrenForUID(uid);
}
setOpenFolders((v) => ({ ...v, [uid]: folderIsOpen }));
}, []);
setOpenFolders((old) => ({ ...old, [uid]: newState }));
},
[loadChildrenForUID]
);
return <DashboardsTree items={flatTree} width={width} height={height} onFolderClick={handleFolderClick} />;
const handleItemSelectionChange = useCallback(
(item: DashboardViewItem, newState: boolean) => {
// Recursively set selection state for this item and all descendants
setSelectedItems((old) =>
produce(old, (draft) => {
function markChildren(kind: DashboardViewItemKind, uid: string) {
draft[kind][uid] = newState;
if (kind !== 'folder') {
return;
}
let children = childrenByUID[uid] ?? [];
for (const child of children) {
markChildren(child.kind, child.uid);
}
}
markChildren(item.kind, item.uid);
// If we're unselecting an item, unselect all ancestors also
if (!newState) {
let nextParentUID = item.parentUID;
while (nextParentUID) {
const parent = findItem(childrenByUID, nextParentUID);
if (!parent) {
break;
}
draft[parent.kind][parent.uid] = false;
nextParentUID = parent.parentUID;
}
}
})
);
},
[childrenByUID]
);
return (
<DashboardsTree
items={flatTree}
width={width}
height={height}
selectedItems={selectedItems}
onFolderClick={handleFolderClick}
onItemSelectionChange={handleItemSelectionChange}
/>
);
}
// Creates a flat list of items, with nested children indicated by its increasing level
@ -54,17 +132,22 @@ function createFlatTree(
openFolders: Record<string, boolean>,
level = 0
): DashboardsTreeItem[] {
function mapItem(item: DashboardViewItem, level: number): DashboardsTreeItem[] {
function mapItem(item: DashboardViewItem, parentUID: string | undefined, level: number): DashboardsTreeItem[] {
const mappedChildren = createFlatTree(item.uid, childrenByUID, openFolders, level + 1);
const isOpen = Boolean(openFolders[item.uid]);
const emptyFolder = childrenByUID[item.uid]?.length === 0;
if (isOpen && emptyFolder) {
mappedChildren.push({ isOpen: false, level: level + 1, item: { kind: 'ui-empty-folder' } });
mappedChildren.push({
isOpen: false,
level: level + 1,
item: { kind: 'ui-empty-folder', uid: item.uid + '-empty-folder' },
});
}
const thisItem = {
item,
parentUID,
level,
isOpen,
};
@ -76,5 +159,25 @@ function createFlatTree(
const isOpen = Boolean(openFolders[folderKey]);
const items = (isOpen && childrenByUID[folderKey]) || [];
return items.flatMap((item) => mapItem(item, level));
return items.flatMap((item) => mapItem(item, rootFolderUID, level));
}
function findItem(
childrenByUID: Record<string, DashboardViewItem[] | undefined>,
uid: string
): DashboardViewItem | undefined {
for (const parentUID in childrenByUID) {
const children = childrenByUID[parentUID];
if (!children) {
continue;
}
for (const child of children) {
if (child.uid === uid) {
return child;
}
}
}
return undefined;
}

View File

@ -19,25 +19,58 @@ describe('browse-dashboards DashboardsTree', () => {
const WIDTH = 800;
const HEIGHT = 600;
const folder = wellFormedFolder();
const folder = wellFormedFolder(1);
const emptyFolderIndicator = wellFormedEmptyFolder();
const dashboard = wellFormedDashboard();
const dashboard = wellFormedDashboard(2);
const noop = () => {};
const selectedItems = {
folder: {},
dashboard: {},
panel: {},
};
it('renders a dashboard item', () => {
render(<DashboardsTree items={[dashboard]} width={WIDTH} height={HEIGHT} onFolderClick={() => {}} />);
render(
<DashboardsTree
items={[dashboard]}
selectedItems={selectedItems}
width={WIDTH}
height={HEIGHT}
onFolderClick={noop}
onItemSelectionChange={noop}
/>
);
expect(screen.queryByText(dashboard.item.title)).toBeInTheDocument();
expect(screen.queryByText('Dashboard')).toBeInTheDocument();
});
it('renders a folder item', () => {
render(<DashboardsTree items={[folder]} width={WIDTH} height={HEIGHT} onFolderClick={() => {}} />);
render(
<DashboardsTree
items={[folder]}
selectedItems={selectedItems}
width={WIDTH}
height={HEIGHT}
onFolderClick={noop}
onItemSelectionChange={noop}
/>
);
expect(screen.queryByText(folder.item.title)).toBeInTheDocument();
expect(screen.queryByText('Folder')).toBeInTheDocument();
});
it('calls onFolderClick when a folder button is clicked', async () => {
const handler = jest.fn();
render(<DashboardsTree items={[folder]} width={WIDTH} height={HEIGHT} onFolderClick={handler} />);
render(
<DashboardsTree
items={[folder]}
selectedItems={selectedItems}
width={WIDTH}
height={HEIGHT}
onFolderClick={handler}
onItemSelectionChange={noop}
/>
);
const folderButton = screen.getByLabelText('Collapse folder');
await userEvent.click(folderButton);
@ -45,7 +78,16 @@ describe('browse-dashboards DashboardsTree', () => {
});
it('renders empty folder indicators', () => {
render(<DashboardsTree items={[emptyFolderIndicator]} width={WIDTH} height={HEIGHT} onFolderClick={() => {}} />);
render(
<DashboardsTree
items={[emptyFolderIndicator]}
selectedItems={selectedItems}
width={WIDTH}
height={HEIGHT}
onFolderClick={noop}
onItemSelectionChange={noop}
/>
);
expect(screen.queryByText('Empty folder')).toBeInTheDocument();
expect(screen.queryByText(emptyFolderIndicator.item.kind)).not.toBeInTheDocument();
});

View File

@ -4,9 +4,11 @@ import { CellProps, Column, TableInstance, useTable } from 'react-table';
import { FixedSizeList as List } from 'react-window';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Checkbox, useStyles2 } from '@grafana/ui';
import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
import { DashboardsTreeItem, INDENT_AMOUNT_CSS_VAR } from '../types';
import { DashboardsTreeItem, DashboardTreeSelection, INDENT_AMOUNT_CSS_VAR } from '../types';
import { NameCell } from './NameCell';
import { TypeCell } from './TypeCell';
@ -15,28 +17,56 @@ interface DashboardsTreeProps {
items: DashboardsTreeItem[];
width: number;
height: number;
selectedItems: DashboardTreeSelection;
onFolderClick: (uid: string, newOpenState: boolean) => void;
onItemSelectionChange: (item: DashboardViewItem, newState: boolean) => void;
}
type DashboardsTreeColumn = Column<DashboardsTreeItem>;
type DashboardsTreeCellProps = CellProps<DashboardsTreeItem, unknown> & {
// Note: userProps for cell renderers (e.g. second argument in `cell.render('Cell', foo)` )
// aren't typed, so we must be careful when accessing this
selectedItems?: DashboardsTreeProps['selectedItems'];
};
const HEADER_HEIGHT = 35;
const ROW_HEIGHT = 35;
export function DashboardsTree({ items, width, height, onFolderClick }: DashboardsTreeProps) {
export function DashboardsTree({
items,
width,
height,
selectedItems,
onFolderClick,
onItemSelectionChange,
}: DashboardsTreeProps) {
const styles = useStyles2(getStyles);
const tableColumns = useMemo(() => {
const checkboxColumn: DashboardsTreeColumn = {
id: 'checkbox',
Header: () => <Checkbox value={false} />,
Cell: () => <Checkbox value={false} />,
Cell: ({ row: { original: row }, selectedItems }: DashboardsTreeCellProps) => {
const item = row.item;
if (item.kind === 'ui-empty-folder' || !selectedItems) {
return <></>;
}
const isSelected = selectedItems?.[item.kind][item.uid] ?? false;
return (
<Checkbox
data-testid={selectors.pages.BrowseDashbards.table.checkbox(item.uid)}
value={isSelected}
onChange={(ev) => onItemSelectionChange(item, ev.currentTarget.checked)}
/>
);
},
};
const nameColumn: DashboardsTreeColumn = {
id: 'name',
Header: <span style={{ paddingLeft: 20 }}>Name</span>,
Cell: (props: CellProps<DashboardsTreeItem, unknown>) => <NameCell {...props} onFolderClick={onFolderClick} />,
Cell: (props: DashboardsTreeCellProps) => <NameCell {...props} onFolderClick={onFolderClick} />,
};
const typeColumn: DashboardsTreeColumn = {
@ -46,11 +76,18 @@ export function DashboardsTree({ items, width, height, onFolderClick }: Dashboar
};
return [checkboxColumn, nameColumn, typeColumn];
}, [onFolderClick]);
}, [onItemSelectionChange, onFolderClick]);
const table = useTable({ columns: tableColumns, data: items });
const { getTableProps, getTableBodyProps, headerGroups } = table;
const virtualData = useMemo(() => {
return {
table,
selectedItems,
};
}, [table, selectedItems]);
return (
<div {...getTableProps()} className={styles.tableRoot} role="table">
{headerGroups.map((headerGroup) => {
@ -75,10 +112,11 @@ export function DashboardsTree({ items, width, height, onFolderClick }: Dashboar
<div {...getTableBodyProps()}>
<List
className="virtual list"
height={height - HEADER_HEIGHT}
width={width}
itemCount={items.length}
itemData={table}
itemData={virtualData}
itemSize={ROW_HEIGHT}
>
{VirtualListRow}
@ -91,24 +129,32 @@ export function DashboardsTree({ items, width, height, onFolderClick }: Dashboar
interface VirtualListRowProps {
index: number;
style: React.CSSProperties;
data: TableInstance<DashboardsTreeItem>;
data: {
table: TableInstance<DashboardsTreeItem>;
selectedItems: Record<DashboardViewItemKind, Record<string, boolean | undefined>>;
};
}
function VirtualListRow({ index, style, data: table }: VirtualListRowProps) {
function VirtualListRow({ index, style, data }: VirtualListRowProps) {
const styles = useStyles2(getStyles);
const { table, selectedItems } = data;
const { rows, prepareRow } = table;
const row = rows[index];
prepareRow(row);
return (
<div {...row.getRowProps({ style })} className={cx(styles.row, styles.bodyRow)}>
<div
{...row.getRowProps({ style })}
className={cx(styles.row, styles.bodyRow)}
data-testid={selectors.pages.BrowseDashbards.table.row(row.original.item.uid)}
>
{row.cells.map((cell) => {
const { key, ...cellProps } = cell.getCellProps();
return (
<div key={key} {...cellProps} className={styles.cell}>
{cell.render('Cell')}
{cell.render('Cell', { selectedItems })}
</div>
);
})}
@ -121,8 +167,7 @@ const getStyles = (theme: GrafanaTheme2) => {
return {
tableRoot: css({
// The Indented component uses this css variable to indent items to their position
// in the tree
// Responsively
[INDENT_AMOUNT_CSS_VAR]: theme.spacing(1),
[theme.breakpoints.up('md')]: {

View File

@ -2,39 +2,117 @@ import { Chance } from 'chance';
import { DashboardViewItem } from 'app/features/search/types';
import { DashboardsTreeItem } from '../types';
import { DashboardsTreeItem, UIDashboardViewItem } from '../types';
export function wellFormedEmptyFolder(
seed = 1,
partial?: Partial<DashboardsTreeItem<UIDashboardViewItem>>
): DashboardsTreeItem<UIDashboardViewItem> {
const random = Chance(seed);
export function wellFormedEmptyFolder(): DashboardsTreeItem {
return {
item: {
kind: 'ui-empty-folder',
uid: random.guid(),
},
level: 0,
isOpen: false,
...partial,
};
}
export function wellFormedDashboard(random = Chance(1)): DashboardsTreeItem<DashboardViewItem> {
export function wellFormedDashboard(
seed = 1,
partial?: Partial<DashboardsTreeItem<DashboardViewItem>>,
itemPartial?: Partial<DashboardViewItem>
): DashboardsTreeItem<DashboardViewItem> {
const random = Chance(seed);
return {
item: {
kind: 'dashboard',
title: random.sentence({ words: 3 }),
uid: random.guid(),
tags: [random.word()],
...itemPartial,
},
level: 0,
isOpen: false,
...partial,
};
}
export function wellFormedFolder(random = Chance(2)): DashboardsTreeItem<DashboardViewItem> {
export function wellFormedFolder(
seed = 1,
partial?: Partial<DashboardsTreeItem<DashboardViewItem>>,
itemPartial?: Partial<DashboardViewItem>
): DashboardsTreeItem<DashboardViewItem> {
const random = Chance(seed);
return {
item: {
kind: 'folder',
title: random.sentence({ words: 3 }),
uid: random.guid(),
...itemPartial,
},
level: 0,
isOpen: true,
...partial,
};
}
export function wellFormedTree() {
let seed = 1;
// prettier-ignore so its easier to see the tree structure
/* prettier-ignore */ const folderA = wellFormedFolder(seed++);
/* prettier-ignore */ const folderA_folderA = wellFormedFolder(seed++, { level: 1}, { parentUID: folderA.item.uid });
/* prettier-ignore */ const folderA_folderB = wellFormedFolder(seed++, { level: 1}, { parentUID: folderA.item.uid });
/* prettier-ignore */ const folderA_folderB_dashbdA = wellFormedDashboard(seed++, { level: 2}, { parentUID: folderA_folderB.item.uid });
/* prettier-ignore */ const folderA_folderB_dashbdB = wellFormedDashboard(seed++, { level: 2}, { parentUID: folderA_folderB.item.uid });
/* prettier-ignore */ const folderA_folderC = wellFormedFolder(seed++, { level: 1},{ parentUID: folderA.item.uid });
/* prettier-ignore */ const folderA_folderC_dashbdA = wellFormedDashboard(seed++, { level: 2}, { parentUID: folderA_folderC.item.uid });
/* prettier-ignore */ const folderA_folderC_dashbdB = wellFormedDashboard(seed++, { level: 2}, { parentUID: folderA_folderC.item.uid });
/* prettier-ignore */ const folderA_dashbdD = wellFormedDashboard(seed++, { level: 1}, { parentUID: folderA.item.uid });
/* prettier-ignore */ const folderB = wellFormedFolder(seed++);
/* prettier-ignore */ const folderB_empty = wellFormedEmptyFolder(seed++);
/* prettier-ignore */ const folderC = wellFormedFolder(seed++);
/* prettier-ignore */ const dashbdD = wellFormedDashboard(seed++);
/* prettier-ignore */ const dashbdE = wellFormedDashboard(seed++);
return [
[
folderA,
folderA_folderA,
folderA_folderB,
folderA_folderB_dashbdA,
folderA_folderB_dashbdB,
folderA_folderC,
folderA_folderC_dashbdA,
folderA_folderC_dashbdB,
folderA_dashbdD,
folderB,
folderB_empty,
folderC,
dashbdD,
dashbdE,
],
{
folderA,
folderA_folderA,
folderA_folderB,
folderA_folderB_dashbdA,
folderA_folderB_dashbdB,
folderA_folderC,
folderA_folderC_dashbdA,
folderA_folderC_dashbdB,
folderA_dashbdD,
folderB,
folderB_empty,
folderC,
dashbdD,
dashbdE,
},
] as const;
}

View File

@ -1,7 +1,8 @@
import { DashboardViewItem as OrigDashboardViewItem } from 'app/features/search/types';
import { DashboardViewItem as OrigDashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
interface UIDashboardViewItem {
export interface UIDashboardViewItem {
kind: 'ui-empty-folder';
uid: string;
}
type DashboardViewItem = OrigDashboardViewItem | UIDashboardViewItem;
@ -12,4 +13,6 @@ export interface DashboardsTreeItem<T extends DashboardViewItem = DashboardViewI
isOpen: boolean;
}
export type DashboardTreeSelection = Record<DashboardViewItemKind, Record<string, boolean | undefined>>;
export const INDENT_AMOUNT_CSS_VAR = '--dashboards-tree-indentation';

View File

@ -51,6 +51,7 @@ async function getChildFolders(parentUid?: string, parentTitle?: string): Promis
uid: item.uid,
title: item.title,
parentTitle,
parentUID: parentUid,
url: `/dashboards/f/${item.uid}/`,
}));
}

View File

@ -81,6 +81,7 @@ export function queryResultToViewItem(
if (parentInfo) {
viewItem.parentTitle = parentInfo.name;
viewItem.parentKind = parentInfo.kind;
viewItem.parentUID = parentUid;
}
}

View File

@ -50,11 +50,13 @@ export interface DashboardSearchItem {
folderUrl?: string;
}
export type DashboardViewItemKind = 'folder' | 'dashboard' | 'panel';
/**
* Type used in the folder view components
*/
export interface DashboardViewItem {
kind: 'folder' | 'dashboard' | 'panel';
kind: DashboardViewItemKind;
uid: string;
title: string;
url?: string;
@ -62,7 +64,7 @@ export interface DashboardViewItem {
icon?: string;
// Most commonly parent folder title, but can be dashboard if panelTitleSearch is enabled
parentUID?: string;
parentTitle?: string;
parentKind?: string;