Alerting: Update rule API to address folders by UID (#74600)

* Change ruler API to expect the folder UID as namespace

* Update example requests

* Fix tests

* Update swagger

* Modify FIle field in /api/prometheus/grafana/api/v1/rules

* Fix ruler export

* Modify folder in responses to be formatted as <parent UID>/<title>

* Add alerting test with nested folders

* Apply suggestion from code review

* Alerting: use folder UID instead of title in rule API (#77166)

Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>

* Drop a few more latent uses of namespace_id

* move getNamespaceKey to models package

* switch GetAlertRulesForScheduling to use folder table

* update GetAlertRulesForScheduling to return folder titles in format `parent_uid/title`.

* fi tests

* add tests for GetAlertRulesForScheduling when parent uid

* fix integration tests after merge

* fix test after merge

* change format of the namespace to JSON array

this is needed for forward compatibility, when we migrate to full paths

* update EF code to decode nested folder

---------

Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com>
Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
Co-authored-by: Alex Weaver <weaver.alex.d@gmail.com>
Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Sofia Papagiannaki
2024-01-17 11:07:39 +02:00
committed by GitHub
parent ec1d4274ed
commit d1dab5828d
48 changed files with 1216 additions and 273 deletions

View File

@@ -5,7 +5,6 @@ import { Route } from 'react-router-dom';
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { selectors } from '@grafana/e2e-selectors';
import { locationService } from '@grafana/runtime';
import { TestProvider } from '../../../../../../test/helpers/TestProvider';
@@ -36,7 +35,6 @@ const ui = {
form: {
nameInput: byRole('textbox', { name: 'name' }),
folder: byTestId('folder-picker'),
folderContainer: byTestId(selectors.components.FolderPicker.containerV2),
group: byTestId('group-picker'),
annotationKey: (idx: number) => byTestId(`annotation-key-${idx}`),
annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
@@ -75,7 +73,7 @@ describe('GrafanaModifyExport', () => {
const grafanaRule = getGrafanaRule(undefined, {
uid: 'test-rule-uid',
title: 'cpu-usage',
namespace_uid: 'folder-test-uid',
namespace_uid: 'folderUID1',
data: [
{
refId: 'A',
@@ -97,21 +95,23 @@ describe('GrafanaModifyExport', () => {
mockSearchApi(server).search([
mockDashboardSearchItem({
title: grafanaRule.namespace.name,
uid: 'folder-test-uid',
uid: 'folderUID1',
url: '',
tags: [],
type: DashboardSearchItemType.DashFolder,
}),
]);
mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, {
[grafanaRule.namespace.name]: [{ name: grafanaRule.group.name, interval: '1m', rules: [grafanaRule.rulerRule!] }],
});
mockAlertRuleApi(server).rulerRuleGroup(
GRAFANA_RULES_SOURCE_NAME,
grafanaRule.namespace.name,
grafanaRule.group.name,
{ name: grafanaRule.group.name, interval: '1m', rules: [grafanaRule.rulerRule!] }
);
mockExportApi(server).modifiedExport(grafanaRule.namespace.name, {
mockAlertRuleApi(server).rulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, 'folderUID1', grafanaRule.group.name, {
name: grafanaRule.group.name,
interval: '1m',
rules: [grafanaRule.rulerRule!],
});
mockExportApi(server).modifiedExport('folderUID1', {
yaml: 'Yaml Export Content',
json: 'Json Export Content',
});
const user = userEvent.setup();
@@ -127,6 +127,7 @@ describe('GrafanaModifyExport', () => {
expect(drawer).toBeInTheDocument();
expect(ui.exportDrawer.yamlTab.get(drawer)).toHaveAttribute('aria-selected', 'true');
await waitFor(() => {
expect(ui.exportDrawer.editor.get(drawer)).toHaveTextContent('Yaml Export Content');
});

View File

@@ -1,6 +1,17 @@
import { DataFrame, FieldType, toDataFrame } from '@grafana/data';
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
import { getSeriesName, formatLabels, getSeriesValue, isEmptySeries, getSeriesLabels } from './util';
import { mockDataSource } from '../../mocks';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import {
decodeGrafanaNamespace,
formatLabels,
getSeriesLabels,
getSeriesName,
getSeriesValue,
isEmptySeries,
} from './util';
const EMPTY_FRAME: DataFrame = toDataFrame([]);
const NAMED_FRAME: DataFrame = {
@@ -34,6 +45,103 @@ describe('formatLabels', () => {
});
});
describe('decodeGrafanaNamespace', () => {
it('should work for regular Grafana namespaces', () => {
const grafanaNamespace: CombinedRuleNamespace = {
name: `my_rule_namespace`,
rulesSource: GRAFANA_RULES_SOURCE_NAME,
groups: [
{
name: 'group1',
rules: [],
totals: {},
},
],
};
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('my_rule_namespace');
});
it('should work for Grafana namespaces in nested folders format', () => {
const grafanaNamespace: CombinedRuleNamespace = {
name: `["parentUID","my_rule_namespace"]`,
rulesSource: GRAFANA_RULES_SOURCE_NAME,
groups: [
{
name: 'group1',
rules: [],
totals: {},
},
],
};
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('my_rule_namespace');
});
it('should default to name if format is invalid: invalid JSON', () => {
const grafanaNamespace: CombinedRuleNamespace = {
name: `["parentUID"`,
rulesSource: GRAFANA_RULES_SOURCE_NAME,
groups: [
{
name: 'group1',
rules: [],
totals: {},
},
],
};
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe(`["parentUID"`);
});
it('should default to name if format is invalid: empty array', () => {
const grafanaNamespace: CombinedRuleNamespace = {
name: `[]`,
rulesSource: GRAFANA_RULES_SOURCE_NAME,
groups: [
{
name: 'group1',
rules: [],
totals: {},
},
],
};
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe(`[]`);
});
it('grab folder name if format is long array', () => {
const grafanaNamespace: CombinedRuleNamespace = {
name: `["parentUID","my_rule_namespace","another_part"]`,
rulesSource: GRAFANA_RULES_SOURCE_NAME,
groups: [
{
name: 'group1',
rules: [],
totals: {},
},
],
};
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('another_part');
});
it('should not change output for cloud namespaces', () => {
const cloudNamespace: CombinedRuleNamespace = {
name: `["parentUID","my_rule_namespace"]`,
rulesSource: mockDataSource(),
groups: [
{
name: 'Prom group',
rules: [],
totals: {},
},
],
};
expect(decodeGrafanaNamespace(cloudNamespace)).toBe(`["parentUID","my_rule_namespace"]`);
});
});
describe('isEmptySeries', () => {
it('should be true for empty series', () => {
expect(isEmptySeries([EMPTY_FRAME])).toBe(true);

View File

@@ -1,4 +1,7 @@
import { DataFrame, Labels, roundDecimals } from '@grafana/data';
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
import { isCloudRulesSource } from '../../utils/datasource';
/**
* ⚠️ `frame.fields` could be an empty array ⚠️
@@ -37,10 +40,29 @@ const formatLabels = (labels: Labels): string => {
.join(', ');
};
/**
* After https://github.com/grafana/grafana/pull/74600,
* Grafana folder names will be returned from the API as a combination of the folder name and parent UID in a format of JSON array,
* where first element is parent UID and the second element is Title.
*/
const decodeGrafanaNamespace = (namespace: CombinedRuleNamespace): string => {
const namespaceName = namespace.name;
if (isCloudRulesSource(namespace.rulesSource)) {
return namespaceName;
}
try {
return JSON.parse(namespaceName).at(-1) ?? namespaceName;
} catch {
return namespaceName;
}
};
const isEmptySeries = (series: DataFrame[]): boolean => {
const isEmpty = series.every((serie) => serie.fields.every((field) => field.values.every((value) => value == null)));
return isEmpty;
};
export { getSeriesName, getSeriesValue, getSeriesLabels, formatLabels, isEmptySeries };
export { decodeGrafanaNamespace, formatLabels, getSeriesLabels, getSeriesName, getSeriesValue, isEmptySeries };

View File

@@ -39,7 +39,7 @@ import { checkForPathSeparator } from './util';
export const MAX_GROUP_RESULTS = 1000;
export const useFolderGroupOptions = (folderTitle: string, enableProvisionedGroups: boolean) => {
export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups: boolean) => {
const dispatch = useDispatch();
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
@@ -52,7 +52,7 @@ export const useFolderGroupOptions = (folderTitle: string, enableProvisionedGrou
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
const grafanaFolders = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);
const folderGroups = grafanaFolders.find((f) => f.name === folderTitle)?.groups ?? [];
const folderGroups = grafanaFolders.find((f) => f.uid === folderUid)?.groups ?? [];
const groupOptions = folderGroups
.map<SelectableValue<string>>((group) => {
@@ -105,7 +105,7 @@ export function FolderAndGroup({
const folder = watch('folder');
const group = watch('group');
const { groupOptions, loading } = useFolderGroupOptions(folder?.title ?? '', enableProvisionedGroups);
const { groupOptions, loading } = useFolderGroupOptions(folder?.uid ?? '', enableProvisionedGroups);
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
@@ -146,55 +146,62 @@ export function FolderAndGroup({
return (
<div className={styles.container}>
<Stack alignItems="center">
<Field
label={
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
Folder
</Label>
}
className={styles.formInput}
error={errors.folder?.message}
invalid={!!errors.folder?.message}
data-testid="folder-picker"
>
{(!isCreatingFolder && (
<InputControl
render={({ field: { ref, ...field } }) => (
<div style={{ width: 420 }}>
<RuleFolderPicker
inputId="folder"
{...field}
enableReset={true}
onChange={({ title, uid }) => {
field.onChange({ title, uid });
resetGroup();
{
<Field
label={
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
Folder
</Label>
}
className={styles.formInput}
error={errors.folder?.message}
data-testid="folder-picker"
>
<Stack direction="row" alignItems="center">
{(!isCreatingFolder && (
<>
<InputControl
render={({ field: { ref, ...field } }) => (
<div style={{ width: 420 }}>
<RuleFolderPicker
inputId="folder"
invalid={!!errors.folder?.message}
{...field}
enableReset={true}
onChange={({ title, uid }) => {
field.onChange({ title, uid });
resetGroup();
}}
/>
</div>
)}
name="folder"
rules={{
required: { value: true, message: 'Select a folder' },
validate: {
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.uid),
},
}}
/>
</div>
)}
name="folder"
rules={{
required: { value: true, message: 'Select a folder' },
validate: {
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title),
},
}}
/>
)) || <div>Creating new folder...</div>}
</Field>
<Box marginTop={2.5} gap={1} display={'flex'} alignItems={'center'}>
<Text color="secondary">or</Text>
<Button
onClick={onOpenFolderCreationModal}
type="button"
icon="plus"
fill="outline"
variant="secondary"
disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
>
New folder
</Button>
</Box>
<Text color="secondary">or</Text>
<Button
onClick={onOpenFolderCreationModal}
type="button"
icon="plus"
fill="outline"
variant="secondary"
disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
>
New folder
</Button>
</>
)) || <div>Creating new folder...</div>}
</Stack>
</Field>
}
{isCreatingFolder && (
<FolderCreationModal onCreate={handleFolderCreation} onClose={() => setIsCreatingFolder(false)} />
)}
</Stack>
{isCreatingFolder && (

View File

@@ -92,16 +92,16 @@ function FolderGroupAndEvaluationInterval({
const { watch, setValue, getValues } = useFormContext<RuleFormValues>();
const [isEditingGroup, setIsEditingGroup] = useState(false);
const [groupName, folderName] = watch(['group', 'folder.title']);
const [groupName, folderUid, folderName] = watch(['group', 'folder.uid', 'folder.title']);
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
const grafanaNamespaces = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);
const existingNamespace = grafanaNamespaces.find((ns) => ns.name === folderName);
const existingNamespace = grafanaNamespaces.find((ns) => ns.uid === folderUid);
const existingGroup = existingNamespace?.groups.find((g) => g.name === groupName);
const isNewGroup = useIsNewGroup(folderName ?? '', groupName);
const isNewGroup = useIsNewGroup(folderUid ?? '', groupName);
useEffect(() => {
if (!isNewGroup && existingGroup?.interval) {
@@ -118,7 +118,7 @@ function FolderGroupAndEvaluationInterval({
const onOpenEditGroupModal = () => setIsEditingGroup(true);
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderName || !groupName;
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !groupName;
const emptyNamespace: CombinedRuleNamespace = {
name: folderName,

View File

@@ -2,11 +2,11 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Tooltip, useStyles2, Stack } from '@grafana/ui';
import { OldFolderPicker, Props as FolderPickerProps } from 'app/core/components/Select/OldFolderPicker';
import { Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { Props as FolderPickerProps, OldFolderPicker } from 'app/core/components/Select/OldFolderPicker';
import { PermissionLevelString, SearchQueryType } from 'app/types';
import { FolderWarning, CustomAdd } from '../../../../../core/components/Select/OldFolderPicker';
import { CustomAdd, FolderWarning } from '../../../../../core/components/Select/OldFolderPicker';
export interface Folder {
title: string;
@@ -15,6 +15,7 @@ export interface Folder {
export interface RuleFolderPickerProps extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
value?: Folder;
invalid?: boolean;
}
const SlashesWarning = () => {
@@ -51,7 +52,6 @@ export function RuleFolderPicker(props: RuleFolderPickerProps) {
showRoot={false}
rootName=""
allowEmpty={true}
initialTitle={value?.title}
initialFolderUid={value?.uid}
searchQueryType={SearchQueryType.AlertFolder}
{...props}

View File

@@ -111,14 +111,14 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
);
}
const useGetGroup = (nameSpace: string, group: string) => {
const useGetGroup = (nameSpaceUID: string, group: string) => {
const { dsFeatures } = useDataSourceFeatures(GRAFANA_RULES_SOURCE_NAME);
const rulerConfig = dsFeatures?.rulerConfig;
const targetGroup = useAsync(async () => {
return rulerConfig ? await fetchRulerRulesGroup(rulerConfig, nameSpace, group) : undefined;
}, [rulerConfig, nameSpace, group]);
return rulerConfig ? await fetchRulerRulesGroup(rulerConfig, nameSpaceUID, group) : undefined;
}, [rulerConfig, nameSpaceUID, group]);
return targetGroup;
};
@@ -166,7 +166,7 @@ export const getPayloadToExport = (
};
const useGetPayloadToExport = (values: RuleFormValues, uid: string) => {
const rulerGroupDto = useGetGroup(values.folder?.title ?? '', values.group);
const rulerGroupDto = useGetGroup(values.folder?.uid ?? '', values.group);
const payload: ModifyExportPayload = useMemo(() => {
return getPayloadToExport(uid, values, rulerGroupDto?.value);
}, [uid, rulerGroupDto, values]);
@@ -182,11 +182,11 @@ const GrafanaRuleDesignExportPreview = ({
const [getExport, exportData] = alertRuleApi.endpoints.exportModifiedRuleGroup.useMutation();
const { loadingGroup, payload } = useGetPayloadToExport(exportValues, uid);
const nameSpace = exportValues.folder?.title ?? '';
const nameSpaceUID = exportValues.folder?.uid ?? '';
useEffect(() => {
!loadingGroup && getExport({ payload, format: exportFormat, nameSpace: nameSpace });
}, [nameSpace, exportFormat, payload, getExport, loadingGroup]);
!loadingGroup && getExport({ payload, format: exportFormat, nameSpaceUID });
}, [nameSpaceUID, exportFormat, payload, getExport, loadingGroup]);
if (exportData.isLoading) {
return <LoadingPlaceholder text="Loading...." />;

View File

@@ -28,6 +28,7 @@ import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
import { decodeGrafanaNamespace } from '../expressions/util';
import { RuleViewerLayout } from '../rule-viewer/RuleViewerLayout';
import { RuleDetailsActionButtons } from '../rules/RuleDetailsActionButtons';
import { RuleDetailsAnnotations } from '../rules/RuleDetailsAnnotations';
@@ -153,7 +154,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
<RuleDetailsDataSources rule={rule} rulesSource={rulesSource} />
{isFederatedRule && <RuleDetailsFederatedSources group={rule.group} />}
<DetailsField label="Namespace / Group" className={styles.rightSideDetails}>
{rule.namespace.name} / {rule.group.name}
{decodeGrafanaNamespace(rule.namespace)} / {rule.group.name}
</DetailsField>
{isGrafanaRulerRule(rule.rulerRule) && <GrafanaRuleUID rule={rule.rulerRule.grafana_alert} />}
</div>

View File

@@ -29,6 +29,7 @@ import { AlertingPageWrapper } from '../../AlertingPageWrapper';
import MoreButton from '../../MoreButton';
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
import { DeclareIncidentMenuItem } from '../../bridges/DeclareIncidentButton';
import { decodeGrafanaNamespace } from '../../expressions/util';
import { Details } from '../tabs/Details';
import { History } from '../tabs/History';
import { InstancesList } from '../tabs/Instances';
@@ -336,6 +337,9 @@ function usePageNav(rule: CombinedRule) {
const isAlertType = isAlertingRule(promRule);
const numberOfInstance = isAlertType ? (promRule.alerts ?? []).length : undefined;
const namespaceName = decodeGrafanaNamespace(rule.namespace);
const groupName = rule.group.name;
const pageNav: NavModelItem = {
...defaultPageNav,
text: rule.name,
@@ -372,14 +376,14 @@ function usePageNav(rule: CombinedRule) {
},
],
parentItem: {
text: rule.group.name,
text: groupName,
url: createListFilterLink([
['namespace', rule.namespace.name],
['group', rule.group.name],
['namespace', namespaceName],
['group', groupName],
]),
parentItem: {
text: rule.namespace.name,
url: createListFilterLink([['namespace', rule.namespace.name]]),
text: namespaceName,
url: createListFilterLink([['namespace', namespaceName]]),
},
},
};

View File

@@ -158,11 +158,12 @@ export interface ModalProps {
onClose: (saved?: boolean) => void;
intervalEditOnly?: boolean;
folderUrl?: string;
folderUid?: string;
hideFolder?: boolean;
}
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
const { namespace, group, onClose, intervalEditOnly } = props;
const { namespace, group, onClose, intervalEditOnly, folderUid } = props;
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
@@ -201,6 +202,7 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
namespaceName: namespace.name,
newNamespaceName: values.namespaceName,
groupInterval: values.groupInterval || undefined,
folderUid,
})
);
};

View File

@@ -27,12 +27,13 @@ interface ModalProps {
namespace: CombinedRuleNamespace;
group: CombinedRuleGroup;
onClose: () => void;
folderUid?: string;
}
type CombinedRuleWithUID = { uid: string } & CombinedRule;
export const ReorderCloudGroupModal = (props: ModalProps) => {
const { group, namespace, onClose } = props;
const { group, namespace, onClose, folderUid } = props;
const [pending, setPending] = useState<boolean>(false);
const [rulesList, setRulesList] = useState<CombinedRule[]>(group.rules);
@@ -63,6 +64,7 @@ export const ReorderCloudGroupModal = (props: ModalProps) => {
groupName: group.name,
rulesSourceName: rulesSourceName,
newRules: rulerRules,
folderUid: folderUid || namespace.name,
})
)
.unwrap()
@@ -70,7 +72,7 @@ export const ReorderCloudGroupModal = (props: ModalProps) => {
setPending(false);
});
},
[group.name, namespace.name, namespace.rulesSource, rulesList]
[group.name, namespace.name, namespace.rulesSource, rulesList, folderUid]
);
// assign unique but stable identifiers to each (alerting / recording) rule

View File

@@ -3,11 +3,11 @@ import pluralize from 'pluralize';
import React, { useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2, Stack } from '@grafana/ui';
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { useDispatch } from 'app/types';
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import { logInfo, LogMessages } from '../../Analytics';
import { LogMessages, logInfo } from '../../Analytics';
import { useFolder } from '../../hooks/useFolder';
import { useHasRuler } from '../../hooks/useHasRuler';
import { deleteRulesGroupAction } from '../../state/actions';
@@ -19,6 +19,7 @@ import { CollapseToggle } from '../CollapseToggle';
import { RuleLocation } from '../RuleLocation';
import { GrafanaRuleFolderExporter } from '../export/GrafanaRuleFolderExporter';
import { GrafanaRuleGroupExporter } from '../export/GrafanaRuleGroupExporter';
import { decodeGrafanaNamespace } from '../expressions/util';
import { ActionIcon } from './ActionIcon';
import { EditCloudGroupModal } from './EditRuleGroupModal';
@@ -204,9 +205,9 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
// ungrouped rules are rules that are in the "default" group name
const groupName = isListView ? (
<RuleLocation namespace={namespace.name} />
<RuleLocation namespace={decodeGrafanaNamespace(namespace)} />
) : (
<RuleLocation namespace={namespace.name} group={group.name} />
<RuleLocation namespace={decodeGrafanaNamespace(namespace)} group={group.name} />
);
const closeEditModal = (saved = false) => {
@@ -278,10 +279,16 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
group={group}
onClose={() => closeEditModal()}
folderUrl={folder?.canEdit ? makeFolderSettingsLink(folder) : undefined}
folderUid={folderUID}
/>
)}
{isReorderingGroup && (
<ReorderCloudGroupModal group={group} namespace={namespace} onClose={() => setIsReorderingGroup(false)} />
<ReorderCloudGroupModal
group={group}
folderUid={folderUID}
namespace={namespace}
onClose={() => setIsReorderingGroup(false)}
/>
)}
<ConfirmModal
isOpen={isDeletingGroup}