Browse dashboards: Hide checkboxes for items user cannot edit (#108427)

* Add test

* BrowseDashboards: Separate folder and dashboard permission checks

* Extract type

* Check for edit permissions

* Prettier

* Refactor test

* Update canEditItemType

* Update render check

* Update the check
This commit is contained in:
Alex Khomenko
2025-07-23 10:10:33 +03:00
committed by GitHub
parent 408ac3086d
commit 08a287e705
15 changed files with 236 additions and 73 deletions
@@ -1,4 +1,4 @@
import { render as rtlRender, screen } from '@testing-library/react';
import { render as rtlRender, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { HttpResponse, http } from 'msw';
import { setupServer, SetupServer } from 'msw/node';
@@ -120,6 +120,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
canEditFolders: true,
canViewPermissions: true,
canSetPermissions: true,
canDeleteDashboards: true,
};
beforeAll(() => {
@@ -157,6 +158,17 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
afterEach(() => {
// Reset permissions back to defaults
Object.assign(mockPermissions, {
canCreateDashboards: true,
canEditDashboards: true,
canCreateFolders: true,
canDeleteFolders: true,
canEditFolders: true,
canViewPermissions: true,
canSetPermissions: true,
canDeleteDashboards: true,
});
jest.restoreAllMocks();
server.resetHandlers();
});
@@ -178,13 +190,8 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('does not show the "New" button if the user does not have permissions', async () => {
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
return {
...mockPermissions,
canCreateDashboards: false,
canCreateFolders: false,
};
});
mockPermissions.canCreateDashboards = false;
mockPermissions.canCreateFolders = false;
render(<BrowseDashboardsPage queryParams={{}} />);
expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument();
@@ -281,13 +288,8 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('does not show the "New" button if the user does not have permissions', async () => {
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
return {
...mockPermissions,
canCreateDashboards: false,
canCreateFolders: false,
};
});
mockPermissions.canCreateDashboards = false;
mockPermissions.canCreateFolders = false;
render(<BrowseDashboardsPage queryParams={{}} />);
expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument();
@@ -299,15 +301,10 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('does not show the "Folder actions" button if the user does not have permissions', async () => {
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
return {
...mockPermissions,
canDeleteFolders: false,
canEditFolders: false,
canSetPermissions: false,
canViewPermissions: false,
};
});
mockPermissions.canDeleteFolders = false;
mockPermissions.canEditFolders = false;
mockPermissions.canSetPermissions = false;
mockPermissions.canViewPermissions = false;
render(<BrowseDashboardsPage queryParams={{}} />);
expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
@@ -319,12 +316,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('does not show the "Edit title" button if the user does not have permissions', async () => {
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => {
return {
...mockPermissions,
canEditFolders: false,
};
});
mockPermissions.canEditFolders = false;
render(<BrowseDashboardsPage queryParams={{}} />);
expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument();
@@ -369,5 +361,60 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
});
it('should not show checkbox for folder when user has dashboards:write but lacks folder edit permissions', async () => {
mockPermissions.canCreateFolders = false;
mockPermissions.canDeleteFolders = false;
mockPermissions.canEditFolders = false;
mockPermissions.canSetPermissions = false;
mockPermissions.canViewPermissions = false;
mockPermissions.canDeleteDashboards = false;
render(<BrowseDashboardsPage queryParams={{}} />);
await waitFor(() => {
const checkbox = screen.queryByTestId(
selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid)
);
expect(checkbox).not.toBeInTheDocument();
});
});
it('should not show checkbox for folder when user has folder:write but lacks folder delete permissions', async () => {
mockPermissions.canCreateFolders = false;
mockPermissions.canDeleteFolders = false;
mockPermissions.canEditFolders = true;
mockPermissions.canSetPermissions = false;
mockPermissions.canViewPermissions = false;
render(<BrowseDashboardsPage queryParams={{}} />);
await waitFor(() => {
const checkbox = screen.queryByTestId(
selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid)
);
expect(checkbox).not.toBeInTheDocument();
});
});
it('should show checkbox for folder when user has folder:write and folder delete permissions', async () => {
mockPermissions.canCreateFolders = false;
mockPermissions.canEditDashboards = false;
mockPermissions.canDeleteDashboards = false;
mockPermissions.canSetPermissions = false;
mockPermissions.canViewPermissions = false;
mockPermissions.canDeleteFolders = true;
mockPermissions.canEditFolders = true;
render(<BrowseDashboardsPage queryParams={{}} />);
const checkbox = await screen.findByTestId(
selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid)
);
expect(checkbox).toBeInTheDocument();
});
});
});
@@ -93,11 +93,23 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
const { data: rootFolderDTO } = useGetFolderQuery(folderDTO ? skipToken : 'general');
const folder = folderDTO ?? rootFolderDTO;
const { canEditFolders, canEditDashboards, canCreateDashboards, canCreateFolders } = getFolderPermissions(folder);
const {
canEditFolders,
canDeleteFolders,
canDeleteDashboards,
canEditDashboards,
canCreateDashboards,
canCreateFolders,
} = getFolderPermissions(folder);
const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
const isProvisionedFolder = folder?.managedBy === ManagerKind.Repo;
const showEditTitle = canEditFolders && folderUID && !isProvisionedFolder;
const canSelect = canEditFolders || canEditDashboards;
const permissions = {
canEditFolders,
canEditDashboards,
canDeleteFolders,
canDeleteDashboards,
};
const onEditTitle = async (newValue: string) => {
if (folderDTO) {
const result = await saveFolder({
@@ -183,14 +195,14 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
{({ width, height }) =>
isSearching ? (
<SearchView
canSelect={canSelect}
permissions={permissions}
width={width}
height={height}
searchState={searchState}
searchStateManager={stateManager}
/>
) : (
<BrowseView canSelect={canSelect} width={width} height={height} folderUID={folderUID} />
<BrowseView permissions={permissions} width={width} height={height} folderUID={folderUID} />
)
}
</AutoSizer>
@@ -34,6 +34,7 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => {
canEditFolders: true,
canViewPermissions: true,
canSetPermissions: true,
canDeleteDashboards: true,
};
beforeEach(() => {
@@ -45,6 +45,7 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => {
canEditFolders: true,
canViewPermissions: true,
canSetPermissions: true,
canDeleteDashboards: true,
};
beforeAll(() => {
@@ -25,8 +25,8 @@ const RecentlyDeletedPage = memo(() => {
const [searchState, stateManager] = useRecentlyDeletedStateManager();
const hasSelection = useHasSelection();
const { canEditFolders, canEditDashboards } = getFolderPermissions();
const canSelect = canEditFolders || canEditDashboards;
const { canEditFolders, canEditDashboards, canDeleteFolders, canDeleteDashboards } = getFolderPermissions();
const permissions = { canEditFolders, canEditDashboards, canDeleteFolders, canDeleteDashboards };
useEffect(() => {
stateManager.initStateFromUrl(undefined);
@@ -75,7 +75,7 @@ const RecentlyDeletedPage = memo(() => {
<AutoSizer>
{({ width, height }) => (
<SearchView
canSelect={canSelect}
permissions={permissions}
width={width}
height={height}
searchStateManager={stateManager}
@@ -42,9 +42,25 @@ jest.mock('app/features/browse-dashboards/api/services', () => {
describe('browse-dashboards BrowseView', () => {
const WIDTH = 800;
const HEIGHT = 600;
const mockPermissions = {
canEditFolders: true,
canEditDashboards: true,
canDeleteFolders: true,
canDeleteDashboards: true,
};
afterEach(() => {
// Reset permissions back to defaults
Object.assign(mockPermissions, {
canEditFolders: true,
canEditDashboards: true,
canDeleteFolders: true,
canDeleteDashboards: true,
});
});
it('expands and collapses a folder', async () => {
render(<BrowseView canSelect folderUID={undefined} width={WIDTH} height={HEIGHT} />);
render(<BrowseView permissions={mockPermissions} folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
await expandFolder(folderA.item);
@@ -55,7 +71,7 @@ describe('browse-dashboards BrowseView', () => {
});
it('checks items when selected', async () => {
render(<BrowseView canSelect folderUID={undefined} width={WIDTH} height={HEIGHT} />);
render(<BrowseView permissions={mockPermissions} folderUID={undefined} width={WIDTH} height={HEIGHT} />);
const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid));
expect(checkbox).not.toBeChecked();
@@ -65,7 +81,7 @@ describe('browse-dashboards BrowseView', () => {
});
it('checks all descendants when a folder is selected', async () => {
render(<BrowseView canSelect folderUID={undefined} width={WIDTH} height={HEIGHT} />);
render(<BrowseView permissions={mockPermissions} folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
// First expand then click folderA
@@ -82,7 +98,7 @@ describe('browse-dashboards BrowseView', () => {
});
it('checks descendants loaded after a folder is selected', async () => {
render(<BrowseView canSelect folderUID={undefined} width={WIDTH} height={HEIGHT} />);
render(<BrowseView permissions={mockPermissions} folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
// First expand then click folderA
@@ -102,7 +118,7 @@ describe('browse-dashboards BrowseView', () => {
});
it('unchecks ancestors when unselecting an item', async () => {
render(<BrowseView canSelect folderUID={undefined} width={WIDTH} height={HEIGHT} />);
render(<BrowseView permissions={mockPermissions} folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
await expandFolder(folderA.item);
@@ -126,7 +142,7 @@ describe('browse-dashboards BrowseView', () => {
});
it('shows indeterminate checkboxes when a descendant is selected', async () => {
render(<BrowseView canSelect={true} folderUID={undefined} width={WIDTH} height={HEIGHT} />);
render(<BrowseView permissions={mockPermissions} folderUID={undefined} width={WIDTH} height={HEIGHT} />);
await screen.findByText(folderA.item.title);
await expandFolder(folderA.item);
@@ -147,12 +163,21 @@ describe('browse-dashboards BrowseView', () => {
describe('when there is no item in the folder', () => {
it('shows a CTA for creating a dashboard if the user has editor rights', async () => {
render(<BrowseView canSelect={true} folderUID={folderB_empty.item.uid} width={WIDTH} height={HEIGHT} />);
render(
<BrowseView permissions={mockPermissions} folderUID={folderB_empty.item.uid} width={WIDTH} height={HEIGHT} />
);
expect(await screen.findByText('Create dashboard')).toBeInTheDocument();
});
it('shows a simple message if the user has viewer rights', async () => {
render(<BrowseView canSelect={false} folderUID={folderB_empty.item.uid} width={WIDTH} height={HEIGHT} />);
mockPermissions.canEditFolders = false;
mockPermissions.canEditDashboards = false;
mockPermissions.canDeleteFolders = false;
mockPermissions.canDeleteDashboards = false;
render(
<BrowseView permissions={mockPermissions} folderUID={folderB_empty.item.uid} width={WIDTH} height={HEIGHT} />
);
expect(await screen.findByText('This folder is empty')).toBeInTheDocument();
});
});
@@ -15,23 +15,25 @@ import {
useLoadNextChildrenPage,
} from '../state/hooks';
import { setFolderOpenState, setItemSelectionState, setAllSelection } from '../state/slice';
import { BrowseDashboardsState, DashboardTreeSelection, SelectionState } from '../types';
import { BrowseDashboardsState, DashboardTreeSelection, SelectionState, BrowseDashboardsPermissions } from '../types';
import { DashboardsTree } from './DashboardsTree';
import { canSelectItems } from './utils';
interface BrowseViewProps {
height: number;
width: number;
folderUID: string | undefined;
canSelect: boolean;
permissions: BrowseDashboardsPermissions;
}
export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) {
export function BrowseView({ folderUID, width, height, permissions }: BrowseViewProps) {
const status = useBrowseLoadingStatus(folderUID);
const dispatch = useDispatch();
const flatTree = useFlatTreeState(folderUID);
const selectedItems = useCheckboxSelectionState();
const childrenByParentUID = useChildrenByParentUIDState();
const canSelect = canSelectItems(permissions);
const handleFolderClick = useCallback(
(clickedFolderUID: string, isOpen: boolean) => {
@@ -156,7 +158,7 @@ export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewPr
return (
<DashboardsTree
canSelect={canSelect}
permissions={permissions}
items={flatTree}
width={width}
height={height}
@@ -7,12 +7,13 @@ import { Checkbox, useStyles2 } from '@grafana/ui';
import { DashboardsTreeCellProps, SelectionState } from '../types';
import { isSharedWithMe } from './utils';
import { isSharedWithMe, canEditItemType } from './utils';
export default function CheckboxCell({
row: { original: row },
isSelected,
onItemSelectionChange,
permissions,
}: DashboardsTreeCellProps) {
const item = row.item;
@@ -32,6 +33,11 @@ export default function CheckboxCell({
return <CheckboxSpacer />;
}
// Check if user can edit this specific item type
if (permissions && !canEditItemType(item.kind, permissions)) {
return <CheckboxSpacer />;
}
const state = isSelected(item);
return (
@@ -23,6 +23,12 @@ function render(...[ui, options]: Parameters<typeof rtlRender>) {
describe('browse-dashboards DashboardsTree', () => {
const WIDTH = 800;
const HEIGHT = 600;
const mockPermissions = {
canEditFolders: true,
canEditDashboards: true,
canDeleteFolders: true,
canDeleteDashboards: true,
};
const folder = wellFormedFolder(1);
const emptyFolderIndicator = wellFormedEmptyFolder();
@@ -36,10 +42,20 @@ describe('browse-dashboards DashboardsTree', () => {
config.sharedWithMeFolderUID = 'sharedwithme';
});
afterEach(() => {
// Reset permissions back to defaults
Object.assign(mockPermissions, {
canEditFolders: true,
canEditDashboards: true,
canDeleteFolders: true,
canDeleteDashboards: true,
});
});
it('renders a dashboard item', () => {
render(
<DashboardsTree
canSelect
permissions={mockPermissions}
items={[dashboard]}
isSelected={isSelected}
width={WIDTH}
@@ -57,9 +73,14 @@ describe('browse-dashboards DashboardsTree', () => {
});
it('does not render checkbox when disabled', () => {
mockPermissions.canEditFolders = false;
mockPermissions.canEditDashboards = false;
mockPermissions.canDeleteFolders = false;
mockPermissions.canDeleteDashboards = false;
render(
<DashboardsTree
canSelect={false}
permissions={mockPermissions}
items={[dashboard]}
isSelected={isSelected}
width={WIDTH}
@@ -79,7 +100,7 @@ describe('browse-dashboards DashboardsTree', () => {
it('renders a folder item', () => {
render(
<DashboardsTree
canSelect
permissions={mockPermissions}
items={[folder]}
isSelected={isSelected}
width={WIDTH}
@@ -98,7 +119,7 @@ describe('browse-dashboards DashboardsTree', () => {
it('renders a folder link', () => {
render(
<DashboardsTree
canSelect
permissions={mockPermissions}
items={[folder]}
isSelected={isSelected}
width={WIDTH}
@@ -119,7 +140,7 @@ describe('browse-dashboards DashboardsTree', () => {
render(
<DashboardsTree
canSelect
permissions={mockPermissions}
items={[sharedWithMe, folder]}
isSelected={isSelected}
width={WIDTH}
@@ -140,7 +161,7 @@ describe('browse-dashboards DashboardsTree', () => {
render(
<DashboardsTree
canSelect
permissions={mockPermissions}
items={[sharedWithMe, folder]}
isSelected={isSelected}
width={WIDTH}
@@ -162,7 +183,7 @@ describe('browse-dashboards DashboardsTree', () => {
const handler = jest.fn();
render(
<DashboardsTree
canSelect
permissions={mockPermissions}
items={[folder]}
isSelected={isSelected}
width={WIDTH}
@@ -183,7 +204,7 @@ describe('browse-dashboards DashboardsTree', () => {
it('renders empty folder indicators', () => {
render(
<DashboardsTree
canSelect
permissions={mockPermissions}
items={[emptyFolderIndicator]}
isSelected={isSelected}
width={WIDTH}
@@ -11,20 +11,26 @@ import { Trans, t } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
import { DashboardViewItem } from 'app/features/search/types';
import { DashboardsTreeCellProps, DashboardsTreeColumn, DashboardsTreeItem, SelectionState } from '../types';
import {
DashboardsTreeCellProps,
DashboardsTreeColumn,
DashboardsTreeItem,
SelectionState,
BrowseDashboardsPermissions,
} from '../types';
import CheckboxCell from './CheckboxCell';
import CheckboxHeaderCell from './CheckboxHeaderCell';
import { NameCell } from './NameCell';
import { TagsCell } from './TagsCell';
import { useCustomFlexLayout } from './customFlexTableLayout';
import { makeRowID } from './utils';
import { makeRowID, canSelectItems } from './utils';
interface DashboardsTreeProps {
items: DashboardsTreeItem[];
width: number;
height: number;
canSelect: boolean;
permissions: BrowseDashboardsPermissions;
isSelected: (kind: DashboardViewItem | '$all') => SelectionState;
onFolderClick: (uid: string, newOpenState: boolean) => void;
onAllSelectionChange: (newState: boolean) => void;
@@ -48,7 +54,7 @@ export function DashboardsTree({
onItemSelectionChange,
isItemLoaded,
requestLoadMore,
canSelect = false,
permissions,
}: DashboardsTreeProps) {
const treeID = useId();
@@ -94,10 +100,11 @@ export function DashboardsTree({
Header: t('browse-dashboards.dashboards-tree.tags-column', 'Tags'),
Cell: TagsCell,
};
const canSelect = canSelectItems(permissions);
const columns = [canSelect && checkboxColumn, nameColumn, tagsColumns].filter(isTruthy);
return columns;
}, [onFolderClick, canSelect]);
}, [onFolderClick, permissions]);
const table = useTable({ columns: tableColumns, data: items }, useCustomFlexLayout);
const { getTableProps, getTableBodyProps, headerGroups } = table;
@@ -109,10 +116,11 @@ export function DashboardsTree({
onAllSelectionChange,
onItemSelectionChange,
treeID,
permissions,
}),
// we need this to rerender if items changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[table, isSelected, onAllSelectionChange, onItemSelectionChange, items, treeID]
[table, isSelected, onAllSelectionChange, onItemSelectionChange, items, treeID, permissions]
);
const handleIsItemLoaded = useCallback(
@@ -156,7 +164,7 @@ export function DashboardsTree({
return (
<div key={key} {...headerProps} role="columnheader" className={styles.cell}>
{column.render('Header', { isSelected, onAllSelectionChange })}
{column.render('Header', { isSelected, onAllSelectionChange, permissions })}
</div>
);
})}
@@ -203,12 +211,13 @@ interface VirtualListRowProps {
onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange'];
onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange'];
treeID: string;
permissions: BrowseDashboardsPermissions;
};
}
function VirtualListRow({ index, style, data }: VirtualListRowProps) {
const styles = useStyles2(getStyles);
const { table, isSelected, onItemSelectionChange, treeID } = data;
const { table, isSelected, onItemSelectionChange, treeID, permissions } = data;
const { rows, prepareRow } = table;
const row = rows[index];
@@ -240,7 +249,7 @@ function VirtualListRow({ index, style, data }: VirtualListRowProps) {
return (
<div key={key} {...cellProps} className={styles.cell}>
{cell.render('Cell', { isSelected, onItemSelectionChange, treeID })}
{cell.render('Cell', { isSelected, onItemSelectionChange, treeID, permissions })}
</div>
);
})}
@@ -32,6 +32,7 @@ describe('browse-dashboards FolderActionsButton', () => {
canEditFolders: true,
canViewPermissions: true,
canSetPermissions: true,
canDeleteDashboards: true,
};
beforeEach(() => {
@@ -11,11 +11,14 @@ import { useDispatch, useSelector } from 'app/types/store';
import { useHasSelection } from '../state/hooks';
import { setAllSelection, setItemSelectionState } from '../state/slice';
import { BrowseDashboardsPermissions } from '../types';
import { canEditItemType, canSelectItems } from './utils';
interface SearchViewProps {
height: number;
width: number;
canSelect: boolean;
permissions: BrowseDashboardsPermissions;
searchState: SearchState;
searchStateManager: SearchStateManager;
emptyState?: ReactNode;
@@ -48,7 +51,7 @@ const initialLoadingView = {
export function SearchView({
width,
height,
canSelect,
permissions,
searchState,
searchStateManager: stateManager,
emptyState: emptyStateProp,
@@ -67,7 +70,12 @@ export function SearchView({
return false;
}
// Currently, this indicates _some_ items are selected, not nessicarily all are
// Check if user has permission to select this item type
if (!canEditItemType(kind, permissions)) {
return false;
}
// Currently, this indicates _some_ items are selected, not necessarily all are
// selected.
if (kind === '*' && uid === '*') {
return hasSelection;
@@ -78,7 +86,7 @@ export function SearchView({
return selectedItems[assertDashboardViewItemKind(kind)][uid] ?? false;
},
[selectedItems, hasSelection]
[selectedItems, hasSelection, permissions]
);
const clearSelection = useCallback(() => {
@@ -87,13 +95,17 @@ export function SearchView({
const handleItemSelectionChange = useCallback(
(kind: string, uid: string) => {
if (!canEditItemType(kind, permissions)) {
return; // Cannot select this item
}
const newIsSelected = !selectionChecker(kind, uid);
dispatch(
setItemSelectionState({ item: { kind: assertDashboardViewItemKind(kind), uid }, isSelected: newIsSelected })
);
},
[selectionChecker, dispatch]
[selectionChecker, dispatch, permissions]
);
if (value.totalRows === 0) {
@@ -113,6 +125,7 @@ export function SearchView({
return <div style={{ width }}>{emptyState}</div>;
}
const canSelect = canSelectItems(permissions);
const props: SearchResultsProps = {
response: value,
selection: canSelect ? selectionChecker : undefined,
@@ -6,7 +6,7 @@ import { DashboardViewItem } from 'app/features/search/types';
import { useChildrenByParentUIDState } from '../state/hooks';
import { findItem } from '../state/utils';
import { DashboardTreeSelection, DashboardViewItemWithUIItems } from '../types';
import { DashboardTreeSelection, DashboardViewItemWithUIItems, BrowseDashboardsPermissions } from '../types';
export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) {
return baseId + item.uid;
@@ -105,3 +105,18 @@ export function collectSelectedItems(
return targets;
}
export function canEditItemType(itemKind: string, permissions: BrowseDashboardsPermissions) {
const { canEditFolders, canDeleteFolders, canEditDashboards, canDeleteDashboards } = permissions;
return itemKind === 'folder'
? Boolean(canEditFolders || canDeleteFolders)
: Boolean(canEditDashboards || canDeleteDashboards);
}
export function canSelectItems(permissions: BrowseDashboardsPermissions) {
const { canEditFolders, canDeleteFolders, canEditDashboards, canDeleteDashboards } = permissions;
// Users can select items only if they have both edit and delete permissions for at least one item type
const canSelectFolders = canEditFolders || canDeleteFolders;
const canSelectDashboards = canEditDashboards || canDeleteDashboards;
return Boolean(canSelectFolders || canSelectDashboards);
}
@@ -20,6 +20,7 @@ export function getFolderPermissions(folderDTO?: FolderDTO) {
const canCreateDashboards = checkFolderPermission(AccessControlAction.DashboardsCreate, folderDTO);
const canCreateFolders = checkCanCreateFolders(folderDTO);
const canDeleteFolders = checkFolderPermission(AccessControlAction.FoldersDelete, folderDTO);
const canDeleteDashboards = checkFolderPermission(AccessControlAction.DashboardsDelete, folderDTO);
const canEditDashboards = checkFolderPermission(AccessControlAction.DashboardsWrite, folderDTO);
const canEditFolders = checkFolderPermission(AccessControlAction.FoldersWrite, folderDTO);
const canSetPermissions = checkFolderPermission(AccessControlAction.FoldersPermissionsWrite, folderDTO);
@@ -33,5 +34,6 @@ export function getFolderPermissions(folderDTO?: FolderDTO) {
canEditFolders,
canSetPermissions,
canViewPermissions,
canDeleteDashboards,
};
}
@@ -49,6 +49,7 @@ interface RendererUserProps {
onAllSelectionChange?: (newState: boolean) => void;
onItemSelectionChange?: (item: DashboardViewItem, newState: boolean) => void;
treeID?: string;
permissions?: BrowseDashboardsPermissions;
}
export type DashboardsTreeColumn = Column<DashboardsTreeItem>;
@@ -60,3 +61,10 @@ export enum SelectionState {
Selected,
Mixed,
}
export interface BrowseDashboardsPermissions {
canEditFolders: boolean;
canEditDashboards: boolean;
canDeleteFolders?: boolean;
canDeleteDashboards?: boolean;
}