diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx new file mode 100644 index 00000000000..29f63d4ea30 --- /dev/null +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -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) { + return
{props.children({ width: 800, height: 600 })}
; + }, + }; +}); + +function render(...[ui, options]: Parameters) { + rtlRender( + + {ui} + , + 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(); + expect(await screen.findByPlaceholderText('Search box')).toBeInTheDocument(); + }); + + it('displays the filters and hides the actions initially', async () => { + render(); + + 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(); + + 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(); + }); +}); diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index eccb6dac905..dc6fe30d9c9 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -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 {} +export interface Props extends GrafanaRouteComponentProps {} // 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 ( - + + + {hasSelection ? : }
@@ -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 diff --git a/public/app/features/browse-dashboards/components/BrowseActions.test.tsx b/public/app/features/browse-dashboards/components/BrowseActions.test.tsx new file mode 100644 index 00000000000..ee56379c92b --- /dev/null +++ b/public/app/features/browse-dashboards/components/BrowseActions.test.tsx @@ -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(); + + expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/browse-dashboards/components/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions.tsx index cf05c7d856e..602707cf35e 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions.tsx @@ -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 ( -
- - -
- - Promise.resolve([])} - getSortOptions={() => Promise.resolve([])} - onLayoutChange={() => {}} - onSortChange={() => {}} - onStarredFilterChange={() => {}} - onTagFilterChange={() => {}} - onDatasourceChange={() => {}} - onPanelTypeChange={() => {}} - onSetIncludePanels={() => {}} - /> +
+ +
); } + +const getStyles = (theme: GrafanaTheme2) => ({ + row: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(1), + marginBottom: theme.spacing(2), + }), +}); diff --git a/public/app/features/browse-dashboards/components/BrowseFilters.tsx b/public/app/features/browse-dashboards/components/BrowseFilters.tsx new file mode 100644 index 00000000000..36fef5bc4fd --- /dev/null +++ b/public/app/features/browse-dashboards/components/BrowseFilters.tsx @@ -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 ( +
+ Promise.resolve([])} + getSortOptions={() => Promise.resolve([])} + onLayoutChange={() => {}} + onSortChange={() => {}} + onStarredFilterChange={() => {}} + onTagFilterChange={() => {}} + onDatasourceChange={() => {}} + onPanelTypeChange={() => {}} + onSetIncludePanels={() => {}} + /> +
+ ); +} diff --git a/public/app/features/browse-dashboards/state/hooks.ts b/public/app/features/browse-dashboards/state/hooks.ts index d5fb06ea59c..e09bdb5e38d 100644 --- a/public/app/features/browse-dashboards/state/hooks.ts +++ b/public/app/features/browse-dashboards/state/hooks.ts @@ -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); }