mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add new button for exporting new alert rule in HCL format (#96785)
* add new button for exporting new alert rule * Fix test * allow only HCL format for exporting new alert rule * fix initial tab * Update public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx Co-authored-by: Konrad Lalik <konradlalik@gmail.com> * Update public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx Co-authored-by: Konrad Lalik <konradlalik@gmail.com> * address review comments * update translations --------- Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
This commit is contained in:
parent
2736fe0568
commit
624f44fdb5
@ -227,6 +227,17 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/export-new-rule',
|
||||
pageClass: 'page-alerting',
|
||||
roles: evaluateAccess([AccessControlAction.AlertingRuleRead]),
|
||||
component: importAlertingComponent(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/components/export/ExportNewGrafanaRule'
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/:sourceName/:id/view',
|
||||
pageClass: 'page-alerting',
|
||||
|
@ -27,6 +27,7 @@ export const LogMessages = {
|
||||
unknownMessageFromError: 'unknown messageFromError',
|
||||
grafanaRecording: 'creating Grafana recording rule from scratch',
|
||||
loadedCentralAlertStateHistory: 'loaded central alert state history',
|
||||
exportNewGrafanaRule: 'exporting new Grafana rule',
|
||||
};
|
||||
|
||||
const { logInfo, logError, logMeasurement, logWarning } = createMonitoringLogger('features.alerting', {
|
||||
|
@ -120,7 +120,7 @@ const ui = {
|
||||
rulesFilterInput: byTestId('search-query-input'),
|
||||
moreErrorsButton: byRole('button', { name: /more errors/ }),
|
||||
editCloudGroupIcon: byTestId('edit-group'),
|
||||
newRuleButton: byText(/new alert rule/i),
|
||||
newRuleButton: byRole('link', { name: 'New alert rule' }),
|
||||
exportButton: byText(/export rules/i),
|
||||
editGroupModal: {
|
||||
dialog: byRole('dialog'),
|
||||
|
@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { AlertingPageWrapper } from '../AlertingPageWrapper';
|
||||
import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm';
|
||||
|
||||
export default function ExportNewGrafanaRule() {
|
||||
return (
|
||||
<ExportNewGrafanaRuleWrapper>
|
||||
<ModifyExportRuleForm />
|
||||
</ExportNewGrafanaRuleWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExportNewGrafanaRuleWrapperProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function ExportNewGrafanaRuleWrapper({ children }: ExportNewGrafanaRuleWrapperProps) {
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
pageNav={{
|
||||
text: 'Export new Grafana rule',
|
||||
subTitle: 'Export a new rule definition in Terraform(HCL) format. Any changes you make will not be saved.',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Drawer } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { RuleInspectorTabs } from '../rule-editor/RuleInspector';
|
||||
|
||||
@ -27,10 +28,17 @@ export function GrafanaExportDrawer({
|
||||
label: provider.name,
|
||||
value: provider.exportFormat,
|
||||
}));
|
||||
const subtitle =
|
||||
formatProviders.length > 1
|
||||
? t(
|
||||
'alerting.export.subtitle.formats',
|
||||
'Select the format and download the file or copy the contents to clipboard'
|
||||
)
|
||||
: t('alerting.export.subtitle.one-format', 'Download the file or copy the contents to clipboard');
|
||||
return (
|
||||
<Drawer
|
||||
title={title}
|
||||
subtitle="Select the format and download the file or copy the contents to clipboard"
|
||||
subtitle={subtitle}
|
||||
tabs={
|
||||
<RuleInspectorTabs<ExportFormats> tabs={grafanaRulesTabs} setActiveTab={onTabChange} activeTab={activeTab} />
|
||||
}
|
||||
|
@ -15,13 +15,18 @@ import { alertRuleApi } from '../../../api/alertRuleApi';
|
||||
import { fetchRulerRulesGroup } from '../../../api/ruler';
|
||||
import { useDataSourceFeatures } from '../../../hooks/useCombinedRule';
|
||||
import { useReturnTo } from '../../../hooks/useReturnTo';
|
||||
import { RuleFormValues } from '../../../types/rule-form';
|
||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||
import { DEFAULT_GROUP_EVALUATION_INTERVAL, formValuesToRulerGrafanaRuleDTO } from '../../../utils/rule-form';
|
||||
import {
|
||||
DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||
formValuesToRulerGrafanaRuleDTO,
|
||||
getDefaultFormValues,
|
||||
getDefaultQueries,
|
||||
} from '../../../utils/rule-form';
|
||||
import { isGrafanaRulerRule } from '../../../utils/rules';
|
||||
import { FileExportPreview } from '../../export/FileExportPreview';
|
||||
import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer';
|
||||
import { ExportFormats, allGrafanaExportProviders } from '../../export/providers';
|
||||
import { ExportFormats, HclExportProvider, allGrafanaExportProviders } from '../../export/providers';
|
||||
import { AlertRuleNameAndMetric } from '../AlertRuleNameInput';
|
||||
import AnnotationsStep from '../AnnotationsStep';
|
||||
import { GrafanaEvaluationBehaviorStep } from '../GrafanaEvaluationBehavior';
|
||||
@ -30,18 +35,30 @@ import { NotificationsStep } from '../NotificationsStep';
|
||||
import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep';
|
||||
|
||||
interface ModifyExportRuleFormProps {
|
||||
alertUid: string;
|
||||
alertUid?: string;
|
||||
ruleForm?: RuleFormValues;
|
||||
}
|
||||
|
||||
export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFormProps) {
|
||||
const defaultValuesForNewRule: RuleFormValues = useMemo(() => {
|
||||
const defaultRuleType = RuleFormType.grafana;
|
||||
|
||||
return {
|
||||
...getDefaultFormValues(),
|
||||
condition: 'C',
|
||||
queries: getDefaultQueries(false),
|
||||
type: defaultRuleType,
|
||||
evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const formAPI = useForm<RuleFormValues>({
|
||||
mode: 'onSubmit',
|
||||
defaultValues: ruleForm,
|
||||
defaultValues: ruleForm ?? defaultValuesForNewRule,
|
||||
shouldFocusError: true,
|
||||
});
|
||||
|
||||
const existing = Boolean(ruleForm); // always should be true
|
||||
const existing = Boolean(ruleForm);
|
||||
const notifyApp = useAppNotification();
|
||||
const { returnTo } = useReturnTo('/alerting/list');
|
||||
|
||||
@ -129,21 +146,21 @@ interface GrafanaRuleDesignExportPreviewProps {
|
||||
exportFormat: ExportFormats;
|
||||
onClose: () => void;
|
||||
exportValues: RuleFormValues;
|
||||
uid: string;
|
||||
uid?: string;
|
||||
}
|
||||
export const getPayloadToExport = (
|
||||
uid: string,
|
||||
formValues: RuleFormValues,
|
||||
existingGroup: RulerRuleGroupDTO<RulerRuleDTO> | null | undefined
|
||||
existingGroup: RulerRuleGroupDTO<RulerRuleDTO> | null | undefined,
|
||||
ruleUid?: string
|
||||
): PostableRulerRuleGroupDTO => {
|
||||
const grafanaRuleDto = formValuesToRulerGrafanaRuleDTO(formValues);
|
||||
|
||||
const updatedRule = { ...grafanaRuleDto, grafana_alert: { ...grafanaRuleDto.grafana_alert, uid: uid } };
|
||||
const updatedRule = { ...grafanaRuleDto, grafana_alert: { ...grafanaRuleDto.grafana_alert, uid: ruleUid } };
|
||||
if (existingGroup?.rules) {
|
||||
// we have to update the rule in the group in the same position if it exists, otherwise we have to add it at the end
|
||||
let alreadyExistsInGroup = false;
|
||||
const updatedRules = existingGroup.rules.map((rule: RulerRuleDTO) => {
|
||||
if (isGrafanaRulerRule(rule) && rule.grafana_alert.uid === uid) {
|
||||
if (isGrafanaRulerRule(rule) && rule.grafana_alert.uid === ruleUid) {
|
||||
alreadyExistsInGroup = true;
|
||||
return updatedRule;
|
||||
} else {
|
||||
@ -167,11 +184,11 @@ export const getPayloadToExport = (
|
||||
}
|
||||
};
|
||||
|
||||
const useGetPayloadToExport = (values: RuleFormValues, uid: string) => {
|
||||
const useGetPayloadToExport = (values: RuleFormValues, ruleUid?: string) => {
|
||||
const rulerGroupDto = useGetGroup(values.folder?.uid ?? '', values.group);
|
||||
const payload: PostableRulerRuleGroupDTO = useMemo(() => {
|
||||
return getPayloadToExport(uid, values, rulerGroupDto?.value);
|
||||
}, [uid, rulerGroupDto, values]);
|
||||
return getPayloadToExport(values, rulerGroupDto?.value, ruleUid);
|
||||
}, [ruleUid, rulerGroupDto, values]);
|
||||
return { payload, loadingGroup: rulerGroupDto.loading };
|
||||
};
|
||||
|
||||
@ -187,7 +204,7 @@ const GrafanaRuleDesignExportPreview = ({
|
||||
const nameSpaceUID = exportValues.folder?.uid ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
!loadingGroup && getExport({ payload, format: exportFormat, nameSpaceUID });
|
||||
!loadingGroup && payload.name && getExport({ payload, format: exportFormat, nameSpaceUID });
|
||||
}, [nameSpaceUID, exportFormat, payload, getExport, loadingGroup]);
|
||||
|
||||
if (exportData.isLoading) {
|
||||
@ -209,11 +226,14 @@ const GrafanaRuleDesignExportPreview = ({
|
||||
interface GrafanaRuleDesignExporterProps {
|
||||
onClose: () => void;
|
||||
exportValues: RuleFormValues;
|
||||
uid: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export const GrafanaRuleDesignExporter = memo(({ onClose, exportValues, uid }: GrafanaRuleDesignExporterProps) => {
|
||||
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||
const exportingNewRule = !uid;
|
||||
const initialTab = exportingNewRule ? 'hcl' : 'yaml';
|
||||
const [activeTab, setActiveTab] = useState<ExportFormats>(initialTab);
|
||||
const formatProviders = exportingNewRule ? [HclExportProvider] : Object.values(allGrafanaExportProviders);
|
||||
|
||||
return (
|
||||
<GrafanaExportDrawer
|
||||
@ -221,7 +241,7 @@ export const GrafanaRuleDesignExporter = memo(({ onClose, exportValues, uid }: G
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
onClose={onClose}
|
||||
formatProviders={Object.values(allGrafanaExportProviders)}
|
||||
formatProviders={formatProviders}
|
||||
>
|
||||
<GrafanaRuleDesignExportPreview
|
||||
exportFormat={activeTab}
|
||||
|
@ -162,16 +162,16 @@ describe('getPayloadFromDto', () => {
|
||||
|
||||
it('should return a ModifyExportPayload with the updated rule added to a group with this rule belongs, in the same position', () => {
|
||||
// for alerting rule
|
||||
const resultForAlerting = getPayloadToExport('uid-rule-2', formValuesForRule2Updated, groupDto);
|
||||
const resultForAlerting = getPayloadToExport(formValuesForRule2Updated, groupDto, 'uid-rule-2');
|
||||
expect(resultForAlerting).toEqual({
|
||||
name: 'Test Group',
|
||||
rules: [rule1, expectedModifiedRule2('uid-rule-2'), rule3, rule4],
|
||||
});
|
||||
// for recording rule
|
||||
const resultForRecording = getPayloadToExport(
|
||||
'uid-rule-4',
|
||||
{ ...formValuesForRecordingRule4Updated, type: RuleFormType.grafanaRecording },
|
||||
groupDto
|
||||
groupDto,
|
||||
'uid-rule-4'
|
||||
);
|
||||
expect(resultForRecording).toEqual({
|
||||
name: 'Test Group',
|
||||
@ -180,16 +180,16 @@ describe('getPayloadFromDto', () => {
|
||||
});
|
||||
it('should return a ModifyExportPayload with the updated rule added to a non empty rule where this rule does not belong, in the last position', () => {
|
||||
// for alerting rule
|
||||
const result = getPayloadToExport('uid-rule-5', formValuesForRule2Updated, groupDto);
|
||||
const result = getPayloadToExport(formValuesForRule2Updated, groupDto, 'uid-rule-5');
|
||||
expect(result).toEqual({
|
||||
name: 'Test Group',
|
||||
rules: [rule1, rule2, rule3, rule4, expectedModifiedRule2('uid-rule-5')],
|
||||
});
|
||||
// for recording rule
|
||||
const resultForRecording = getPayloadToExport(
|
||||
'uid-rule-5',
|
||||
{ ...formValuesForRecordingRule4Updated, type: RuleFormType.grafanaRecording },
|
||||
groupDto
|
||||
groupDto,
|
||||
'uid-rule-5'
|
||||
);
|
||||
expect(resultForRecording).toEqual({
|
||||
name: 'Test Group',
|
||||
@ -202,7 +202,7 @@ describe('getPayloadFromDto', () => {
|
||||
name: 'Empty Group',
|
||||
rules: [],
|
||||
};
|
||||
const result = getPayloadToExport('uid-rule-2', formValuesForRule2Updated, emptyGroupDto);
|
||||
const result = getPayloadToExport(formValuesForRule2Updated, emptyGroupDto, 'uid-rule-2');
|
||||
expect(result).toEqual({
|
||||
name: 'Empty Group',
|
||||
rules: [expectedModifiedRule2('uid-rule-2')],
|
||||
|
@ -27,6 +27,7 @@ import { useUnifiedAlertingSelector } from '../hooks/useUnifiedAlertingSelector'
|
||||
import { fetchAllPromAndRulerRulesAction, fetchAllPromRulesAction, fetchRulerRulesAction } from '../state/actions';
|
||||
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
|
||||
import { getAllRulesSourceNames, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { createRelativeUrl } from '../utils/url';
|
||||
|
||||
const VIEWS = {
|
||||
groups: RuleListGroupView,
|
||||
@ -119,7 +120,17 @@ const RuleListV1 = () => {
|
||||
return (
|
||||
// We don't want to show the Loading... indicator for the whole page.
|
||||
// We show separate indicators for Grafana-managed and Cloud rules
|
||||
<AlertingPageWrapper navId="alert-list" isLoading={false} actions={hasAlertRulesCreated && <CreateAlertButton />}>
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
isLoading={false}
|
||||
actions={
|
||||
hasAlertRulesCreated && (
|
||||
<Stack gap={1}>
|
||||
<CreateAlertButton /> <ExportNewRuleButton />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Stack direction="column">
|
||||
<RuleListErrors />
|
||||
<RulesFilter onClear={onFilterCleared} />
|
||||
@ -169,3 +180,21 @@ export function CreateAlertButton() {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ExportNewRuleButton() {
|
||||
const returnTo = location.pathname + location.search;
|
||||
const url = createRelativeUrl(`/alerting/export-new-rule`, {
|
||||
returnTo,
|
||||
});
|
||||
return (
|
||||
<LinkButton
|
||||
href={url}
|
||||
icon="download-alt"
|
||||
variant="secondary"
|
||||
tooltip="Export new grafana rule"
|
||||
onClick={() => logInfo(LogMessages.exportNewGrafanaRule)}
|
||||
>
|
||||
<Trans i18nKey="alerting.list-view.section.grafanaManaged.export-new-rule">Export new alert rule</Trans>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
@ -280,6 +280,12 @@
|
||||
"contactPointFilter": {
|
||||
"label": "Contact point"
|
||||
},
|
||||
"export": {
|
||||
"subtitle": {
|
||||
"formats": "Select the format and download the file or copy the contents to clipboard",
|
||||
"one-format": "Download the file or copy the contents to clipboard"
|
||||
}
|
||||
},
|
||||
"folderAndGroup": {
|
||||
"evaluation": {
|
||||
"modal": {
|
||||
@ -301,6 +307,7 @@
|
||||
"title": "Data source-managed"
|
||||
},
|
||||
"grafanaManaged": {
|
||||
"export-new-rule": "Export new alert rule",
|
||||
"export-rules": "Export rules",
|
||||
"loading": "Loading...",
|
||||
"new-recording-rule": "New recording rule",
|
||||
|
@ -280,6 +280,12 @@
|
||||
"contactPointFilter": {
|
||||
"label": "Cőʼnŧäčŧ pőįʼnŧ"
|
||||
},
|
||||
"export": {
|
||||
"subtitle": {
|
||||
"formats": "Ŝęľęčŧ ŧĥę ƒőřmäŧ äʼnđ đőŵʼnľőäđ ŧĥę ƒįľę őř čőpy ŧĥę čőʼnŧęʼnŧş ŧő čľįpþőäřđ",
|
||||
"one-format": "Đőŵʼnľőäđ ŧĥę ƒįľę őř čőpy ŧĥę čőʼnŧęʼnŧş ŧő čľįpþőäřđ"
|
||||
}
|
||||
},
|
||||
"folderAndGroup": {
|
||||
"evaluation": {
|
||||
"modal": {
|
||||
@ -301,6 +307,7 @@
|
||||
"title": "Đäŧä şőūřčę-mäʼnäģęđ"
|
||||
},
|
||||
"grafanaManaged": {
|
||||
"export-new-rule": "Ēχpőřŧ ʼnęŵ äľęřŧ řūľę",
|
||||
"export-rules": "Ēχpőřŧ řūľęş",
|
||||
"loading": "Ŀőäđįʼnģ...",
|
||||
"new-recording-rule": "Ńęŵ řęčőřđįʼnģ řūľę",
|
||||
|
Loading…
Reference in New Issue
Block a user