diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index 78d416cae1c..27c5724157b 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -9,6 +9,7 @@ import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps import BrowseDashboardsPage, { Props } from './BrowseDashboardsPage'; import { wellFormedTree } from './fixtures/dashboardsTreeItem.fixture'; +import * as permissions from './permissions'; const [mockTree, { dashbdD }] = wellFormedTree(); jest.mock('react-virtualized-auto-sizer', () => { @@ -57,6 +58,14 @@ describe('browse-dashboards BrowseDashboardsPage', () => { props = { ...getRouteComponentProps(), }; + + jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => { + return { + canEditInFolder: true, + canCreateDashboards: true, + canCreateFolder: true, + }; + }); }); it('displays a search input', async () => { diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 63421067dd9..bda3a89aa66 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -17,6 +17,7 @@ import { BrowseFilters } from './components/BrowseFilters'; import { BrowseView } from './components/BrowseView'; import { CreateNewButton } from './components/CreateNewButton'; import { SearchView } from './components/SearchView'; +import { getFolderPermissions } from './permissions'; import { useHasSelection } from './state'; export interface BrowseDashboardsPageRouteParams { @@ -52,8 +53,22 @@ const BrowseDashboardsPage = memo(({ match }: Props) => { const navModel = useMemo(() => (folderDTO ? buildNavModel(folderDTO) : undefined), [folderDTO]); const hasSelection = useHasSelection(); + const { canEditInFolder, canCreateDashboards, canCreateFolder } = getFolderPermissions(folderDTO); + return ( - }> + + ) + } + > { {({ width, height }) => isSearching ? ( - + ) : ( - + ) } diff --git a/public/app/features/browse-dashboards/components/BrowseView.test.tsx b/public/app/features/browse-dashboards/components/BrowseView.test.tsx index dcba29461f8..24d262105ca 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.test.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.test.tsx @@ -32,7 +32,7 @@ describe('browse-dashboards BrowseView', () => { const HEIGHT = 600; it('expands and collapses a folder', async () => { - render(); + render(); await screen.findByText(folderA.item.title); await expandFolder(folderA.item.uid); @@ -43,7 +43,7 @@ describe('browse-dashboards BrowseView', () => { }); it('checks items when selected', async () => { - render(); + render(); const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashbdD.item.uid)); expect(checkbox).not.toBeChecked(); @@ -53,7 +53,7 @@ describe('browse-dashboards BrowseView', () => { }); it('checks all descendants when a folder is selected', async () => { - render(); + render(); await screen.findByText(folderA.item.title); // First expand then click folderA @@ -72,7 +72,7 @@ describe('browse-dashboards BrowseView', () => { }); it('checks descendants loaded after a folder is selected', async () => { - render(); + render(); await screen.findByText(folderA.item.title); // First expand then click folderA @@ -94,7 +94,7 @@ describe('browse-dashboards BrowseView', () => { }); it('unchecks ancestors when unselecting an item', async () => { - render(); + render(); await screen.findByText(folderA.item.title); await expandFolder(folderA.item.uid); diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx index e67c2fbc4e3..d3f9b2e927d 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -18,9 +18,10 @@ interface BrowseViewProps { height: number; width: number; folderUID: string | undefined; + canSelect: boolean; } -export function BrowseView({ folderUID, width, height }: BrowseViewProps) { +export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) { const dispatch = useDispatch(); const flatTree = useFlatTreeState(folderUID); const selectedItems = useCheckboxSelectionState(); @@ -49,6 +50,7 @@ export function BrowseView({ folderUID, width, height }: BrowseViewProps) { return ( ); + render(); const newButton = screen.getByText('New'); await userEvent.click(newButton); } @@ -26,4 +26,24 @@ describe('NewActionsButton', () => { expect(screen.getByText('New Folder')).toHaveAttribute('href', '/dashboards/folder/new'); expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import'); }); + + it('should only render dashboard items when folder creation is disabled', async () => { + render(); + const newButton = screen.getByText('New'); + await userEvent.click(newButton); + + expect(screen.getByText('New Dashboard')).toBeInTheDocument(); + expect(screen.getByText('Import')).toBeInTheDocument(); + expect(screen.queryByText('New Folder')).not.toBeInTheDocument(); + }); + + it('should only render folder item when dashboard creation is disabled', async () => { + render(); + const newButton = screen.getByText('New'); + await userEvent.click(newButton); + + expect(screen.queryByText('New Dashboard')).not.toBeInTheDocument(); + expect(screen.queryByText('Import')).not.toBeInTheDocument(); + expect(screen.getByText('New Folder')).toBeInTheDocument(); + }); }); diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx index 01e32bf8fbb..ac38ca4945f 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx @@ -13,14 +13,22 @@ interface Props { * Pass a folder UID in which the dashboard or folder will be created */ inFolder?: string; + canCreateFolder: boolean; + canCreateDashboard: boolean; } -export function CreateNewButton({ inFolder }: Props) { +export function CreateNewButton({ inFolder, canCreateDashboard, canCreateFolder }: Props) { const newMenu = ( - - - + {canCreateDashboard && ( + + )} + {canCreateFolder && ( + + )} + {canCreateDashboard && ( + + )} ); diff --git a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx index 65b498d29e0..0f3192636e6 100644 --- a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx +++ b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { assertIsDefined } from 'test/helpers/asserts'; +import { selectors } from '@grafana/e2e-selectors'; + import { wellFormedDashboard, wellFormedEmptyFolder, wellFormedFolder } from '../fixtures/dashboardsTreeItem.fixture'; import { DashboardsTree } from './DashboardsTree'; @@ -30,6 +32,7 @@ describe('browse-dashboards DashboardsTree', () => { it('renders a dashboard item', () => { render( { expect(screen.queryByText(dashboard.item.title)).toBeInTheDocument(); expect(screen.queryByText('Dashboard')).toBeInTheDocument(); expect(screen.queryByText(assertIsDefined(dashboard.item.tags)[0])).toBeInTheDocument(); + expect(screen.getByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashboard.item.uid))).toBeInTheDocument(); + }); + + it('does not render checkbox when disabled', () => { + render( + + ); + expect( + screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashboard.item.uid)) + ).not.toBeInTheDocument(); }); it('renders a folder item', () => { render( { const handler = jest.fn(); render( { it('renders empty folder indicators', () => { render( void; onAllSelectionChange: (newState: boolean) => void; onItemSelectionChange: (item: DashboardViewItem, newState: boolean) => void; + canSelect: boolean; } type DashboardsTreeColumn = Column; @@ -46,34 +47,37 @@ export function DashboardsTree({ onFolderClick, onAllSelectionChange, onItemSelectionChange, + canSelect = false, }: DashboardsTreeProps) { const styles = useStyles2(getStyles); const tableColumns = useMemo(() => { - const checkboxColumn: DashboardsTreeColumn = { - id: 'checkbox', - width: 0, - Header: ({ selectedItems }: DashboardTreeHeaderProps) => { - const isAllSelected = selectedItems?.$all ?? false; - return onAllSelectionChange(ev.currentTarget.checked)} />; - }, - Cell: ({ row: { original: row }, selectedItems }: DashboardsTreeCellProps) => { - const item = row.item; - if (item.kind === 'ui-empty-folder' || !selectedItems) { - return <>; + const checkboxColumn: DashboardsTreeColumn | null = canSelect + ? { + id: 'checkbox', + width: 0, + Header: ({ selectedItems }: DashboardTreeHeaderProps) => { + const isAllSelected = selectedItems?.$all ?? false; + return onAllSelectionChange(ev.currentTarget.checked)} />; + }, + 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 ( + onItemSelectionChange(item, ev.currentTarget.checked)} + /> + ); + }, } - - const isSelected = selectedItems?.[item.kind][item.uid] ?? false; - - return ( - onItemSelectionChange(item, ev.currentTarget.checked)} - /> - ); - }, - }; + : null; const nameColumn: DashboardsTreeColumn = { id: 'name', @@ -95,9 +99,10 @@ export function DashboardsTree({ Header: 'Tags', Cell: TagsCell, }; + const columns = [canSelect && checkboxColumn, nameColumn, typeColumn, tagsColumns].filter(isTruthy); - return [checkboxColumn, nameColumn, typeColumn, tagsColumns]; - }, [onItemSelectionChange, onAllSelectionChange, onFolderClick]); + return columns; + }, [onItemSelectionChange, onAllSelectionChange, onFolderClick, canSelect]); const table = useTable({ columns: tableColumns, data: items }, useCustomFlexLayout); const { getTableProps, getTableBodyProps, headerGroups } = table; diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx index 92e985f4d0f..ad29eb87190 100644 --- a/public/app/features/browse-dashboards/components/SearchView.tsx +++ b/public/app/features/browse-dashboards/components/SearchView.tsx @@ -12,9 +12,10 @@ import { setAllSelection, setItemSelectionState, useHasSelection } from '../stat interface SearchViewProps { height: number; width: number; + canSelect: boolean; } -export function SearchView({ width, height }: SearchViewProps) { +export function SearchView({ width, height, canSelect }: SearchViewProps) { const dispatch = useDispatch(); const selectedItems = useSelector((wholeState) => wholeState.browseDashboards.selectedItems); const hasSelection = useHasSelection(); @@ -73,8 +74,8 @@ export function SearchView({ width, height }: SearchViewProps) { const props: SearchResultsProps = { response: value, - selection: selectionChecker, - selectionToggle: handleItemSelectionChange, + selection: canSelect ? selectionChecker : undefined, + selectionToggle: canSelect ? handleItemSelectionChange : undefined, clearSelection, width: width, height: height, diff --git a/public/app/features/browse-dashboards/permissions.ts b/public/app/features/browse-dashboards/permissions.ts new file mode 100644 index 00000000000..0e155f3e257 --- /dev/null +++ b/public/app/features/browse-dashboards/permissions.ts @@ -0,0 +1,26 @@ +import { contextSrv } from 'app/core/core'; +import { AccessControlAction, FolderDTO } from 'app/types'; + +function checkFolderPermission(action: AccessControlAction, fallback: boolean, folderDTO?: FolderDTO) { + return folderDTO + ? contextSrv.hasAccessInMetadata(action, folderDTO, fallback) + : contextSrv.hasAccess(action, fallback); +} + +export function getFolderPermissions(folderDTO?: FolderDTO) { + // It is possible to have edit permissions for folders and dashboards, without being able to save, hence 'canSave' + const canEditInFolderFallback = folderDTO ? folderDTO.canSave : contextSrv.hasEditPermissionInFolders; + + const canEditInFolder = checkFolderPermission(AccessControlAction.FoldersWrite, canEditInFolderFallback, folderDTO); + const canCreateFolder = checkFolderPermission(AccessControlAction.FoldersCreate, contextSrv.isEditor); + const canCreateDashboards = checkFolderPermission( + AccessControlAction.DashboardsCreate, + canEditInFolderFallback || !!folderDTO?.canSave + ); + + return { + canEditInFolder, + canCreateDashboards, + canCreateFolder, + }; +}