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:
Ashley Harrison 2023-04-21 15:18:40 +01:00 committed by GitHub
parent bbce69f295
commit 1cad819670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 196 additions and 36 deletions

View File

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

View File

@ -2,8 +2,9 @@ import { css } from '@emotion/css';
import React, { memo, useMemo } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
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 { GrafanaRouteComponentProps } from 'app/core/navigation/types';
@ -12,15 +13,17 @@ import { parseRouteParams } from '../search/utils';
import { skipToken, useGetFolderQuery } from './api/browseDashboardsAPI';
import { BrowseActions } from './components/BrowseActions';
import { BrowseFilters } from './components/BrowseFilters';
import { BrowseView } from './components/BrowseView';
import { SearchView } from './components/SearchView';
import { useHasSelection } from './state';
export interface BrowseDashboardsPageRouteParams {
uid?: string;
slug?: string;
}
interface Props extends GrafanaRouteComponentProps<BrowseDashboardsPageRouteParams> {}
export interface Props extends GrafanaRouteComponentProps<BrowseDashboardsPageRouteParams> {}
// 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 navModel = useMemo(() => (folderDTO ? buildNavModel(folderDTO) : undefined), [folderDTO]);
const hasSelection = useHasSelection();
return (
<Page navId="dashboards/browse" pageNav={navModel}>
<Page.Contents className={styles.pageContents}>
<BrowseActions />
<Input placeholder="Search box" />
{hasSelection ? <BrowseActions /> : <BrowseFilters />}
<div className={styles.subView}>
<AutoSizer>
@ -56,11 +62,12 @@ const BrowseDashboardsPage = memo(({ match, location }: Props) => {
);
});
const getStyles = () => ({
const getStyles = (theme: GrafanaTheme2) => ({
pageContents: css({
display: 'grid',
gridTemplateRows: 'auto 1fr',
gridTemplateRows: 'auto auto 1fr',
height: '100%',
rowGap: theme.spacing(1),
}),
// AutoSizer needs an element to measure the full height available

View File

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

View File

@ -1,39 +1,41 @@
import React, { useMemo } from 'react';
import { css } from '@emotion/css';
import React from 'react';
import { Input } from '@grafana/ui';
import { ActionRow } from 'app/features/search/page/components/ActionRow';
import { SearchLayout } from 'app/features/search/types';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, useStyles2 } from '@grafana/ui';
export interface Props {}
export function BrowseActions() {
const fakeState = useMemo(() => {
return {
query: '',
tag: [],
starred: false,
layout: SearchLayout.Folders,
eventTrackingNamespace: 'manage_dashboards' as const,
};
}, []);
const styles = useStyles2(getStyles);
const onMove = () => {
// TODO real implemenation, stub for now
console.log('onMoveClicked');
};
const onDelete = () => {
// TODO real implementation, stub for now
console.log('onDeleteClicked');
};
return (
<div>
<Input placeholder="Search box" />
<br />
<ActionRow
includePanels={false}
state={fakeState}
getTagOptions={() => Promise.resolve([])}
getSortOptions={() => Promise.resolve([])}
onLayoutChange={() => {}}
onSortChange={() => {}}
onStarredFilterChange={() => {}}
onTagFilterChange={() => {}}
onDatasourceChange={() => {}}
onPanelTypeChange={() => {}}
onSetIncludePanels={() => {}}
/>
<div className={styles.row} data-testid="manage-actions">
<Button onClick={onMove} variant="secondary">
Move
</Button>
<Button onClick={onDelete} variant="destructive">
Delete
</Button>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
row: css({
display: 'flex',
flexDirection: 'row',
gap: theme.spacing(1),
marginBottom: theme.spacing(2),
}),
});

View File

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

View File

@ -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) {
return useSelector((state) => flatTreeSelector(state, folderUID));
}
export function useHasSelection() {
return useSelector((state) => hasSelectionSelector(state));
}
export function useSelectedItemsState() {
return useSelector((wholeState: StoreState) => wholeState.browseDashboards.selectedItems);
}