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 = (
);
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,
+ };
+}