From 0cb6f9584bf77a7dbdfcc6ce1032db2b4bdc15e2 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 23 May 2025 17:35:54 +0300 Subject: [PATCH] Restore dashboards: Re-enable FE feature toggle (#105820) * Add dashboardRestore toggle * Restore the toggle on FE * Add navtree item * Fix lint * Rename feature toggle * Use the renamed toggle --- .../src/types/featureToggles.gen.ts | 5 ++++ pkg/services/featuremgmt/registry.go | 8 +++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 +++ pkg/services/featuremgmt/toggles_gen.json | 29 +++++++++++++++++++ pkg/services/navtree/navtreeimpl/navtree.go | 9 ++++++ .../BrowseDashboardsPage.tsx | 21 +++++++------- .../BrowseActions/BrowseActions.tsx | 2 +- .../components/BrowseActions/DeleteModal.tsx | 4 +-- .../dashboard-scene/saving/shared.tsx | 4 +-- .../settings/DeleteDashboardButton.tsx | 6 ++-- .../DeleteDashboard/DeleteDashboardModal.tsx | 4 +-- .../SaveDashboard/SaveDashboardErrorProxy.tsx | 4 +-- public/app/routes/routes.tsx | 7 +++++ 14 files changed, 85 insertions(+), 23 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 64d213a904b..e2a79ca824f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1028,4 +1028,9 @@ export interface FeatureToggles { * Use proxy-based read-only objects for plugin extensions instead of deep cloning */ extensionsReadOnlyProxy?: boolean; + /** + * Enables restore deleted dashboards feature + * @default false + */ + restoreDashboards?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 94a41812b98..1bdb9bb20d3 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1767,6 +1767,14 @@ var ( HideFromDocs: true, FrontendOnly: true, }, + { + Name: "restoreDashboards", + Description: "Enables restore deleted dashboards feature", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + HideFromAdminPage: true, + Expression: "false", + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index fb9a6dddad9..5123d14872a 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -231,3 +231,4 @@ alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,fal alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true extensionsReadOnlyProxy,experimental,@grafana/plugins-platform-backend,false,false,true +restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index ea0db108a09..83f95f37f1e 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -934,4 +934,8 @@ const ( // FlagExtensionsReadOnlyProxy // Use proxy-based read-only objects for plugin extensions instead of deep cloning FlagExtensionsReadOnlyProxy = "extensionsReadOnlyProxy" + + // FlagRestoreDashboards + // Enables restore deleted dashboards feature + FlagRestoreDashboards = "restoreDashboards" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index fba14fd6db1..4702b756557 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -777,6 +777,21 @@ "frontend": true } }, + { + "metadata": { + "name": "dashboardRestore", + "resourceVersion": "1747744953711", + "creationTimestamp": "2024-05-16T17:36:26Z", + "deletionTimestamp": "2025-04-03T07:52:54Z" + }, + "spec": { + "description": "Enables deleted dashboard restore feature", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "hideFromAdminPage": true, + "expression": "false" + } + }, { "metadata": { "name": "dashboardScene", @@ -2897,6 +2912,20 @@ "expression": "true" } }, + { + "metadata": { + "name": "restoreDashboards", + "resourceVersion": "1748002635285", + "creationTimestamp": "2025-05-23T12:17:15Z" + }, + "spec": { + "description": "Enables restore deleted dashboards feature", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "hideFromAdminPage": true, + "expression": "false" + } + }, { "metadata": { "name": "rolePickerDrawer", diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 5f31b4b7205..327f74ea34c 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -399,6 +399,15 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt Icon: "library-panel", }) } + + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagRestoreDashboards) && (c.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) { + dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ + Text: "Recently deleted", + SubTitle: "Any items listed here for more than 30 days will be automatically deleted.", + Id: "dashboards/recently-deleted", + Url: s.cfg.AppSubURL + "/dashboard/recently-deleted", + }) + } } if hasAccess(ac.EvalPermission(dashboards.ActionDashboardsCreate)) { diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index e52accf33ad..15fd546a36b 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -6,7 +6,7 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; @@ -137,16 +137,15 @@ const BrowseDashboardsPage = memo(() => { renderTitle={renderTitle} actions={ <> - {false && - hasAdminRights && ( // TODO: change this to a feature flag when dashboard restore is reworked - - Recently deleted - - )} + {config.featureToggles.restoreDashboards && hasAdminRights && ( + + Recently deleted + + )} {folderDTO && } {(canCreateDashboards || canCreateFolders) && ( - {false && ( // TODO: change this to a feature flag when dashboard restore is reworked + {config.featureToggles.restoreDashboards && ( <> diff --git a/public/app/features/dashboard-scene/saving/shared.tsx b/public/app/features/dashboard-scene/saving/shared.tsx index 0af0f89752c..e40fb6e1885 100644 --- a/public/app/features/dashboard-scene/saving/shared.tsx +++ b/public/app/features/dashboard-scene/saving/shared.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, useTranslate } from '@grafana/i18n'; -import { isFetchError } from '@grafana/runtime'; +import { config, isFetchError } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Alert, Box, Button, Stack } from '@grafana/ui'; @@ -42,7 +42,7 @@ export interface NameAlreadyExistsErrorProps { export function NameAlreadyExistsError({ cancelButton, saveButton }: NameAlreadyExistsErrorProps) { const { t } = useTranslate(); - const isRestoreDashboardsEnabled = false; + const isRestoreDashboardsEnabled = config.featureToggles.restoreDashboards; return isRestoreDashboardsEnabled ? (

diff --git a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx index 02c70bcc215..f1e22bce428 100644 --- a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx @@ -2,7 +2,7 @@ import { useAsyncFn, useToggle } from 'react-use'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, useTranslate } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal, Modal, Space, Text, TextLink } from '@grafana/ui'; import { useDeleteItemsMutation } from '../../browse-dashboards/api/browseDashboardsAPI'; @@ -34,7 +34,7 @@ export function DeleteDashboardButton({ dashboard }: ButtonProps) { dashboard: 1, }, source: 'dashboard_scene_settings', - restore_enabled: false, + restore_enabled: Boolean(config.featureToggles.restoreDashboards), }); toggleModal(); if (dashboard.state.uid) { @@ -83,7 +83,7 @@ export function DeleteDashboardModal({ dashboardTitle, onConfirm, onClose }: Del isOpen={true} body={ <> - {false && ( // TODO: re-enable when restore is reworked + {config.featureToggles.restoreDashboards && ( <> diff --git a/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx b/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx index e06b3e818a7..25f3871524e 100644 --- a/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx +++ b/public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import { Trans, useTranslate } from '@grafana/i18n'; -import { locationService, reportInteraction } from '@grafana/runtime'; +import { config, locationService, reportInteraction } from '@grafana/runtime'; import { Modal, Button, Text, Space, TextLink } from '@grafana/ui'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { cleanUpDashboardAndVariables } from 'app/features/dashboard/state/actions'; @@ -34,7 +34,7 @@ const DeleteDashboardModalUnconnected = ({ hideModal, cleanUpDashboardAndVariabl dashboard: 1, }, source: 'dashboard_settings', - restore_enabled: false, + restore_enabled: Boolean(config.featureToggles.restoreDashboards), }); await deleteItems({ selectedItems: { diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx index 827d127389b..5f7675b47da 100644 --- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, useTranslate } from '@grafana/i18n'; -import { FetchError } from '@grafana/runtime'; +import { config, FetchError } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Button, ConfirmModal, Modal, useStyles2 } from '@grafana/ui'; @@ -32,7 +32,7 @@ export const SaveDashboardErrorProxy = ({ }: SaveDashboardErrorProxyProps) => { const { onDashboardSave } = useDashboardSave(); const { t } = useTranslate(); - const isRestoreDashboardsEnabled = false; + const isRestoreDashboardsEnabled = config.featureToggles.restoreDashboards; return ( <> diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index c79d39a2bb5..249a12c4354 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -527,6 +527,13 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "BookmarksPage"*/ 'app/features/bookmarks/BookmarksPage') ), }, + config.featureToggles.restoreDashboards && { + path: '/dashboard/recently-deleted', + roles: () => ['Admin', 'ServerAdmin'], + component: SafeDynamicImport( + () => import(/* webpackChunkName: "RecentlyDeletedPage" */ 'app/features/browse-dashboards/RecentlyDeletedPage') + ), + }, { // Redirect the /femt dev page to the root path: '/femt',