mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Nested folders: Add dummy Move/Delete actions (#67051)
* slight refactor, show actions on selection * add unit tests * refactor logic into custom selector + hook
This commit is contained in:
parent
bbce69f295
commit
1cad819670
@ -0,0 +1,91 @@
|
|||||||
|
import { render as rtlRender, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React, { ComponentProps } from 'react';
|
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
|
|
||||||
|
import BrowseDashboardsPage, { Props } from './BrowseDashboardsPage';
|
||||||
|
import { wellFormedTree } from './fixtures/dashboardsTreeItem.fixture';
|
||||||
|
const [mockTree, { dashbdD }] = wellFormedTree();
|
||||||
|
|
||||||
|
jest.mock('react-virtualized-auto-sizer', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
default(props: ComponentProps<typeof AutoSizer>) {
|
||||||
|
return <div>{props.children({ width: 800, height: 600 })}</div>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function render(...[ui, options]: Parameters<typeof rtlRender>) {
|
||||||
|
rtlRender(
|
||||||
|
<TestProvider
|
||||||
|
storeState={{
|
||||||
|
navIndex: {
|
||||||
|
'dashboards/browse': {
|
||||||
|
text: 'Dashboards',
|
||||||
|
id: 'dashboards/browse',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ui}
|
||||||
|
</TestProvider>,
|
||||||
|
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 BrowseDashboardsPage', () => {
|
||||||
|
let props: Props;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
props = {
|
||||||
|
...getRouteComponentProps(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a search input', async () => {
|
||||||
|
render(<BrowseDashboardsPage {...props} />);
|
||||||
|
expect(await screen.findByPlaceholderText('Search box')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the filters and hides the actions initially', async () => {
|
||||||
|
render(<BrowseDashboardsPage {...props} />);
|
||||||
|
|
||||||
|
expect(await screen.findByText('Sort')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('Filter by tag')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByRole('button', { name: 'Move' })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selecting an item hides the filters and shows the actions instead', async () => {
|
||||||
|
render(<BrowseDashboardsPage {...props} />);
|
||||||
|
|
||||||
|
const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashbdD.item.uid));
|
||||||
|
await userEvent.click(checkbox);
|
||||||
|
|
||||||
|
// Check the filters are now hidden
|
||||||
|
expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Sort')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check the actions are now visible
|
||||||
|
expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -2,8 +2,9 @@ import { css } from '@emotion/css';
|
|||||||
import React, { memo, useMemo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { locationSearchToObject } from '@grafana/runtime';
|
import { locationSearchToObject } from '@grafana/runtime';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { Input, useStyles2 } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
@ -12,15 +13,17 @@ import { parseRouteParams } from '../search/utils';
|
|||||||
|
|
||||||
import { skipToken, useGetFolderQuery } from './api/browseDashboardsAPI';
|
import { skipToken, useGetFolderQuery } from './api/browseDashboardsAPI';
|
||||||
import { BrowseActions } from './components/BrowseActions';
|
import { BrowseActions } from './components/BrowseActions';
|
||||||
|
import { BrowseFilters } from './components/BrowseFilters';
|
||||||
import { BrowseView } from './components/BrowseView';
|
import { BrowseView } from './components/BrowseView';
|
||||||
import { SearchView } from './components/SearchView';
|
import { SearchView } from './components/SearchView';
|
||||||
|
import { useHasSelection } from './state';
|
||||||
|
|
||||||
export interface BrowseDashboardsPageRouteParams {
|
export interface BrowseDashboardsPageRouteParams {
|
||||||
uid?: string;
|
uid?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends GrafanaRouteComponentProps<BrowseDashboardsPageRouteParams> {}
|
export interface Props extends GrafanaRouteComponentProps<BrowseDashboardsPageRouteParams> {}
|
||||||
|
|
||||||
// New Browse/Manage/Search Dashboards views for nested folders
|
// New Browse/Manage/Search Dashboards views for nested folders
|
||||||
|
|
||||||
@ -34,11 +37,14 @@ const BrowseDashboardsPage = memo(({ match, location }: Props) => {
|
|||||||
|
|
||||||
const { data: folderDTO } = useGetFolderQuery(folderUID ?? skipToken);
|
const { data: folderDTO } = useGetFolderQuery(folderUID ?? skipToken);
|
||||||
const navModel = useMemo(() => (folderDTO ? buildNavModel(folderDTO) : undefined), [folderDTO]);
|
const navModel = useMemo(() => (folderDTO ? buildNavModel(folderDTO) : undefined), [folderDTO]);
|
||||||
|
const hasSelection = useHasSelection();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navId="dashboards/browse" pageNav={navModel}>
|
<Page navId="dashboards/browse" pageNav={navModel}>
|
||||||
<Page.Contents className={styles.pageContents}>
|
<Page.Contents className={styles.pageContents}>
|
||||||
<BrowseActions />
|
<Input placeholder="Search box" />
|
||||||
|
|
||||||
|
{hasSelection ? <BrowseActions /> : <BrowseFilters />}
|
||||||
|
|
||||||
<div className={styles.subView}>
|
<div className={styles.subView}>
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
@ -56,11 +62,12 @@ const BrowseDashboardsPage = memo(({ match, location }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const getStyles = () => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
pageContents: css({
|
pageContents: css({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateRows: 'auto 1fr',
|
gridTemplateRows: 'auto auto 1fr',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
rowGap: theme.spacing(1),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// AutoSizer needs an element to measure the full height available
|
// AutoSizer needs an element to measure the full height available
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { BrowseActions } from './BrowseActions';
|
||||||
|
|
||||||
|
describe('browse-dashboards BrowseActions', () => {
|
||||||
|
it('displays Move and Delete buttons', () => {
|
||||||
|
render(<BrowseActions />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,39 +1,41 @@
|
|||||||
import React, { useMemo } from 'react';
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { Input } from '@grafana/ui';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { ActionRow } from 'app/features/search/page/components/ActionRow';
|
import { Button, useStyles2 } from '@grafana/ui';
|
||||||
import { SearchLayout } from 'app/features/search/types';
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
export function BrowseActions() {
|
export function BrowseActions() {
|
||||||
const fakeState = useMemo(() => {
|
const styles = useStyles2(getStyles);
|
||||||
return {
|
|
||||||
query: '',
|
const onMove = () => {
|
||||||
tag: [],
|
// TODO real implemenation, stub for now
|
||||||
starred: false,
|
console.log('onMoveClicked');
|
||||||
layout: SearchLayout.Folders,
|
};
|
||||||
eventTrackingNamespace: 'manage_dashboards' as const,
|
|
||||||
};
|
const onDelete = () => {
|
||||||
}, []);
|
// TODO real implementation, stub for now
|
||||||
|
console.log('onDeleteClicked');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.row} data-testid="manage-actions">
|
||||||
<Input placeholder="Search box" />
|
<Button onClick={onMove} variant="secondary">
|
||||||
|
Move
|
||||||
<br />
|
</Button>
|
||||||
|
<Button onClick={onDelete} variant="destructive">
|
||||||
<ActionRow
|
Delete
|
||||||
includePanels={false}
|
</Button>
|
||||||
state={fakeState}
|
|
||||||
getTagOptions={() => Promise.resolve([])}
|
|
||||||
getSortOptions={() => Promise.resolve([])}
|
|
||||||
onLayoutChange={() => {}}
|
|
||||||
onSortChange={() => {}}
|
|
||||||
onStarredFilterChange={() => {}}
|
|
||||||
onTagFilterChange={() => {}}
|
|
||||||
onDatasourceChange={() => {}}
|
|
||||||
onPanelTypeChange={() => {}}
|
|
||||||
onSetIncludePanels={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
row: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ActionRow } from 'app/features/search/page/components/ActionRow';
|
||||||
|
import { SearchLayout } from 'app/features/search/types';
|
||||||
|
|
||||||
|
export function BrowseFilters() {
|
||||||
|
const fakeState = useMemo(() => {
|
||||||
|
return {
|
||||||
|
query: '',
|
||||||
|
tag: [],
|
||||||
|
starred: false,
|
||||||
|
layout: SearchLayout.Folders,
|
||||||
|
eventTrackingNamespace: 'manage_dashboards' as const,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ActionRow
|
||||||
|
includePanels={false}
|
||||||
|
state={fakeState}
|
||||||
|
getTagOptions={() => Promise.resolve([])}
|
||||||
|
getSortOptions={() => Promise.resolve([])}
|
||||||
|
onLayoutChange={() => {}}
|
||||||
|
onSortChange={() => {}}
|
||||||
|
onStarredFilterChange={() => {}}
|
||||||
|
onTagFilterChange={() => {}}
|
||||||
|
onDatasourceChange={() => {}}
|
||||||
|
onPanelTypeChange={() => {}}
|
||||||
|
onSetIncludePanels={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -15,10 +15,23 @@ const flatTreeSelector = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasSelectionSelector = createSelector(
|
||||||
|
(wholeState: StoreState) => wholeState.browseDashboards.selectedItems,
|
||||||
|
(selectedItems) => {
|
||||||
|
return Object.values(selectedItems).some((selectedItem) =>
|
||||||
|
Object.values(selectedItem).some((isSelected) => isSelected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export function useFlatTreeState(folderUID: string | undefined) {
|
export function useFlatTreeState(folderUID: string | undefined) {
|
||||||
return useSelector((state) => flatTreeSelector(state, folderUID));
|
return useSelector((state) => flatTreeSelector(state, folderUID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useHasSelection() {
|
||||||
|
return useSelector((state) => hasSelectionSelector(state));
|
||||||
|
}
|
||||||
|
|
||||||
export function useSelectedItemsState() {
|
export function useSelectedItemsState() {
|
||||||
return useSelector((wholeState: StoreState) => wholeState.browseDashboards.selectedItems);
|
return useSelector((wholeState: StoreState) => wholeState.browseDashboards.selectedItems);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user