mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 12:44:10 -06:00
NestedFolders: Virtual tree view (#66102)
* poc for virtual tree * remove my silly debug stuff * wip * style table * improve styles * fix * start to split tree into seperate component * cleanup unused function * split into more components * more better * secondary color type column * simple tests for DashboardsTree * restore styles from dodgy rebase * remove my weirdo text component thing
This commit is contained in:
parent
f58a63b2df
commit
9b15c79e19
@ -1,6 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { locationSearchToObject } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
@ -22,6 +25,7 @@ interface Props extends GrafanaRouteComponentProps<BrowseDashboardsPageRoutePara
|
||||
// New Browse/Manage/Search Dashboards views for nested folders
|
||||
|
||||
const BrowseDashboardsPage = memo(({ match, location }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { uid: folderUID } = match.params;
|
||||
|
||||
const searchState = useMemo(() => {
|
||||
@ -33,16 +37,37 @@ const BrowseDashboardsPage = memo(({ match, location }: Props) => {
|
||||
|
||||
return (
|
||||
<Page navId="dashboards/browse" pageNav={navModel}>
|
||||
<Page.Contents>
|
||||
<Page.Contents className={styles.pageContents}>
|
||||
<BrowseActions />
|
||||
|
||||
{folderDTO && <pre>{JSON.stringify(folderDTO, null, 2)}</pre>}
|
||||
|
||||
{searchState.query ? <SearchView searchState={searchState} /> : <BrowseView folderUID={folderUID} />}
|
||||
<div className={styles.subView}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) =>
|
||||
searchState.query ? (
|
||||
<SearchView searchState={searchState} />
|
||||
) : (
|
||||
<BrowseView width={width} height={height} folderUID={folderUID} />
|
||||
)
|
||||
}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
});
|
||||
|
||||
const getStyles = () => ({
|
||||
pageContents: css({
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
height: '100%',
|
||||
}),
|
||||
|
||||
// AutoSizer needs an element to measure the full height available
|
||||
subView: css({
|
||||
height: '100%',
|
||||
}),
|
||||
});
|
||||
|
||||
BrowseDashboardsPage.displayName = 'BrowseDashboardsPage';
|
||||
export default BrowseDashboardsPage;
|
||||
|
@ -1,101 +1,80 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Icon, IconButton, Link } from '@grafana/ui';
|
||||
import { getFolderChildren } from 'app/features/search/service/folders';
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
|
||||
type NestedData = Record<string, DashboardViewItem[] | undefined>;
|
||||
import { DashboardsTreeItem } from '../types';
|
||||
|
||||
import { DashboardsTree } from './DashboardsTree';
|
||||
|
||||
interface BrowseViewProps {
|
||||
height: number;
|
||||
width: number;
|
||||
folderUID: string | undefined;
|
||||
}
|
||||
|
||||
export function BrowseView({ folderUID }: BrowseViewProps) {
|
||||
const [nestedData, setNestedData] = useState<NestedData>({});
|
||||
export function BrowseView({ folderUID, width, height }: BrowseViewProps) {
|
||||
const [openFolders, setOpenFolders] = useState<Record<string, boolean>>({ [folderUID ?? '$$root']: true });
|
||||
|
||||
// Note: entire implementation of this component must be replaced.
|
||||
// This is just to show proof of concept for fetching and showing the data
|
||||
// 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 childItems = await getFolderChildren(uid, undefined, true);
|
||||
setChildrenByUID((v) => ({ ...v, [folderKey]: childItems }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const folderKey = folderUID ?? '$$root';
|
||||
|
||||
getFolderChildren(folderUID, undefined, true).then((children) => {
|
||||
setNestedData((v) => ({ ...v, [folderKey]: children }));
|
||||
});
|
||||
loadChildrenForUID(folderUID);
|
||||
}, [folderUID]);
|
||||
|
||||
const items = nestedData[folderUID ?? '$$root'] ?? [];
|
||||
|
||||
const handleNodeClick = useCallback(
|
||||
(uid: string) => {
|
||||
if (nestedData[uid]) {
|
||||
setNestedData((v) => ({ ...v, [uid]: undefined }));
|
||||
return;
|
||||
}
|
||||
|
||||
getFolderChildren(uid).then((children) => {
|
||||
setNestedData((v) => ({ ...v, [uid]: children }));
|
||||
});
|
||||
},
|
||||
[nestedData]
|
||||
const flatTree = useMemo(
|
||||
() => createFlatTree(folderUID, childrenByUID, openFolders),
|
||||
[folderUID, childrenByUID, openFolders]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Browse view</p>
|
||||
const handleFolderClick = useCallback((uid: string, folderIsOpen: boolean) => {
|
||||
if (folderIsOpen) {
|
||||
loadChildrenForUID(uid);
|
||||
}
|
||||
|
||||
<ul style={{ marginLeft: 16 }}>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<li key={item.uid}>
|
||||
<BrowseItem item={item} nestedData={nestedData} onFolderClick={handleNodeClick} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
setOpenFolders((v) => ({ ...v, [uid]: folderIsOpen }));
|
||||
}, []);
|
||||
|
||||
return <DashboardsTree items={flatTree} width={width} height={height} onFolderClick={handleFolderClick} />;
|
||||
}
|
||||
|
||||
function BrowseItem({
|
||||
item,
|
||||
nestedData,
|
||||
onFolderClick,
|
||||
}: {
|
||||
item: DashboardViewItem;
|
||||
nestedData: NestedData;
|
||||
onFolderClick: (uid: string) => void;
|
||||
}) {
|
||||
const childItems = nestedData[item.uid];
|
||||
// Creates a flat list of items, with nested children indicated by its increasing level
|
||||
function createFlatTree(
|
||||
rootFolderUID: string | undefined,
|
||||
childrenByUID: Record<string, DashboardViewItem[] | undefined>,
|
||||
openFolders: Record<string, boolean>,
|
||||
level = 0
|
||||
): DashboardsTreeItem[] {
|
||||
function mapItem(item: DashboardViewItem, level: number): DashboardsTreeItem[] {
|
||||
const mappedChildren = createFlatTree(item.uid, childrenByUID, openFolders, level + 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{item.kind === 'folder' ? (
|
||||
<IconButton onClick={() => onFolderClick(item.uid)} name={childItems ? 'angle-down' : 'angle-right'} />
|
||||
) : (
|
||||
<span style={{ paddingRight: 20 }} />
|
||||
)}
|
||||
<Icon name={item.kind === 'folder' ? (childItems ? 'folder-open' : 'folder') : 'apps'} />{' '}
|
||||
<Link href={item.kind === 'folder' ? `/nested-dashboards/f/${item.uid}` : `/d/${item.uid}`}>{item.title}</Link>
|
||||
</div>
|
||||
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' } });
|
||||
}
|
||||
|
||||
{childItems && (
|
||||
<ul style={{ marginLeft: 16 }}>
|
||||
{childItems.length === 0 && (
|
||||
<li>
|
||||
<em>Empty folder</em>
|
||||
</li>
|
||||
)}
|
||||
{childItems.map((childItem) => {
|
||||
return (
|
||||
<li key={childItem.uid}>
|
||||
<BrowseItem item={childItem} nestedData={nestedData} onFolderClick={onFolderClick} />{' '}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const thisItem = {
|
||||
item,
|
||||
level,
|
||||
isOpen,
|
||||
};
|
||||
|
||||
return [thisItem, ...mappedChildren];
|
||||
}
|
||||
|
||||
const folderKey = rootFolderUID ?? '$$root';
|
||||
const isOpen = Boolean(openFolders[folderKey]);
|
||||
const items = (isOpen && childrenByUID[folderKey]) || [];
|
||||
|
||||
return items.flatMap((item) => mapItem(item, level));
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
import { 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 { locationService } from '@grafana/runtime';
|
||||
|
||||
import { wellFormedDashboard, wellFormedEmptyFolder, wellFormedFolder } from '../fixtures/dashboardsTreeItem.fixture';
|
||||
|
||||
import { DashboardsTree } from './DashboardsTree';
|
||||
|
||||
function render(...args: Parameters<typeof rtlRender>) {
|
||||
const [ui, options] = args;
|
||||
|
||||
rtlRender(<Router history={locationService.getHistory()}>{ui}</Router>, options);
|
||||
}
|
||||
|
||||
describe('browse-dashboards DashboardsTree', () => {
|
||||
const WIDTH = 800;
|
||||
const HEIGHT = 600;
|
||||
|
||||
const folder = wellFormedFolder();
|
||||
const emptyFolderIndicator = wellFormedEmptyFolder();
|
||||
const dashboard = wellFormedDashboard();
|
||||
|
||||
it('renders a dashboard item', () => {
|
||||
render(<DashboardsTree items={[dashboard]} width={WIDTH} height={HEIGHT} onFolderClick={() => {}} />);
|
||||
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={() => {}} />);
|
||||
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} />);
|
||||
const folderButton = screen.getByLabelText('Collapse folder');
|
||||
await userEvent.click(folderButton);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(folder.item.uid, false);
|
||||
});
|
||||
|
||||
it('renders empty folder indicators', () => {
|
||||
render(<DashboardsTree items={[emptyFolderIndicator]} width={WIDTH} height={HEIGHT} onFolderClick={() => {}} />);
|
||||
expect(screen.queryByText('Empty folder')).toBeInTheDocument();
|
||||
expect(screen.queryByText(emptyFolderIndicator.item.kind)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,165 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
import { CellProps, Column, TableInstance, useTable } from 'react-table';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Checkbox, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { DashboardsTreeItem, INDENT_AMOUNT_CSS_VAR } from '../types';
|
||||
|
||||
import { NameCell } from './NameCell';
|
||||
import { TypeCell } from './TypeCell';
|
||||
|
||||
interface DashboardsTreeProps {
|
||||
items: DashboardsTreeItem[];
|
||||
width: number;
|
||||
height: number;
|
||||
onFolderClick: (uid: string, newOpenState: boolean) => void;
|
||||
}
|
||||
|
||||
type DashboardsTreeColumn = Column<DashboardsTreeItem>;
|
||||
|
||||
const HEADER_HEIGHT = 35;
|
||||
const ROW_HEIGHT = 35;
|
||||
|
||||
export function DashboardsTree({ items, width, height, onFolderClick }: DashboardsTreeProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
const checkboxColumn: DashboardsTreeColumn = {
|
||||
id: 'checkbox',
|
||||
Header: () => <Checkbox value={false} />,
|
||||
Cell: () => <Checkbox value={false} />,
|
||||
};
|
||||
|
||||
const nameColumn: DashboardsTreeColumn = {
|
||||
id: 'name',
|
||||
Header: <span style={{ paddingLeft: 20 }}>Name</span>,
|
||||
Cell: (props: CellProps<DashboardsTreeItem, unknown>) => <NameCell {...props} onFolderClick={onFolderClick} />,
|
||||
};
|
||||
|
||||
const typeColumn: DashboardsTreeColumn = {
|
||||
id: 'type',
|
||||
Header: 'Type',
|
||||
Cell: TypeCell,
|
||||
};
|
||||
|
||||
return [checkboxColumn, nameColumn, typeColumn];
|
||||
}, [onFolderClick]);
|
||||
|
||||
const table = useTable({ columns: tableColumns, data: items });
|
||||
const { getTableProps, getTableBodyProps, headerGroups } = table;
|
||||
|
||||
return (
|
||||
<div {...getTableProps()} className={styles.tableRoot} role="table">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({
|
||||
style: { width },
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={key} {...headerGroupProps} className={cx(styles.row, styles.headerRow)}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key, ...headerProps } = column.getHeaderProps();
|
||||
|
||||
return (
|
||||
<div key={key} {...headerProps} role="columnheader" className={styles.cell}>
|
||||
{column.render('Header')}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div {...getTableBodyProps()}>
|
||||
<List
|
||||
height={height - HEADER_HEIGHT}
|
||||
width={width}
|
||||
itemCount={items.length}
|
||||
itemData={table}
|
||||
itemSize={ROW_HEIGHT}
|
||||
>
|
||||
{VirtualListRow}
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface VirtualListRowProps {
|
||||
index: number;
|
||||
style: React.CSSProperties;
|
||||
data: TableInstance<DashboardsTreeItem>;
|
||||
}
|
||||
|
||||
function VirtualListRow({ index, style, data: table }: VirtualListRowProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { rows, prepareRow } = table;
|
||||
|
||||
const row = rows[index];
|
||||
prepareRow(row);
|
||||
|
||||
return (
|
||||
<div {...row.getRowProps({ style })} className={cx(styles.row, styles.bodyRow)}>
|
||||
{row.cells.map((cell) => {
|
||||
const { key, ...cellProps } = cell.getCellProps();
|
||||
|
||||
return (
|
||||
<div key={key} {...cellProps} className={styles.cell}>
|
||||
{cell.render('Cell')}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const columnSizing = 'auto 2fr 1fr';
|
||||
|
||||
return {
|
||||
tableRoot: css({
|
||||
// The Indented component uses this css variable to indent items to their position
|
||||
// in the tree
|
||||
[INDENT_AMOUNT_CSS_VAR]: theme.spacing(1),
|
||||
|
||||
[theme.breakpoints.up('md')]: {
|
||||
[INDENT_AMOUNT_CSS_VAR]: theme.spacing(3),
|
||||
},
|
||||
}),
|
||||
|
||||
cell: css({
|
||||
padding: theme.spacing(1),
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}),
|
||||
|
||||
row: css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: columnSizing,
|
||||
alignItems: 'center',
|
||||
}),
|
||||
|
||||
headerRow: css({
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
height: HEADER_HEIGHT,
|
||||
}),
|
||||
|
||||
bodyRow: css({
|
||||
height: ROW_HEIGHT,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colors.emphasize(theme.colors.background.primary, 0.03),
|
||||
},
|
||||
}),
|
||||
|
||||
link: css({
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
20
public/app/features/browse-dashboards/components/Indent.tsx
Normal file
20
public/app/features/browse-dashboards/components/Indent.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { INDENT_AMOUNT_CSS_VAR } from '../types';
|
||||
|
||||
interface IndentProps {
|
||||
children?: React.ReactNode;
|
||||
level: number;
|
||||
}
|
||||
|
||||
export function Indent({ children, level }: IndentProps) {
|
||||
const theme = useTheme2();
|
||||
|
||||
// DashboardsTree responsively sets the value of INDENT_AMOUNT_CSS_VAR
|
||||
// but we also have a fallback just in case it's not set for some reason...
|
||||
const space = `var(${INDENT_AMOUNT_CSS_VAR}, ${theme.spacing(2)})`;
|
||||
|
||||
return <span style={{ paddingLeft: `calc(${space} * ${level})` }}>{children}</span>;
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { CellProps } from 'react-table';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconButton, Link, useStyles2 } from '@grafana/ui';
|
||||
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
|
||||
|
||||
import { DashboardsTreeItem } from '../types';
|
||||
|
||||
import { Indent } from './Indent';
|
||||
|
||||
type NameCellProps = CellProps<DashboardsTreeItem, unknown> & {
|
||||
onFolderClick: (uid: string, newOpenState: boolean) => void;
|
||||
};
|
||||
|
||||
export function NameCell({ row: { original: data }, onFolderClick }: NameCellProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { item, level, isOpen } = data;
|
||||
|
||||
if (item.kind === 'ui-empty-folder') {
|
||||
return (
|
||||
<>
|
||||
<Indent level={level} />
|
||||
<span className={styles.folderButtonSpacer} />
|
||||
<em>Empty folder</em>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const chevronIcon = isOpen ? 'angle-down' : 'angle-right';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Indent level={level} />
|
||||
|
||||
{item.kind === 'folder' ? (
|
||||
<IconButton
|
||||
size="md"
|
||||
onClick={() => onFolderClick(item.uid, !isOpen)}
|
||||
name={chevronIcon}
|
||||
ariaLabel={isOpen ? 'Collapse folder' : 'Expand folder'}
|
||||
/>
|
||||
) : (
|
||||
<span className={styles.folderButtonSpacer} />
|
||||
)}
|
||||
|
||||
<Link
|
||||
href={item.kind === 'folder' ? `/nested-dashboards/f/${item.uid}` : `/d/${item.uid}`}
|
||||
className={styles.link}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
// Should be the same size as the <IconButton /> so Dashboard name is aligned to Folder name siblings
|
||||
folderButtonSpacer: css({
|
||||
paddingLeft: `calc(${getSvgSize('md')}px + ${theme.spacing(0.5)})`,
|
||||
}),
|
||||
link: css({
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
@ -0,0 +1,45 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { CellProps } from 'react-table';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, useStyles2 } from '@grafana/ui';
|
||||
import { getIconForKind } from 'app/features/search/service/utils';
|
||||
|
||||
import { DashboardsTreeItem } from '../types';
|
||||
|
||||
export function TypeCell({ row: { original: data } }: CellProps<DashboardsTreeItem, unknown>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const iconName = getIconForKind(data.item.kind);
|
||||
|
||||
switch (data.item.kind) {
|
||||
case 'dashboard':
|
||||
return (
|
||||
<span className={styles.text}>
|
||||
<Icon name={iconName} /> Dashboard
|
||||
</span>
|
||||
);
|
||||
case 'folder':
|
||||
return (
|
||||
<span className={styles.text}>
|
||||
<Icon name={iconName} /> Folder
|
||||
</span>
|
||||
);
|
||||
case 'panel':
|
||||
return (
|
||||
<span className={styles.text}>
|
||||
<Icon name={iconName} /> Panel
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
text: css({
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
};
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { Chance } from 'chance';
|
||||
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
|
||||
import { DashboardsTreeItem } from '../types';
|
||||
|
||||
export function wellFormedEmptyFolder(): DashboardsTreeItem {
|
||||
return {
|
||||
item: {
|
||||
kind: 'ui-empty-folder',
|
||||
},
|
||||
level: 0,
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function wellFormedDashboard(random = Chance(1)): DashboardsTreeItem<DashboardViewItem> {
|
||||
return {
|
||||
item: {
|
||||
kind: 'dashboard',
|
||||
title: random.sentence({ words: 3 }),
|
||||
uid: random.guid(),
|
||||
tags: [random.word()],
|
||||
},
|
||||
level: 0,
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function wellFormedFolder(random = Chance(2)): DashboardsTreeItem<DashboardViewItem> {
|
||||
return {
|
||||
item: {
|
||||
kind: 'folder',
|
||||
title: random.sentence({ words: 3 }),
|
||||
uid: random.guid(),
|
||||
},
|
||||
level: 0,
|
||||
isOpen: true,
|
||||
};
|
||||
}
|
15
public/app/features/browse-dashboards/types.ts
Normal file
15
public/app/features/browse-dashboards/types.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { DashboardViewItem as OrigDashboardViewItem } from 'app/features/search/types';
|
||||
|
||||
interface UIDashboardViewItem {
|
||||
kind: 'ui-empty-folder';
|
||||
}
|
||||
|
||||
type DashboardViewItem = OrigDashboardViewItem | UIDashboardViewItem;
|
||||
|
||||
export interface DashboardsTreeItem<T extends DashboardViewItem = DashboardViewItem> {
|
||||
item: T;
|
||||
level: number;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export const INDENT_AMOUNT_CSS_VAR = '--dashboards-tree-indentation';
|
@ -115,7 +115,7 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }}>
|
||||
<div style={{ content: 'auto-sizer-wrapper', height: '100%', width: '100%' }}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
const props: SearchResultsProps = {
|
||||
|
Loading…
Reference in New Issue
Block a user