Repository: grafana/grafana
Pull request #103744: Alerting: Rework rule editor layout
Author: login: gillesdemey
Labels: name: area/alerting
description: Grafana Alerting
name: area/frontend
name: add to changelog
name: no-backport
description: Skip backport of PR
Created: 2025-04-09T18:05:24Z | Merged: 2025-04-29T13:45:45Z

**What is this feature?**

This PR reworks the layout of the alert rule form creation / editing / cloning to no longer use `AppChrome`. 

Most notably this PR introduces a header similar to the alert rule detail view for editing rules.

**Special notes for your reviewer:**

Please check that:
- [ ] 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 2518e0548be9b..0a9111cfa7249 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -1020,7 +1020,122 @@ exports[`better eslint`] = {
       [0, 0, 0, "Do not use any type assertions.", "0"]
     ],
     "public/app/features/alerting/unified/components/rule-editor/RuleInspector.tsx:5381": [
+<<<<<<< HEAD
+      [0, 0, 0, "Do not use any type assertions.", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx:5381": [
+      [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.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. Wrap text with <Trans />", "1"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/rule-types/GrafanaManagedAlert.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/rule-types/MimirOrLokiAlert.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rule-editor/rule-types/MimirOrLokiRecordingRule.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "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/FederatedRuleWarning.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rule-viewer/PausedBadge.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rules/AlertStateTag.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rules/CloneRule.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
+    ],
+    "public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
+    ],
+    "public/app/features/alerting/unified/components/rules/RuleConfigStatus.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
+    ],
+    "public/app/features/alerting/unified/components/rules/RuleListErrors.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
+    ],
+    "public/app/features/alerting/unified/components/rules/RuleListStateSection.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rules/RuleState.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rules/RuleStats.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rules/RulesGroup.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
+    ],
+    "public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/settings/VersionManager.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
+    ],
+    "public/app/features/alerting/unified/components/silences/SilencedAlertsTableRow.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+    ],
+    "public/app/features/alerting/unified/components/silences/SilencedInstancesPreview.tsx:5381": [
+      [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
+=======
       [0, 0, 0, "Do not use any type assertions.", "0"]
+>>>>>>> origin/main
     ],
     "public/app/features/alerting/unified/components/silences/SilencesEditor.tsx:5381": [
       [0, 0, 0, "Do not use any type assertions.", "0"]
diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx
index 655b33b2e7e71..faed2a0ac8b48 100644
--- a/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx
+++ b/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx
@@ -78,7 +78,7 @@ describe('RuleEditor grafana recording rules', () => {
       await user.type(await ui.inputs.metric.find(), 'metricName');
       await selectFolderAndGroup(user);
 
-      await user.click(ui.buttons.saveAndExit.get());
+      await user.click(ui.buttons.save.get());
 
       const requests = await capture;
       const serializedRequests = await serializeRequests(requests);
@@ -96,7 +96,7 @@ describe('RuleEditor grafana recording rules', () => {
       await user.type(await ui.inputs.name.find(), 'my great new rule');
       await selectFolderAndGroup(user);
 
-      await user.click(ui.buttons.saveAndExit.get());
+      await user.click(ui.buttons.save.get());
       const requests = await capture;
       expect(requests).toHaveLength(0);
     });
diff --git a/public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.tsx b/public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.tsx
index 36844b9b75c37..2e8bcdfc5679b 100644
--- a/public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/DurationQuickPick.tsx
@@ -43,9 +43,13 @@ export function DurationQuickPick({ selectedDuration, groupEvaluationInterval, o
             onSelect(duration);
           }}
         >
-          {duration === '0s' ? t('alerting.duration-quick-pick.none', 'None') : duration}
+          {stringifyPendingPeriod(duration)}
         </Button>
       ))}
     </Stack>
   );
 }
+
+export function stringifyPendingPeriod(duration: string): string {
+  return duration === '0s' ? t('alerting.duration-quick-pick.none', 'None') : duration;
+}
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
index 88549aa16f125..cf8dfd3f5897f 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
@@ -5,8 +5,7 @@ import { useParams } from 'react-router-dom-v5-compat';
 
 import { GrafanaTheme2 } from '@grafana/data';
 import { config, locationService } from '@grafana/runtime';
-import { Alert, Button, ConfirmModal, Spinner, Stack, useStyles2 } from '@grafana/ui';
-import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
+import { Alert, Button, Spinner, Stack, useStyles2 } from '@grafana/ui';
 import { useAppNotification } from 'app/core/copy/appNotification';
 import { contextSrv } from 'app/core/core';
 import { Trans, t } from 'app/core/internationalization';
@@ -22,7 +21,7 @@ import {
   rulerRuleType,
 } from 'app/features/alerting/unified/utils/rules';
 import { isExpressionQuery } from 'app/features/expressions/guards';
-import { RuleGroupIdentifier, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting';
+import { RuleGroupIdentifier, RuleWithLocation } from 'app/types/unified-alerting';
 import { PostableRuleGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
 
 import {
@@ -41,10 +40,7 @@ import {
   RulerGroupUpdatedResponse,
   isGrafanaGroupUpdatedResponse,
 } from '../../../api/alertRuleModel';
-import { shouldUseAlertingListViewV2, shouldUsePrometheusRulesPrimary } from '../../../featureToggles';
-import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup';
 import { useAddRuleToRuleGroup, useUpdateRuleInRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
-import { useReturnTo } from '../../../hooks/useReturnTo';
 import {
   defaultFormValuesForRuleType,
   formValuesFromExistingRule,
@@ -63,9 +59,7 @@ import {
   formValuesToRulerGrafanaRuleDTO,
   formValuesToRulerRuleDTO,
 } from '../../../utils/rule-form';
-import * as ruleId from '../../../utils/rule-id';
-import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id';
-import { createRelativeUrl } from '../../../utils/url';
+import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier } from '../../../utils/rule-id';
 import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
 import { AlertRuleNameAndMetric } from '../AlertRuleNameInput';
 import AnnotationsStep from '../AnnotationsStep';
@@ -83,27 +77,21 @@ type Props = {
   isManualRestore?: boolean;
 };
 
-const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary();
-const alertingListViewV2 = shouldUseAlertingListViewV2();
-
 export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) => {
   const styles = useStyles2(getStyles);
   const notifyApp = useAppNotification();
-  const { redirectToDetailsPage } = useRedirectToDetailsPage();
+
+  const routeParams = useParams<{ type: string; id: string }>();
+  const uidFromParams = routeParams.id;
+
+  const { redirectToDetailsPage } = useRedirectToDetailsPage(uidFromParams);
   const [showEditYaml, setShowEditYaml] = useState(false);
 
-  const [deleteRuleFromGroup] = useDeleteRuleFromGroup();
   const [addRuleToRuleGroup] = useAddRuleToRuleGroup();
   const [updateRuleInRuleGroup] = useUpdateRuleInRuleGroup();
 
-  const { returnTo } = useReturnTo();
-  const routeParams = useParams<{ type: string; id: string }>();
   const ruleType = translateRouteParamToRuleType(routeParams.type);
 
-  const uidFromParams = routeParams.id || '';
-
-  const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
-
   const defaultValues: RuleFormValues = useMemo(() => {
     // If we have an existing AND a prefill, then we're coming from the restore dialog
     // and we want to merge the two
@@ -157,7 +145,7 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) =>
   };
 
   // @todo why is error not propagated to form?
-  const submit = async (values: RuleFormValues, exitOnSave: boolean) => {
+  const submit = async (values: RuleFormValues): Promise<void> => {
     const { type, evaluateEvery } = values;
 
     if (conditionErrorMsg !== '') {
@@ -208,41 +196,8 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) =>
       );
     }
 
-    const { dataSourceName, namespaceName, groupName } = targetRuleGroupIdentifier;
-
-    // V2 list is based on eventually consistent Prometheus API.
-    // When a new rule group is created it takes a while for the new rule group to be reflected in the V2 list.
-    // To avoid user confusion we redirect to the details page which is driven by a strongly consistent Ruler API..
-    if (alertingListViewV2) {
-      redirectToDetailsPage(ruleDefinition, targetRuleGroupIdentifier, saveResult);
-      return;
-    }
-
-    if (exitOnSave) {
-      const returnToUrl = returnTo || getReturnToUrl(targetRuleGroupIdentifier, ruleDefinition);
-
-      locationService.push(returnToUrl);
-      return;
-    } else {
-      // we stay in the same page
-
-      // Cloud Ruler rules identifier changes on update due to containing rule name and hash components
-      // After successful update we need to update the URL to avoid displaying 404 errors
-      if (rulerRuleType.dataSource.rule(ruleDefinition)) {
-        const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition);
-        locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`);
-      }
-    }
-  };
-
-  const deleteRule = async () => {
-    if (existing) {
-      const ruleGroupIdentifier = getRuleGroupLocationFromRuleWithLocation(existing);
-      const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule);
-
-      await deleteRuleFromGroup.execute(ruleGroupIdentifier, ruleIdentifier);
-      locationService.replace(returnTo ?? '/alerting/list');
-    }
+    redirectToDetailsPage(ruleDefinition, targetRuleGroupIdentifier, saveResult);
+    return;
   };
 
   const onInvalid: SubmitErrorHandler<RuleFormValues> = (errors): void => {
@@ -266,49 +221,14 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) =>
     locationService.getHistory().goBack();
   };
 
-  const actionButtons = (
-    <Stack justifyContent="flex-end" alignItems="center">
-      <Button
-        data-testid="save-rule-and-exit"
-        variant="primary"
-        type="button"
-        size="sm"
-        onClick={handleSubmit((values) => submit(values, true), onInvalid)}
-        disabled={isSubmitting}
-      >
-        {isSubmitting && <Spinner className={styles.buttonSpinner} inline={true} />}
-        <Trans i18nKey="alerting.alert-rule-form.action-buttons.save-exit">Save rule and exit</Trans>
-      </Button>
-      <Button variant="secondary" disabled={isSubmitting} type="button" onClick={cancelRuleCreation} size="sm">
-        <Trans i18nKey="alerting.common.cancel">Cancel</Trans>
-      </Button>
-      {existing ? (
-        <Button fill="outline" variant="destructive" type="button" onClick={() => setShowDeleteModal(true)} size="sm">
-          <Trans i18nKey="alerting.alert-rule-form.action-buttons.delete">Delete</Trans>
-        </Button>
-      ) : null}
-      {existing && isCortexLokiOrRecordingRule(watch) && (
-        <Button
-          variant="secondary"
-          type="button"
-          onClick={() => setShowEditYaml(true)}
-          disabled={isSubmitting}
-          size="sm"
-        >
-          <Trans i18nKey="alerting.alert-rule-form.action-buttons.edit-yaml">Edit YAML</Trans>
-        </Button>
-      )}
-    </Stack>
-  );
-
-  const isPaused = rulerRuleType.grafana.alertingRule(existing?.rule) && isPausedRule(existing?.rule);
-
   if (!type) {
     return null;
   }
+
+  const isPaused = rulerRuleType.grafana.rule(existing?.rule) && isPausedRule(existing?.rule);
+
   return (
     <FormProvider {...formAPI}>
-      <AppChromeUpdate actions={actionButtons} />
       <form onSubmit={(e) => e.preventDefault()} className={styles.form}>
         <div className={styles.contentOuter}>
           {isManualRestore && (
@@ -348,39 +268,55 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) =>
                 {!isRecordingRuleByType(type) && <AnnotationsStep />}
               </>
             )}
+
+            {/* actions */}
+            <Stack direction="row" alignItems="center">
+              <Button
+                data-testid="save-rule"
+                variant="primary"
+                type="button"
+                onClick={handleSubmit((values) => submit(values), onInvalid)}
+                disabled={isSubmitting}
+                icon={isSubmitting ? 'spinner' : undefined}
+              >
+                {isSubmitting && <Spinner className={styles.buttonSpinner} inline={true} />}
+                <Trans i18nKey="alerting.alert-rule-form.action-buttons.save">Save</Trans>
+              </Button>
+
+              <Button variant="secondary" disabled={isSubmitting} type="button" onClick={cancelRuleCreation}>
+                <Trans i18nKey="alerting.common.cancel">Cancel</Trans>
+              </Button>
+
+              {existing && isCortexLokiOrRecordingRule(watch) && (
+                <Button variant="secondary" type="button" onClick={() => setShowEditYaml(true)} disabled={isSubmitting}>
+                  <Trans i18nKey="alerting.alert-rule-form.action-buttons.edit-yaml">Edit YAML</Trans>
+                </Button>
+              )}
+            </Stack>
           </Stack>
         </div>
       </form>
-      {showDeleteModal ? (
-        <ConfirmModal
-          isOpen={true}
-          title={t('alerting.alert-rule-form.title-delete-rule', 'Delete rule')}
-          body="Deleting this rule will permanently remove it. Are you sure you want to delete this rule?"
-          confirmText="Yes, delete"
-          icon="exclamation-triangle"
-          onConfirm={deleteRule}
-          onDismiss={() => setShowDeleteModal(false)}
-        />
-      ) : null}
+
       {showEditYaml && (
         <>
-          {isGrafanaManagedRuleByType(type) && (
+          {grafanaTypeRule && uidFromParams && (
             <GrafanaRuleExporter alertUid={uidFromParams} onClose={() => setShowEditYaml(false)} />
           )}
 
-          {!isGrafanaManagedRuleByType(type) && <RuleInspector onClose={() => setShowEditYaml(false)} />}
+          {!grafanaTypeRule && <RuleInspector onClose={() => setShowEditYaml(false)} />}
         </>
       )}
     </FormProvider>
   );
 };
 
-function useRedirectToDetailsPage() {
+function useRedirectToDetailsPage(existingUid?: string) {
   const notifyApp = useAppNotification();
 
   const redirectGrafanaRule = useCallback(
     (saveResult: GrafanaGroupUpdatedResponse) => {
-      const newOrUpdatedRuleUid = saveResult.created?.at(0) || saveResult.updated?.at(0);
+      // if the response contains no created or updated rules, we'll use the existing UID.
+      const newOrUpdatedRuleUid = (saveResult.created?.at(0) || saveResult.updated?.at(0)) ?? existingUid;
       if (newOrUpdatedRuleUid) {
         locationService.replace(
           rulesNav.detailsPageLink('grafana', { uid: newOrUpdatedRuleUid, ruleSourceName: 'grafana' })
@@ -393,7 +329,7 @@ function useRedirectToDetailsPage() {
         logWarning('Cannot navigate to the new rule details page. The rule was created but the UID is missing.');
       }
     },
-    [notifyApp]
+    [existingUid, notifyApp]
   );
 
   const redirectCloudRulerRule = useCallback((rule: RulerRuleDTO, groupId: RuleGroupIdentifier) => {
@@ -427,27 +363,6 @@ function useRedirectToDetailsPage() {
   return { redirectToDetailsPage };
 }
 
-function getReturnToUrl(groupId: RuleGroupIdentifier, rule: RulerRuleDTO | PostableRuleGrafanaRuleDTO) {
-  const { dataSourceName, namespaceName, groupName } = groupId;
-
-  if (prometheusRulesPrimary && rulerRuleType.dataSource.rule(rule)) {
-    const ruleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, rule);
-    return createViewLinkFromIdentifier(ruleIdentifier);
-  }
-
-  // TODO We could add namespace and group filters but for GMA the namespace = uid which doesn't work with the filters
-  return '/alerting/list';
-}
-
-// The result of this function is passed to locationService.push()
-// Hence it cannot contain the subpath prefix, so we cannot use createRelativeUrl for it
-function createViewLinkFromIdentifier(identifier: RuleIdentifier, returnTo?: string) {
-  const paramId = encodeURIComponent(ruleId.stringifyIdentifier(identifier));
-  const paramSource = encodeURIComponent(identifier.ruleSourceName);
-
-  return createRelativeUrl(`/alerting/${paramSource}/${paramId}/view`, returnTo ? { returnTo } : {});
-}
-
 const isCortexLokiOrRecordingRule = (watch: UseFormWatch<RuleFormValues>) => {
   const [ruleType, dataSourceName] = watch(['type', 'dataSourceName']);
 
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
index adc000d45ebf1..8e0c66438e4df 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
@@ -6,7 +6,6 @@ import { Button, LinkButton, LoadingPlaceholder, Stack } from '@grafana/ui';
 import { useAppNotification } from 'app/core/copy/appNotification';
 import { Trans, t } from 'app/core/internationalization';
 
-import { AppChromeUpdate } from '../../../../../../core/components/AppChrome/AppChromeUpdate';
 import {
   PostableRulerRuleGroupDTO,
   RulerRuleDTO,
@@ -82,38 +81,38 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
     setExportData(undefined);
   }, [setExportData]);
 
-  const actionButtons = [
-    <LinkButton href={returnTo} key="cancel" size="sm" variant="secondary" onClick={() => submit(undefined)}>
-      <Trans i18nKey="alerting.common.cancel">Cancel</Trans>
-    </LinkButton>,
-    <Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues), onInvalid)}>
-      <Trans i18nKey="alerting.modify-export-rule-form.action-buttons.export">Export</Trans>
-    </Button>,
-  ];
-
   return (
     <FormProvider {...formAPI}>
-      <AppChromeUpdate actions={actionButtons} />
-      <form onSubmit={(e) => e.preventDefault()}>
-        <div>
-          <Stack direction="column" gap={3}>
-            {/* Step 1 */}
-            <AlertRuleNameAndMetric />
-            {/* Step 2 */}
-            <QueryAndExpressionsStep editingExistingRule={existing} onDataChange={checkAlertCondition} mode="draft" />
-            {/* Step 3-4-5 */}
-            <GrafanaFolderAndLabelsStep />
-
-            {/* Step 4 & 5 */}
-            <GrafanaEvaluationBehaviorStep existing={Boolean(existing)} enableProvisionedGroups={true} />
-            {/* Notifications step*/}
-            <NotificationsStep alertUid={alertUid} />
-            {/* Annotations only for cloud and Grafana */}
-            <AnnotationsStep />
-          </Stack>
-        </div>
-      </form>
-      {exportData && <GrafanaRuleDesignExporter exportValues={exportData} onClose={onClose} uid={alertUid} />}
+      <Stack direction="column">
+        <form onSubmit={(e) => e.preventDefault()}>
+          <div>
+            <Stack direction="column" gap={3}>
+              {/* Step 1 */}
+              <AlertRuleNameAndMetric />
+              {/* Step 2 */}
+              <QueryAndExpressionsStep editingExistingRule={existing} onDataChange={checkAlertCondition} mode="draft" />
+              {/* Step 3-4-5 */}
+              <GrafanaFolderAndLabelsStep />
+
+              {/* Step 4 & 5 */}
+              <GrafanaEvaluationBehaviorStep existing={Boolean(existing)} enableProvisionedGroups={true} />
+              {/* Notifications step*/}
+              <NotificationsStep alertUid={alertUid} />
+              {/* Annotations only for cloud and Grafana */}
+              <AnnotationsStep />
+            </Stack>
+          </div>
+        </form>
+        {exportData && <GrafanaRuleDesignExporter exportValues={exportData} onClose={onClose} uid={alertUid} />}
+        <Stack direction="row">
+          <Button key="export-rule" onClick={formAPI.handleSubmit((formValues) => submit(formValues), onInvalid)}>
+            <Trans i18nKey="alerting.modify-export-rule-form.action-buttons.export">Export</Trans>
+          </Button>
+          <LinkButton href={returnTo} key="cancel" variant="secondary" onClick={() => submit(undefined)}>
+            <Trans i18nKey="alerting.common.cancel">Cancel</Trans>
+          </LinkButton>
+        </Stack>
+      </Stack>
     </FormProvider>
   );
 }
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx
index 0a949d9bccb8e..381ce9cb6ca0c 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx
@@ -97,7 +97,7 @@ describe('Can create a new grafana managed alert using simplified routing', () =
 
     // do not select a contact point
     // save and check that call to backend was not made
-    await user.click(ui.buttons.saveAndExit.get());
+    await user.click(ui.buttons.save.get());
     expect(await screen.findByText('Contact point is required.')).toBeInTheDocument();
     const capturedRequests = await capture;
 
@@ -129,7 +129,7 @@ describe('Can create a new grafana managed alert using simplified routing', () =
     await selectContactPoint(user, contactPointName);
 
     // save and check what was sent to backend
-    await user.click(ui.buttons.saveAndExit.get());
+    await user.click(ui.buttons.save.get());
     const requests = await capture;
 
     const serializedRequests = await serializeRequests(requests);
@@ -162,7 +162,7 @@ describe('Can create a new grafana managed alert using simplified routing', () =
       await selectContactPoint(user, contactPointName);
 
       // save and check what was sent to backend
-      await user.click(ui.buttons.saveAndExit.get());
+      await user.click(ui.buttons.save.get());
       const requests = await capture;
       const serializedRequests = await serializeRequests(requests);
       expect(serializedRequests).toMatchSnapshot();
@@ -180,7 +180,7 @@ describe('Can create a new grafana managed alert using simplified routing', () =
       await selectFolderAndGroup(user);
 
       // save and check what was sent to backend
-      await user.click(ui.buttons.saveAndExit.get());
+      await user.click(ui.buttons.save.get());
       const requests = await capture;
       const serializedRequests = await serializeRequests(requests);
       expect(serializedRequests).toMatchSnapshot();
@@ -198,7 +198,7 @@ describe('Can create a new grafana managed alert using simplified routing', () =
       await user.click(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Notification).get()); // switch notifications step to advanced mode
 
       // save and check what was sent to backend
-      await user.click(ui.buttons.saveAndExit.get());
+      await user.click(ui.buttons.save.get());
       const requests = await capture;
       const serializedRequests = await serializeRequests(requests);
       expect(serializedRequests).toMatchSnapshot();
@@ -218,7 +218,7 @@ describe('Can create a new grafana managed alert using simplified routing', () =
       await user.click(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Query).get()); // switch query step to advanced mode
 
       // save and check what was sent to backend
-      await user.click(ui.buttons.saveAndExit.get());
+      await user.click(ui.buttons.save.get());
       const requests = await capture;
       const serializedRequests = await serializeRequests(requests);
       expect(serializedRequests).toMatchSnapshot();
diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
index bce2689be6492..90e6f9ff1da9d 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
@@ -275,16 +275,17 @@ interface TitleProps {
   health?: RuleHealth;
   ruleType?: PromRuleType;
   ruleOrigin?: RulePluginOrigin;
+  returnToHref?: string;
 }
 
-export const Title = ({ name, paused = false, state, health, ruleType, ruleOrigin }: TitleProps) => {
+export const Title = ({ name, paused = false, state, health, ruleType, ruleOrigin, returnToHref = '' }: TitleProps) => {
   const isRecordingRule = ruleType === PromRuleType.Recording;
 
-  const { returnTo } = useReturnTo('/alerting/list');
+  const { returnTo } = useReturnTo(returnToHref);
 
   return (
     <Stack direction="row" gap={1} minWidth={0} alignItems="center">
-      <LinkButton variant="secondary" icon="angle-left" href={returnTo} />
+      {returnToHref && <LinkButton variant="secondary" icon="angle-left" href={returnTo} />}
       {ruleOrigin && <PluginOriginBadge pluginId={ruleOrigin.pluginId} size="lg" />}
       <Text variant="h1" truncate>
         {name}
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
index d7e6839d81086..787a055003789 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx
@@ -70,7 +70,7 @@ interface EvaluationBehaviorSummaryProps {
 }
 
 const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) => {
-  const every = rule.group.interval;
+  const interval = rule.group.interval;
   const lastEvaluation = rule.promRule?.lastEvaluation;
   const lastEvaluationDuration = rule.promRule?.evaluationTime;
   const metric = rulerRuleType.grafana.recordingRule(rule.rulerRule)
@@ -87,10 +87,10 @@ const EvaluationBehaviorSummary = ({ rule }: EvaluationBehaviorSummaryProps) =>
           {metric}
         </DetailsField>
       )}
-      {every && (
+      {interval && (
         <DetailsField label={t('alerting.evaluation-behavior-summary.label-evaluate', 'Evaluate')} horizontal={true}>
-          <Trans i18nKey="alerting.evaluation-behavior-summary.evaluate" values={{ every }}>
-            Every {{ every }}
+          <Trans i18nKey="alerting.evaluation-behavior-summary.evaluate" values={{ interval }}>
+            Every {{ interval }}
           </Trans>
         </DetailsField>
       )}
diff --git a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx
index 298be9b15298e..c59101fca8ed4 100644
--- a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx
+++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx
@@ -1,9 +1,10 @@
 import * as React from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
-import { getWrapper, render, waitFor, waitForElementToBeRemoved, within } from 'test/test-utils';
-import { byRole, byTestId, byText } from 'testing-library-selector';
+import { getWrapper, render, waitFor, within } from 'test/test-utils';
+import { byRole, byTestId } from 'testing-library-selector';
 
 import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
+import { DashboardSearchItemType } from 'app/features/search/types';
 import { AccessControlAction } from 'app/types';
 import { RuleWithLocation } from 'app/types/unified-alerting';
 import {
@@ -18,19 +19,22 @@ import { setupMswServer } from '../mockApi';
 import {
   grantUserPermissions,
   mockDataSource,
+  mockFolder,
   mockRulerAlertingRule,
   mockRulerGrafanaRule,
   mockRulerRuleGroup,
 } from '../mocks';
 import { grafanaRulerRule } from '../mocks/grafanaRulerApi';
 import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from '../mocks/rulerApi';
+import { setFolderResponse } from '../mocks/server/configure';
 import { AlertingQueryRunner } from '../state/AlertingQueryRunner';
 import { setupDataSources } from '../testSetup/datasources';
 import { RuleFormValues } from '../types/rule-form';
 import { Annotation } from '../utils/constants';
 import { hashRulerRule } from '../utils/rule-id';
 
-import { CloneRuleEditor, cloneRuleDefinition } from './CloneRuleEditor';
+import { ExistingRuleEditor } from './ExistingRuleEditor';
+import { cloneRuleDefinition } from './clone.utils';
 import { getDefaultFormValues } from './formDefaults';
 
 jest.mock('../components/rule-editor/ExpressionEditor', () => ({
@@ -54,7 +58,6 @@ const ui = {
     annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
     labelValue: (idx: number) => byTestId(`label-value-${idx}`),
   },
-  loadingIndicator: byText('Loading the rule...'),
 };
 
 const Providers = getWrapper({ renderWithRouter: true });
@@ -68,18 +71,50 @@ function Wrapper({ children }: React.PropsWithChildren<{}>) {
 }
 
 describe('CloneRuleEditor', function () {
-  grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]);
+  const folder = {
+    title: 'Folder A',
+    uid: grafanaRulerRule.grafana_alert.namespace_uid,
+    id: 1,
+    type: DashboardSearchItemType.DashDB,
+    accessControl: {
+      [AccessControlAction.AlertingRuleUpdate]: true,
+    },
+  };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    grantUserPermissions([
+      AccessControlAction.AlertingRuleRead,
+      AccessControlAction.AlertingRuleCreate,
+      AccessControlAction.DataSourcesRead,
+      AccessControlAction.FoldersRead,
+      AccessControlAction.AlertingRuleExternalRead,
+      AccessControlAction.AlertingRuleExternalWrite,
+    ]);
+
+    const dataSources = {
+      default: mockDataSource({
+        uid: MIMIR_DATASOURCE_UID,
+        type: 'prometheus',
+        name: 'Mimir',
+        isDefault: true,
+      }),
+    };
+    setupDataSources(dataSources.default);
+    setFolderResponse(mockFolder(folder));
+  });
 
   describe('Grafana-managed rules', function () {
     it('should populate form values from the existing alert rule', async function () {
-      setupDataSources();
-
       render(
-        <CloneRuleEditor sourceRuleId={{ uid: grafanaRulerRule.grafana_alert.uid, ruleSourceName: 'grafana' }} />,
+        <ExistingRuleEditor
+          identifier={{ uid: grafanaRulerRule.grafana_alert.uid, ruleSourceName: 'grafana' }}
+          clone={true}
+        />,
         { wrapper: Wrapper }
       );
 
-      await waitForElementToBeRemoved(ui.loadingIndicator.query());
       await waitFor(() => {
         expect(within(ui.inputs.group.get()).queryByTestId('Spinner')).not.toBeInTheDocument();
       });
@@ -130,20 +165,19 @@ describe('CloneRuleEditor', function () {
       });
 
       render(
-        <CloneRuleEditor
-          sourceRuleId={{
+        <ExistingRuleEditor
+          identifier={{
             ruleSourceName: 'my-prom-ds',
             namespace: 'namespace-one',
             groupName: 'group1',
             ruleName: 'First Ruler Rule',
             rulerRuleHash: hashRulerRule(originRule),
           }}
+          clone={true}
         />,
         { wrapper: Wrapper }
       );
 
-      await waitForElementToBeRemoved(ui.loadingIndicator.query());
-
       await waitFor(() => {
         expect(ui.inputs.name.get()).toHaveValue('First Ruler Rule (copy)');
       });
diff --git a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx
index 9eb61f1e13b2f..3626894fac11d 100644
--- a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx
+++ b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx
@@ -1,76 +1,133 @@
-import { Alert, LoadingPlaceholder } from '@grafana/ui';
-import { useQueryParams } from 'app/core/hooks/useQueryParams';
+import { NavModelItem } from '@grafana/data';
+import { Alert, Stack } from '@grafana/ui';
 import { Trans, t } from 'app/core/internationalization';
 import { RuleIdentifier } from 'app/types/unified-alerting';
 
 import { AlertWarning } from '../AlertWarning';
+import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
 import { AlertRuleForm } from '../components/rule-editor/alert-rule-form/AlertRuleForm';
+import { FederatedRuleWarning } from '../components/rule-viewer/FederatedRuleWarning';
 import { useRuleWithLocation } from '../hooks/useCombinedRule';
 import { useIsRuleEditable } from '../hooks/useIsRuleEditable';
 import { RuleFormValues } from '../types/rule-form';
+import { Annotation } from '../utils/constants';
 import { stringifyErrorLike } from '../utils/misc';
+import { rulerRuleToFormValues } from '../utils/rule-form';
 import * as ruleId from '../utils/rule-id';
+import { isFederatedRuleGroup, rulerRuleType } from '../utils/rules';
 
+import { defaultPageNav } from './RuleEditor';
+import { cloneRuleDefinition } from './clone.utils';
 interface ExistingRuleEditorProps {
   identifier: RuleIdentifier;
-  /** Provide prefill if we are trying to restore an old version of an alert rule but we need the user to manually tweak the values */
+  // Provide prefill if we are trying to restore an old version of an alert rule but we need the user to manually tweak the values
   prefill?: Partial<RuleFormValues>;
+  // indicate if this is a manual restore
+  isManualRestore?: boolean;
+  // indicate if this is a cloning operation
+  clone?: boolean;
 }
 
-export function ExistingRuleEditor({ identifier, prefill }: ExistingRuleEditorProps) {
-  const [queryParams] = useQueryParams();
-  const isManualRestore = Boolean(queryParams.isManualRestore);
-
+export function ExistingRuleEditor({
+  identifier,
+  prefill,
+  isManualRestore = false,
+  clone = false,
+}: ExistingRuleEditorProps) {
+  const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier);
   const {
     loading: loadingAlertRule,
     result: ruleWithLocation,
     error: fetchRuleError,
   } = useRuleWithLocation({ ruleIdentifier: identifier });
-
-  const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier);
   const {
     isEditable,
     loading: loadingEditable,
     error: errorEditable,
   } = useIsRuleEditable(ruleSourceName, ruleWithLocation?.rule);
 
-  // error handling for fetching rule and rule RBAC
   if (fetchRuleError || errorEditable) {
     return (
-      <Alert
-        severity="error"
-        title={t('alerting.existing-rule-editor.title-failed-to-load-rule', 'Failed to load rule')}
-      >
-        {stringifyErrorLike(errorEditable ?? fetchRuleError)}
-      </Alert>
+      <AlertingPageWrapper navId="alert-list" pageNav={getPageNav()}>
+        <Alert
+          severity="error"
+          title={t('alerting.existing-rule-editor.title-failed-to-load-rule', 'Failed to load rule')}
+        >
+          {stringifyErrorLike(errorEditable ?? fetchRuleError)}
+        </Alert>
+      </AlertingPageWrapper>
     );
   }
 
   const loading = loadingAlertRule || loadingEditable;
-
   if (loading) {
-    return <LoadingPlaceholder text={t('alerting.existing-rule-editor.text-loading-rule', 'Loading rule...')} />;
+    return (
+      <AlertingPageWrapper navId="alert-list" pageNav={getPageNav()} isLoading={true}>
+        {null}
+      </AlertingPageWrapper>
+    );
   }
 
   if (!ruleWithLocation && !loading) {
     return (
-      <AlertWarning title={t('alerting.existing-rule-editor.title-rule-not-found', 'Rule not found')}>
-        <Trans i18nKey="alerting.existing-rule-editor.sorry-this-rule-does-not-exist">
-          Sorry! This rule does not exist.
-        </Trans>
-      </AlertWarning>
+      <AlertingPageWrapper navId="alert-list" pageNav={getPageNav()}>
+        <AlertWarning title={t('alerting.existing-rule-editor.title-rule-not-found', 'Rule not found')}>
+          <Trans i18nKey="alerting.existing-rule-editor.sorry-this-rule-does-not-exist">
+            Sorry! This rule does not exist.
+          </Trans>
+        </AlertWarning>
+      </AlertingPageWrapper>
     );
   }
 
   if (isEditable === false) {
     return (
-      <AlertWarning title={t('alerting.existing-rule-editor.title-cannot-edit-rule', 'Cannot edit rule')}>
-        <Trans i18nKey="alerting.existing-rule-editor.sorry-permission">
-          Sorry! You do not have permission to edit this rule.
-        </Trans>
-      </AlertWarning>
+      <AlertingPageWrapper navId="alert-list" pageNav={getPageNav()}>
+        <AlertWarning title={t('alerting.existing-rule-editor.title-cannot-edit-rule', 'Cannot edit rule')}>
+          <Trans i18nKey="alerting.existing-rule-editor.sorry-permission">
+            Sorry! You do not have permission to edit this rule.
+          </Trans>
+        </AlertWarning>
+      </AlertingPageWrapper>
     );
   }
 
-  return <AlertRuleForm existing={ruleWithLocation} prefill={prefill} isManualRestore={isManualRestore} />;
+  // we shouldn't get here because loading / error handling happens before this
+  if (!ruleWithLocation) {
+    return null;
+  }
+
+  const rulerRule = ruleWithLocation.rule;
+  const summary = rulerRuleType.any.alertingRule(rulerRule) ? rulerRule.annotations?.[Annotation.summary] : null;
+
+  const isFederatedRule = isFederatedRuleGroup(ruleWithLocation.group);
+  const isRecordingRule = rulerRuleType.any.recordingRule(rulerRule);
+
+  const pageTitle = isRecordingRule
+    ? t('alerting.editor.edit-recording-rule', 'Edit recording rule')
+    : t('alerting.editor.edit-alert-rule', 'Edit alert rule');
+
+  return (
+    <AlertingPageWrapper
+      navId="alert-list"
+      subTitle={
+        <Stack direction="column">
+          {summary}
+          {/* alerts and notifications and stuff */}
+          {isFederatedRule && <FederatedRuleWarning />}
+        </Stack>
+      }
+      pageNav={getPageNav({ text: pageTitle })}
+    >
+      {clone ? (
+        <AlertRuleForm prefill={rulerRuleToFormValues(cloneRuleDefinition(ruleWithLocation))} />
+      ) : (
+        <AlertRuleForm existing={ruleWithLocation} prefill={prefill} isManualRestore={isManualRestore} />
+      )}
+    </AlertingPageWrapper>
+  );
 }
+
+const getPageNav = (pageNavOptions?: Partial<NavModelItem>): NavModelItem => {
+  return { ...defaultPageNav, id: 'alert-rule-edit', text: '', ...pageNavOptions };
+};
diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx
index 8a06815b6b07b..eb9d5b005fa79 100644
--- a/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx
+++ b/public/app/features/alerting/unified/rule-editor/RuleEditor.tsx
@@ -1,9 +1,7 @@
-import { useCallback } from 'react';
 import { useParams } from 'react-router-dom-v5-compat';
 
 import { NavModelItem } from '@grafana/data';
 import { Trans, t } from 'app/core/internationalization';
-import { RuleIdentifier } from 'app/types/unified-alerting';
 
 import { AlertWarning } from '../AlertWarning';
 import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
@@ -13,91 +11,98 @@ import { useRulesAccess } from '../utils/accessControlHooks';
 import * as ruleId from '../utils/rule-id';
 import { withPageErrorBoundary } from '../withPageErrorBoundary';
 
-import { CloneRuleEditor } from './CloneRuleEditor';
 import { ExistingRuleEditor } from './ExistingRuleEditor';
 import { formValuesFromQueryParams, translateRouteParamToRuleType } from './formDefaults';
-
-type RuleEditorPathParams = {
+export type RuleEditorPathParams = {
   id?: string;
   type?: 'recording' | 'alerting' | 'grafana-recording';
 };
 
-const defaultPageNav: Partial<NavModelItem> = {
-  icon: 'bell',
+export const defaultPageNav: Partial<NavModelItem> = {
   id: 'alert-rule-view',
 };
 
-// sadly we only get the "type" when a new rule is being created, when editing an existing recording rule we can't actually know it from the URL
-const getPageNav = (identifier?: RuleIdentifier, type?: RuleEditorPathParams['type']) => {
-  if (type === 'recording' || type === 'grafana-recording') {
-    if (identifier) {
-      // this branch should never trigger actually, the type param isn't used when editing rules
-      return { ...defaultPageNav, id: 'alert-rule-edit', text: 'Edit recording rule' };
-    } else {
-      return { ...defaultPageNav, id: 'alert-rule-add', text: 'New recording rule' };
-    }
+const RuleEditor = () => {
+  const { identifier } = useRuleEditorPathParams();
+  const cloneIdentifier = useIdentifierFromCopy();
+  const isManualRestore = useManualRestore();
+
+  const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess();
+
+  if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) {
+    return (
+      <AlertWarning title={t('alerting.rule-editor.get-content.title-cannot-create-rules', 'Cannot create rules')}>
+        <Trans i18nKey="alerting.rule-editor.get-content.sorry-allowed-create-rules">
+          Sorry! You are not allowed to create rules.
+        </Trans>
+      </AlertWarning>
+    );
+  }
+
+  if (identifier && !canEditRules(identifier.ruleSourceName)) {
+    return (
+      <AlertWarning title={t('alerting.rule-editor.get-content.title-cannot-edit-rules', 'Cannot edit rules')}>
+        <Trans i18nKey="alerting.rule-editor.get-content.sorry-allowed-rules">
+          Sorry! You are not allowed to edit rules.
+        </Trans>
+      </AlertWarning>
+    );
   }
 
   if (identifier) {
-    // keep this one ambiguous, don't mentiond a specific alert type here
-    return { ...defaultPageNav, id: 'alert-rule-edit', text: 'Edit rule' };
-  } else {
-    return { ...defaultPageNav, id: 'alert-rule-add', text: 'New alert rule' };
+    return (
+      <ExistingRuleEditor key={JSON.stringify(identifier)} identifier={identifier} isManualRestore={isManualRestore} />
+    );
   }
+
+  if (cloneIdentifier) {
+    return (
+      <ExistingRuleEditor
+        key={JSON.stringify(identifier)}
+        identifier={cloneIdentifier}
+        clone={true}
+        isManualRestore={isManualRestore}
+      />
+    );
+  }
+
+  // for new alerting or recording rules
+  return <NewRuleEditor />;
 };
 
-const RuleEditor = () => {
-  const { identifier, type } = useRuleEditorPathParams();
-  const { copyFromIdentifier, queryDefaults, isManualRestore } = useRuleEditorQueryParams();
+export const RECORDING_TYPE = ['grafana-recording', 'recording'];
 
-  const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess();
+/**
+ * This one is used for creating new rules (both alerting and recording rules)
+ */
+function NewRuleEditor() {
+  const prefill = useDefaultsFromQuery();
+  const isManualRestore = useManualRestore();
+  const { type = '', identifier = '' } = useRuleEditorPathParams();
+
+  const isExisting = Boolean(identifier);
+  const isRecordingRule = RECORDING_TYPE.includes(type);
+
+  const newText = isRecordingRule
+    ? t('alerting.editor.new-recording-rule', 'New recording rule')
+    : t('alerting.editor.new-alert-rule', 'New alert rule');
 
-  const getContent = useCallback(() => {
-    if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) {
-      return (
-        <AlertWarning title={t('alerting.rule-editor.get-content.title-cannot-create-rules', 'Cannot create rules')}>
-          <Trans i18nKey="alerting.rule-editor.get-content.sorry-allowed-create-rules">
-            Sorry! You are not allowed to create rules.
-          </Trans>
-        </AlertWarning>
-      );
-    }
-
-    if (identifier && !canEditRules(identifier.ruleSourceName)) {
-      return (
-        <AlertWarning title={t('alerting.rule-editor.get-content.title-cannot-edit-rules', 'Cannot edit rules')}>
-          <Trans i18nKey="alerting.rule-editor.get-content.sorry-allowed-rules">
-            Sorry! You are not allowed to edit rules.
-          </Trans>
-        </AlertWarning>
-      );
-    }
-
-    if (identifier) {
-      return <ExistingRuleEditor key={JSON.stringify(identifier)} identifier={identifier} prefill={queryDefaults} />;
-    }
-
-    if (copyFromIdentifier) {
-      return <CloneRuleEditor sourceRuleId={copyFromIdentifier} />;
-    }
-    // new alert rule
-    return <AlertRuleForm prefill={queryDefaults} isManualRestore={isManualRestore} />;
-  }, [
-    canCreateCloudRules,
-    canCreateGrafanaRules,
-    canEditRules,
-    copyFromIdentifier,
-    identifier,
-    queryDefaults,
-    isManualRestore,
-  ]);
+  const editText = isRecordingRule
+    ? t('alerting.editor.edit-recording-rule', 'Edit recording rule')
+    : t('alerting.editor.edit-alert-rule', 'Edit alert rule');
 
   return (
-    <AlertingPageWrapper navId="alert-list" pageNav={getPageNav(identifier, type)}>
-      {getContent()}
+    <AlertingPageWrapper
+      navId="alert-list"
+      pageNav={{
+        id: 'alert-rule-add',
+        text: isExisting ? editText : newText,
+      }}
+    >
+      <AlertRuleForm prefill={prefill} isManualRestore={isManualRestore} />
     </AlertingPageWrapper>
   );
-};
+}
 
 // The pageNav property makes it difficult to only rely on AlertingPageWrapper
 // to catch errors.
@@ -112,13 +117,16 @@ function useRuleEditorPathParams() {
   return { identifier, type };
 }
 
-function useRuleEditorQueryParams() {
-  const { type } = useParams<RuleEditorPathParams>();
-
+function useIdentifierFromCopy() {
   const [searchParams] = useURLSearchParams();
   const copyFromId = searchParams.get('copyFrom') ?? undefined;
-  const copyFromIdentifier = ruleId.tryParse(copyFromId);
-  const isManualRestore = searchParams.has('isManualRestore');
+
+  return ruleId.tryParse(copyFromId);
+}
+
+function useDefaultsFromQuery() {
+  const { type } = useRuleEditorPathParams();
+  const [searchParams] = useURLSearchParams();
 
   const ruleType = translateRouteParamToRuleType(type);
 
@@ -126,5 +134,12 @@ function useRuleEditorQueryParams() {
     ? formValuesFromQueryParams(searchParams.get('defaults') ?? '', ruleType)
     : undefined;
 
-  return { copyFromIdentifier, queryDefaults, isManualRestore };
+  return queryDefaults;
+}
+
+function useManualRestore() {
+  const [searchParams] = useURLSearchParams();
+  const isManualRestore = searchParams.has('isManualRestore');
+
+  return isManualRestore;
 }
diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx
index 0ed8255932b45..185a6b69d32a7 100644
--- a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx
+++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx
@@ -80,7 +80,7 @@ describe('RuleEditor cloud', () => {
 
     // save and check what was sent to backend
     const capture = captureRequests();
-    await user.click(ui.buttons.saveAndExit.get());
+    await user.click(ui.buttons.save.get());
     const requests = await capture;
 
     const serializedRequests = await serializeRequests(requests);
diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx
index ad58c4a002d21..b8f3992e31eb3 100644
--- a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx
+++ b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx
@@ -96,27 +96,19 @@ describe('RuleEditor grafana managed rules', () => {
     //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(screen.getByText('New folder')).toBeInTheDocument();
 
     //check that slashed folders are not in the list
     expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
     expect(ui.inputs.folder.get()).not.toHaveTextContent(new RegExp(slashedFolder.title));
 
-    //check that slashes warning is only shown once user search slashes
-    //todo: move this test to a unit test in FolderAndGroup unit test
-    // const folderInput = await ui.inputs.folderContainer.find();
-    // expect(within(folderInput).queryByText("Folders with '/' character are not allowed.")).not.toBeInTheDocument();
-    // await user.type(within(folderInput).getByRole('combobox'), 'new slashed //');
-    // expect(within(folderInput).getByText("Folders with '/' character are not allowed.")).toBeInTheDocument();
-    // await user.keyboard('{backspace} {backspace}{backspace}');
-    // expect(within(folderInput).queryByText("Folders with '/' character are not allowed.")).not.toBeInTheDocument();
-
     // add an annotation
     await user.click(screen.getByText('Add custom annotation'));
     await user.type(screen.getByPlaceholderText('Enter custom annotation name...'), 'custom');
     await user.type(screen.getByPlaceholderText('Enter custom annotation content...'), 'value');
 
     // save and check what was sent to backend
-    await user.click(ui.buttons.saveAndExit.get());
+    await user.click(ui.buttons.save.get());
   });
 
   it('saves evaluation interval correctly', async () => {
@@ -140,7 +132,7 @@ describe('RuleEditor grafana managed rules', () => {
       (req) => req.method === 'POST' && req.url.includes('/api/ruler/grafana/api/v1/rules/uuid020c61ef')
     );
 
-    await user.click(ui.buttons.saveAndExit.get());
+    await user.click(ui.buttons.save.get());
 
     const [request] = await capture;
     const postBody = await request.json();
@@ -158,6 +150,6 @@ describe('Data source managed rules', () => {
 
   it('should show an error if the data source does not exist', async () => {
     renderRuleEditor('cri%24grafana-cloudd%24delete me%24delete me 3%24recording_rule_delete_2%24-476183141');
-    expect(await screen.findByText(/unable to find data source/i)).toBeInTheDocument();
+    expect(await screen.findByText(/not found/i)).toBeInTheDocument();
   });
 });
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 9505c96fd2386..0c9433406c79f 100644
--- a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
+++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react';
 import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
 import { clickSelectOption, selectOptionInTest } from 'test/helpers/selectOptionInTest';
-import { screen } from 'test/test-utils';
+import { screen, waitFor } from 'test/test-utils';
 import { byRole } from 'testing-library-selector';
 
 import { contextSrv } from 'app/core/services/context_srv';
@@ -73,7 +73,7 @@ describe('RuleEditor grafana managed rules', () => {
     await clickSelectOption(groupInput, grafanaRulerGroup.name);
     await user.type(ui.inputs.annotationValue(1).get(), 'some description');
 
-    await user.click(ui.buttons.saveAndExit.get());
+    await user.click(ui.buttons.save.get());
 
     expect(await screen.findByRole('status')).toHaveTextContent('Rule added successfully');
     const requests = await capture;
@@ -128,12 +128,12 @@ describe('RuleEditor grafana managed rules', () => {
     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));
+    await waitFor(() => 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());
+    await user.click(ui.buttons.save.get());
 
     expect(await screen.findByRole('status')).toHaveTextContent('Rule added successfully');
     const requests = await capture;
diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx
index 29c1f926dbe00..19166c01ea4b5 100644
--- a/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx
+++ b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx
@@ -80,7 +80,7 @@ describe('RuleEditor recording rules', () => {
     await userEvent.type(await ui.inputs.expr.find(), 'up == 1');
 
     // try to save, find out that recording rule name is invalid
-    await userEvent.click(ui.buttons.saveAndExit.get());
+    await userEvent.click(ui.buttons.save.get());
     await waitFor(() =>
       expect(
         byText(
@@ -95,7 +95,7 @@ describe('RuleEditor recording rules', () => {
 
     // save and check what was sent to backend
     const capture = captureRequests();
-    await userEvent.click(ui.buttons.saveAndExit.get());
+    await userEvent.click(ui.buttons.save.get());
     const requests = await capture;
 
     const serializedRequests = await serializeRequests(requests);
diff --git a/public/app/features/alerting/unified/rule-editor/clone.utils.ts b/public/app/features/alerting/unified/rule-editor/clone.utils.ts
new file mode 100644
index 0000000000000..a4b38a3b7a76f
--- /dev/null
+++ b/public/app/features/alerting/unified/rule-editor/clone.utils.ts
@@ -0,0 +1,39 @@
+import { cloneDeep } from 'lodash';
+
+import { RuleWithLocation } from 'app/types/unified-alerting';
+import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
+
+import { generateCopiedName } from '../utils/duplicate';
+import { getRuleName, rulerRuleType } from '../utils/rules';
+
+export function changeRuleName(rule: RulerRuleDTO, newName: string) {
+  if (rulerRuleType.grafana.rule(rule)) {
+    rule.grafana_alert.title = newName;
+  }
+  if (rulerRuleType.dataSource.alertingRule(rule)) {
+    rule.alert = newName;
+  }
+
+  if (rulerRuleType.dataSource.recordingRule(rule)) {
+    rule.record = newName;
+  }
+}
+
+export function cloneRuleDefinition(rule: RuleWithLocation<RulerRuleDTO>) {
+  const ruleClone = cloneDeep(rule);
+  changeRuleName(
+    ruleClone.rule,
+    generateCopiedName(getRuleName(ruleClone.rule), ruleClone.group.rules.map(getRuleName))
+  );
+
+  if (rulerRuleType.grafana.rule(ruleClone.rule)) {
+    ruleClone.rule.grafana_alert.uid = '';
+
+    // Provisioned alert rules have provisioned alert group which cannot be used in UI
+    if (Boolean(ruleClone.rule.grafana_alert.provenance)) {
+      ruleClone.group = { name: '', rules: ruleClone.group.rules };
+    }
+  }
+
+  return ruleClone;
+}
diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts
index 033a93c12729c..daa97475a52c5 100644
--- a/public/app/features/alerting/unified/utils/misc.ts
+++ b/public/app/features/alerting/unified/utils/misc.ts
@@ -19,6 +19,7 @@ import {
   DataSourceRuleGroupIdentifier,
   FilterState,
   RuleIdentifier,
+  RuleWithLocation,
   RulesSource,
   SilenceFilterState,
 } from 'app/types/unified-alerting';
@@ -55,6 +56,15 @@ export function createViewLinkV2(
   return rulesNav.detailsPageLink(ruleSourceName, identifier, returnTo ? { returnTo } : undefined);
 }
 
+export function createViewLinkFromRuleWithLocation(ruleWithLocation: RuleWithLocation) {
+  const ruleSourceName = ruleWithLocation.ruleSourceName;
+  const identifier = ruleId.fromRuleWithLocation(ruleWithLocation);
+  const paramId = encodeURIComponent(ruleId.stringifyIdentifier(identifier));
+  const paramSource = encodeURIComponent(ruleSourceName);
+
+  return createRelativeUrl(`/alerting/${paramSource}/${paramId}/view`);
+}
+
 export function createExploreLink(datasource: DataSourceRef, query: string) {
   const { uid, type } = datasource;
 
diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts
index 13aa998298353..bbd7a08db9585 100644
--- a/public/app/features/alerting/unified/utils/rules.ts
+++ b/public/app/features/alerting/unified/utils/rules.ts
@@ -208,6 +208,10 @@ export function getPendingPeriod(rule: CombinedRule): string | undefined {
   return undefined;
 }
 
+export function getPendingPeriodFromRulerRule(rule: RulerRuleDTO) {
+  return rulerRuleType.any.alertingRule(rule) ? rule.for : undefined;
+}
+
 export function getKeepFiringfor(rule: CombinedRule): string | undefined {
   if (rulerRuleType.any.recordingRule(rule.rulerRule)) {
     return undefined;
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 1c8e02997c4e8..7f4d3da2ecbf9 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -408,11 +408,9 @@
     },
     "alert-rule-form": {
       "action-buttons": {
-        "delete": "Delete",
         "edit-yaml": "Edit YAML",
-        "save-exit": "Save rule and exit"
-      },
-      "title-delete-rule": "Delete rule"
+        "save": "Save"
+      }
     },
     "alert-rule-name-and-metric": {
       "aria-label-name": "name",
@@ -895,12 +893,18 @@
       "text-loading-template": "Loading template...",
       "title-failed-to-fetch-notification-template": "Failed to fetch notification template"
     },
+    "editor": {
+      "edit-alert-rule": "Edit alert rule",
+      "edit-recording-rule": "Edit recording rule",
+      "new-alert-rule": "New alert rule",
+      "new-recording-rule": "New recording rule"
+    },
     "error-modal": {
       "failed-to-update-your-configuration": "Failed to update your configuration:",
       "title-something-went-wrong": "Something went wrong"
     },
     "evaluation-behavior-summary": {
-      "evaluate": "Every {{every}}",
+      "evaluate": "Every {{interval}}",
       "label-evaluate": "Evaluate",
       "label-evaluation-time": "Evaluation time",
       "label-last-evaluation": "Last evaluation",
@@ -929,7 +933,6 @@
     "existing-rule-editor": {
       "sorry-permission": "Sorry! You do not have permission to edit this rule.",
       "sorry-this-rule-does-not-exist": "Sorry! This rule does not exist.",
-      "text-loading-rule": "Loading rule...",
       "title-cannot-edit-rule": "Cannot edit rule",
       "title-failed-to-load-rule": "Failed to load rule",
       "title-rule-not-found": "Rule not found"
diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx
index 9e601269b11af..0375a9123bdc0 100644
--- a/public/test/helpers/alertingRuleEditor.tsx
+++ b/public/test/helpers/alertingRuleEditor.tsx
@@ -39,7 +39,7 @@ export const ui = {
       byTestId(selectors.components.AlertRules.stepAdvancedModeSwitch(stepNo.toString())),
   },
   buttons: {
-    saveAndExit: byRole('button', { name: 'Save rule and exit' }),
+    save: byTestId('save-rule'),
     addAnnotation: byRole('button', { name: /Add info/ }),
     addLabel: byRole('button', { name: /Add label/ }),
     preview: byRole('button', { name: /^Preview$/ }),


Reviews:
user: login: gillesdemey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T19:24:49Z
user: login: gillesdemey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T19:37:43Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:47:33Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:48:18Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:15:56Z
user: login: soniaAguilarPeiron
body: Code LGTM ✨!, I would like to test it in the ephemeral instance before merging it
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T13:17:52Z
user: login: soniaAguilarPeiron
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:44:06Z
user: login: soniaAguilarPeiron
body: LGTM 🚀! Added a non blocking comment 👀
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:45:44Z

Review comments:
diff_hunk: @@ -235,16 +230,6 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) =>
     }
   };
 
-  const deleteRule = async () => {
path: public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
user: login: gillesdemey
body: We're no longer doing "delete rule" from the edit page, it's always been weird and a non-existent pattern in Grafana
created_at: 2025-04-09T19:24:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1174,7 +1166,7 @@
           "new-recording-rule": "New recording rule",
           "title": "Grafana-managed"
         },
-        "loading-rules_one": "Loading rules from {{count}} source",
+        "loading-rules_one": "Loading rules from {{count}} sources",
path: public/locales/en-US/grafana.json
user: login: gillesdemey
body: can't rightly say where these changes are coming from 🤔 any thoughts perhaps @tomratcliffe ?
created_at: 2025-04-09T19:37:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -112,19 +114,29 @@ function useRuleEditorPathParams() {
   return { identifier, type };
 }
 
-function useRuleEditorQueryParams() {
-  const { type } = useParams<RuleEditorPathParams>();
-
+function useIdentifierFromCopy() {
   const [searchParams] = useURLSearchParams();
   const copyFromId = searchParams.get('copyFrom') ?? undefined;
-  const copyFromIdentifier = ruleId.tryParse(copyFromId);
-  const isManualRestore = searchParams.has('isManualRestore');
+
+  return ruleId.tryParse(copyFromId);
+}
+
+function useDefaultsFromQuery() {
+  const { type } = useRuleEditorPathParams();
+  const [searchParams] = useURLSearchParams();
 
   const ruleType = translateRouteParamToRuleType(type);
 
   const queryDefaults = searchParams.has('defaults')
     ? formValuesFromQueryParams(searchParams.get('defaults') ?? '', ruleType)
     : undefined;
 
-  return { copyFromIdentifier, queryDefaults, isManualRestore };
+  return queryDefaults;
+}
+
+function useManualRestore() {
path: public/app/features/alerting/unified/rule-editor/RuleEditor.tsx
user: login: soniaAguilarPeiron
body: 💯
created_at: 2025-04-10T12:47:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,29 +96,19 @@ describe('RuleEditor grafana managed rules', () => {
     //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(screen.getByText('New folder')).toBeInTheDocument();
 
     //check that slashed folders are not in the list
     expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
     expect(ui.inputs.folder.get()).not.toHaveTextContent(new RegExp(slashedFolder.title));
 
-    //check that slashes warning is only shown once user search slashes
path: public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx
user: login: soniaAguilarPeiron
body: 😅
created_at: 2025-04-10T12:48:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1174,7 +1166,7 @@
           "new-recording-rule": "New recording rule",
           "title": "Grafana-managed"
         },
-        "loading-rules_one": "Loading rules from {{count}} source",
+        "loading-rules_one": "Loading rules from {{count}} sources",
path: public/locales/en-US/grafana.json
user: login: soniaAguilarPeiron
body: this is from a previous PR, we updated manually (to 'source') the json following the instructions [here](https://github.com/grafana/grafana/blob/7c605f024f2ee0d6889673a6d8c806a51f3c5ccd/contribute/internationalization.md?plain=1#L9) 
created_at: 2025-04-10T13:15:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,16 +275,17 @@ interface TitleProps {
   health?: RuleHealth;
   ruleType?: PromRuleType;
   ruleOrigin?: RulePluginOrigin;
+  returnToHref?: string;
path: public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
user: login: soniaAguilarPeiron
body: I don't see any place where this new property is used?
created_at: 2025-04-29T13:44:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: gillesdemey
created_at: 2025-04-09T18:20:35Z
body: Setting this back to draft until I've fixed those conflicts – it's been a while since I started noodling on this :)
author_association: CONTRIBUTOR
user: login: soniaAguilarPeiron
created_at: 2025-04-10T12:36:14Z
body: /deploy-to-hg
author_association: CONTRIBUTOR
user: login: ephemeral-instances-bot[bot]
created_at: 2025-04-10T12:38:10Z
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/14380593663)
- Slack channel: #proj-ephemeral-hg-instances
- Building instance with **alerting/rework-alert-creation-layout** 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-04-10T12:54:57Z
body: **Error building instance**: Contact #proj-ephemeral-hg-instances if it is not a compile error. [Logs](https://github.com/grafana/grafana/actions/runs/14380593663)



<details><summary>Error message</summary>

handling pull request comment event: calling gcom to update instance config: unexpected response status: status=409 responseBody={
  "code": "Conflict",
  "message": "Hosted Grafana error: operation in progress, please try again later",
  "requestId": "9addb993-a94c-4a0f-90cf-88a50b6415b9"
}</details>
author_association: NONE
user: login: soniaAguilarPeiron
created_at: 2025-04-10T13:54:48Z
body: /deploy-to-hg
author_association: CONTRIBUTOR
user: login: ephemeral-instances-bot[bot]
created_at: 2025-04-10T13:58:27Z
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/14382272547)
- Slack channel: #proj-ephemeral-hg-instances
- Building instance with **alerting/rework-alert-creation-layout** 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-04-10T14:17:12Z
body: - Your instance can be accessed at: https://ephemeral15111821103744gilles.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: gillesdemey
created_at: 2025-04-14T13:58:30Z
body: /deploy-to-hg
author_association: CONTRIBUTOR
user: login: ephemeral-instances-bot[bot]
created_at: 2025-04-14T14:18:15Z
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/14447554173)
- Slack channel: #proj-ephemeral-hg-instances
- Building instance with **alerting/rework-alert-creation-layout** 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-04-14T14:36:40Z
body: - Your instance can be accessed at: https://ephemeral15111821103744gilles.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: dhalachliyski
created_at: 2025-04-14T15:05:53Z
body: A couple of comments from my side:
- When in "New alert rule", the names of the buttons should be "Create" | "Cancel". For editing we keep "Save" | "Cancel"
- When editing, use a standard header titled "Edit alert rule" and don't put any buttons in the header. It is important to let the users know where they are - they just clicked on Edit, so they're in editing mode. The first field on the form is the name, which coupled with the page header should be enough. If they need a quick way to go back, let them use the browser back button (but we need to handle cases where they've edited something and have forgotten to save - e.g. using the standard browser dialog warning that you'll lose your progress if you navigate)

author_association: CONTRIBUTOR
user: login: tomratcliffe
created_at: 2025-04-14T15:11:38Z
body: > When in "New alert rule", the names of the buttons should be "Create" | "Cancel". For editing we keep "Save" | "Cancel"

Adding to that - elsewhere, we use "Save contact point" etc. I think the buttons should be consistent and perhaps say "Create alert rule"/"Save alert rule". WDYT?

> When editing (...) don't put any buttons in the header

I feel like those back buttons are intuitive in the header - we use them elsewhere and it feels like a good pattern for navigating back within the context of Grafana. Perhaps its just muscle memory, but the back button on the view rule page feels "right" to me. I was going to say regardless that currently in this PR, the back button on "Edit" is always sending you back to the View page, which feels wrong
author_association: CONTRIBUTOR
user: login: gillesdemey
created_at: 2025-04-23T16:56:13Z
body: /deploy-to-hg
author_association: CONTRIBUTOR
user: login: ephemeral-instances-bot[bot]
created_at: 2025-04-23T17:16:36Z
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/14623746845)
- Slack channel: #proj-ephemeral-hg-instances
- Building instance with **alerting/rework-alert-creation-layout** 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-04-23T17:35:20Z
body: **Error building instance**: Contact #proj-ephemeral-hg-instances if it is not a compile error. [Logs](https://github.com/grafana/grafana/actions/runs/14623746845)



<details><summary>Error message</summary>

handling pull request comment event: calling gcom to upsert instance: updating instance: unexpected response status: status=409 responseBody={
  "code": "InvalidArgument",
  "message": "Unexpected parameter: version",
  "requestId": "c1e1b9ac-0ce7-41eb-93b8-8615496a533d"
}</details>
author_association: NONE