Repository: grafana/grafana
Pull request #101869: Alerting: Recover deleted alert rules
Author: login: soniaAguilarPeiron
Labels: name: area/alerting
description: Grafana Alerting
name: area/backend
name: area/frontend
name: add to changelog
name: no-backport
description: Skip backport of PR
name: add to what's new
Created: 2025-03-10T10:46:57Z | Merged: 2025-03-27T14:39:27Z

**What is this feature?**

Currently, alert rules are permanently deleted. 
We recently introduced a soft-delete feature in the BE (protected by a feature toggle) => all alert rules are soft-deleted when the ff is enabled.

This PR allows restoring deleted rules when using this new soft-delete..
The feature **is only available for Grafana-managed rules, and only for admin users.**

This UI feature is enabled by default by the `alertingRuleRecoverDeleted` feature toggle.
And it's only available if soft delete is enabled (`alertRuleRestore` feature toggle)

It depends on this [PR](https://github.com/grafana/grafana/pull/101429) to be merged

**Why do we need this feature?**

Recovering deleted alert rules can be super valuable for admin users.

**Who is this feature for?**

Only admin users.

**Which issue(s) does this PR fix?**:

Fixes https://github.com/grafana/alerting-squad/issues/937?issue=grafana%7Calerting-squad%7C1047

**Special notes for your reviewer:**

https://github.com/user-attachments/assets/d34b5453-f540-4399-94f4-76949d6c3c47

**Updated the naming from trash => recently deleted**

<img width="711" alt="Screenshot 2025-03-24 at 12 09 53" src="https://github.com/user-attachments/assets/37dc8bbf-218b-4ce1-a6a8-e864ecf7b983" />


<img width="1032" alt="Screenshot 2025-03-24 at 12 10 00" src="https://github.com/user-attachments/assets/c8145700-e0e2-4952-bd8f-30f092112109" />


Please check that:
- [ ] It works as expected from a user's perspective.
- [ ] If this is a pre-GA feature, it is behind a feature toggle.
- [ ] The docs are updated, and if this is a [notable improvement](https://grafana.com/docs/writers-toolkit/contribute/release-notes/#how-to-determine-if-content-belongs-in-whats-new), it's added to our [What's New](https://grafana.com/docs/writers-toolkit/contribute/release-notes/) doc.



Diff:
diff --git a/.betterer.results b/.betterer.results
index c7f5454900f67..5c91eb8941a87 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -2041,14 +2041,6 @@ exports[`better eslint`] = {
     "public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx:5381": [
       [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
     ],
-    "public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx:5381": [
-      [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
-      [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
-      [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
-      [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
-      [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
-      [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"]
-    ],
     "public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx:5381": [
       [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
     ],
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 59699d7de9180..4ed992d0956e2 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -1048,4 +1048,9 @@ export interface FeatureToggles {
   * Enables the unified storage grpc connection pool
   */
   unifiedStorageGrpcConnectionPool?: boolean;
+  /**
+  * Enables the UI functionality to recover and view deleted alert rules
+  * @default true
+  */
+  alertingRuleRecoverDeleted?: boolean;
 }
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 49d12b76675a4..e651f09a6ee5f 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1806,6 +1806,16 @@ var (
 			HideFromAdminPage: true,
 			HideFromDocs:      true,
 		},
+		{
+			Name:              "alertingRuleRecoverDeleted",
+			Description:       "Enables the UI functionality to recover and view deleted alert rules",
+			FrontendOnly:      true,
+			Stage:             FeatureStageGeneralAvailability,
+			Owner:             grafanaAlertingSquad,
+			HideFromAdminPage: true,
+			HideFromDocs:      true,
+			Expression:        "true", // enabled by default
+		},
 	}
 )
 
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index b9f364935e623..a6c70e9389efb 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -238,3 +238,4 @@ noBackdropBlur,experimental,@grafana/grafana-frontend-platform,false,false,true
 alertingMigrationUI,experimental,@grafana/alerting-squad,false,false,true
 unifiedStorageHistoryPruner,experimental,@grafana/search-and-storage,false,false,false
 unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false
+alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,true
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 058e30627c48a..8917e5b6ebfa6 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -962,4 +962,8 @@ const (
 	// FlagUnifiedStorageGrpcConnectionPool
 	// Enables the unified storage grpc connection pool
 	FlagUnifiedStorageGrpcConnectionPool = "unifiedStorageGrpcConnectionPool"
+
+	// FlagAlertingRuleRecoverDeleted
+	// Enables the UI functionality to recover and view deleted alert rules
+	FlagAlertingRuleRecoverDeleted = "alertingRuleRecoverDeleted"
 )
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 75d8edd849d4f..34c64774a3a9e 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -437,6 +437,25 @@
         "expression": "false"
       }
     },
+    {
+      "metadata": {
+        "name": "alertingRuleRecoverDeleted",
+        "resourceVersion": "1741351039908",
+        "creationTimestamp": "2025-03-07T09:25:14Z",
+        "annotations": {
+          "grafana.app/updatedTimestamp": "2025-03-07 12:37:19.908258 +0000 UTC"
+        }
+      },
+      "spec": {
+        "description": "Enables the UI functionality to recover and view deleted alert rules",
+        "stage": "GA",
+        "codeowner": "@grafana/alerting-squad",
+        "frontend": true,
+        "hideFromAdminPage": true,
+        "hideFromDocs": true,
+        "expression": "true"
+      }
+    },
     {
       "metadata": {
         "name": "alertingRuleVersionHistoryRestore",
diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go
index e859596520f48..1016b4373cb66 100644
--- a/pkg/services/navtree/navtreeimpl/navtree.go
+++ b/pkg/services/navtree/navtreeimpl/navtree.go
@@ -489,6 +489,14 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
 			})
 		}
 	}
+	if c.SignedInUser.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) {
+		alertChildNavs = append(alertChildNavs, &navtree.NavLink{
+			Text:     "Recently deleted",
+			SubTitle: "Any items listed here for more than 30 days will be automatically deleted.",
+			Id:       "alerts/recently-deleted",
+			Url:      s.cfg.AppSubURL + "/alerting/recently-deleted",
+		})
+	}
 
 	if c.SignedInUser.GetOrgRole() == org.RoleAdmin {
 		alertChildNavs = append(alertChildNavs, &navtree.NavLink{
diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts
index eb533d88b07f5..b278a27f7c0f7 100644
--- a/public/app/core/utils/navBarItem-translations.ts
+++ b/public/app/core/utils/navBarItem-translations.ts
@@ -89,6 +89,8 @@ export function getNavTitle(navId: string | undefined) {
       return t('nav.alerting-groups.title', 'Active notifications');
     case 'alerting-admin':
       return t('nav.alerting-admin.title', 'Settings');
+    case 'alerts/recently-deleted':
+      return t('nav.alerts-recently-deleted.title', 'Recently deleted');
     case 'cfg':
       return t('nav.config.title', 'Administration');
     case 'cfg/general':
@@ -253,6 +255,8 @@ export function getNavSubTitle(navId: string | undefined) {
       return t('nav.alerting-silences.subtitle', 'Stop notifications from one or more alerting rules');
     case 'groups':
       return t('nav.alerting-groups.subtitle', 'See grouped alerts with active notifications');
+    case 'alerts/recently-deleted':
+      return t('nav.alerts-recently-deleted.subtitle', 'See recently deleted alert rules');
     case 'datasources':
       return t('nav.datasources.subtitle', 'Add and configure data sources');
     case 'correlations':
diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx
index e0cb793b65584..1bd1159efd90a 100644
--- a/public/app/features/alerting/routes.tsx
+++ b/public/app/features/alerting/routes.tsx
@@ -1,3 +1,5 @@
+import { Navigate } from 'react-router-dom-v5-compat';
+
 import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
 import { config } from 'app/core/config';
 import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/types';
@@ -13,6 +15,7 @@ import {
   PERMISSIONS_NOTIFICATION_POLICIES_READ,
 } from './unified/components/notification-policies/permissions';
 import { PERMISSIONS_TEMPLATES } from './unified/components/templates/permissions';
+import { shouldAllowRecoveringDeletedRules } from './unified/featureToggles';
 import { evaluateAccess } from './unified/utils/access-control';
 
 export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
@@ -209,6 +212,18 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
           )
       ),
     },
+    {
+      path: '/alerting/recently-deleted/',
+      roles: () => ['Admin'],
+      component: shouldAllowRecoveringDeletedRules()
+        ? importAlertingComponent(
+            () =>
+              import(
+                /* webpackChunkName: "RecentlyDeleted" */ 'app/features/alerting/unified/components/rules/deleted-rules/DeletedRulesPage'
+              )
+          )
+        : () => <Navigate replace to="/alerting/list" />,
+    },
     {
       path: '/alerting/new/:type?',
       pageClass: 'page-alerting',
diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts
index c209ec34249c2..415d0bd140808 100644
--- a/public/app/features/alerting/unified/Analytics.ts
+++ b/public/app/features/alerting/unified/Analytics.ts
@@ -217,6 +217,14 @@ export const trackRuleVersionsRestoreFail = async (
   reportInteraction('grafana_alerting_rule_versions_restore_error', { ...payload });
 };
 
+export const trackDeletedRuleRestoreSuccess = async () => {
+  reportInteraction('grafana_alerting_deleted_rule_restore_success');
+};
+
+export const trackDeletedRuleRestoreFail = async () => {
+  reportInteraction('grafana_alerting_deleted_rule_restore_error');
+};
+
 interface RulesSearchInteractionPayload {
   filter: string;
   triggeredBy: 'typing' | 'component';
diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts
index 080ea10fe2f8d..4eefa3b8b67b2 100644
--- a/public/app/features/alerting/unified/api/alertRuleApi.ts
+++ b/public/app/features/alerting/unified/api/alertRuleApi.ts
@@ -13,10 +13,12 @@ import {
   AlertQuery,
   Annotations,
   GrafanaAlertStateDecision,
+  GrafanaRuleDefinition,
   Labels,
   PostableRulerRuleGroupDTO,
   PromRulesResponse,
   RulerGrafanaRuleDTO,
+  RulerGrafanaRulesConfigDTO,
   RulerRuleGroupDTO,
   RulerRulesConfigDTO,
 } from 'app/types/unified-alerting-dto';
@@ -283,7 +285,11 @@ export const alertRuleApi = alertingApi.injectEndpoints({
 
     deleteRuleGroupFromNamespace: build.mutation<
       RulerRuleGroupDTO,
-      WithNotificationOptions<{ rulerConfig: RulerDataSourceConfig; namespace: string; group: string }>
+      WithNotificationOptions<{
+        rulerConfig: RulerDataSourceConfig;
+        namespace: string;
+        group: string;
+      }>
     >({
       query: ({ rulerConfig, namespace, group, notificationOptions }) => {
         const successMessage = t('alerting.rule-groups.delete.success', 'Successfully deleted rule group');
@@ -302,6 +308,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({
       invalidatesTags: (_result, _error, { namespace, group, rulerConfig }) => [
         { type: 'RuleGroup', id: `${rulerConfig.dataSourceUid}/${namespace}/${group}` },
         { type: 'RuleNamespace', id: `${rulerConfig.dataSourceUid}/${namespace}` },
+        'DeletedRules',
       ],
     }),
 
@@ -339,6 +346,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({
             { type: 'GrafanaRulerRule', id: rule.grafana_alert.uid } as const,
             { type: 'GrafanaRulerRuleVersion', id: rule.grafana_alert.uid } as const,
           ]),
+          'DeletedRules',
         ];
       },
     }),
@@ -413,5 +421,17 @@ export const alertRuleApi = alertingApi.injectEndpoints({
       }),
       keepUnusedDataFor: 0,
     }),
+    getDeletedRules: build.query<Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>, {}>({
+      query: () => ({
+        url: `/api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rules/`,
+        params: { deleted: 'true' },
+      }),
+      transformResponse: (response: RulerGrafanaRulesConfigDTO) => {
+        const values = Object.values(response);
+        const deletedRules = values.length > 0 ? values[0][0]?.rules : [];
+        return deletedRules;
+      },
+      providesTags: ['DeletedRules'],
+    }),
   }),
 });
diff --git a/public/app/features/alerting/unified/api/alertingApi.ts b/public/app/features/alerting/unified/api/alertingApi.ts
index ae45cc7678d0b..e3c28894ea51d 100644
--- a/public/app/features/alerting/unified/api/alertingApi.ts
+++ b/public/app/features/alerting/unified/api/alertingApi.ts
@@ -129,6 +129,7 @@ export const alertingApi = createApi({
     'ContactPoint',
     'ContactPointsStatus',
     'Receiver',
+    'DeletedRules',
   ],
   endpoints: () => ({}),
 });
diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
index f7a1237ae9f44..afb6fe0c30c3f 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
@@ -1,6 +1,7 @@
 import { AppEvents } from '@grafana/data';
 import { ComponentSize, Dropdown, Menu } from '@grafana/ui';
 import appEvents from 'app/core/app_events';
+import { t } from 'app/core/internationalization';
 import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule';
 import MoreButton from 'app/features/alerting/unified/components/MoreButton';
 import { useRulePluginLinkExtension } from 'app/features/alerting/unified/plugins/useRulePluginLinkExtensions';
@@ -89,15 +90,33 @@ const AlertRuleMenu = ({
       {canPause && rulerRuleType.grafana.rule(rulerRule) && groupIdentifier.groupOrigin === 'grafana' && (
         <MenuItemPauseRule rule={rulerRule} groupIdentifier={groupIdentifier} onPauseChange={onPauseChange} />
       )}
-      {canSilence && <Menu.Item label="Silence notifications" icon="bell-slash" onClick={handleSilence} />}
+      {canSilence && (
+        <Menu.Item
+          label={t('alerting.alert-menu.silence-notifications', 'Silence notifications')}
+          icon="bell-slash"
+          onClick={handleSilence}
+        />
+      )}
       {/* TODO Migrate Declare Incident to plugin links extensions */}
       {shouldShowDeclareIncidentButton && <DeclareIncidentMenuItem title={promRule.name} url={''} />}
-      {canDuplicate && <Menu.Item label="Duplicate" icon="copy" onClick={() => handleDuplicateRule(identifier)} />}
+      {canDuplicate && (
+        <Menu.Item
+          label={t('alerting.alert-menu.duplicate', 'Duplicate')}
+          icon="copy"
+          onClick={() => handleDuplicateRule(identifier)}
+        />
+      )}
       {showDivider && <Menu.Divider />}
-      {shareUrl && <Menu.Item label="Copy link" icon="share-alt" onClick={() => copyToClipboard(shareUrl)} />}
+      {shareUrl && (
+        <Menu.Item
+          label={t('alerting.alert-menu.copy-link', 'Copy link')}
+          icon="share-alt"
+          onClick={() => copyToClipboard(shareUrl)}
+        />
+      )}
       {canExport && (
         <Menu.Item
-          label="Export"
+          label={t('alerting.alert-menu.export', 'Export')}
           icon="download-alt"
           childItems={[<ExportMenuItem key="export-with-modifications" identifier={identifier} />]}
         />
@@ -114,7 +133,7 @@ const AlertRuleMenu = ({
         <>
           <Menu.Divider />
           <Menu.Item
-            label="Delete"
+            label={t('alerting.common.delete', 'Delete')}
             icon="trash-alt"
             destructive
             onClick={() => handleDelete(rulerRule, groupIdentifier)}
@@ -144,7 +163,14 @@ const ExportMenuItem = ({ identifier }: ExportMenuItemProps) => {
     }
   );
 
-  return <Menu.Item key="with-modifications" label="With modifications" icon="file-edit-alt" url={url} />;
+  return (
+    <Menu.Item
+      key="with-modifications"
+      label={t('alerting.alert-menu.with-modifications', 'With modifications')}
+      icon="file-edit-alt"
+      url={url}
+    />
+  );
 };
 
 function copyToClipboard(text: string) {
diff --git a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
index c408a4fc37e68..c860823da5364 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
@@ -2,10 +2,11 @@ import { useCallback, useMemo, useState } from 'react';
 
 import { locationService } from '@grafana/runtime';
 import { ConfirmModal } from '@grafana/ui';
+import { t } from 'app/core/internationalization';
 import { dispatch } from 'app/store/store';
 import { EditableRuleIdentifier, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
 
-import { shouldUsePrometheusRulesPrimary } from '../../featureToggles';
+import { shouldAllowRecoveringDeletedRules, shouldUsePrometheusRulesPrimary } from '../../featureToggles';
 import { useDeleteRuleFromGroup } from '../../hooks/ruleGroup/useDeleteRuleFromGroup';
 import { usePrometheusConsistencyCheck } from '../../hooks/usePrometheusConsistencyCheck';
 import { fetchPromAndRulerRulesAction, fetchRulerRulesAction } from '../../state/actions';
@@ -25,6 +26,7 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
   const [ruleToDelete, setRuleToDelete] = useState<DeleteRuleInfo>();
   const [deleteRuleFromGroup] = useDeleteRuleFromGroup();
   const { waitForRemoval } = usePrometheusConsistencyCheck();
+  const isSoftDeleteEnabled = shouldAllowRecoveringDeletedRules();
 
   const dismissModal = useCallback(() => {
     setRuleToDelete(undefined);
@@ -68,15 +70,25 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
     () => (
       <ConfirmModal
         isOpen={Boolean(ruleToDelete)}
-        title="Delete rule"
-        body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
+        title={t('alerting.delete-rule-modal.title', 'Delete rule')}
+        body={
+          isSoftDeleteEnabled
+            ? t(
+                'alerting.delete-rule-modal.with-soft-delete',
+                'Are you sure you want to delete this rule? This rule will be recoverable from the Recently deleted page by a user with an admin role.'
+              )
+            : t(
+                'alerting.delete-rule-modal.without-soft-delete',
+                'Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?'
+              )
+        }
         confirmText="Yes, delete"
         icon="exclamation-triangle"
         onConfirm={deleteRule}
         onDismiss={dismissModal}
       />
     ),
-    [ruleToDelete, deleteRule, dismissModal]
+    [ruleToDelete, deleteRule, dismissModal, isSoftDeleteEnabled]
   );
 
   return [modal, showModal, dismissModal];
diff --git a/public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx b/public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
new file mode 100644
index 0000000000000..ad8846052d6aa
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
@@ -0,0 +1,167 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerGrafanaRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deleted-rules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deleted-rules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deleted-rules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    await redirectToRestoreForm(ruleToRestore);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deleted-rules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePreview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.deleted-rules.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule by editing the rule and saving it.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
+      onDismiss={onDismiss}
+    />
+  );
+};
+
+function RulePreview({ rule }: { rule: RulerRuleDTO }) {
+  const styles = useStyles2(getStyles);
+  return (
+    <div className={styles.content}>
+      <CodeEditor
+        width="100%"
+        height={600}
+        language={'json'}
+        value={JSON.stringify(rule, null, 4)}
+        monacoOptions={{
+          minimap: {
+            enabled: false,
+          },
+          scrollBeyondLastLine: false,
+          lineNumbers: 'on',
+          readOnly: true,
+        }}
+      />
+    </div>
+  );
+}
+
+const getStyles = () => ({
+  content: css({
+    flex: '1 1 100%',
+  }),
+  modal: css({
+    width: '700px',
+  }),
+});
+
+export function useRestoreDeletedRule() {
+  const [addRuleToRuleGroup] = useAddRuleToRuleGroup();
+
+  return useAsync(async (deletedRule: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
+    const ruleGroupIdentifier = {
+      dataSourceName: GRAFANA_RULES_SOURCE_NAME,
+      namespaceName: deletedRule.grafana_alert.namespace_uid,
+      groupName: deletedRule.grafana_alert.rule_group,
+    };
+    // save the new rule to the rule group
+    return addRuleToRuleGroup.execute(ruleGroupIdentifier, deletedRule);
+  });
+}
+
+const redirectToRestoreForm = async (ruleToRecover: RulerGrafanaRuleDTO) => {
+  let formValues: Partial<RuleFormValues> | undefined;
+  const namespaceName = await backendSrv
+    .getFolderByUid(ruleToRecover.grafana_alert.namespace_uid)
+    .then((folder) => folder.title);
+
+  try {
+    formValues = grafanaRuleDtoToFormValues(ruleToRecover, namespaceName);
+  } catch (err) {
+    const message = `Error getting rule values from the deleted rule: ${getMessageFromError(err)}`;
+    throw new Error(message);
+  }
+
+  const urlPath = rulerRuleType.grafana.recordingRule(ruleToRecover)
+    ? '/alerting/new/grafana-recording'
+    : '/alerting/new';
+
+  const ruleFormUrl = createRelativeUrl(urlPath, {
+    isManualRestore: 'true',
+    defaults: JSON.stringify(formValues),
+    returnTo: location.pathname + location.search,
+  });
+
+  locationService.push(ruleFormUrl);
+};
diff --git a/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.test.tsx b/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.test.tsx
new file mode 100644
index 0000000000000..04e3cb8701517
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.test.tsx
@@ -0,0 +1,53 @@
+import { render, screen } from 'test/test-utils';
+
+import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
+import { DashboardSearchItemType } from 'app/features/search/types';
+import { AccessControlAction } from 'app/types/accessControl';
+
+import { setupMswServer } from '../../../mockApi';
+import { mockFolder } from '../../../mocks';
+import { grafanaRulerRule } from '../../../mocks/grafanaRulerApi';
+import { setFolderResponse } from '../../../mocks/server/configure';
+import { grantPermissionsHelper } from '../../../test/test-utils';
+
+import { DeletedRules } from './DeletedRules';
+
+setupMswServer();
+describe('render Deleted rules page', () => {
+  grantPermissionsHelper([
+    AccessControlAction.AlertingRuleCreate,
+    AccessControlAction.AlertingRuleRead,
+    AccessControlAction.AlertingRuleUpdate,
+    AccessControlAction.AlertingRuleDelete,
+    AccessControlAction.AlertingInstanceCreate,
+  ]);
+  it('should show recently deleted rules, and restore button', async () => {
+    const folder = {
+      title: 'Folder A',
+      uid: grafanaRulerRule.grafana_alert.namespace_uid,
+      id: 1,
+      type: DashboardSearchItemType.DashDB,
+      accessControl: {
+        [AccessControlAction.AlertingRuleUpdate]: true,
+      },
+    };
+    setFolderResponse(mockFolder(folder));
+
+    const { user } = render(
+      <>
+        <AppNotificationList />
+        <DeletedRules deletedRules={[grafanaRulerRule]} />
+      </>
+    );
+    expect(screen.getByText('Grafana-rule')).toBeInTheDocument();
+
+    const restoreButtons = screen.getAllByRole('button', { name: /restore/i });
+    await user.click(restoreButtons[0]);
+    expect(
+      screen.getByText(/are you sure you want to restore this deleted alert rule definition\?/i)
+    ).toBeInTheDocument();
+
+    await user.click(screen.getByText(/yes, restore deleted rule/i));
+    expect(await screen.findByRole('status')).toHaveTextContent('Rule added successfully');
+  });
+});
diff --git a/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx b/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
new file mode 100644
index 0000000000000..fde47a4a4721a
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
@@ -0,0 +1,137 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, EmptyState, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>;
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deleted-rules.unknown', 'Unknown');
+
+  if (deletedRules.length === 0) {
+    return (
+      <EmptyState
+        message={t('alerting.deleted-rules.empty-state-title', 'No recently deleted rules found')}
+        variant="not-found"
+      />
+    );
+  }
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find((rule) => getRowId(rule.grafana_alert) === id);
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deleted-rules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deleted-rules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deleted-rules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.namespace_uid;
+      },
+    },
+    {
+      id: 'group',
+      header: t('alerting.deleted-rules.table.group', 'Group'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.rule_group;
+      },
+    },
+    {
+      id: 'created',
+      header: t('alerting.deleted-rules.table.updated', 'Deletion Date'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        const value = row.original.grafana_alert.updated;
+        if (!value) {
+          return unknown;
+        }
+        return dateTimeFormat(value) + ' (' + dateTimeFormatTimeAgo(value) + ')';
+      },
+    },
+    {
+      id: 'actions',
+      disableGrow: true,
+      cell: ({ row }) => {
+        return (
+          <Stack direction="row" alignItems="center" justifyContent="flex-end">
+            <Button
+              variant="secondary"
+              size="sm"
+              icon="history"
+              onClick={() => {
+                showConfirmation(getRowId(row.original.grafana_alert));
+              }}
+            >
+              <Trans i18nKey="alerting.deleted-rules.restore">Restore</Trans>
+            </Button>
+          </Stack>
+        );
+      },
+    },
+  ];
+
+  return (
+    <>
+      <InteractiveTable
+        pageSize={DELETED_RULES_PAGE_SIZE}
+        columns={columns}
+        data={deletedRules}
+        getRowId={(row) => {
+          return getRowId(row.grafana_alert);
+        }}
+      />
+      <ConfirmRestoreDeletedRuleModal
+        ruleToRestore={restoreRule}
+        isOpen={confirmRestore}
+        onDismiss={hideConfirmation}
+        onRestoreSucess={trackDeletedRuleRestoreSuccess}
+        onRestoreError={trackDeletedRuleRestoreFail}
+      />
+    </>
+  );
+}
+
+function getRowId(row: GrafanaRuleDefinition) {
+  return row.guid || row.uid;
+}
diff --git a/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRulesPage.tsx b/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRulesPage.tsx
new file mode 100644
index 0000000000000..07f3807ff40ca
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRulesPage.tsx
@@ -0,0 +1,36 @@
+import { Alert } from '@grafana/ui';
+import { t } from 'app/core/internationalization';
+
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { withPageErrorBoundary } from '../../../withPageErrorBoundary';
+import { AlertingPageWrapper } from '../../AlertingPageWrapper';
+
+import { DeletedRules } from './DeletedRules';
+
+function DeletedrulesPage() {
+  const {
+    currentData = [],
+    isLoading,
+    error,
+  } = alertRuleApi.endpoints.getDeletedRules.useQuery({
+    rulerConfig: GRAFANA_RULER_CONFIG,
+    filter: {}, // todo: add filters, and limit?????
+  });
+
+  return (
+    <AlertingPageWrapper navId="alerts/recently-deleted" isLoading={isLoading}>
+      <>
+        {error && (
+          <Alert title={t('alerting.deleted-rules.errorloading', 'Failed to load alert deleted rules')}>
+            {stringifyErrorLike(error)}
+          </Alert>
+        )}
+        <DeletedRules deletedRules={currentData} />
+      </>
+    </AlertingPageWrapper>
+  );
+}
+
+export default withPageErrorBoundary(DeletedrulesPage);
diff --git a/public/app/features/alerting/unified/components/rules/deleted-rules/useRestoreDeletedRule.ts b/public/app/features/alerting/unified/components/rules/deleted-rules/useRestoreDeletedRule.ts
new file mode 100644
index 0000000000000..2fa1477c7136a
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/deleted-rules/useRestoreDeletedRule.ts
@@ -0,0 +1,19 @@
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+
+export function useRestoreDeletedRule() {
+  const [addRuleToRuleGroup] = useAddRuleToRuleGroup();
+
+  return useAsync(async (deletedRule: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
+    const ruleGroupIdentifier = {
+      dataSourceName: GRAFANA_RULES_SOURCE_NAME,
+      namespaceName: deletedRule.grafana_alert.namespace_uid,
+      groupName: deletedRule.grafana_alert.rule_group,
+    };
+    // save the new rule to the rule group
+    return addRuleToRuleGroup.execute(ruleGroupIdentifier, deletedRule);
+  });
+}
diff --git a/public/app/features/alerting/unified/featureToggles.ts b/public/app/features/alerting/unified/featureToggles.ts
index 3f35bb2479e7d..659f4ec703b2a 100644
--- a/public/app/features/alerting/unified/featureToggles.ts
+++ b/public/app/features/alerting/unified/featureToggles.ts
@@ -1,6 +1,11 @@
 import { config } from '@grafana/runtime';
 
+import { isAdmin } from './utils/misc';
+
 export const shouldUsePrometheusRulesPrimary = () => config.featureToggles.alertingPrometheusRulesPrimary ?? false;
 
 export const useGrafanaManagedRecordingRulesSupport = () =>
   config.unifiedAlerting.recordingRulesEnabled && config.featureToggles.grafanaManagedRecordingRules;
+
+export const shouldAllowRecoveringDeletedRules = () =>
+  (isAdmin() && config.featureToggles.alertingRuleRecoverDeleted && config.featureToggles.alertRuleRestore) ?? false;
diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx
index 5c1859b58653c..5ee31b47542a8 100644
--- a/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx
+++ b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx
@@ -47,7 +47,7 @@ const getPageNav = (identifier?: RuleIdentifier, type?: RuleEditorPathParams['ty
 
 const RuleEditor = () => {
   const { identifier, type } = useRuleEditorPathParams();
-  const { copyFromIdentifier, queryDefaults } = useRuleEditorQueryParams();
+  const { copyFromIdentifier, queryDefaults, isManualRestore } = useRuleEditorQueryParams();
 
   const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess();
 
@@ -68,8 +68,16 @@ const RuleEditor = () => {
       return <CloneRuleEditor sourceRuleId={copyFromIdentifier} />;
     }
     // new alert rule
-    return <AlertRuleForm prefill={queryDefaults} />;
-  }, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, identifier, queryDefaults]);
+    return <AlertRuleForm prefill={queryDefaults} isManualRestore={isManualRestore} />;
+  }, [
+    canCreateCloudRules,
+    canCreateGrafanaRules,
+    canEditRules,
+    copyFromIdentifier,
+    identifier,
+    queryDefaults,
+    isManualRestore,
+  ]);
 
   return (
     <AlertingPageWrapper navId="alert-list" pageNav={getPageNav(identifier, type)}>
@@ -97,6 +105,7 @@ function useRuleEditorQueryParams() {
   const [searchParams] = useURLSearchParams();
   const copyFromId = searchParams.get('copyFrom') ?? undefined;
   const copyFromIdentifier = ruleId.tryParse(copyFromId);
+  const isManualRestore = searchParams.has('isManualRestore');
 
   const ruleType = translateRouteParamToRuleType(type);
 
@@ -104,5 +113,5 @@ function useRuleEditorQueryParams() {
     ? formValuesFromQueryParams(searchParams.get('defaults') ?? '', ruleType)
     : undefined;
 
-  return { copyFromIdentifier, queryDefaults };
+  return { copyFromIdentifier, queryDefaults, isManualRestore };
 }
diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
index 94888d4c9b132..9505c96fd2386 100644
--- a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
+++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
@@ -7,12 +7,16 @@ import { byRole } from 'testing-library-selector';
 import { contextSrv } from 'app/core/services/context_srv';
 import { setupMswServer } from 'app/features/alerting/unified/mockApi';
 import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
+import { DashboardSearchItemType } from 'app/features/search/types';
 import { AccessControlAction } from 'app/types';
 
-import { grantUserPermissions, mockDataSource } from '../mocks';
-import { grafanaRulerGroup } from '../mocks/grafanaRulerApi';
+import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks';
+import { grafanaRulerGroup, grafanaRulerRule } from '../mocks/grafanaRulerApi';
+import { setFolderResponse } from '../mocks/server/configure';
 import { captureRequests, serializeRequests } from '../mocks/server/events';
 import { setupDataSources } from '../testSetup/datasources';
+import { Annotation } from '../utils/constants';
+import { grafanaRuleDtoToFormValues } from '../utils/rule-form';
 
 jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
   AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) => <div>{actions}</div>,
@@ -104,4 +108,36 @@ describe('RuleEditor grafana managed rules', () => {
     await user.click(ui.buttons.preview.get());
     expect(await screen.findByText(/you cannot use time series data as an alert condition/i)).toBeInTheDocument();
   });
+  it('can restore grafana managed alert when isManualRestore is passed as query param', async () => {
+    const folder = {
+      title: 'Folder A',
+      uid: grafanaRulerRule.grafana_alert.namespace_uid,
+      id: 1,
+      type: DashboardSearchItemType.DashDB,
+      accessControl: {
+        [AccessControlAction.AlertingRuleUpdate]: true,
+      },
+    };
+    setFolderResponse(mockFolder(folder));
+    const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
+    const grafanaRuleJson = JSON.stringify(grafanaRuleDtoToFormValues(grafanaRulerRule, folder.title));
+
+    const { user } = renderRuleEditor(undefined, undefined, grafanaRuleJson); // isManualRestore=true
+
+    // check that it's filled in
+    const nameInput = await ui.inputs.name.find();
+    expect(nameInput).toHaveValue(grafanaRulerRule.grafana_alert.title);
+    //check that folder is in the list
+    expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
+    expect(ui.inputs.annotationValue(0).get()).toHaveValue(grafanaRulerRule.annotations[Annotation.summary]);
+
+    expect(ui.manualRestoreBanner.get()).toBeInTheDocument(); // check that manual restore banner is shown
+
+    await user.click(ui.buttons.saveAndExit.get());
+
+    expect(await screen.findByRole('status')).toHaveTextContent('Rule added successfully');
+    const requests = await capture;
+    const serializedRequests = await serializeRequests(requests);
+    expect(serializedRequests).toMatchSnapshot();
+  });
 });
diff --git a/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap
index 821c074014f39..692484862f3f2 100644
--- a/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap
+++ b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap
@@ -166,3 +166,113 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1
   },
 ]
 `;
+
+exports[`RuleEditor grafana managed rules can restore grafana managed alert when isManualRestore is passed as query param 1`] = `
+[
+  {
+    "body": {
+      "interval": "1m",
+      "name": "grafana-group-1",
+      "rules": [
+        {
+          "annotations": {
+            "summary": "Test alert",
+          },
+          "for": "5m",
+          "grafana_alert": {
+            "condition": "A",
+            "data": [
+              {
+                "datasourceUid": "datasource-uid",
+                "model": {
+                  "datasource": {
+                    "type": "prometheus",
+                    "uid": "datasource-uid",
+                  },
+                  "expression": "vector(1)",
+                  "queryType": "alerting",
+                  "refId": "A",
+                },
+                "queryType": "alerting",
+                "refId": "A",
+                "relativeTimeRange": {
+                  "from": 1000,
+                  "to": 2000,
+                },
+              },
+            ],
+            "exec_err_state": "Error",
+            "is_paused": false,
+            "namespace_uid": "uuid020c61ef",
+            "no_data_state": "NoData",
+            "rule_group": "grafana-group-1",
+            "title": "Grafana-rule",
+            "uid": "4d7125fee983",
+          },
+          "labels": {
+            "region": "nasa",
+            "severity": "critical",
+          },
+        },
+        {
+          "annotations": {
+            "summary": "Test alert",
+          },
+          "for": "5m",
+          "grafana_alert": {
+            "condition": "A",
+            "data": [
+              {
+                "datasourceUid": "datasource-uid",
+                "model": {
+                  "datasource": {
+                    "type": "prometheus",
+                    "uid": "datasource-uid",
+                  },
+                  "expression": "vector(1)",
+                  "instant": true,
+                  "queryType": "alerting",
+                  "range": false,
+                  "refId": "A",
+                },
+                "queryType": "alerting",
+                "refId": "A",
+                "relativeTimeRange": {
+                  "from": 1000,
+                  "to": 2000,
+                },
+              },
+            ],
+            "exec_err_state": "Error",
+            "is_paused": false,
+            "metadata": {
+              "editor_settings": {
+                "simplified_notifications_section": true,
+                "simplified_query_and_expressions_section": false,
+              },
+            },
+            "no_data_state": "NoData",
+            "title": "Grafana-rule",
+          },
+          "labels": {
+            "region": "nasa",
+            "severity": "critical",
+          },
+        },
+      ],
+    },
+    "headers": [
+      [
+        "content-type",
+        "application/json",
+      ],
+      [
+        "accept",
+        "application/json, text/plain, */*",
+      ],
+    ],
+    "method": "POST",
+    "url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
+  },
+]
+`;
diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts
index fcf9ee3d77e5a..d89c976bf393f 100644
--- a/public/app/features/alerting/unified/utils/rule-form.ts
+++ b/public/app/features/alerting/unified/utils/rule-form.ts
@@ -32,6 +32,7 @@ import {
   Labels,
   PostableRuleGrafanaRuleDTO,
   RulerAlertingRuleDTO,
+  RulerGrafanaRuleDTO,
   RulerRecordingRuleDTO,
   RulerRuleDTO,
 } from 'app/types/unified-alerting-dto';
@@ -365,6 +366,56 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
   }
 }
 
+export function grafanaRuleDtoToFormValues(rule: RulerGrafanaRuleDTO, namespace: string): RuleFormValues {
+  const defaultFormValues = getDefaultFormValues();
+
+  const ga = rule.grafana_alert;
+  const duration = rule.for;
+  const annotations = rule.annotations;
+  const labels = rule.labels;
+
+  const commonProperties = {
+    ...defaultFormValues,
+    name: ga.title,
+    queries: ga.data,
+    condition: ga.condition,
+    annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(annotations, false)),
+    labels: listifyLabelsOrAnnotations(labels, true),
+    folder: { title: namespace, uid: ga.namespace_uid },
+    isPaused: ga.is_paused,
+  };
+
+  if (rulerRuleType.grafana.recordingRule(rule)) {
+    // grafana recording rule
+    return {
+      ...commonProperties,
+      type: RuleFormType.grafanaRecording,
+      group: ga.rule_group,
+      metric: ga.record?.metric,
+    };
+  }
+
+  // grafana alerting rule
+  const routingSettings: AlertManagerManualRouting | undefined = getContactPointsFromDTO(ga);
+  if (ga.no_data_state !== undefined && ga.exec_err_state !== undefined) {
+    return {
+      ...commonProperties,
+      type: RuleFormType.grafana,
+      group: ga.rule_group,
+      evaluateFor: duration || '0',
+      noDataState: ga.no_data_state,
+      execErrState: ga.exec_err_state,
+
+      contactPoints: routingSettings,
+      manualRouting: Boolean(routingSettings),
+
+      editorSettings: getEditorSettingsFromDTO(ga),
+    };
+  } else {
+    throw new Error('Unexpected type of rule for grafana rules source');
+  }
+}
+
 export function alertingRulerRuleToRuleForm(
   rule: RulerAlertingRuleDTO
 ): Pick<
diff --git a/public/app/features/connections/__mocks__/store.navIndex.mock.ts b/public/app/features/connections/__mocks__/store.navIndex.mock.ts
index 125090b00d42c..c4cb2abca351e 100644
--- a/public/app/features/connections/__mocks__/store.navIndex.mock.ts
+++ b/public/app/features/connections/__mocks__/store.navIndex.mock.ts
@@ -170,6 +170,12 @@ export const navIndex: NavIndex = {
         url: '/alerting/new',
         hideFromTabs: true,
       },
+      {
+        id: 'alerts/recently-deleted',
+        text: 'Recently deleted',
+        icon: 'trash-alt',
+        url: '/alerting/recently-deleted',
+      },
     ],
     parentItem: {
       id: 'home',
diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts
index 495c034028b34..de7a68ac1aab4 100644
--- a/public/app/types/unified-alerting-dto.ts
+++ b/public/app/types/unified-alerting-dto.ts
@@ -270,6 +270,7 @@ export interface PostableGrafanaRuleDefinition {
 export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
   id?: string;
   uid: string;
+  guid?: string;
   namespace_uid: string;
   rule_group: string;
   provenance?: string;
@@ -312,4 +313,8 @@ export type RulerRuleGroupDTO<R = RulerRuleDTO> = {
 
 export type PostableRulerRuleGroupDTO = RulerRuleGroupDTO<PostableRuleDTO>;
 
+export type RulerGrafanaRuleGroupDTO = RulerRuleGroupDTO<RulerGrafanaRuleDTO>;
+
 export type RulerRulesConfigDTO = { [namespace: string]: RulerRuleGroupDTO[] };
+
+export type RulerGrafanaRulesConfigDTO = { [namespace: string]: RulerGrafanaRuleGroupDTO[] };
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 9603556bc66e0..858e1b19f60fb 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -299,6 +299,13 @@
       "state-no-data": "Alert state if no data or all values are null",
       "target-datasource-uid": "Target data source"
     },
+    "alert-menu": {
+      "copy-link": "Copy link",
+      "duplicate": "Duplicate",
+      "export": "Export",
+      "silence-notifications": "Silence notifications",
+      "with-modifications": "With modifications"
+    },
     "alert-recording-rule-form": {
       "evaluation-behaviour": {
         "description": {
@@ -465,6 +472,31 @@
       "missing-reference": "Expression \"{{source}}\" failed to run because \"{{target}}\" is missing or also failed.",
       "self-reference": "You can't link an expression to itself"
     },
+    "delete-rule-modal": {
+      "title": "Delete rule",
+      "with-soft-delete": "Are you sure you want to delete this rule? This rule will be recoverable from the Recently deleted page by a user with an admin role.",
+      "without-soft-delete": "Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
+    },
+    "deleted-rules": {
+      "empty-state-title": "No recently deleted rules found",
+      "errorloading": "Failed to load alert deleted rules",
+      "restore": "Restore",
+      "restore-deleted-manually": "Your alert rule could not be restored. This may be due to changes to other entities such as contact points, data sources etc. Please manually restore the deleted rule by editing the rule and saving it.",
+      "restore-modal": {
+        "body": "Are you sure you want to restore this deleted alert rule definition?",
+        "confirm": "Yes, restore deleted rule",
+        "error": "Could not restore deleted alert rule",
+        "title": "Restore deleted alert rule"
+      },
+      "table": {
+        "folder": "Folder",
+        "group": "Group",
+        "title": "Title",
+        "updated": "Deletion Date",
+        "updatedBy": "Deleted By"
+      },
+      "unknown": "Unknown"
+    },
     "draggable-rules-table": {
       "evals-to-start-alerting": "Evaluations to start alerting",
       "pending-period": "Pending period",
@@ -3706,6 +3738,10 @@
       "subtitle": "Alerting and incident management apps",
       "title": "Alerts & IRM"
     },
+    "alerts-recently-deleted": {
+      "subtitle": "See recently deleted alert rules",
+      "title": "Recently deleted"
+    },
     "api-keys": {
       "subtitle": "Manage and create API keys that are used to interact with Grafana HTTP APIs",
       "title": "API keys"
diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx
index 4b50dce768e30..d15eee0a08b38 100644
--- a/public/test/helpers/alertingRuleEditor.tsx
+++ b/public/test/helpers/alertingRuleEditor.tsx
@@ -13,6 +13,7 @@ export enum GrafanaRuleFormStep {
 
 export const ui = {
   loadingIndicator: byText('Loading rule...'),
+  manualRestoreBanner: byText(/restoring rule manually/i),
   inputs: {
     name: byRole('textbox', { name: 'name' }),
     metric: byRole('textbox', { name: 'metric' }),
@@ -45,7 +46,17 @@ export const ui = {
     preview: byRole('button', { name: /^Preview$/ }),
   },
 };
-export function renderRuleEditor(identifier?: string, recording?: 'recording' | 'grafana-recording') {
+export function renderRuleEditor(
+  identifier?: string,
+  recording?: 'recording' | 'grafana-recording',
+  restoreFrom?: string
+) {
+  const isManualRestore = Boolean(restoreFrom);
+  const restoreFromEncoded = restoreFrom ? encodeURIComponent(restoreFrom) : '';
+  const newAlertRuleRoute =
+    `/alerting/new/${recording ?? 'alerting'}` +
+    (isManualRestore ? `?isManualRestore=true&defaults=${restoreFromEncoded}` : '');
+  const initialEntries = [identifier ? `/alerting/${identifier}/edit` : newAlertRuleRoute];
   return render(
     <>
       <AppNotificationList />
@@ -56,7 +67,7 @@ export function renderRuleEditor(identifier?: string, recording?: 'recording' |
     </>,
     {
       historyOptions: {
-        initialEntries: [identifier ? `/alerting/${identifier}/edit` : `/alerting/new/${recording ?? 'alerting'}`],
+        initialEntries: initialEntries,
       },
     }
   );


Reviews:
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T08:37:53Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T08:42:44Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T08:43:49Z
user: login: konrad147
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:49:46Z
user: login: konrad147
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:02:59Z
user: login: konrad147
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:04:56Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T11:13:15Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T11:14:25Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T12:30:16Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T16:18:37Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T07:47:35Z
user: login: konrad147
body: LGTM! left a few non-blocking nits
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T09:32:33Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T10:59:28Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T11:07:36Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:07:49Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:08:34Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:33:07Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:33:51Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:38:15Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:39:31Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:40:51Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:41:14Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:41:39Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:42:48Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:43:59Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:44:48Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:46:43Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:54:45Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:55:09Z
user: login: tomratcliffe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:17:10Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:37:19Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:42:52Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:49:26Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T14:37:02Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T14:37:53Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T14:55:21Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T14:56:25Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:50:11Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:18:56Z
user: login: tomratcliffe
body: 🚀 Looks great to me! Nice one
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:51:58Z

Review comments:
diff_hunk: @@ -11,8 +11,6 @@ import (
 	"github.com/grafana/grafana-app-sdk/app"
 )
 
path: pkg/apis/dashboard_manifest.go
user: login: soniaAguilarPeiron
body: I tried to discard this change, but it seems not to work???
created_at: 2025-03-12T08:37:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,15 +90,33 @@ const AlertRuleMenu = ({
       {canPause && rulerRuleType.grafana.rule(rulerRule) && groupIdentifier.groupOrigin === 'grafana' && (
         <MenuItemPauseRule rule={rulerRule} groupIdentifier={groupIdentifier} onPauseChange={onPauseChange} />
       )}
-      {canSilence && <Menu.Item label="Silence notifications" icon="bell-slash" onClick={handleSilence} />}
+      {canSilence && (
path: public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
user: login: soniaAguilarPeiron
body: sorry, my prettier 🫠
created_at: 2025-03-20T08:42:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,7 @@
 import { AppEvents } from '@grafana/data';
 import { ComponentSize, Dropdown, Menu } from '@grafana/ui';
 import appEvents from 'app/core/app_events';
+import { t } from 'app/core/internationalization';
path: public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
user: login: soniaAguilarPeiron
body: changes in this file are only about translations (this is because i added some changes for deleting rule permanently that i finally reverted)
created_at: 2025-03-20T08:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,6 +25,8 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
   const [ruleToDelete, setRuleToDelete] = useState<DeleteRuleInfo>();
   const [deleteRuleFromGroup] = useDeleteRuleFromGroup();
   const { waitForRemoval } = usePrometheusConsistencyCheck();
+  const isSoftDeleteEnabled =
+    config.featureToggles.alertRuleRestore && config.featureToggles.alertingRuleRecoverDeleted;
path: public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
user: login: konrad147
body: Should we move this to a small util function? I see we check a few times for both flags.
We already have a `public/app/features/alerting/unified/featureToggles.ts` file which exists exactly for that
created_at: 2025-03-21T09:19:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    if (!rulerRuleType.grafana.rule(ruleToRestore)) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    if (!rulerRuleType.grafana.rule(ruleToRestore)) {
+      return;
+    }
+    const namespaceName = await backendSrv
+      .getFolderByUid(ruleToRestore.grafana_alert.namespace_uid)
+      .then((folder) => folder.title);
+    createAlert(ruleToRestore, namespaceName);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
+      onDismiss={onDismiss}
+    />
+  );
+};
+
+function RulePeview({ rule }: { rule: RulerRuleDTO }) {
+  const styles = useStyles2(getStyles);
+  return (
+    <div className={styles.content}>
+      <CodeEditor
+        width="100%"
+        height={600}
+        language={'json'}
+        value={JSON.stringify(rule, null, 4)}
+        monacoOptions={{
+          minimap: {
+            enabled: false,
+          },
+          scrollBeyondLastLine: false,
+          lineNumbers: 'on',
+          readOnly: true,
+        }}
+      />
+    </div>
+  );
+}
+
+const getStyles = () => ({
+  content: css({
+    flex: '1 1 100%',
+  }),
+  modal: css({
+    width: '700px',
+  }),
+});
+
+export function useRestoreDeletedRule() {
+  const [addRuleToRuleGroup] = useAddRuleToRuleGroup();
+
+  return useAsync(async (deletedRule: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
+    const ruleGroupIdentifier = {
+      dataSourceName: GRAFANA_RULES_SOURCE_NAME,
+      namespaceName: deletedRule.grafana_alert.namespace_uid,
+      groupName: deletedRule.grafana_alert.rule_group,
+    };
+    // save the new rule to the rule group
+    return addRuleToRuleGroup.execute(ruleGroupIdentifier, deletedRule);
+  });
+}
+
+const createAlert = async (ruleToRecover: RulerGrafanaRuleDTO, namespace: string) => {
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: konrad147
body: Should we rename to something like `redirectToRestoreForm`?
Maybe this function could fetch the namespace name from Uid, to keep most of the implementation in this function?
created_at: 2025-03-21T12:21:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    if (!rulerRuleType.grafana.rule(ruleToRestore)) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    if (!rulerRuleType.grafana.rule(ruleToRestore)) {
+      return;
+    }
+    const namespaceName = await backendSrv
+      .getFolderByUid(ruleToRestore.grafana_alert.namespace_uid)
+      .then((folder) => folder.title);
+    createAlert(ruleToRestore, namespaceName);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: konrad147
body: This looks a bit confusing. Why we wan to invoke `onManualRestore` if there is an error?
created_at: 2025-03-21T12:23:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    if (!rulerRuleType.grafana.rule(ruleToRestore)) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    if (!rulerRuleType.grafana.rule(ruleToRestore)) {
+      return;
+    }
+    const namespaceName = await backendSrv
+      .getFolderByUid(ruleToRestore.grafana_alert.namespace_uid)
+      .then((folder) => folder.title);
+    createAlert(ruleToRestore, namespaceName);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: konrad147
body: It looks like there are 2 options for restoring? One automatically restores the rule and the other goes through the form, am I correct?
I think it would be good to make it more explicit
created_at: 2025-03-21T12:25:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Alert, Button, Column, InteractiveTable, LoadingPlaceholder, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { rulerRuleType } from '../../../utils/rules';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+export function DeletedRules() {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerRuleDTO | undefined>();
+
+  const {
+    currentData = [],
+    isLoading,
+    error,
+  } = alertRuleApi.endpoints.getDeletedRules.useQuery({
+    rulerConfig: GRAFANA_RULER_CONFIG,
+    filter: {}, // todo: add filters, and limit?????
+  });
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const values = Object.values(currentData);
+  const deletedRules = values.length > 0 ? values[0][0]?.rules : [];
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => rulerRuleType.grafana.rule(rule) && getRowId(rule.grafana_alert) === id
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: konrad147
body: This endpoint only works for GMA, should we change it's response type to GMA ruler rules?
We wouldn't need to add these typechecks everywhere
created_at: 2025-03-21T12:29:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Alert, Button, Column, InteractiveTable, LoadingPlaceholder, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { rulerRuleType } from '../../../utils/rules';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+export function DeletedRules() {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerRuleDTO | undefined>();
+
+  const {
+    currentData = [],
+    isLoading,
+    error,
+  } = alertRuleApi.endpoints.getDeletedRules.useQuery({
+    rulerConfig: GRAFANA_RULER_CONFIG,
+    filter: {}, // todo: add filters, and limit?????
+  });
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const values = Object.values(currentData);
+  const deletedRules = values.length > 0 ? values[0][0]?.rules : [];
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => rulerRuleType.grafana.rule(rule) && getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  if (isLoading) {
+    return <LoadingPlaceholder text={t('alerting.common.loading', 'Loading...')} />;
+  }
+
+  if (error) {
+    return (
+      <Alert title={t('alerting.deletedRules.errorloading', 'Failed to load alert deleted rules')}>
+        {stringifyErrorLike(error)}
+      </Alert>
+    );
+  }
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        return row.original.grafana_alert.namespace_uid;
+      },
+    },
+    {
+      id: 'group',
+      header: t('alerting.deletedRules.table.group', 'Group'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        return row.original.grafana_alert.rule_group;
+      },
+    },
+    {
+      id: 'created',
+      header: t('alerting.deletedRules.table.updated', 'Deletion Date'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        const value = row.original.grafana_alert.updated;
+        if (!value) {
+          return unknown;
+        }
+        return dateTimeFormat(value) + ' (' + dateTimeFormatTimeAgo(value) + ')';
+      },
+    },
+    {
+      id: 'actions',
+      disableGrow: true,
+      cell: ({ row }) => {
+        return (
+          <Stack direction="row" alignItems="center" justifyContent="flex-end">
+            <Button
+              variant="secondary"
+              size="sm"
+              icon="history"
+              onClick={() => {
+                showConfirmation(
+                  rulerRuleType.grafana.rule(row.original) ? getRowId(row.original.grafana_alert) : 'unknown'
+                );
+              }}
+            >
+              <Trans i18nKey="alerting.deletedRules.restore">Restore</Trans>
+            </Button>
+          </Stack>
+        );
+      },
+    },
+  ];
+
+  return (
+    <>
+      <InteractiveTable
+        pageSize={DELETED_RULES_PAGE_SIZE}
+        columns={columns}
+        data={deletedRules}
+        getRowId={(row) => {
+          if (!rulerRuleType.grafana.rule(row)) {
+            return 'unknown';
+          }
+          return getRowId(row.grafana_alert);
+        }}
+      />
+      <ConfirmRestoreDeletedRuleModal
+        ruleToRestore={restoreRule}
+        isOpen={confirmRestore}
+        onDismiss={hideConfirmation}
+        onRestoreSucess={() => trackDeletedRuleRestoreSuccess()}
+        onRestoreError={() => trackDeletedRuleRestoreFail()}
+      />
+    </>
+  );
+}
+
+function getRowId(row: GrafanaRuleDefinition) {
+  return `${row.namespace_uid}${row.namespace_uid}${row.title}${row.version}`;
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: konrad147
body: Is there `rule_uid` available for us? What happens if there were many rules with the same title?
created_at: 2025-03-21T12:32:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,14 @@
+import { withPageErrorBoundary } from '../../../withPageErrorBoundary';
+import { AlertingPageWrapper } from '../../AlertingPageWrapper';
+
+import { DeletedRules } from './DeletedRules';
+
+function DeletedrulesPage() {
+  return (
+    <AlertingPageWrapper navId="alerts-trash" isLoading={false}>
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRulesPage.tsx
user: login: konrad147
body: I think the UX could be better if we loaded the rules here and used `isLoading` property of `AlertingPageWrapper` to handle the loading state.
We wouldn't need to handle the loading state inside the `DeletedRules` component and it would give us better loading consistency
created_at: 2025-03-21T12:33:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -365,6 +366,59 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
   }
 }
 
+export function grafanaRuleDtoToFormValues(rule: RulerGrafanaRuleDTO, namespace: string): RuleFormValues {
+  const defaultFormValues = getDefaultFormValues();
+
+  const ga = rule.grafana_alert;
+  const duration = rule.for;
+  const annotations = rule.annotations;
+  const labels = rule.labels;
+  if (rulerRuleType.grafana.rule(rule)) {
path: public/app/features/alerting/unified/utils/rule-form.ts
user: login: konrad147
body: According to the comment, it should handle recording rules, so shouldn't the condition be `rulerRuleType.grafana.recordingRule(rule)`?
created_at: 2025-03-21T12:47:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Alert, Button, Column, InteractiveTable, LoadingPlaceholder, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { rulerRuleType } from '../../../utils/rules';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+export function DeletedRules() {
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: konrad147
body: Can we add a few tests for this component?
created_at: 2025-03-21T15:02:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,8 +68,16 @@ const RuleEditor = () => {
       return <CloneRuleEditor sourceRuleId={copyFromIdentifier} />;
     }
     // new alert rule
-    return <AlertRuleForm prefill={queryDefaults} />;
-  }, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, identifier, queryDefaults]);
+    return <AlertRuleForm prefill={queryDefaults} isManualRestore={isManualRestore} />;
path: public/app/features/alerting/unified/rule-editor/RuleEditor.tsx
user: login: konrad147
body: Would be good to have some tests covering this `isManualRestore` mode. We already have quite a few tests for the form component, so should be difficult to add new ones
created_at: 2025-03-21T15:04:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    if (!rulerRuleType.grafana.rule(ruleToRestore)) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    if (!rulerRuleType.grafana.rule(ruleToRestore)) {
+      return;
+    }
+    const namespaceName = await backendSrv
+      .getFolderByUid(ruleToRestore.grafana_alert.namespace_uid)
+      .then((folder) => folder.title);
+    createAlert(ruleToRestore, namespaceName);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: soniaAguilarPeiron
body: > Why we wan to invoke onManualRestore if there is an error?

If we get an error while restoring, means that something is not correct at this moment , for example, the contact point does not exist anymore, or the datasource.... then we allow users to manually edit , fix and restore. It's the same pattern we use for restoring versions.

created_at: 2025-03-24T11:13:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Alert, Button, Column, InteractiveTable, LoadingPlaceholder, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { rulerRuleType } from '../../../utils/rules';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+export function DeletedRules() {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerRuleDTO | undefined>();
+
+  const {
+    currentData = [],
+    isLoading,
+    error,
+  } = alertRuleApi.endpoints.getDeletedRules.useQuery({
+    rulerConfig: GRAFANA_RULER_CONFIG,
+    filter: {}, // todo: add filters, and limit?????
+  });
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const values = Object.values(currentData);
+  const deletedRules = values.length > 0 ? values[0][0]?.rules : [];
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => rulerRuleType.grafana.rule(rule) && getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  if (isLoading) {
+    return <LoadingPlaceholder text={t('alerting.common.loading', 'Loading...')} />;
+  }
+
+  if (error) {
+    return (
+      <Alert title={t('alerting.deletedRules.errorloading', 'Failed to load alert deleted rules')}>
+        {stringifyErrorLike(error)}
+      </Alert>
+    );
+  }
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        return row.original.grafana_alert.namespace_uid;
+      },
+    },
+    {
+      id: 'group',
+      header: t('alerting.deletedRules.table.group', 'Group'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        return row.original.grafana_alert.rule_group;
+      },
+    },
+    {
+      id: 'created',
+      header: t('alerting.deletedRules.table.updated', 'Deletion Date'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        if (!rulerRuleType.grafana.rule(row.original)) {
+          return unknown;
+        }
+        const value = row.original.grafana_alert.updated;
+        if (!value) {
+          return unknown;
+        }
+        return dateTimeFormat(value) + ' (' + dateTimeFormatTimeAgo(value) + ')';
+      },
+    },
+    {
+      id: 'actions',
+      disableGrow: true,
+      cell: ({ row }) => {
+        return (
+          <Stack direction="row" alignItems="center" justifyContent="flex-end">
+            <Button
+              variant="secondary"
+              size="sm"
+              icon="history"
+              onClick={() => {
+                showConfirmation(
+                  rulerRuleType.grafana.rule(row.original) ? getRowId(row.original.grafana_alert) : 'unknown'
+                );
+              }}
+            >
+              <Trans i18nKey="alerting.deletedRules.restore">Restore</Trans>
+            </Button>
+          </Stack>
+        );
+      },
+    },
+  ];
+
+  return (
+    <>
+      <InteractiveTable
+        pageSize={DELETED_RULES_PAGE_SIZE}
+        columns={columns}
+        data={deletedRules}
+        getRowId={(row) => {
+          if (!rulerRuleType.grafana.rule(row)) {
+            return 'unknown';
+          }
+          return getRowId(row.grafana_alert);
+        }}
+      />
+      <ConfirmRestoreDeletedRuleModal
+        ruleToRestore={restoreRule}
+        isOpen={confirmRestore}
+        onDismiss={hideConfirmation}
+        onRestoreSucess={() => trackDeletedRuleRestoreSuccess()}
+        onRestoreError={() => trackDeletedRuleRestoreFail()}
+      />
+    </>
+  );
+}
+
+function getRowId(row: GrafanaRuleDefinition) {
+  return `${row.namespace_uid}${row.namespace_uid}${row.title}${row.version}`;
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: soniaAguilarPeiron
body: true!, yes, let's add the uid
created_at: 2025-03-24T11:14:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Alert, Button, Column, InteractiveTable, LoadingPlaceholder, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { rulerRuleType } from '../../../utils/rules';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+export function DeletedRules() {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerRuleDTO | undefined>();
+
+  const {
+    currentData = [],
+    isLoading,
+    error,
+  } = alertRuleApi.endpoints.getDeletedRules.useQuery({
+    rulerConfig: GRAFANA_RULER_CONFIG,
+    filter: {}, // todo: add filters, and limit?????
+  });
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const values = Object.values(currentData);
+  const deletedRules = values.length > 0 ? values[0][0]?.rules : [];
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => rulerRuleType.grafana.rule(rule) && getRowId(rule.grafana_alert) === id
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: soniaAguilarPeiron
body: 💯
created_at: 2025-03-24T12:30:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -365,6 +366,59 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
   }
 }
 
+export function grafanaRuleDtoToFormValues(rule: RulerGrafanaRuleDTO, namespace: string): RuleFormValues {
+  const defaultFormValues = getDefaultFormValues();
+
+  const ga = rule.grafana_alert;
+  const duration = rule.for;
+  const annotations = rule.annotations;
+  const labels = rule.labels;
+  if (rulerRuleType.grafana.rule(rule)) {
path: public/app/features/alerting/unified/utils/rule-form.ts
user: login: soniaAguilarPeiron
body: opsss!! good catch!
created_at: 2025-03-24T16:18:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Alert, Button, Column, InteractiveTable, LoadingPlaceholder, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { rulerRuleType } from '../../../utils/rules';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+export function DeletedRules() {
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: soniaAguilarPeiron
body: done!
created_at: 2025-03-26T07:47:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,14 +70,18 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
       <ConfirmModal
         isOpen={Boolean(ruleToDelete)}
         title="Delete rule"
-        body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
+        body={
+          isSoftDeleteEnabled
+            ? 'Are you sure you want to delete this rule? This rule will be recoverable from the Recently deleted page.'
+            : 'Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?'
+        }
path: public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
user: login: konrad147
body: I wonder if we should make this distinction. The `Recently deleted` page is only available for admins, so if you're Editor, you won't be able to recover rules easily
created_at: 2025-03-26T09:15:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerGrafanaRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    await redirectToRestoreForm(ruleToRestore);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule.
+              </Trans>
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: konrad147
body: The message might not be clear - `Manually restore` sounds quite technical and can be confusing. We might need to give the user specific instructions on what they need to do
created_at: 2025-03-26T09:21:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,3 +1,5 @@
 import { config } from '@grafana/runtime';
 
 export const shouldUsePrometheusRulesPrimary = () => config.featureToggles.alertingPrometheusRulesPrimary ?? false;
+
+export const isRecoverDeletedRulesEnabled = () => (config.featureToggles.alertingRuleRecoverDeleted && config.featureToggles.alertRuleRestore)?? false;
path: public/app/features/alerting/unified/featureToggles.ts
user: login: konrad147
body: Can we follow a similar naming patter? e.g. `shouldUseRecoverDeletedRules`
created_at: 2025-03-26T09:24:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -312,4 +312,8 @@ export type RulerRuleGroupDTO<R = RulerRuleDTO> = {
 
 export type PostableRulerRuleGroupDTO = RulerRuleGroupDTO<PostableRuleDTO>;
 
+export type RulerGrafanaRuleGroupDTO = RulerRuleGroupDTO<RulerGrafanaRuleDTO>;
+
 export type RulerRulesConfigDTO = { [namespace: string]: RulerRuleGroupDTO[] };
+
+export type RulerGrafanaRulesConfigDTO = { [namespace: string]: RulerGrafanaRuleGroupDTO[] };
path: public/app/types/unified-alerting-dto.ts
user: login: konrad147
body: ❤️ 
created_at: 2025-03-26T09:26:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,14 +70,18 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
       <ConfirmModal
         isOpen={Boolean(ruleToDelete)}
         title="Delete rule"
-        body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
+        body={
+          isSoftDeleteEnabled
+            ? 'Are you sure you want to delete this rule? This rule will be recoverable from the Recently deleted page.'
+            : 'Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?'
+        }
path: public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
user: login: soniaAguilarPeiron
body: But they will be able to recover them by asking the admin. Maybe we can add a note: "Ask the admin to recover deleted rules."?
created_at: 2025-03-26T10:59:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,14 +70,18 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
       <ConfirmModal
         isOpen={Boolean(ruleToDelete)}
         title="Delete rule"
-        body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
+        body={
+          isSoftDeleteEnabled
+            ? 'Are you sure you want to delete this rule? This rule will be recoverable from the Recently deleted page.'
+            : 'Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?'
+        }
path: public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
user: login: soniaAguilarPeiron
body: I updated with :
`Are you sure you want to delete this rule? This rule will be recoverable from the Recently deleted page by a user with an admin role.`
created_at: 2025-03-26T11:07:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,6 +89,8 @@ export function getNavTitle(navId: string | undefined) {
       return t('nav.alerting-groups.title', 'Active notifications');
     case 'alerting-admin':
       return t('nav.alerting-admin.title', 'Settings');
+    case 'alerts-trash':
+      return t('nav.alerts-trash.title', 'Trash');
path: public/app/core/utils/navBarItem-translations.ts
user: login: tomratcliffe
body: TODO: remove?
created_at: 2025-03-26T12:07:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3384,6 +3410,10 @@
       "subtitle": "Alerting and incident management apps",
       "title": "Alerts & IRM"
     },
+    "alerts-trash": {
path: public/locales/en-US/grafana.json
user: login: tomratcliffe
body: Probably want to remove references to `trash` in the code as well?
created_at: 2025-03-26T12:08:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -413,5 +418,11 @@ export const alertRuleApi = alertingApi.injectEndpoints({
       }),
       keepUnusedDataFor: 0,
     }),
+    getDeletedRules: build.query<RulerGrafanaRulesConfigDTO, {}>({
+      query: () => ({
+        url: `/api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rules/`,
+        params: { deleted: 'true' },
+      }),
+    }),
path: public/app/features/alerting/unified/api/alertRuleApi.ts
user: login: tomratcliffe
body: We need to add a tag to this endpoint, and invalidate it when we delete a rule - otherwise when you delete a rule and go straight to the new page, you don't see it

(or we could reuse existing tags, but might be clearer for a new one)
created_at: 2025-03-26T12:33:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.namespace_uid;
+      },
+    },
+    {
+      id: 'group',
+      header: t('alerting.deletedRules.table.group', 'Group'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.rule_group;
+      },
+    },
+    {
+      id: 'created',
+      header: t('alerting.deletedRules.table.updated', 'Deletion Date'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        const value = row.original.grafana_alert.updated;
+        if (!value) {
+          return unknown;
+        }
+        return dateTimeFormat(value) + ' (' + dateTimeFormatTimeAgo(value) + ')';
+      },
+    },
+    {
+      id: 'actions',
+      disableGrow: true,
+      cell: ({ row }) => {
+        return (
+          <Stack direction="row" alignItems="center" justifyContent="flex-end">
+            <Button
+              variant="secondary"
+              size="sm"
+              icon="history"
+              onClick={() => {
+                showConfirmation(getRowId(row.original.grafana_alert)
+                );
+              }}
+            >
+              <Trans i18nKey="alerting.deletedRules.restore">Restore</Trans>
+            </Button>
+          </Stack>
+        );
+      },
+    },
+  ];
+
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: tomratcliffe
body: Can we add an empty state here?

```tsx
  if (deletedRules.length === 0) {
    return (
      <EmptyState
        message={t('alerting.deleted-rules.empty-state-title', 'No recently deleted rules found')}
        variant="not-found"
      />
    );
  }
```
created_at: 2025-03-26T12:33:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.namespace_uid;
+      },
+    },
+    {
+      id: 'group',
+      header: t('alerting.deletedRules.table.group', 'Group'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.rule_group;
+      },
+    },
+    {
+      id: 'created',
+      header: t('alerting.deletedRules.table.updated', 'Deletion Date'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        const value = row.original.grafana_alert.updated;
+        if (!value) {
+          return unknown;
+        }
+        return dateTimeFormat(value) + ' (' + dateTimeFormatTimeAgo(value) + ')';
+      },
+    },
+    {
+      id: 'actions',
+      disableGrow: true,
+      cell: ({ row }) => {
+        return (
+          <Stack direction="row" alignItems="center" justifyContent="flex-end">
+            <Button
+              variant="secondary"
+              size="sm"
+              icon="history"
+              onClick={() => {
+                showConfirmation(getRowId(row.original.grafana_alert)
+                );
+              }}
+            >
+              <Trans i18nKey="alerting.deletedRules.restore">Restore</Trans>
+            </Button>
+          </Stack>
+        );
+      },
+    },
+  ];
+
+  return (
+    <>
+      <InteractiveTable
+        pageSize={DELETED_RULES_PAGE_SIZE}
+        columns={columns}
+        data={deletedRules}
+        getRowId={(row) => {
+          return getRowId(row.grafana_alert);
+        }}
+      />
+      <ConfirmRestoreDeletedRuleModal
+        ruleToRestore={restoreRule}
+        isOpen={confirmRestore}
+        onDismiss={hideConfirmation}
+        onRestoreSucess={trackDeletedRuleRestoreSuccess}
+        onRestoreError={trackDeletedRuleRestoreFail}
+      />
+    </>
+  );
+}
+
+function getRowId(row: GrafanaRuleDefinition) {
+  return `${row.namespace_uid}${row.namespace_uid}${row.title}${row.uid}${row.version}`;
+}
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: tomratcliffe
body: If you delete two different rules with the same name, then you only see one because of the ID calculation here
To recreate, copy a rule, and then delete the copy. Then do it again - you'll only see one of the `rule title (copy)` in the table

If we change this to `row.guid` (which we seem to have as part of the deleted rule object), then we can see each instance of deleted rules. I think the types need to be different/separated from the `GrafanaRuleDefinition` then?
created_at: 2025-03-26T12:38:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.namespace_uid;
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: tomratcliffe
body: Would be nice to show the namespace title rather than UID, but we'd have to fetch each from the API 🤔 
created_at: 2025-03-26T12:39:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.namespace_uid;
+      },
+    },
+    {
+      id: 'group',
+      header: t('alerting.deletedRules.table.group', 'Group'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.rule_group;
+      },
+    },
+    {
+      id: 'created',
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: tomratcliffe
body: Do you think it's also worth telling the user roughly how long until it's permanently deleted? Or is the `time since deletion` enough? WDYT?
created_at: 2025-03-26T12:40:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: tomratcliffe
body: 💯 Nice one
created_at: 2025-03-26T12:41:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: tomratcliffe
body: (don't think this needs to be exported)
created_at: 2025-03-26T12:41:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,14 +70,18 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => {
       <ConfirmModal
         isOpen={Boolean(ruleToDelete)}
         title="Delete rule"
-        body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
+        body={
+          isSoftDeleteEnabled
+            ? 'Are you sure you want to delete this rule? This rule will be recoverable from the Recently deleted page by a user with an admin role.'
+            : 'Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?'
+        }
path: public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx
user: login: tomratcliffe
body: nit: we should translate these as well?
created_at: 2025-03-26T12:42:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerGrafanaRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    await redirectToRestoreForm(ruleToRestore);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule by editing the rule and saving it.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
+      onDismiss={onDismiss}
+    />
+  );
+};
+
+function RulePeview({ rule }: { rule: RulerRuleDTO }) {
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: tomratcliffe
body: nit: `RulePreview`
created_at: 2025-03-26T12:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerGrafanaRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    await redirectToRestoreForm(ruleToRestore);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule by editing the rule and saving it.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
+      onDismiss={onDismiss}
+    />
+  );
+};
+
+function RulePeview({ rule }: { rule: RulerRuleDTO }) {
+  const styles = useStyles2(getStyles);
+  return (
+    <div className={styles.content}>
+      <CodeEditor
+        width="100%"
+        height={600}
+        language={'json'}
+        value={JSON.stringify(rule, null, 4)}
+        monacoOptions={{
+          minimap: {
+            enabled: false,
+          },
+          scrollBeyondLastLine: false,
+          lineNumbers: 'on',
+          readOnly: true,
+        }}
+      />
+    </div>
+  );
+}
+
+const getStyles = () => ({
+  content: css({
+    flex: '1 1 100%',
+  }),
+  modal: css({
+    width: '700px',
+  }),
+});
+
+export function useRestoreDeletedRule() {
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: tomratcliffe
body: Could this go in a separate file?
created_at: 2025-03-26T12:44:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerGrafanaRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    await redirectToRestoreForm(ruleToRestore);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule by editing the rule and saving it.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
+      onDismiss={onDismiss}
+    />
+  );
+};
+
+function RulePeview({ rule }: { rule: RulerRuleDTO }) {
+  const styles = useStyles2(getStyles);
+  return (
+    <div className={styles.content}>
+      <CodeEditor
+        width="100%"
+        height={600}
+        language={'json'}
+        value={JSON.stringify(rule, null, 4)}
+        monacoOptions={{
+          minimap: {
+            enabled: false,
+          },
+          scrollBeyondLastLine: false,
+          lineNumbers: 'on',
+          readOnly: true,
+        }}
+      />
+    </div>
+  );
+}
+
+const getStyles = () => ({
+  content: css({
+    flex: '1 1 100%',
+  }),
+  modal: css({
+    width: '700px',
+  }),
+});
+
+export function useRestoreDeletedRule() {
+  const [addRuleToRuleGroup] = useAddRuleToRuleGroup();
+
+  return useAsync(async (deletedRule: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
+    const ruleGroupIdentifier = {
+      dataSourceName: GRAFANA_RULES_SOURCE_NAME,
+      namespaceName: deletedRule.grafana_alert.namespace_uid,
+      groupName: deletedRule.grafana_alert.rule_group,
+    };
+    // save the new rule to the rule group
+    return addRuleToRuleGroup.execute(ruleGroupIdentifier, deletedRule);
+  });
+}
+
+const redirectToRestoreForm = async (ruleToRecover: RulerGrafanaRuleDTO) => {
+  let formValues: Partial<RuleFormValues> | undefined;
+  const namespaceName = await backendSrv
+    .getFolderByUid(ruleToRecover.grafana_alert.namespace_uid)
+    .then((folder) => folder.title);
+
+  try {
+    formValues = grafanaRuleDtoToFormValues(ruleToRecover, namespaceName);
+  } catch (err) {
+    const message = `Error getting rule values from the deleted rule: ${getMessageFromError(err)}`;
+    throw new Error(message);
+  }
+
+  const urlPath = rulerRuleType.any.recordingRule(ruleToRecover) ? '/alerting/new/grafana-recording' : '/alerting/new';
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: tomratcliffe
body: nit: Restoring is only supported for Grafana managed, right? Might be clearer to check `rulerRuleType.grafana.recording()` instead? As reading this implies that all recording rules go to the `grafana-recording` form
created_at: 2025-03-26T12:46:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Alert } from '@grafana/ui';
+import { t } from 'app/core/internationalization';
+
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { withPageErrorBoundary } from '../../../withPageErrorBoundary';
+import { AlertingPageWrapper } from '../../AlertingPageWrapper';
+
+import { DeletedRules } from './DeletedRules';
+
+function DeletedrulesPage() {
+  const {
+    currentData = [],
+    isLoading,
+    error,
+  } = alertRuleApi.endpoints.getDeletedRules.useQuery({
+    rulerConfig: GRAFANA_RULER_CONFIG,
+    filter: {}, // todo: add filters, and limit?????
+  });
+  const values = Object.values(currentData);
+  const deletedRules = values.length > 0 ? values[0][0]?.rules : [];
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRulesPage.tsx
user: login: tomratcliffe
body: It feels like some of this manipulation could happen in the API endpoint instead? As `transformResponse`?
created_at: 2025-03-26T12:54:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,10 @@
 import { config } from '@grafana/runtime';
 
+import { isAdmin } from './utils/misc';
+
 export const shouldUsePrometheusRulesPrimary = () => config.featureToggles.alertingPrometheusRulesPrimary ?? false;
 
 export const useGrafanaManagedRecordingRulesSupport = () =>
   config.unifiedAlerting.recordingRulesEnabled && config.featureToggles.grafanaManagedRecordingRules;
+
+export const shouldIAllowRecoveringDeletedRules = () => (isAdmin() && config.featureToggles.alertingRuleRecoverDeleted && config.featureToggles.alertRuleRestore)?? false;
path: public/app/features/alerting/unified/featureToggles.ts
user: login: tomratcliffe
body: nit: * `shouldAllowRecoveringDeletedRules` ?
created_at: 2025-03-26T12:55:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -365,6 +366,57 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
   }
 }
 
+export function grafanaRuleDtoToFormValues(rule: RulerGrafanaRuleDTO, namespace: string): RuleFormValues {
+  const defaultFormValues = getDefaultFormValues();
+
+  const ga = rule.grafana_alert;
+  const duration = rule.for;
+  const annotations = rule.annotations;
+  const labels = rule.labels;
+  if (rulerRuleType.grafana.recordingRule(rule)) {
path: public/app/features/alerting/unified/utils/rule-form.ts
user: login: tomratcliffe
body: Could we extract the common properties between both types into an object? And then spread that onto each, along with `defaultFormValues`, so we don't have too much duplication 
(i.e. name, annotations, labels etc are mostly the same)
created_at: 2025-03-26T13:17:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.namespace_uid;
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: soniaAguilarPeiron
body: yes, exactly.... 🫠
created_at: 2025-03-26T13:37:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerGrafanaRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    await redirectToRestoreForm(ruleToRestore);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule by editing the rule and saving it.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
+      onDismiss={onDismiss}
+    />
+  );
+};
+
+function RulePeview({ rule }: { rule: RulerRuleDTO }) {
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: soniaAguilarPeiron
body: 🤦‍♀️ 
created_at: 2025-03-26T13:42:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,6 +89,8 @@ export function getNavTitle(navId: string | undefined) {
       return t('nav.alerting-groups.title', 'Active notifications');
     case 'alerting-admin':
       return t('nav.alerting-admin.title', 'Settings');
+    case 'alerts-trash':
+      return t('nav.alerts-trash.title', 'Trash');
path: public/app/core/utils/navBarItem-translations.ts
user: login: soniaAguilarPeiron
body: I have to update this , as now we use alerts/recently-deleted as the id in the navtree
created_at: 2025-03-26T13:49:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.namespace_uid;
+      },
+    },
+    {
+      id: 'group',
+      header: t('alerting.deletedRules.table.group', 'Group'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.rule_group;
+      },
+    },
+    {
+      id: 'created',
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: soniaAguilarPeiron
body: maybe we would be adding too much info here?.... the endpoint is going to be updated to return deleted rules updated by this fields. I think this would help users to have a clear idea which rules are going to be deleted soon.
created_at: 2025-03-26T14:37:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: soniaAguilarPeiron
body: 💯
created_at: 2025-03-26T14:37:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+import { css } from '@emotion/css';
+import { ComponentProps } from 'react';
+
+import { locationService } from '@grafana/runtime';
+import { Alert, CodeEditor, ConfirmModal, Stack, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { backendSrv } from 'app/core/services/backend_srv';
+import { getMessageFromError } from 'app/core/utils/errors';
+import { useAsync } from 'app/features/alerting/unified/hooks/useAsync';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { useAddRuleToRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
+import { RuleFormValues } from '../../../types/rule-form';
+import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { grafanaRuleDtoToFormValues } from '../../../utils/rule-form';
+import { rulerRuleType } from '../../../utils/rules';
+import { createRelativeUrl } from '../../../utils/url';
+
+type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
+  isOpen: boolean;
+  ruleToRestore?: RulerGrafanaRuleDTO | undefined;
+  onRestoreSucess: () => void;
+  onRestoreError: (error: Error) => void;
+};
+
+export const ConfirmRestoreDeletedRuleModal = ({
+  isOpen,
+  ruleToRestore,
+  onDismiss,
+  onRestoreSucess,
+  onRestoreError,
+}: ModalProps) => {
+  const [restoreMethod, { error }] = useRestoreDeletedRule();
+  const title = t('alerting.deletedRules.restore-modal.title', 'Restore deleted alert rule');
+  const errorTitle = t('alerting.deletedRules.restore-modal.error', 'Could not restore deleted alert rule');
+  const confirmText = !error
+    ? t('alerting.deletedRules.restore-modal.confirm', 'Yes, restore deleted rule')
+    : 'Manually restore the rule';
+
+  const styles = useStyles2(getStyles);
+
+  async function onRestoreConfirm() {
+    if (!ruleToRestore) {
+      return;
+    }
+    return restoreMethod
+      .execute(ruleToRestore)
+      .then(() => {
+        onDismiss();
+        onRestoreSucess();
+      })
+      .catch((err) => {
+        onRestoreError(err);
+      });
+  }
+
+  async function onManualRestore() {
+    if (!ruleToRestore) {
+      return;
+    }
+    await redirectToRestoreForm(ruleToRestore);
+  }
+
+  return (
+    <ConfirmModal
+      isOpen={isOpen}
+      title={title}
+      confirmText={confirmText}
+      modalClass={styles.modal}
+      confirmButtonVariant={!error ? 'destructive' : 'primary'}
+      body={
+        <Stack direction="column" gap={2}>
+          <Trans i18nKey="alerting.deletedRules.restore-modal.body">
+            Are you sure you want to restore this deleted alert rule definition?
+          </Trans>
+
+          <div>{ruleToRestore && <RulePeview rule={ruleToRestore} />}</div>
+          {error && (
+            <Alert severity="warning" title={errorTitle}>
+              <Trans i18nKey="alerting.alertVersionHistory.restore-deleted-manually">
+                Your alert rule could not be restored. This may be due to changes to other entities such as contact
+                points, data sources etc. Please manually restore the deleted rule by editing the rule and saving it.
+              </Trans>
+              <pre style={{ marginBottom: 0 }}>
+                <code>{stringifyErrorLike(error)}</code>
+              </pre>
+            </Alert>
+          )}
+        </Stack>
+      }
+      onConfirm={!error ? onRestoreConfirm : onManualRestore}
+      onDismiss={onDismiss}
+    />
+  );
+};
+
+function RulePeview({ rule }: { rule: RulerRuleDTO }) {
+  const styles = useStyles2(getStyles);
+  return (
+    <div className={styles.content}>
+      <CodeEditor
+        width="100%"
+        height={600}
+        language={'json'}
+        value={JSON.stringify(rule, null, 4)}
+        monacoOptions={{
+          minimap: {
+            enabled: false,
+          },
+          scrollBeyondLastLine: false,
+          lineNumbers: 'on',
+          readOnly: true,
+        }}
+      />
+    </div>
+  );
+}
+
+const getStyles = () => ({
+  content: css({
+    flex: '1 1 100%',
+  }),
+  modal: css({
+    width: '700px',
+  }),
+});
+
+export function useRestoreDeletedRule() {
+  const [addRuleToRuleGroup] = useAddRuleToRuleGroup();
+
+  return useAsync(async (deletedRule: RulerGrafanaRuleDTO<GrafanaRuleDefinition>) => {
+    const ruleGroupIdentifier = {
+      dataSourceName: GRAFANA_RULES_SOURCE_NAME,
+      namespaceName: deletedRule.grafana_alert.namespace_uid,
+      groupName: deletedRule.grafana_alert.rule_group,
+    };
+    // save the new rule to the rule group
+    return addRuleToRuleGroup.execute(ruleGroupIdentifier, deletedRule);
+  });
+}
+
+const redirectToRestoreForm = async (ruleToRecover: RulerGrafanaRuleDTO) => {
+  let formValues: Partial<RuleFormValues> | undefined;
+  const namespaceName = await backendSrv
+    .getFolderByUid(ruleToRecover.grafana_alert.namespace_uid)
+    .then((folder) => folder.title);
+
+  try {
+    formValues = grafanaRuleDtoToFormValues(ruleToRecover, namespaceName);
+  } catch (err) {
+    const message = `Error getting rule values from the deleted rule: ${getMessageFromError(err)}`;
+    throw new Error(message);
+  }
+
+  const urlPath = rulerRuleType.any.recordingRule(ruleToRecover) ? '/alerting/new/grafana-recording' : '/alerting/new';
path: public/app/features/alerting/unified/components/rules/deleted-rules/ConfirmRestoreDeletedRuleModal.tsx
user: login: soniaAguilarPeiron
body: agree
created_at: 2025-03-26T14:55:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Alert } from '@grafana/ui';
+import { t } from 'app/core/internationalization';
+
+import { alertRuleApi } from '../../../api/alertRuleApi';
+import { GRAFANA_RULER_CONFIG } from '../../../api/featureDiscoveryApi';
+import { stringifyErrorLike } from '../../../utils/misc';
+import { withPageErrorBoundary } from '../../../withPageErrorBoundary';
+import { AlertingPageWrapper } from '../../AlertingPageWrapper';
+
+import { DeletedRules } from './DeletedRules';
+
+function DeletedrulesPage() {
+  const {
+    currentData = [],
+    isLoading,
+    error,
+  } = alertRuleApi.endpoints.getDeletedRules.useQuery({
+    rulerConfig: GRAFANA_RULER_CONFIG,
+    filter: {}, // todo: add filters, and limit?????
+  });
+  const values = Object.values(currentData);
+  const deletedRules = values.length > 0 ? values[0][0]?.rules : [];
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRulesPage.tsx
user: login: soniaAguilarPeiron
body: good idea!
created_at: 2025-03-26T14:56:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -413,5 +418,11 @@ export const alertRuleApi = alertingApi.injectEndpoints({
       }),
       keepUnusedDataFor: 0,
     }),
+    getDeletedRules: build.query<RulerGrafanaRulesConfigDTO, {}>({
+      query: () => ({
+        url: `/api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rules/`,
+        params: { deleted: 'true' },
+      }),
+    }),
path: public/app/features/alerting/unified/api/alertRuleApi.ts
user: login: soniaAguilarPeiron
body: I tested, and i see the recent deleted rule 🤔
created_at: 2025-03-26T15:50:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
+import { Button, Column, InteractiveTable, Stack } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { trackDeletedRuleRestoreFail, trackDeletedRuleRestoreSuccess } from '../../../Analytics';
+import { UpdatedByUser } from '../../rule-viewer/tabs/version-history/UpdatedBy';
+
+import { ConfirmRestoreDeletedRuleModal } from './ConfirmRestoreDeletedRuleModal';
+
+export const DELETED_RULES_PAGE_SIZE = 30;
+
+interface DeletedRulesProps {
+  deletedRules: Array<RulerGrafanaRuleDTO<GrafanaRuleDefinition>>
+}
+export function DeletedRules({ deletedRules }: DeletedRulesProps) {
+  const [confirmRestore, setConfirmRestore] = useState(false);
+  const [restoreRule, setRestoreRule] = useState<RulerGrafanaRuleDTO | undefined>();
+
+  const unknown = t('alerting.deletedRules.unknown', 'Unknown');
+
+  const showConfirmation = (id: string) => {
+    const ruleTorestore = deletedRules.find(
+      (rule) => getRowId(rule.grafana_alert) === id
+    );
+    if (!ruleTorestore) {
+      return;
+    }
+
+    setConfirmRestore(true);
+    setRestoreRule(ruleTorestore);
+  };
+
+  const hideConfirmation = () => {
+    setConfirmRestore(false);
+  };
+
+  const columns: Array<Column<(typeof deletedRules)[0]>> = [
+    {
+      id: 'createdBy',
+      header: t('alerting.deletedRules.table.updatedBy', 'Deleted By'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return <UpdatedByUser user={row.original.grafana_alert.updated_by} />;
+      },
+    },
+    {
+      id: 'title',
+      header: t('alerting.deletedRules.table.title', 'Title'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.title;
+      },
+    },
+    {
+      id: 'folder',
+      header: t('alerting.deletedRules.table.folder', 'Folder'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.namespace_uid;
+      },
+    },
+    {
+      id: 'group',
+      header: t('alerting.deletedRules.table.group', 'Group'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        return row.original.grafana_alert.rule_group;
+      },
+    },
+    {
+      id: 'created',
+      header: t('alerting.deletedRules.table.updated', 'Deletion Date'),
+      disableGrow: true,
+      cell: ({ row }) => {
+        const value = row.original.grafana_alert.updated;
+        if (!value) {
+          return unknown;
+        }
+        return dateTimeFormat(value) + ' (' + dateTimeFormatTimeAgo(value) + ')';
+      },
+    },
+    {
+      id: 'actions',
+      disableGrow: true,
+      cell: ({ row }) => {
+        return (
+          <Stack direction="row" alignItems="center" justifyContent="flex-end">
+            <Button
+              variant="secondary"
+              size="sm"
+              icon="history"
+              onClick={() => {
+                showConfirmation(getRowId(row.original.grafana_alert)
+                );
+              }}
+            >
+              <Trans i18nKey="alerting.deletedRules.restore">Restore</Trans>
+            </Button>
+          </Stack>
+        );
+      },
+    },
+  ];
+
+  return (
+    <>
+      <InteractiveTable
+        pageSize={DELETED_RULES_PAGE_SIZE}
+        columns={columns}
+        data={deletedRules}
+        getRowId={(row) => {
+          return getRowId(row.grafana_alert);
+        }}
+      />
+      <ConfirmRestoreDeletedRuleModal
+        ruleToRestore={restoreRule}
+        isOpen={confirmRestore}
+        onDismiss={hideConfirmation}
+        onRestoreSucess={trackDeletedRuleRestoreSuccess}
+        onRestoreError={trackDeletedRuleRestoreFail}
+      />
+    </>
+  );
+}
+
+function getRowId(row: GrafanaRuleDefinition) {
+  return `${row.namespace_uid}${row.namespace_uid}${row.title}${row.uid}${row.version}`;
+}
path: public/app/features/alerting/unified/components/rules/deleted-rules/DeletedRules.tsx
user: login: soniaAguilarPeiron
body: you're right, but I would add the guid as optional in the `GrafanaRuleDefinition`, as in this [PR](https://github.com/grafana/grafana/pull/101321) you can see that the guid is also added in the alert rules table.
created_at: 2025-03-26T16:18:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: soniaAguilarPeiron
created_at: 2025-03-12T08:57:12Z
body: /deploy-to-hg
author_association: CONTRIBUTOR
user: login: ephemeral-instances-bot[bot]
created_at: 2025-03-12T08:59:09Z
body: - Preparing your instance. A comment containing your instance's url will be added to this PR when the instance is ready.
- Your instance will be ready in ~10 minutes. [Follow the workflow progress](https://github.com/grafana/grafana/actions/runs/13807383093)
- Slack channel: #proj-ephemeral-hg-instances
- Building instance with **alerting/deleted-rules** oss branch and **main** enterprise branch. [How to choose a branch](https://github.com/grafana/hosted-grafana/wiki/Ephemeral-instances#deploying-a-grafana-and-grafana-enterprise-branch-together)
author_association: NONE
user: login: ephemeral-instances-bot[bot]
created_at: 2025-03-12T09:17:42Z
body: - Your instance can be accessed at: https://ephemeral15111821101869soniaa.grafana-dev.net
- The instance is not using the CDN assets.
- [How to access / How to update instance config / How to build a specific branch](https://github.com/grafana/hosted-grafana/wiki/Ephemeral-instances#choosing-which-oss-and-enterprise-branches-are-used-to-build-the-instance)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-20T08:46:28Z
body: Since you've added the `Add to what's new` label, consider drafting a [What's new note](https://admin.grafana.com/content-admin/#/collections/whats-new/new) for this feature.
<!-- Sticky Pull Request Comment -->
author_association: CONTRIBUTOR