Compare commits

...

4 Commits

Author SHA1 Message Date
Clarity-89
0821ad9beb Merge branch 'main' into restore-dashboards/delete-permissions 2026-01-14 12:10:00 +02:00
Clarity-89
9cb0a8fe93 Merge branch 'main' into restore-dashboards/delete-permissions 2026-01-08 09:46:53 +02:00
Clarity-89
8caa52ca0f Expand check 2026-01-05 16:25:49 +02:00
Clarity-89
eba4c8ecc2 Restore dashboards: Update permissions 2026-01-05 13:46:43 +02:00
7 changed files with 52 additions and 19 deletions

View File

@@ -408,7 +408,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
}
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagRestoreDashboards) && (c.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) {
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagRestoreDashboards) && hasAccess(ac.EvalPermission(dashboards.ActionDashboardsDelete)) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Recently deleted",
SubTitle: "Any items listed here for more than 30 days will be automatically deleted.",

View File

@@ -55,6 +55,13 @@ jest.mock('@grafana/runtime', () => {
};
});
jest.mock('@grafana/runtime/internal', () => {
return {
...jest.requireActual('@grafana/runtime/internal'),
evaluateBooleanFlag: jest.fn().mockReturnValue(false),
};
});
function render(ui: Parameters<typeof testRender>[0], options: Parameters<typeof testRender>[1] = {}) {
return testRender(ui, {
preloadedState: {
@@ -113,6 +120,30 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument();
});
it('shows the "Recently deleted" button when restore is enabled and user can delete dashboards', async () => {
const previousFlag = config.featureToggles.restoreDashboards;
config.featureToggles.restoreDashboards = true;
render(<BrowseDashboardsPage queryParams={{}} />);
await screen.findByPlaceholderText('Search for dashboards and folders');
expect(await screen.findByRole('link', { name: 'Recently deleted' })).toBeInTheDocument();
config.featureToggles.restoreDashboards = previousFlag;
});
it('does not show the "Recently deleted" button when user cannot delete dashboards', async () => {
const previousFlag = config.featureToggles.restoreDashboards;
config.featureToggles.restoreDashboards = true;
mockPermissions.canDeleteDashboards = false;
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
render(<BrowseDashboardsPage queryParams={{}} />);
await screen.findByPlaceholderText('Search for dashboards and folders');
expect(screen.queryByRole('link', { name: 'Recently deleted' })).not.toBeInTheDocument();
config.featureToggles.restoreDashboards = previousFlag;
});
it('does not show the "New" button if the user does not have permissions', async () => {
mockPermissions.canCreateDashboards = false;
mockPermissions.canCreateFolders = false;

View File

@@ -11,6 +11,7 @@ import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui';
import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks';
import { Page } from 'app/core/components/Page/Page';
import { getConfig } from 'app/core/config';
import { AccessControlAction } from 'app/types/accessControl';
import { useDispatch } from 'app/types/store';
import { FolderRepo } from '../../core/components/NestedFolderPicker/FolderRepo';
@@ -123,7 +124,6 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
canCreateDashboards,
canCreateFolders,
} = getFolderPermissions(folder);
const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
const isProvisionedFolder = folder?.managedBy === ManagerKind.Repo;
const showEditTitle = canEditFolders && folderUID && !isProvisionedFolder;
const permissions = {
@@ -174,15 +174,16 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
renderTitle={renderTitle}
actions={
<>
{config.featureToggles.restoreDashboards && hasAdminRights && (
<LinkButton
variant="secondary"
href={getConfig().appSubUrl + '/dashboard/recently-deleted'}
onClick={handleButtonClickToRecentlyDeleted}
>
<Trans i18nKey="browse-dashboards.actions.button-to-recently-deleted">Recently deleted</Trans>
</LinkButton>
)}
{config.featureToggles.restoreDashboards &&
(canDeleteDashboards || contextSrv.hasPermission(AccessControlAction.DashboardsDelete)) && (
<LinkButton
variant="secondary"
href={getConfig().appSubUrl + '/dashboard/recently-deleted'}
onClick={handleButtonClickToRecentlyDeleted}
>
<Trans i18nKey="browse-dashboards.actions.button-to-recently-deleted">Recently deleted</Trans>
</LinkButton>
)}
{folderDTO && <FolderActionsButton folder={folderDTO} repoType={repoType} isReadOnlyRepo={isReadOnlyRepo} />}
{(canCreateDashboards || canCreateFolders) && (
<CreateNewButton

View File

@@ -59,9 +59,9 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
<Text element="p">
<Trans i18nKey="browse-dashboards.action.delete-modal-restore-dashboards-text">
This action will delete the selected folders immediately. Deleted dashboards will be kept in the
history for up to 12 months and can be restored by your organization administrator during that time.
The history is limited to 1000 dashboards older ones may be removed sooner if the limit is reached.
Folders cannot be restored.
history for up to 12 months. Users with delete permissions can restore the dashboards they deleted,
and admins can restore any user's deleted dashboards. The history is limited to 1000 dashboards — older
ones may be removed sooner if the limit is reached. Folders cannot be restored.
</Trans>
</Text>
<Space v={2} />

View File

@@ -19,8 +19,8 @@ export const RecentlyDeletedEmptyState = ({ searchState }: RecentlyDeletedEmptyS
role="alert"
>
<Trans i18nKey={'recently-deleted.page.no-deleted-dashboards-text'}>
When you delete a dashboard, it will appear here for 30 days before being permanently deleted. Your organization
administrator can restore recently-deleted dashboards.
When you delete a dashboard, it will appear here for 30 days before being permanently deleted. Users with delete
permissions can restore the dashboards they deleted, and admins can restore any user's deleted dashboards.
</Trans>
</EmptyState>
);

View File

@@ -81,8 +81,9 @@ export function DeleteDashboardModal({ dashboardTitle, onConfirm, onClose }: Del
<Text element="p">
<Trans i18nKey="dashboard-settings.delete-modal-restore-dashboards-text">
This action will delete the dashboard. Deleted dashboards will be kept in the history for up to 12
months and can be restored by your organization administrator during that time. The history is limited
to 1000 dashboardsolder ones will be removed sooner if the limit is reached.
months. Users with delete permissions can restore the dashboards they deleted, and admins can restore
any user's deleted dashboards. The history is limited to 1000 dashboardsolder ones will be removed
sooner if the limit is reached.
</Trans>
</Text>
<Space v={1} />

View File

@@ -534,7 +534,7 @@ export function getAppRoutes(): RouteDescriptor[] {
},
config.featureToggles.restoreDashboards && {
path: '/dashboard/recently-deleted',
roles: () => ['Admin', 'ServerAdmin'],
roles: () => contextSrv.evaluatePermission([AccessControlAction.DashboardsDelete]),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "RecentlyDeletedPage" */ 'app/features/browse-dashboards/RecentlyDeletedPage')
),