Skip to content

Commit

Permalink
PMM-12741 Extract alert rule from template submission from grafana
Browse files Browse the repository at this point in the history
  • Loading branch information
matejkubinec committed Feb 23, 2024
1 parent a2f0113 commit 32b83e7
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 166 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function MoreActionsRuleButtons({}: Props) {
<>
{/* @PERCONA */}
<LinkButton
href={urlUtil.renderUrl('alerting/new/alerting', { returnTo: location.pathname + location.search })}
href={urlUtil.renderUrl('alerting/new-from-template', { returnTo: location.pathname + location.search })}
icon="plus"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const NoRulesSplash = () => {
<EmptyListCTA
title=""
buttonIcon="plus"
buttonLink={'alerting/new/alerting'}
buttonLink="alerting/new-from-template"
buttonTitle="New alert rule from template"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
/>
Expand Down
7 changes: 0 additions & 7 deletions public/app/features/alerting/unified/state/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit';
import { isEmpty } from 'lodash';

import { locationService } from '@grafana/runtime';
import { formatCreateAPIPayload } from 'app/percona/integrated-alerting/components/TemplateStep/TemplateStep.utils';
import { AlertRulesService } from 'app/percona/shared/services/AlertRules/AlertRules.service';
import {
AlertmanagerAlert,
AlertManagerCortexConfig,
Expand Down Expand Up @@ -459,11 +457,6 @@ export const saveRuleFormAction = createAsyncThunk(
const rulerClient = getRulerClient(rulerConfig);
identifier = await rulerClient.saveGrafanaRule(values, evaluateEvery, existing);
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
// @PERCONA
// Added our type case
} else if (type === RuleFormType.templated) {
await AlertRulesService.create(formatCreateAPIPayload(values), undefined, true);
identifier = { uid: '', ruleSourceName: 'grafana' };
} else {
throw new Error('Unexpected rule form type');
}
Expand Down
15 changes: 0 additions & 15 deletions public/app/features/alerting/unified/types/rule-form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { Template } from 'app/percona/integrated-alerting/components/AlertRuleTemplate/AlertRuleTemplate.types';
import { FiltersForm } from 'app/percona/integrated-alerting/components/TemplateStep/TemplateStep.types';
import { Severity } from 'app/percona/shared/core';
import { AlertQuery, GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';

import { Folder } from '../components/rule-editor/RuleFolderPicker';
Expand All @@ -9,8 +6,6 @@ export enum RuleFormType {
grafana = 'grafana',
cloudAlerting = 'cloud-alerting',
cloudRecording = 'cloud-recording',
// @PERCONA
templated = 'templated',
}

export interface ContactPoints {
Expand Down Expand Up @@ -47,14 +42,4 @@ export interface RuleFormValues {
keepFiringForTime?: number;
keepFiringForTimeUnit?: string;
expression: string;

// @PERCONA
// templated rules
// to avoid keeping the name between Percona / Grafana rule forms
ruleName: string;
template: Template | null;
// This is the same as evaluateFor, but we have a different validation
duration: string;
filters: FiltersForm[];
severity: keyof typeof Severity | null;
}
Original file line number Diff line number Diff line change
@@ -1,66 +1,66 @@
import React, { FC, useMemo } from 'react';
import { AxiosError } from 'axios';
import React, { FC, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Link } from 'react-router-dom';

import { locationService } from '@grafana/runtime';
import { Button, HorizontalGroup, Spinner, useStyles2 } from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { Page } from 'app/core/components/Page/Page';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
import { saveRuleFormAction } from 'app/features/alerting/unified/state/actions';
import { RuleFormType, RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { initialAsyncRequestState } from 'app/features/alerting/unified/utils/redux';
import { getDefaultFormValues, getDefaultQueries, MINUTE } from 'app/features/alerting/unified/utils/rule-form';
import { useDispatch } from 'app/types';
import { PMM_ALERTING_CREATE_ALERT_TEMPLATE } from 'app/percona/shared/components/PerconaBootstrapper/PerconaNavigation';
import { ApiErrorResponse } from 'app/percona/shared/core';
import { logger } from 'app/percona/shared/helpers/logger';
import { AlertRulesService } from 'app/percona/shared/services/AlertRules/AlertRules.service';

import { TemplatedAlertFormValues } from '../../types';
import { TemplateStep } from '../TemplateStep/TemplateStep';
import { formatCreateAPIPayload } from '../TemplateStep/TemplateStep.utils';

import { getStyles } from './AlertRuleFromTemplate.styles';

export const AlertRuleFromTemplate: FC = () => {
const dispatch = useDispatch();
const [isSubmitting, setIsSubmitting] = useState(false);
const notifyApp = useAppNotification();
const [queryParams] = useQueryParams();
const evaluateEvery = MINUTE;
const returnTo = !queryParams['returnTo'] ? '/alerting/list' : String(queryParams['returnTo']);
const defaultValues: RuleFormValues = useMemo(() => {
return {
...getDefaultFormValues(),
condition: 'C',
queries: getDefaultQueries(),
evaluateEvery: evaluateEvery,
type: RuleFormType.templated,
};
}, [evaluateEvery]);
const defaultValues: TemplatedAlertFormValues = useMemo(
() => ({
duration: '1m',
// TODO: group interval - isn't handled on BE currently
evaluateFor: '1m',
filters: [],
ruleName: '',
severity: null,
template: null,
folder: null,
group: '',
}),
[]
);
const methods = useForm({
mode: 'onSubmit',
defaultValues,
shouldFocusError: true,
});
const styles = useStyles2(getStyles);
const submitState = useUnifiedAlertingSelector((state) => state.ruleForm.saveRule) || initialAsyncRequestState;

const submit = (values: RuleFormValues) => {
dispatch(
saveRuleFormAction({
values: {
...defaultValues,
...values,
annotations:
values.annotations
?.map(({ key, value }) => ({ key: key.trim(), value: value.trim() }))
.filter(({ key, value }) => !!key && !!value) ?? [],
labels:
values.labels
?.map(({ key, value }) => ({ key: key.trim(), value: value.trim() }))
.filter(({ key }) => !!key) ?? [],
},
redirectOnSave: returnTo,
initialAlertRuleName: defaultValues.name,
evaluateEvery: evaluateEvery,
})
);
const submit = async (values: TemplatedAlertFormValues) => {
setIsSubmitting(true);

try {
await AlertRulesService.create(formatCreateAPIPayload(values), undefined, true);
notifyApp.success(`Rule "${values.ruleName}" saved.`);

locationService.push(returnTo);
} catch (error) {
logger.error(error);
const message = (error as AxiosError<ApiErrorResponse>)?.response?.data?.message;
notifyApp.error(message || 'Failed to save rule');
}

setIsSubmitting(false);
};

const onInvalid = () => {
Expand All @@ -74,13 +74,13 @@ export const AlertRuleFromTemplate: FC = () => {
type="button"
size="sm"
onClick={methods.handleSubmit((values) => submit(values), onInvalid)}
disabled={submitState.loading}
disabled={isSubmitting}
>
{submitState.loading && <Spinner className={styles.buttonSpinner} inline={true} />}
{isSubmitting && <Spinner className={styles.buttonSpinner} inline={true} />}
Save rule and exit
</Button>
<Link to={returnTo}>
<Button variant="secondary" disabled={submitState.loading} type="button" size="sm">
<Button variant="secondary" disabled={isSubmitting} type="button" size="sm">
Cancel
</Button>
</Link>
Expand All @@ -90,7 +90,7 @@ export const AlertRuleFromTemplate: FC = () => {
return (
<FormProvider {...methods}>
<AppChromeUpdate actions={actionButtons} />
<Page navId="integrated-alerting-new-from-template">
<Page navId="alert-list" pageNav={PMM_ALERTING_CREATE_ALERT_TEMPLATE}>
<TemplateStep />
</Page>
</FormProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const AlertRuleTemplateActions: FC<AlertRuleTemplateActionsProps> = ({ te
<LinkButton
icon="plus"
fill="text"
href={`/alerting/new?returnTo=%2Falerting%2Falert-rule-templates&template=${template.name}`}
href={`/alerting/new-from-template?returnTo=%2Falerting%2Falert-rule-templates&template=${template.name}`}
data-testid="create-from-template-button"
>
Create alert rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Field, Input, Select } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { FolderAndGroup } from 'app/features/alerting/unified/components/rule-editor/FolderAndGroup';
import { fetchExternalAlertmanagersConfigAction } from 'app/features/alerting/unified/state/actions';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { initialAsyncRequestState } from 'app/features/alerting/unified/utils/redux';
import { durationValidationPattern, parseDurationToMilliseconds } from 'app/features/alerting/unified/utils/time';
import {
Expand All @@ -16,6 +15,8 @@ import { fetchTemplatesAction } from 'app/percona/shared/core/reducers';
import { getTemplates } from 'app/percona/shared/core/selectors';
import { useDispatch, useSelector } from 'app/types';

import { TemplatedAlertFormValues } from '../../types';

import { AdvancedRuleSection } from './AdvancedRuleSection/AdvancedRuleSection';
import TemplateFiltersField from './TemplateFiltersField';
import { SEVERITY_OPTIONS } from './TemplateStep.constants';
Expand All @@ -28,7 +29,7 @@ export const TemplateStep: FC = () => {
setValue,
getValues,
formState: { errors },
} = useFormContext<RuleFormValues>();
} = useFormContext<TemplatedAlertFormValues>();
const dispatch = useDispatch();
const templates = useRef<Template[]>([]);
const [currentTemplate, setCurrentTemplate] = useState<Template>();
Expand Down Expand Up @@ -85,9 +86,7 @@ export const TemplateStep: FC = () => {

useEffect(() => {
const getData = async () => {
// @PERCONA_TODO check if it's fetching the correct one
dispatch(fetchExternalAlertmanagersConfigAction());
// dispatch(fetchExternalAlertmanagersConfigAction('grafana'));
await dispatch(fetchExternalAlertmanagersConfigAction());
const { templates } = await dispatch(fetchTemplatesAction()).unwrap();

if (selectedTemplate) {
Expand All @@ -106,7 +105,6 @@ export const TemplateStep: FC = () => {
}, [dispatch, handleTemplateChange, selectedTemplate, setRuleNameAfterTemplate, setValue]);

return (
// <RuleEditorSection stepNo={2} title="Template details">
<>
<Field
label={Messages.templateField}
Expand Down Expand Up @@ -134,8 +132,8 @@ export const TemplateStep: FC = () => {
<Field
label={Messages.nameField}
description={Messages.tooltips.name}
error={errors.name?.message}
invalid={!!errors.name?.message}
error={errors.ruleName?.message}
invalid={!!errors.ruleName?.message}
>
<Input id="ruleName" {...register('ruleName', { required: { value: true, message: Messages.errors.name } })} />
</Field>
Expand Down Expand Up @@ -218,78 +216,13 @@ export const TemplateStep: FC = () => {
/>
</Field>

{/* TODO */}
<FolderAndGroup enableProvisionedGroups={true} />

{/* <div className={styles.folderAndGroupSelect}>
<Field
label={
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
<Stack gap={0.5}>
Folder
<Tooltip
placement="top"
content={
<div>
Each folder has unique folder permission. When you store multiple rules in a folder, the folder
access permissions get assigned to the rules.
</div>
}
>
<Icon name="info-circle" size="xs" />
</Tooltip>
</Stack>
</Label>
}
className={styles.folderAndGroupInput}
error={errors.folder?.message}
invalid={!!errors.folder?.message}
data-testid="folder-picker"
>
<InputControl
render={({ field: { ref, ...field } }) => (
<RuleFolderPicker
inputId="folder"
{...field}
enableCreateNew={contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
enableReset={true}
filter={folderFilter}
// @PERCONA_TODO
// dissalowSlashes={true}
/>
)}
name="folder"
rules={{
required: { value: true, message: 'Please select a folder' },
validate: {
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title),
},
}}
/>
</Field>
<Field
label="Group"
data-testid="group-picker"
description="Rules within the same group are evaluated after the same time interval."
className={styles.folderAndGroupInput}
error={errors.group?.message}
invalid={!!errors.group?.message}
>
<Input
id="group"
{...register('group', {
required: { value: true, message: 'Must enter a group name' },
})}
/>
</Field>
</div> */}
<FolderAndGroup enableProvisionedGroups />

<TemplateFiltersField />

{currentTemplate && (
<AdvancedRuleSection expression={currentTemplate.expr} summary={currentTemplate.annotations?.summary} />
)}
{/* </RuleEditorSection> */}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { durationToMilliseconds, parseDuration, SelectableValue } from '@grafana/data';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { Template } from 'app/percona/integrated-alerting/components/AlertRuleTemplate/AlertRuleTemplate.types';
import { AlertRuleCreatePayload, AlertRulesListResponseChannel, Severity } from 'app/percona/shared/core';

import { TemplatedAlertFormValues } from '../../types';

export const formatChannelsOptions = (channels: string[]): Array<SelectableValue<string>> =>
channels
Expand All @@ -20,7 +20,7 @@ export const formatTemplateOptions = (templates: Template[]): Array<SelectableVa
}))
: [];

export const formatCreateAPIPayload = (data: RuleFormValues): AlertRuleCreatePayload => {
export const formatCreateAPIPayload = (data: TemplatedAlertFormValues): AlertRuleCreatePayload => {
const { duration, filters, ruleName, severity, template, folder, group } = data;
const durationObj = parseDuration(duration);
const durationSeconds = durationToMilliseconds(durationObj) / 1000;
Expand Down
18 changes: 18 additions & 0 deletions public/app/percona/integrated-alerting/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import { CSSProperties } from 'react';

import { Folder } from 'app/features/alerting/unified/components/rule-editor/RuleFolderPicker';
import { Template } from 'app/percona/integrated-alerting/components/AlertRuleTemplate/AlertRuleTemplate.types';
import { FiltersForm } from 'app/percona/integrated-alerting/components/TemplateStep/TemplateStep.types';
import { Severity } from 'app/percona/shared/core';

export interface UploadAlertRuleTemplatePayload {
yaml: string;
}

export interface TemplatedAlertFormValues {
duration: string;
filters: FiltersForm[];
ruleName: string;
severity: keyof typeof Severity | null;
template: Template | null;
folder: Folder | null;
group: string;

// TODO: group interval - isn't handled on BE yet.
evaluateFor?: string;
}

declare module 'react-table' {
interface Row {
isExpanded: boolean;
Expand Down
Loading

0 comments on commit 32b83e7

Please sign in to comment.