Alerting: Migration UI (#102010)
* add feature toggle alertingMigrationUI * wip * wip * wip2 * add button to export in alert list and add target folder in the form * update namings * protext migration route with alertingMigrationUI feature toggle * remove unused error style * add namspace and group filter * fix translations and prettier * fix duplication * use the new endpoint * update right roles for the FE route * refactor * notify success * address feedback * fix translations * Simplify migration UI for alert rules to GMA format Extracts migration button component and reuses it in banner for better code organization and consistency. * Fix root folder check in import alert rules UI Replace hardcoded empty string check with lodash isEmpty for folder UID validation * Add collapsible section for import rule options * address feedback * Split useAlertRuleSuggestions, one for labels, and other for namespaces * refactor splited hooks * Alerting: (PR review) Redesign slightly + use combobox (#102614) Redesign slightly + use combobox * udpate naming * remove eslint-disable-next-line @grafana/no-untranslated-strings and add hasDataSourcesConfigured as a condition to render the banner * use button instead of banner * fix routes * use new api * update api * remove query params * update translations * update ff files * skip subpath when redirecting to the alert list view with filters * prettier * disable provenance * prettier * Don't send target forlder uid in the payload as undefined if it's not selected * prettier * add confirmation modal with a preview * remove unnecessary export * Add target datasource UID for recording rules * Use skipToken * Add recording rule datasource and update designs * Use different hook to fetch rulerRules --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com> Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
This commit is contained in:
co-authored by
Gilles De Mey
Tom Ratcliffe
parent
9111a2839c
commit
9b91dc7855
+1
-3
@@ -2069,9 +2069,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rules/CloudRules.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"]
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
|
||||
@@ -212,6 +212,18 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/import-datasource-managed-rules',
|
||||
roles: evaluateAccess([AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalRead]),
|
||||
component: config.featureToggles.alertingMigrationUI
|
||||
? importAlertingComponent(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "AlertingImportFromDSRules"*/ 'app/features/alerting/unified/components/import-to-gma/ImportFromDSRules'
|
||||
)
|
||||
)
|
||||
: () => <Navigate replace to="/alerting/list" />,
|
||||
},
|
||||
{
|
||||
path: '/alerting/recently-deleted/',
|
||||
roles: () => ['Admin'],
|
||||
@@ -224,6 +236,18 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
)
|
||||
: () => <Navigate replace to="/alerting/list" />,
|
||||
},
|
||||
{
|
||||
path: '/alerting/import-datasource-managed-rules',
|
||||
roles: evaluateAccess([AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalRead]),
|
||||
component: config.featureToggles.alertingMigrationUI
|
||||
? importAlertingComponent(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "AlertingImportFromDSRules"*/ 'app/features/alerting/unified/components/import-to-gma/ImportFromDSRules'
|
||||
)
|
||||
)
|
||||
: () => <Navigate replace to="/alerting/list" />,
|
||||
},
|
||||
{
|
||||
path: '/alerting/new/:type?',
|
||||
pageClass: 'page-alerting',
|
||||
|
||||
@@ -225,6 +225,14 @@ export const trackDeletedRuleRestoreFail = async () => {
|
||||
reportInteraction('grafana_alerting_deleted_rule_restore_error');
|
||||
};
|
||||
|
||||
export const trackImportToGMASuccess = async () => {
|
||||
reportInteraction('grafana_alerting_import_to_gma_success');
|
||||
};
|
||||
|
||||
export const trackImportToGMAError = async () => {
|
||||
reportInteraction('grafana_alerting_import_to_gma_error');
|
||||
};
|
||||
|
||||
interface RulesSearchInteractionPayload {
|
||||
filter: string;
|
||||
triggeredBy: 'typing' | 'component';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
|
||||
export const convertToGMAApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
convertToGMA: build.mutation<
|
||||
void,
|
||||
{
|
||||
targetFolderUID?: string;
|
||||
dataSourceUID: string;
|
||||
pauseRecordingRules?: boolean;
|
||||
pauseAlerts?: boolean;
|
||||
payload: RulerRulesConfigDTO;
|
||||
/** Target data source UID to store recording rules in */
|
||||
targetDatasourceUID?: string;
|
||||
}
|
||||
>({
|
||||
query: ({ payload, targetFolderUID, pauseRecordingRules, pauseAlerts, dataSourceUID, targetDatasourceUID }) => ({
|
||||
url: `/api/convert/prometheus/config/v1/rules`,
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
headers: {
|
||||
'X-Grafana-Alerting-Datasource-UID': dataSourceUID,
|
||||
'X-Grafana-Alerting-Recording-Rules-Paused': pauseRecordingRules,
|
||||
'X-Grafana-Alerting-Alert-Rules-Paused': pauseAlerts,
|
||||
'X-Disable-Provenance': true,
|
||||
...(targetFolderUID ? { 'X-Grafana-Alerting-Folder-UID': targetFolderUID } : {}),
|
||||
...(targetDatasourceUID ? { 'X-Grafana-Alerting-Target-Datasource-UID': targetDatasourceUID } : {}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ComponentProps } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { CodeEditor, ConfirmModal, Icon, 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';
|
||||
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { trackImportToGMAError, trackImportToGMASuccess } from '../../Analytics';
|
||||
import { convertToGMAApi } from '../../api/convertToGMAApi';
|
||||
import { createListFilterLink } from '../../utils/navigation';
|
||||
import { useGetRulerRules } from '../rule-editor/useAlertRuleSuggestions';
|
||||
|
||||
import { ImportFormValues } from './ImportFromDSRules';
|
||||
|
||||
type ModalProps = Pick<ComponentProps<typeof ConfirmModal>, 'isOpen' | 'onDismiss'> & {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
export const ConfirmConversionModal = ({ isOpen, onDismiss }: ModalProps) => {
|
||||
const { watch } = useFormContext<ImportFormValues>();
|
||||
|
||||
const [
|
||||
targetFolder,
|
||||
selectedDatasourceName,
|
||||
selectedDatasourceUID,
|
||||
pauseRecordingRules,
|
||||
pauseAlertingRules,
|
||||
namespace,
|
||||
ruleGroup,
|
||||
targetDatasourceUID,
|
||||
] = watch([
|
||||
'targetFolder',
|
||||
'selectedDatasourceName',
|
||||
'selectedDatasourceUID',
|
||||
'pauseRecordingRules',
|
||||
'pauseAlertingRules',
|
||||
'namespace',
|
||||
'ruleGroup',
|
||||
'targetDatasourceUID',
|
||||
]);
|
||||
const { rulerRules } = useGetRulerRules(selectedDatasourceName || undefined);
|
||||
const [convert] = convertToGMAApi.useConvertToGMAMutation();
|
||||
const notifyApp = useAppNotification();
|
||||
const rulerRulesToPayload = filterRulerRulesConfig(rulerRules, namespace, ruleGroup);
|
||||
|
||||
async function onConvertConfirm() {
|
||||
try {
|
||||
await convert({
|
||||
dataSourceUID: selectedDatasourceUID,
|
||||
targetFolderUID: targetFolder?.uid,
|
||||
pauseRecordingRules: pauseRecordingRules,
|
||||
pauseAlerts: pauseAlertingRules,
|
||||
payload: rulerRulesToPayload,
|
||||
targetDatasourceUID,
|
||||
}).unwrap();
|
||||
|
||||
const isRootFolder = isEmpty(targetFolder?.uid);
|
||||
|
||||
trackImportToGMASuccess();
|
||||
const ruleListUrl = createListFilterLink(isRootFolder ? [] : [['namespace', targetFolder?.title ?? '']], {
|
||||
skipSubPath: true,
|
||||
});
|
||||
notifyApp.success(
|
||||
t('alerting.import-to-gma.success', 'Successfully imported alert rules to Grafana-managed rules.')
|
||||
);
|
||||
locationService.push(ruleListUrl);
|
||||
} catch (error) {
|
||||
trackImportToGMAError();
|
||||
notifyApp.error(
|
||||
t('alerting.import-to-gma.error', 'Failed to import alert rules: {{error}}', {
|
||||
error: stringifyErrorLike(error),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
return (
|
||||
<ConfirmModal
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
confirmText={confirmText}
|
||||
confirmButtonVariant="primary"
|
||||
body={
|
||||
<Stack direction="column" gap={2}>
|
||||
<Stack direction="row" gap={1} alignItems={'self-start'}>
|
||||
<Text color="warning">
|
||||
<Icon name="exclamation-triangle" />
|
||||
</Text>
|
||||
<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>
|
||||
</Stack>
|
||||
<Text variant="h6">
|
||||
<Trans i18nKey="alerting.to-gma.confirm-modal.summary">
|
||||
These are the list of rules that will be imported:
|
||||
</Trans>
|
||||
</Text>
|
||||
{rulerRulesToPayload && <RulesPreview rules={rulerRulesToPayload} />}
|
||||
</Stack>
|
||||
}
|
||||
onConfirm={onConvertConfirm}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function filterRulerRulesConfig(
|
||||
rulerRulesConfig: RulerRulesConfigDTO,
|
||||
namespace?: string,
|
||||
groupName?: string
|
||||
): RulerRulesConfigDTO {
|
||||
const filteredConfig: RulerRulesConfigDTO = {};
|
||||
|
||||
Object.entries(rulerRulesConfig).forEach(([ns, groups]) => {
|
||||
if (namespace && ns !== namespace) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredGroups = groups.filter((group) => {
|
||||
if (groupName && group.name !== groupName) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredGroups.length > 0) {
|
||||
filteredConfig[ns] = filteredGroups;
|
||||
}
|
||||
});
|
||||
|
||||
return filteredConfig;
|
||||
}
|
||||
|
||||
function RulesPreview({ rules }: { rules: RulerRulesConfigDTO }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<CodeEditor
|
||||
width="100%"
|
||||
height={500}
|
||||
language={'json'}
|
||||
value={JSON.stringify(rules, null, 4)}
|
||||
monacoOptions={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: 'on',
|
||||
readOnly: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = () => ({
|
||||
content: css({
|
||||
flex: '1 1 100%',
|
||||
}),
|
||||
modal: css({
|
||||
width: '700px',
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
Divider,
|
||||
Field,
|
||||
InlineField,
|
||||
InlineSwitch,
|
||||
LinkButton,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@grafana/ui';
|
||||
import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
|
||||
import { Folder } from '../../types/rule-form';
|
||||
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
|
||||
import { AlertingPageWrapper } from '../AlertingPageWrapper';
|
||||
import { CloudRulesSourcePicker } from '../rule-editor/CloudRulesSourcePicker';
|
||||
|
||||
import { ConfirmConversionModal } from './ConfirmConvertModal';
|
||||
import { NamespaceAndGroupFilter } from './NamespaceAndGroupFilter';
|
||||
|
||||
export interface ImportFormValues {
|
||||
selectedDatasourceUID: string;
|
||||
selectedDatasourceName: string | null;
|
||||
pauseAlertingRules: boolean;
|
||||
pauseRecordingRules: boolean;
|
||||
targetFolder?: Folder;
|
||||
namespace?: string;
|
||||
ruleGroup?: string;
|
||||
targetDatasourceUID?: string;
|
||||
}
|
||||
|
||||
const ImportFromDSRules = () => {
|
||||
const formAPI = useForm<ImportFormValues>({
|
||||
defaultValues: {
|
||||
selectedDatasourceUID: undefined,
|
||||
selectedDatasourceName: '',
|
||||
pauseAlertingRules: true,
|
||||
pauseRecordingRules: true,
|
||||
targetFolder: undefined,
|
||||
targetDatasourceUID: undefined,
|
||||
},
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting },
|
||||
} = formAPI;
|
||||
|
||||
const [optionsShowing, toggleOptions] = useToggle(false);
|
||||
const [targetFolder, selectedDatasourceName] = watch(['targetFolder', 'selectedDatasourceName']);
|
||||
const [showConfirmModal, setShowConfirmModal] = useToggle(false);
|
||||
|
||||
const onSubmit = async () => {
|
||||
setShowConfirmModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
pageNav={{
|
||||
text: t('alerting.import-to-gma.pageTitle', 'Import alert rules from a datasource to Grafana-managed rules'),
|
||||
}}
|
||||
>
|
||||
<Stack gap={2} direction={'column'}>
|
||||
<FormProvider {...formAPI}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack direction="column" gap={1}>
|
||||
<Field
|
||||
label={t('alerting.import-to-gma.datasource.label', 'Data source')}
|
||||
invalid={!!errors.selectedDatasourceName}
|
||||
error={errors.selectedDatasourceName?.message}
|
||||
htmlFor="datasource-picker"
|
||||
>
|
||||
<Controller
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<CloudRulesSourcePicker
|
||||
{...field}
|
||||
width={50}
|
||||
inputId="datasource-picker"
|
||||
onChange={(ds: DataSourceInstanceSettings) => {
|
||||
setValue('selectedDatasourceUID', ds.uid);
|
||||
setValue('selectedDatasourceName', ds.name);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
name="selectedDatasourceName"
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: t('alerting.import-to-gma.datasource.required-message', 'Please select a datasource'),
|
||||
},
|
||||
}}
|
||||
control={control}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Collapse
|
||||
label={t('alerting.import-to-gma.optional-settings', 'Optional settings')}
|
||||
isOpen={optionsShowing}
|
||||
onToggle={toggleOptions}
|
||||
collapsible={true}
|
||||
>
|
||||
<Box marginLeft={1}>
|
||||
<Box marginBottom={2}>
|
||||
<Text variant="h5">
|
||||
{t('alerting.import-to-gma.import-location-and-filters', 'Import location and filters')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Field
|
||||
label={t('alerting.import-to-gma.target-folder.label', 'Target folder')}
|
||||
description={t(
|
||||
'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"
|
||||
>
|
||||
<Controller
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<Stack width={50}>
|
||||
<NestedFolderPicker
|
||||
showRootFolder={false}
|
||||
invalid={!!errors.targetFolder?.message}
|
||||
{...field}
|
||||
value={targetFolder?.uid}
|
||||
onChange={(uid, title) => {
|
||||
if (uid && title) {
|
||||
setValue('targetFolder', { title, uid });
|
||||
} else {
|
||||
setValue('targetFolder', undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
name="targetFolder"
|
||||
control={control}
|
||||
/>
|
||||
</Field>
|
||||
<NamespaceAndGroupFilter rulesSourceName={selectedDatasourceName || undefined} />
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
<Box marginLeft={1} marginBottom={1}>
|
||||
<Text variant="h5">{t('alerting.import-to-gma.alert-rules', 'Alert rules')}</Text>
|
||||
</Box>
|
||||
|
||||
<InlineField
|
||||
transparent={true}
|
||||
label={t('alerting.import-to-gma.pause.label', 'Pause imported alerting rules')}
|
||||
labelWidth={30}
|
||||
htmlFor="pause-alerting-rules"
|
||||
>
|
||||
<InlineSwitch transparent id="pause-alerting-rules" {...register('pauseAlertingRules')} />
|
||||
</InlineField>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
<Box marginBottom={1} marginLeft={1}>
|
||||
<Text variant="h5">{t('alerting.import-to-gma.recording-rules', 'Recording rules')}</Text>
|
||||
</Box>
|
||||
|
||||
<InlineField
|
||||
transparent={true}
|
||||
label={t('alerting.import-to-gma.pause-recording.label', 'Pause imported recording rules')}
|
||||
labelWidth={30}
|
||||
htmlFor="pause-recording-rules"
|
||||
>
|
||||
<InlineSwitch transparent id="pause-recording-rules" {...register('pauseRecordingRules')} />
|
||||
</InlineField>
|
||||
|
||||
<Box marginLeft={1} width={50}>
|
||||
<Field
|
||||
required={false}
|
||||
id="target-data-source"
|
||||
label={t('alerting.recording-rules.label-target-data-source', 'Target data source')}
|
||||
description={t(
|
||||
'alerting.recording-rules.description-target-data-source',
|
||||
'The Prometheus data source to store recording rules in'
|
||||
)}
|
||||
error={errors.targetDatasourceUID?.message}
|
||||
invalid={!!errors.targetDatasourceUID?.message}
|
||||
>
|
||||
<Controller
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<DataSourcePicker
|
||||
{...field}
|
||||
current={field.value}
|
||||
noDefault
|
||||
// Filter with `filter` prop instead of `type` prop to avoid showing the `-- Grafana --` data source
|
||||
filter={(ds: DataSourceInstanceSettings) => ds.type === 'prometheus'}
|
||||
onChange={(ds: DataSourceInstanceSettings) => {
|
||||
setValue('targetDatasourceUID', ds.uid);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
name="targetDatasourceUID"
|
||||
control={control}
|
||||
/>
|
||||
</Field>
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
|
||||
<Box marginTop={2}>
|
||||
<Stack gap={1}>
|
||||
<Button type="submit" variant="primary" disabled={isSubmitting || !selectedDatasourceName}>
|
||||
<Stack direction="row" gap={2} alignItems="center">
|
||||
{isSubmitting && <Spinner inline={true} />}
|
||||
<Trans i18nKey="alerting.import-to-gma.action-button">Import</Trans>
|
||||
</Stack>
|
||||
</Button>
|
||||
|
||||
<LinkButton variant="secondary" href="/alerting/list">
|
||||
<Trans i18nKey="common.cancel">Cancel</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
<ConfirmConversionModal isOpen={showConfirmModal} onDismiss={() => setShowConfirmModal(false)} />
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Stack>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default withPageErrorBoundary(ImportFromDSRules);
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Combobox, ComboboxOption, Field, Stack } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { useGetNameSpacesByDatasourceName } from '../rule-editor/useAlertRuleSuggestions';
|
||||
|
||||
import { ImportFormValues } from './ImportFromDSRules';
|
||||
|
||||
interface Props {
|
||||
rulesSourceName?: string;
|
||||
}
|
||||
|
||||
export const NamespaceAndGroupFilter = ({ rulesSourceName }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
} = useFormContext<ImportFormValues>();
|
||||
|
||||
const namespace = watch('namespace');
|
||||
const { namespaceGroups, isLoading } = useGetNameSpacesByDatasourceName(rulesSourceName);
|
||||
|
||||
const namespaceOptions: Array<ComboboxOption<string>> = useMemo(
|
||||
() =>
|
||||
Array.from(namespaceGroups.keys()).map((namespace) => ({
|
||||
label: namespace,
|
||||
value: namespace,
|
||||
})),
|
||||
[namespaceGroups]
|
||||
);
|
||||
|
||||
const groupOptions: Array<ComboboxOption<string>> = useMemo(
|
||||
() => (namespace && namespaceGroups.get(namespace)?.map((group) => ({ label: group, value: group }))) || [],
|
||||
[namespace, namespaceGroups]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset namespace/group if datasource changes
|
||||
setValue('namespace', '');
|
||||
setValue('ruleGroup', '');
|
||||
}, [rulesSourceName, setValue]);
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap={2}>
|
||||
<Field
|
||||
htmlFor="namespace-picker"
|
||||
data-testid="namespace-picker"
|
||||
label={t('alerting.import-to-gma.namespace.label', 'Namespace')}
|
||||
description={t('alerting.import-to-gma.namespace.description', 'Type to search for an existing namespace')}
|
||||
error={errors.namespace?.message}
|
||||
invalid={!!errors.namespace?.message}
|
||||
>
|
||||
<Controller
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<Combobox
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
setValue('ruleGroup', ''); //reset if namespace changes
|
||||
onChange(value.value);
|
||||
}}
|
||||
id="namespace-picker"
|
||||
placeholder={t('alerting.namespace-and-group-filter.select-namespace', 'Select namespace')}
|
||||
options={namespaceOptions}
|
||||
width={42}
|
||||
loading={isLoading}
|
||||
disabled={isLoading || !rulesSourceName}
|
||||
/>
|
||||
)}
|
||||
name="namespace"
|
||||
control={control}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
htmlFor="group-picker"
|
||||
data-testid="group-picker"
|
||||
label={t('alerting.import-to-gma.group.label', 'Group')}
|
||||
description={t('alerting.import-to-gma.group.description', 'Type to search for an existing group')}
|
||||
error={errors.ruleGroup?.message}
|
||||
invalid={!!errors.ruleGroup?.message}
|
||||
>
|
||||
<Controller
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<Combobox
|
||||
{...field}
|
||||
options={groupOptions}
|
||||
width={42}
|
||||
onChange={(value) => {
|
||||
setValue('ruleGroup', value.value ?? '');
|
||||
}}
|
||||
id="group-picker"
|
||||
placeholder={t('alerting.namespace-and-group-filter.select-group', 'Select group')}
|
||||
loading={isLoading}
|
||||
disabled={isLoading || !namespace || !rulesSourceName}
|
||||
/>
|
||||
)}
|
||||
name="ruleGroup"
|
||||
control={control}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -89,7 +89,7 @@ export const AlertRuleNameAndMetric = () => {
|
||||
label={t('alerting.recording-rules.label-target-data-source', 'Target data source')}
|
||||
description={t(
|
||||
'alerting.recording-rules.description-target-data-source',
|
||||
'The Prometheus data source to store the recording rule in'
|
||||
'The Prometheus data source to store recording rules in'
|
||||
)}
|
||||
error={errors.targetDatasourceUid?.message}
|
||||
invalid={!!errors.targetDatasourceUid?.message}
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
import { DataSourcePicker, DataSourcePickerProps } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
|
||||
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
|
||||
|
||||
interface Props {
|
||||
interface Props extends DataSourcePickerProps {
|
||||
disabled?: boolean;
|
||||
onChange: (ds: DataSourceInstanceSettings) => void;
|
||||
value: string | null;
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import { Field, VirtualizedSelect, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
|
||||
import { useAlertRuleSuggestions } from './useAlertRuleSuggestions';
|
||||
import { useGetNameSpacesByDatasourceName } from './useAlertRuleSuggestions';
|
||||
|
||||
interface Props {
|
||||
rulesSourceName: string;
|
||||
@@ -22,7 +22,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const style = useStyles2(getStyle);
|
||||
const { namespaceGroups, isLoading } = useAlertRuleSuggestions(rulesSourceName);
|
||||
const { namespaceGroups, isLoading } = useGetNameSpacesByDatasourceName(rulesSourceName);
|
||||
|
||||
const namespace = watch('namespace');
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isRecordingRuleByType } from '../../../utils/rules';
|
||||
import AlertLabelDropdown from '../../AlertLabelDropdown';
|
||||
import { AlertLabels } from '../../AlertLabels';
|
||||
import { NeedHelpInfo } from '../NeedHelpInfo';
|
||||
import { useAlertRuleSuggestions } from '../useAlertRuleSuggestions';
|
||||
import { useGetLabelsFromDataSourceName } from '../useAlertRuleSuggestions';
|
||||
|
||||
import { AddButton, RemoveButton } from './LabelsButtons';
|
||||
|
||||
@@ -109,7 +109,7 @@ export function useCombinedLabels(
|
||||
selectedKey: string
|
||||
) {
|
||||
// ------- Get labels keys and their values from existing alerts
|
||||
const { isLoading, labels: labelsByKeyFromExisingAlerts } = useAlertRuleSuggestions(dataSourceName);
|
||||
const { labels: labelsByKeyFromExisingAlerts, isLoading } = useGetLabelsFromDataSourceName(dataSourceName);
|
||||
// ------- Get only the keys from the ops labels, as we will fetch the values for the keys once the key is selected.
|
||||
const { loading: isLoadingLabels, labelsOpsKeys = [] } = useGetOpsLabelsKeys(
|
||||
!labelsPluginInstalled || loadingLabelsPlugin
|
||||
|
||||
+60
-13
@@ -1,3 +1,4 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { RuleNamespace } from 'app/types/unified-alerting';
|
||||
@@ -7,13 +8,13 @@ import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { featureDiscoveryApi } from '../../api/featureDiscoveryApi';
|
||||
import { shouldUsePrometheusRulesPrimary } from '../../featureToggles';
|
||||
|
||||
const { usePrometheusRuleNamespacesQuery, useLazyRulerRulesQuery } = alertRuleApi;
|
||||
const { usePrometheusRuleNamespacesQuery, useLazyRulerRulesQuery, useRulerRulesQuery } = alertRuleApi;
|
||||
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
||||
|
||||
const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary();
|
||||
const emptyRulerConfig: RulerRulesConfigDTO = {};
|
||||
|
||||
export function useAlertRuleSuggestions(rulesSourceName: string) {
|
||||
export function useGetLabelsFromDataSourceName(rulesSourceName: string) {
|
||||
const { data: features, isLoading: isFeaturesLoading } = useDiscoverDsFeaturesQuery({ rulesSourceName });
|
||||
|
||||
// emptyRulerConfig is used to prevent from triggering labels' useMemo all the time
|
||||
@@ -32,6 +33,42 @@ export function useAlertRuleSuggestions(rulesSourceName: string) {
|
||||
}
|
||||
}, [features?.rulerConfig, fetchRulerRules]);
|
||||
|
||||
const labels = useMemo(() => {
|
||||
if (isPrometheusRulesLoading || isRulerRulesLoading) {
|
||||
return new Map<string, Set<string>>();
|
||||
}
|
||||
|
||||
if (prometheusRulesPrimary) {
|
||||
return promNamespacesToLabels(promNamespaces);
|
||||
}
|
||||
|
||||
return rulerRulesToLabels(rulerRules);
|
||||
}, [promNamespaces, rulerRules, isPrometheusRulesLoading, isRulerRulesLoading]);
|
||||
|
||||
return { labels, isLoading: isPrometheusRulesLoading || isRulerRulesLoading || isFeaturesLoading };
|
||||
}
|
||||
|
||||
export function useGetNameSpacesByDatasourceName(rulesSourceName?: string) {
|
||||
const { data: features, isLoading: isFeaturesLoading } = useDiscoverDsFeaturesQuery(
|
||||
rulesSourceName ? { rulesSourceName } : skipToken,
|
||||
{ skip: !rulesSourceName }
|
||||
);
|
||||
|
||||
// emptyRulerConfig is used to prevent from triggering labels' useMemo all the time
|
||||
// rulerRules = {} creates a new object and triggers useMemo to recalculate labels
|
||||
const [fetchRulerRules, { data: rulerRules = emptyRulerConfig, isLoading: isRulerRulesLoading }] =
|
||||
useLazyRulerRulesQuery();
|
||||
|
||||
const { data: promNamespaces = [], isLoading: isPrometheusRulesLoading } = usePrometheusRuleNamespacesQuery(
|
||||
rulesSourceName && prometheusRulesPrimary ? { ruleSourceName: rulesSourceName } : skipToken
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (features?.rulerConfig && !prometheusRulesPrimary) {
|
||||
fetchRulerRules({ rulerConfig: features.rulerConfig });
|
||||
}
|
||||
}, [features?.rulerConfig, fetchRulerRules]);
|
||||
|
||||
const namespaceGroups = useMemo(() => {
|
||||
if (isPrometheusRulesLoading || isRulerRulesLoading) {
|
||||
return new Map<string, string[]>();
|
||||
@@ -44,19 +81,29 @@ export function useAlertRuleSuggestions(rulesSourceName: string) {
|
||||
return rulerRulesToNamespaceGroups(rulerRules);
|
||||
}, [promNamespaces, rulerRules, isPrometheusRulesLoading, isRulerRulesLoading]);
|
||||
|
||||
const labels = useMemo(() => {
|
||||
if (isPrometheusRulesLoading || isRulerRulesLoading) {
|
||||
return new Map<string, Set<string>>();
|
||||
}
|
||||
return {
|
||||
namespaceGroups,
|
||||
isLoading: isPrometheusRulesLoading || isRulerRulesLoading || isFeaturesLoading,
|
||||
promNamespaces,
|
||||
};
|
||||
}
|
||||
|
||||
if (prometheusRulesPrimary) {
|
||||
return promNamespacesToLabels(promNamespaces);
|
||||
}
|
||||
export function useGetRulerRules(
|
||||
/** Rules source name to fetch namespaces from */
|
||||
rulesSourceName?: string
|
||||
) {
|
||||
const { data: features, isLoading: isFeaturesLoading } = useDiscoverDsFeaturesQuery(
|
||||
rulesSourceName ? { rulesSourceName } : skipToken
|
||||
);
|
||||
|
||||
return rulerRulesToLabels(rulerRules);
|
||||
}, [promNamespaces, rulerRules, isPrometheusRulesLoading, isRulerRulesLoading]);
|
||||
const { data: rulerRules = emptyRulerConfig, isLoading: isRulerRulesLoading } = useRulerRulesQuery(
|
||||
features?.rulerConfig ? { rulerConfig: features.rulerConfig } : skipToken
|
||||
);
|
||||
|
||||
return { namespaceGroups, labels, isLoading: isPrometheusRulesLoading || isRulerRulesLoading || isFeaturesLoading };
|
||||
return {
|
||||
isLoading: isRulerRulesLoading || isFeaturesLoading,
|
||||
rulerRules,
|
||||
};
|
||||
}
|
||||
|
||||
function promNamespacesToNamespaceGroups(promNamespaces: RuleNamespace[]) {
|
||||
@@ -70,7 +117,7 @@ function promNamespacesToNamespaceGroups(promNamespaces: RuleNamespace[]) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
function rulerRulesToNamespaceGroups(rulerConfig: RulerRulesConfigDTO) {
|
||||
export function rulerRulesToNamespaceGroups(rulerConfig: RulerRulesConfigDTO) {
|
||||
const result = new Map<string, string[]>();
|
||||
Object.entries(rulerConfig).forEach(([namespace, groups]) => {
|
||||
result.set(
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||
import { LinkButton, LoadingPlaceholder, Pagination, Spinner, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Badge, LinkButton, LoadingPlaceholder, Pagination, Spinner, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
||||
@@ -15,6 +16,7 @@ import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelect
|
||||
import { getPaginationStyles } from '../../styles/pagination';
|
||||
import { getRulesDataSources, getRulesSourceUid } from '../../utils/datasource';
|
||||
import { isAsyncRequestStatePending } from '../../utils/redux';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
|
||||
import { RulesGroup } from './RulesGroup';
|
||||
import { useCombinedGroupNamespace } from './useCombinedGroupNamespace';
|
||||
@@ -37,7 +39,6 @@ export const CloudRules = ({ namespaces, expandAll }: Props) => {
|
||||
);
|
||||
|
||||
const hasSomeResults = rulesDataSources.some((ds) => Boolean(promRules[ds.name]?.result?.length));
|
||||
|
||||
const hasDataSourcesConfigured = rulesDataSources.length > 0;
|
||||
const hasDataSourcesLoading = dataSourcesLoading.length > 0;
|
||||
const hasNamespaces = namespaces.length > 0;
|
||||
@@ -48,39 +49,64 @@ export const CloudRules = ({ namespaces, expandAll }: Props) => {
|
||||
DEFAULT_PER_PAGE_PAGINATION
|
||||
);
|
||||
|
||||
const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule);
|
||||
const [viewExternalRuleSupported, viewExternalRuleAllowed] = useAlertingAbility(AlertingAction.ViewExternalAlertRule);
|
||||
|
||||
const canViewCloudRules = viewExternalRuleSupported && viewExternalRuleAllowed;
|
||||
const canCreateGrafanaRules = createRuleSupported && createRuleAllowed;
|
||||
const canMigrateToGMA =
|
||||
hasDataSourcesConfigured && canCreateGrafanaRules && canViewCloudRules && config.featureToggles.alertingMigrationUI;
|
||||
|
||||
return (
|
||||
<section className={styles.wrapper}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className={styles.headerRow}>
|
||||
<Text element="h2" variant="h5">
|
||||
<Trans i18nKey="alerting.list-view.section.dataSourceManaged.title">Data source-managed</Trans>
|
||||
</Text>
|
||||
{dataSourcesLoading.length ? (
|
||||
<LoadingPlaceholder
|
||||
className={styles.loader}
|
||||
text={`Loading rules from ${dataSourcesLoading.length} ${pluralize('source', dataSourcesLoading.length)}`}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<CreateRecordingRuleButton />
|
||||
<Stack gap={2} direction="column">
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className={styles.headerRow}>
|
||||
<Text element="h2" variant="h5">
|
||||
<Trans i18nKey="alerting.list-view.section.dataSourceManaged.title">Data source-managed</Trans>
|
||||
</Text>
|
||||
{dataSourcesLoading.length ? (
|
||||
<LoadingPlaceholder
|
||||
className={styles.loader}
|
||||
text={t(
|
||||
'alerting.list-view.section.loading-rules',
|
||||
'Loading rules from {{numberOfSources}} {{sources}}',
|
||||
{ numberOfSource: dataSourcesLoading.length, sources: pluralize('source', dataSourcesLoading.length) }
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<Stack gap={1}>
|
||||
{canMigrateToGMA && <MigrateToGMAButton />}
|
||||
<CreateRecordingRuleButton />
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{pageItems.map(({ group, namespace }) => {
|
||||
return (
|
||||
<RulesGroup
|
||||
group={group}
|
||||
key={`${getRulesSourceUid(namespace.rulesSource)}-${namespace.name}-${group.name}`}
|
||||
namespace={namespace}
|
||||
expandAll={expandAll}
|
||||
viewMode={'grouped'}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{pageItems.map(({ group, namespace }) => (
|
||||
<RulesGroup
|
||||
group={group}
|
||||
key={`${getRulesSourceUid(namespace.rulesSource)}-${namespace.name}-${group.name}`}
|
||||
namespace={namespace}
|
||||
expandAll={expandAll}
|
||||
viewMode={'grouped'}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!hasDataSourcesConfigured && <p>There are no Prometheus or Loki data sources configured.</p>}
|
||||
{hasDataSourcesConfigured && !hasDataSourcesLoading && !hasNamespaces && <p>No rules found.</p>}
|
||||
{!hasDataSourcesConfigured && (
|
||||
<p>
|
||||
<Trans i18nKey="alerting.list-view.no-prom-or-loki-rules">
|
||||
There are no Prometheus or Loki data sources configured
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
{hasDataSourcesConfigured && !hasDataSourcesLoading && !hasNamespaces && (
|
||||
<p>
|
||||
<Trans i18nKey="alerting.list-view.no-rules">No rules found.</Trans>
|
||||
</p>
|
||||
)}
|
||||
{!hasSomeResults && hasDataSourcesLoading && <Spinner size="xl" className={styles.spinner} />}
|
||||
|
||||
<Pagination
|
||||
@@ -121,7 +147,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
|
||||
export function CreateRecordingRuleButton() {
|
||||
const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
|
||||
@@ -144,3 +169,20 @@ export function CreateRecordingRuleButton() {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function MigrateToGMAButton() {
|
||||
const importUrl = createRelativeUrl('/alerting/import-datasource-managed-rules');
|
||||
return (
|
||||
<LinkButton variant="secondary" href={importUrl} icon="arrow-up">
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Trans i18nKey="alerting.rule-list.import-to-gma.text">Import to Grafana-managed rules</Trans>
|
||||
<Badge
|
||||
text={t('alerting.rule-list.import-to-gma.new-badge', 'New!')}
|
||||
aria-label="new"
|
||||
color="blue"
|
||||
icon="rocket"
|
||||
/>
|
||||
</Stack>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import { createRelativeUrl } from './url';
|
||||
|
||||
type QueryParams = ConstructorParameters<typeof URLSearchParams>[0];
|
||||
|
||||
export const createListFilterLink = (values: Array<[string, string]>) => {
|
||||
export const createListFilterLink = (values: Array<[string, string]>, options?: { skipSubPath?: boolean }) => {
|
||||
const params = new URLSearchParams([['search', values.map(([key, value]) => `${key}:"${value}"`).join(' ')]]);
|
||||
return createRelativeUrl(`/alerting/list`, params);
|
||||
return createRelativeUrl(`/alerting/list`, params, { skipSubPath: options?.skipSubPath });
|
||||
};
|
||||
|
||||
export const alertListPageLink = (queryParams: Record<string, string> = {}, options?: { skipSubPath?: boolean }) =>
|
||||
|
||||
@@ -582,6 +582,44 @@
|
||||
"group-loader": {
|
||||
"group-load-failed": "Failed to load rules from group {{ groupName }} in {{ namespaceName }}"
|
||||
},
|
||||
"import-from-dsrules": {
|
||||
"description-folder-import-rules": "The folder to import the rules to"
|
||||
},
|
||||
"import-to-gma": {
|
||||
"action-button": "Import",
|
||||
"alert-rules": "Alert rules",
|
||||
"confirm-modal": {
|
||||
"confirm": "Yes, import",
|
||||
"title": "Confirm import"
|
||||
},
|
||||
"datasource": {
|
||||
"label": "Data source",
|
||||
"required-message": "Please select a datasource"
|
||||
},
|
||||
"error": "Failed to import alert rules: {{error}}",
|
||||
"group": {
|
||||
"description": "Type to search for an existing group",
|
||||
"label": "Group"
|
||||
},
|
||||
"import-location-and-filters": "Import location and filters",
|
||||
"namespace": {
|
||||
"description": "Type to search for an existing namespace",
|
||||
"label": "Namespace"
|
||||
},
|
||||
"optional-settings": "Optional settings",
|
||||
"pageTitle": "Import alert rules from a datasource to Grafana-managed rules",
|
||||
"pause": {
|
||||
"label": "Pause imported alerting rules"
|
||||
},
|
||||
"pause-recording": {
|
||||
"label": "Pause imported recording rules"
|
||||
},
|
||||
"recording-rules": "Recording rules",
|
||||
"success": "Successfully imported alert rules to Grafana-managed rules.",
|
||||
"target-folder": {
|
||||
"label": "Target folder"
|
||||
}
|
||||
},
|
||||
"irm-integration": {
|
||||
"connection-method": "How to connect to IRM",
|
||||
"disabled-description": "Enable Grafana IRM to use this integration",
|
||||
@@ -607,6 +645,8 @@
|
||||
"new-recording-rule": "New recording rule",
|
||||
"provisioning": "You can also define rules through file provisioning or Terraform. <2>Learn more</2>"
|
||||
},
|
||||
"no-prom-or-loki-rules": "There are no Prometheus or Loki data sources configured",
|
||||
"no-rules": "No rules found.",
|
||||
"section": {
|
||||
"dataSourceManaged": {
|
||||
"title": "Data source-managed"
|
||||
@@ -617,7 +657,8 @@
|
||||
"loading": "Loading...",
|
||||
"new-recording-rule": "New recording rule",
|
||||
"title": "Grafana-managed"
|
||||
}
|
||||
},
|
||||
"loading-rules": "Loading rules from {{numberOfSources}} {{sources}}"
|
||||
}
|
||||
},
|
||||
"manage-permissions": {
|
||||
@@ -636,6 +677,10 @@
|
||||
"save": "Save mute timing",
|
||||
"saving": "Saving mute timing"
|
||||
},
|
||||
"namespace-and-group-filter": {
|
||||
"select-group": "Select group",
|
||||
"select-namespace": "Select namespace"
|
||||
},
|
||||
"notification-preview": {
|
||||
"alertmanager": "Alertmanager:",
|
||||
"error": "Could not load routing preview for {{alertmanager}}",
|
||||
@@ -731,7 +776,7 @@
|
||||
"previewCondition": "Preview alert rule condition"
|
||||
},
|
||||
"recording-rules": {
|
||||
"description-target-data-source": "The Prometheus data source to store the recording rule in",
|
||||
"description-target-data-source": "The Prometheus data source to store recording rules in",
|
||||
"label-target-data-source": "Target data source"
|
||||
},
|
||||
"rule-form": {
|
||||
@@ -846,6 +891,10 @@
|
||||
"no-more-results": "No more results – showing {{numberOfRules}} rules",
|
||||
"no-rules-found": "No alert or recording rules matched your current set of filters."
|
||||
},
|
||||
"import-to-gma": {
|
||||
"new-badge": "New!",
|
||||
"text": "Import to Grafana-managed rules"
|
||||
},
|
||||
"more": "More",
|
||||
"new-alert-rule": "New alert rule",
|
||||
"new-datasource-recording-rule": "New Data source recording rule",
|
||||
@@ -935,6 +984,12 @@
|
||||
},
|
||||
"threshold": {
|
||||
"to": "TO"
|
||||
},
|
||||
"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:"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations": {
|
||||
|
||||
Reference in New Issue
Block a user