Alerting: Detect target folder rules and show warning (#103673)

* detect target folder rules and show warning

* show detected rules in target folder in a collapsed section

* fix detecting rules that might be overwritten

* refactor

* remove undefined

* fix text
This commit is contained in:
Sonia Aguilar 2025-04-10 16:18:20 +02:00 committed by GitHub
parent e6c945b535
commit b4442b4f22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 200 additions and 42 deletions

View File

@ -1,10 +1,11 @@
import { css } from '@emotion/css';
import { isEmpty } from 'lodash';
import { ComponentProps } from 'react';
import { ComponentProps, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { useToggle } from 'react-use';
import { locationService } from '@grafana/runtime';
import { Alert, CodeEditor, ConfirmModal, Modal, Stack, Text, useStyles2 } from '@grafana/ui';
import { Alert, CodeEditor, Collapse, ConfirmModal, Modal, Stack, Text, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { Trans, t } from 'app/core/internationalization';
import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
@ -14,9 +15,9 @@ import { trackImportToGMAError, trackImportToGMASuccess } from '../../Analytics'
import { convertToGMAApi } from '../../api/convertToGMAApi';
import { GRAFANA_ORIGIN_LABEL } from '../../utils/labels';
import { createListFilterLink } from '../../utils/navigation';
import { useGetRulerRules } from '../rule-editor/useAlertRuleSuggestions';
import { ImportFormValues } from './ImportFromDSRules';
import { useGetRulesThatMightBeOverwritten, useGetRulesToBeImported } from './hooks';
type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
isOpen: boolean;
@ -58,14 +59,35 @@ export const ConfirmConversionModal = ({ isOpen, onDismiss }: ModalProps) => {
'ruleGroup',
'targetDatasourceUID',
]);
const { rulerRules } = useGetRulerRules(selectedDatasourceName || undefined);
const dataSourceToFetch = isOpen ? (selectedDatasourceName ?? '') : undefined;
const { rulesToBeImported, isloadingCloudRules } = useGetRulesToBeImported(!isOpen, dataSourceToFetch);
const { filteredConfig: rulerRulesToPayload, someRulesAreSkipped } = useMemo(
() => filterRulerRulesConfig(rulesToBeImported, namespace, ruleGroup),
[rulesToBeImported, namespace, ruleGroup]
);
const { rulesThatMightBeOverwritten } = useGetRulesThatMightBeOverwritten(!isOpen, targetFolder, rulerRulesToPayload);
const [convert] = convertToGMAApi.useConvertToGMAMutation();
const notifyApp = useAppNotification();
const { filteredConfig: rulerRulesToPayload, someRulesAreSkipped } = filterRulerRulesConfig(
rulerRules,
namespace,
ruleGroup
);
if (isloadingCloudRules) {
return (
<Modal
isOpen={isOpen}
title={t('alerting.import-to-gma.confirm-modal.loading', 'Loading...')}
onDismiss={onDismiss}
onClickBackdrop={onDismiss}
>
<Text>
{t(
'alerting.import-to-gma.confirm-modal.loading-body',
'Preparing data to be imported.This can take a while...'
)}
</Text>
</Modal>
);
}
async function onConvertConfirm() {
try {
@ -122,7 +144,7 @@ export const ConfirmConversionModal = ({ isOpen, onDismiss }: ModalProps) => {
// translations for texts in the modal
const title = t('alerting.import-to-gma.confirm-modal.title', 'Confirm import');
const confirmText = t('alerting.import-to-gma.confirm-modal.confirm', 'Yes, import');
const confirmText = t('alerting.import-to-gma.confirm-modal.confirm', 'Import');
return (
<ConfirmModal
isOpen={isOpen}
@ -132,19 +154,12 @@ export const ConfirmConversionModal = ({ isOpen, onDismiss }: ModalProps) => {
modalClass={styles.modal}
body={
<Stack direction="column" gap={2}>
<Alert title={t('alerting.to-gma.confirm-modal.title-warning', 'Warning')} severity="warning">
<Text variant="body">
<Trans i18nKey="alerting.to-gma.confirm-modal.body">
If the target folder is not empty, some rules may be overwritten or removed. Are you sure you want to
import these alert rules to Grafana-managed rules?
</Trans>
</Text>
</Alert>
{!isEmpty(rulesThatMightBeOverwritten) && (
<TargetFolderNotEmptyWarning targetFolderRules={rulesThatMightBeOverwritten} />
)}
{someRulesAreSkipped && <AlertSomeRulesSkipped />}
<Text variant="h6">
<Trans i18nKey="alerting.to-gma.confirm-modal.summary">
These are the list of rules that will be imported:
</Trans>
<Trans i18nKey="alerting.to-gma.confirm-modal.summary">The following alert rules will be imported:</Trans>
</Text>
{rulerRulesToPayload && <RulesPreview rules={rulerRulesToPayload} />}
</Stack>
@ -227,3 +242,32 @@ const getStyles = () => ({
width: '800px',
}),
});
function TargetFolderNotEmptyWarning({ targetFolderRules }: { targetFolderRules: RulerRulesConfigDTO }) {
const [showTargetRules, toggleShowTargetRules] = useToggle(false);
return (
<Stack direction="column" gap={2}>
<Alert title={t('alerting.to-gma.confirm-modal.title-warning', 'Warning')} severity="warning">
<Text variant="body">
<Trans i18nKey="alerting.to-gma.confirm-modal.body">
The target folder is not empty, some rules may be overwritten or removed. Are you sure you want to import
these alert rules to Grafana-managed rules?
</Trans>
</Text>
</Alert>
{targetFolderRules && (
<Collapse
label={t(
'alerting.import-to-gma.confirm-modal.target-folder-rules',
'Target folder rules that might be overwritten'
)}
isOpen={showTargetRules}
onToggle={toggleShowTargetRules}
collapsible={true}
>
<RulesPreview rules={targetFolderRules} />
</Collapse>
)}
</Stack>
);
}

View File

@ -143,7 +143,6 @@ const ImportFromDSRules = () => {
'alerting.import-from-dsrules.description-folder-import-rules',
'The folder to import the rules to'
)}
invalid={!!errors.selectedDatasourceName}
error={errors.selectedDatasourceName?.message}
htmlFor="folder-picker"
>

View File

@ -0,0 +1,108 @@
import { isEmpty } from 'lodash';
import { useEffect, useState } from 'react';
import { lastValueFrom } from 'rxjs';
import { getBackendSrv } from '@grafana/runtime';
import { FolderDTO } from 'app/types';
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { alertRuleApi } from '../../api/alertRuleApi';
import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi';
import { Folder } from '../../types/rule-form';
import { useGetRulerRules } from '../rule-editor/useAlertRuleSuggestions';
async function getNestedFoldersIn(uid: string) {
const response = await lastValueFrom(
getBackendSrv().fetch<FolderDTO[]>({
url: `/api/folders`,
params: { parentUid: uid },
method: 'GET',
showErrorAlert: false,
showSuccessAlert: false,
})
);
return response?.data;
}
export function useGetNestedFolders(folderUID: string, skip = false) {
const [nestedFolders, setNestedFolders] = useState<FolderDTO[]>([]);
useEffect(() => {
(async () => {
const nestedFoldersIn = skip ? [] : await getNestedFoldersIn(folderUID);
setNestedFolders(nestedFoldersIn);
})();
}, [folderUID, skip]);
return nestedFolders;
}
export function useGetRulesThatMightBeOverwritten(
skip: boolean,
targetFolder: Folder | undefined,
rulesToBeImported: RulerRulesConfigDTO
) {
// get nested folders in the target folder
const nestedFoldersInTargetFolder = useGetNestedFolders(targetFolder?.uid || '', skip);
const skipFiltering = skip || nestedFoldersInTargetFolder.length === 0;
const rulesThatMightBeOverwritten = useFilterRulesThatMightBeOverwritten(
nestedFoldersInTargetFolder,
rulesToBeImported,
skipFiltering
);
return { rulesThatMightBeOverwritten };
}
export function useGetRulesToBeImported(skip: boolean, selectedDatasourceName: string | undefined) {
// we need to skip fetching and filtering if the modal is not open
const dataSourceToFetch = !skip ? selectedDatasourceName : undefined;
const { rulerRules: rulesToBeImported, isLoading: isloadingCloudRules } = useGetRulerRules(dataSourceToFetch);
return { rulesToBeImported, isloadingCloudRules };
}
function useFilterRulesThatMightBeOverwritten(
targetNestedFolders: FolderDTO[],
rulesToBeImported: RulerRulesConfigDTO,
skip = true
): RulerRulesConfigDTO {
const [fetchRulesByFolderUID] = alertRuleApi.endpoints.rulerNamespace.useLazyQuery();
const [rulesThatMightBeOverwritten, setRulesThatMightBeOverwritten] = useState<RulerRulesConfigDTO>({});
useEffect(() => {
if (skip || isEmpty(targetNestedFolders) || isEmpty(rulesToBeImported)) {
setRulesThatMightBeOverwritten({});
return;
}
// filter targetNestedFolders to only include folders that are in the rulesToBeImported
const targetNestedFoldersFiltered = targetNestedFolders.filter((folder) => {
return Object.keys(rulesToBeImported).includes(folder.title);
});
const fetchRules = async () => {
const results: RulerRulesConfigDTO = {};
await Promise.all(
targetNestedFoldersFiltered.map(async (folder) => {
const { data: rules } = await fetchRulesByFolderUID({
namespace: folder.uid,
rulerConfig: GRAFANA_RULER_CONFIG,
});
if (rules) {
const folderWithParentTitle = Object.keys(rules)[0];
if (folderWithParentTitle) {
results[folderWithParentTitle] = rules[folderWithParentTitle] || [];
}
}
})
);
setRulesThatMightBeOverwritten(results);
};
fetchRules();
}, [targetNestedFolders, rulesToBeImported, skip, fetchRulesByFolderUID]);
return rulesThatMightBeOverwritten;
}

View File

@ -24,9 +24,8 @@ import {
import { Trans, t } from 'app/core/internationalization';
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { alertRuleApi } from '../../api/alertRuleApi';
import { GRAFANA_RULER_CONFIG } from '../../api/featureDiscoveryApi';
import { evaluateEveryValidationOptions } from '../../group-details/validation';
import { useFetchGroupsForFolder } from '../../hooks/useFetchGroupsForFolder';
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults';
import { RuleFormValues } from '../../types/rule-form';
import {
@ -48,21 +47,6 @@ import { RuleEditorSection } from './RuleEditorSection';
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
export const MAX_GROUP_RESULTS = 1000;
const useFetchGroupsForFolder = (folderUid: string) => {
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
// for our folders
return alertRuleApi.endpoints.rulerNamespace.useQuery(
{
namespace: folderUid,
rulerConfig: GRAFANA_RULER_CONFIG,
},
{
refetchOnMountOrArgChange: true,
skip: !folderUid,
}
);
};
const namespaceToGroupOptions = (rulerNamespace: RulerRulesConfigDTO, enableProvisionedGroups: boolean) => {
const folderGroups = Object.values(rulerNamespace).flat();

View File

@ -0,0 +1,20 @@
import { alertRuleApi } from '../api/alertRuleApi';
import { GRAFANA_RULER_CONFIG } from '../api/featureDiscoveryApi';
/**
* Fetch groups for a given folder UID.
* This hook only returns the rules that are directly in the folder. Rules in subfolders are not included.
* @param folderUid - The UID of the folder to fetch groups for.
*/
export const useFetchGroupsForFolder = (folderUid: string) => {
return alertRuleApi.endpoints.rulerNamespace.useQuery(
{
namespace: folderUid,
rulerConfig: GRAFANA_RULER_CONFIG,
},
{
refetchOnMountOrArgChange: true,
skip: !folderUid,
}
);
};

View File

@ -1047,13 +1047,16 @@
"additional-settings": "Additional settings",
"alert-rules": "Alert rules",
"confirm-modal": {
"confirm": "Yes, import",
"confirm": "Import",
"loading": "Loading...",
"loading-body": "Preparing data to be imported.This can take a while...",
"no-rules-body": "There are no rules to import. Please select a different namespace or rule group.",
"no-rules-title": "No rules to import",
"plugin-rules-warning": {
"text": "We have detected that some rules are managed by plugins. These rules will not be imported.",
"title": "Some rules are excluded from import"
},
"target-folder-rules": "Target folder rules that might be overwritten",
"title": "Confirm import"
},
"datasource": {
@ -2004,8 +2007,8 @@
},
"to-gma": {
"confirm-modal": {
"body": "If the target folder is not empty, some rules may be overwritten or removed. Are you sure you want to import these alert rules to Grafana-managed rules?",
"summary": "These are the list of rules that will be imported:",
"body": "The target folder is not empty, some rules may be overwritten or removed. Are you sure you want to import these alert rules to Grafana-managed rules?",
"summary": "The following alert rules will be imported:",
"title-warning": "Warning"
}
},