mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: editing existing rules via UI (#33005)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { PageToolbar, ToolbarButton, useStyles, CustomScrollbar, Spinner, Alert } from '@grafana/ui';
|
||||
import { PageToolbar, ToolbarButton, useStyles, CustomScrollbar, Spinner, Alert, InfoBox } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { AlertTypeStep } from './AlertTypeStep';
|
||||
@@ -9,98 +9,105 @@ import { DetailsStep } from './DetailsStep';
|
||||
import { QueryStep } from './QueryStep';
|
||||
import { useForm, FormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaAlertState } from 'app/types/unified-alerting-dto';
|
||||
//import { locationService } from '@grafana/runtime';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { SAMPLE_QUERIES } from '../../mocks/grafana-queries';
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { initialAsyncRequestState } from '../../utils/redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { saveRuleFormAction } from '../../state/actions';
|
||||
import { cleanUpAction } from 'app/core/actions/cleanUp';
|
||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||
import { rulerRuleToFormValues, defaultFormValues } from '../../utils/rule-form';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type Props = {};
|
||||
type Props = {
|
||||
existing?: RuleWithLocation;
|
||||
};
|
||||
|
||||
const defaultValues: RuleFormValues = Object.freeze({
|
||||
name: '',
|
||||
labels: [{ key: '', value: '' }],
|
||||
annotations: [{ key: '', value: '' }],
|
||||
dataSourceName: null,
|
||||
|
||||
// threshold
|
||||
folder: null,
|
||||
queries: SAMPLE_QUERIES, // @TODO remove the sample eventually
|
||||
condition: '',
|
||||
noDataState: GrafanaAlertState.NoData,
|
||||
execErrState: GrafanaAlertState.Alerting,
|
||||
evaluateEvery: '1m',
|
||||
evaluateFor: '5m',
|
||||
|
||||
// system
|
||||
expression: '',
|
||||
forTime: 1,
|
||||
forTimeUnit: 'm',
|
||||
});
|
||||
|
||||
export const AlertRuleForm: FC<Props> = () => {
|
||||
export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(cleanUpAction({ stateSelector: (state) => state.unifiedAlerting.ruleForm }));
|
||||
};
|
||||
}, [dispatch]);
|
||||
const defaultValues: RuleFormValues = useMemo(() => {
|
||||
if (existing) {
|
||||
return rulerRuleToFormValues(existing);
|
||||
}
|
||||
return defaultFormValues;
|
||||
}, [existing]);
|
||||
|
||||
const formAPI = useForm<RuleFormValues>({
|
||||
mode: 'onSubmit',
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const { handleSubmit, watch } = formAPI;
|
||||
const { handleSubmit, watch, errors } = formAPI;
|
||||
|
||||
const hasErrors = !!Object.values(errors).filter((x) => !!x).length;
|
||||
|
||||
const type = watch('type');
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
|
||||
const showStep2 = Boolean(dataSourceName && type);
|
||||
const showStep2 = Boolean(type && (type === RuleFormType.threshold || !!dataSourceName));
|
||||
|
||||
const submitState = useUnifiedAlertingSelector((state) => state.ruleForm.saveRule) || initialAsyncRequestState;
|
||||
useCleanup((state) => state.unifiedAlerting.ruleForm.saveRule);
|
||||
|
||||
const submit = (values: RuleFormValues) => {
|
||||
const submit = (values: RuleFormValues, exitOnSave: boolean) => {
|
||||
console.log('submit', values);
|
||||
dispatch(
|
||||
saveRuleFormAction({
|
||||
...values,
|
||||
annotations: values.annotations.filter(({ key }) => !!key),
|
||||
labels: values.labels.filter(({ key }) => !!key),
|
||||
values: {
|
||||
...values,
|
||||
annotations: values.annotations?.filter(({ key }) => !!key) ?? [],
|
||||
labels: values.labels?.filter(({ key }) => !!key) ?? [],
|
||||
},
|
||||
existing,
|
||||
exitOnSave,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormContext {...formAPI}>
|
||||
<form onSubmit={handleSubmit(submit)} className={styles.form}>
|
||||
<form onSubmit={handleSubmit((values) => submit(values, false))} className={styles.form}>
|
||||
<PageToolbar title="Create alert rule" pageIcon="bell" className={styles.toolbar}>
|
||||
<ToolbarButton variant="default" disabled={submitState.loading}>
|
||||
Cancel
|
||||
</ToolbarButton>
|
||||
<ToolbarButton variant="primary" type="submit" disabled={submitState.loading}>
|
||||
<Link to="/alerting/list">
|
||||
<ToolbarButton variant="default" disabled={submitState.loading} type="button">
|
||||
Cancel
|
||||
</ToolbarButton>
|
||||
</Link>
|
||||
<ToolbarButton
|
||||
variant="primary"
|
||||
type="button"
|
||||
onClick={handleSubmit((values) => submit(values, false))}
|
||||
disabled={submitState.loading}
|
||||
>
|
||||
{submitState.loading && <Spinner className={styles.buttonSpiner} inline={true} />}
|
||||
Save
|
||||
</ToolbarButton>
|
||||
<ToolbarButton variant="primary" disabled={submitState.loading}>
|
||||
<ToolbarButton
|
||||
variant="primary"
|
||||
type="button"
|
||||
onClick={handleSubmit((values) => submit(values, true))}
|
||||
disabled={submitState.loading}
|
||||
>
|
||||
{submitState.loading && <Spinner className={styles.buttonSpiner} inline={true} />}
|
||||
Save and exit
|
||||
</ToolbarButton>
|
||||
</PageToolbar>
|
||||
<div className={styles.contentOutter}>
|
||||
<CustomScrollbar autoHeightMin="100%">
|
||||
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
||||
<div className={styles.contentInner}>
|
||||
{hasErrors && (
|
||||
<InfoBox severity="error">
|
||||
There are errors in the form below. Please fix them and try saving again.
|
||||
</InfoBox>
|
||||
)}
|
||||
{submitState.error && (
|
||||
<Alert severity="error" title="Error saving rule">
|
||||
{submitState.error.message || (submitState.error as any)?.data?.message || String(submitState.error)}
|
||||
</Alert>
|
||||
)}
|
||||
<AlertTypeStep />
|
||||
<AlertTypeStep editingExistingRule={!!existing} />
|
||||
{showStep2 && (
|
||||
<>
|
||||
<QueryStep />
|
||||
|
||||
@@ -24,7 +24,11 @@ const alertTypeOptions: SelectableValue[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const AlertTypeStep: FC = () => {
|
||||
interface Props {
|
||||
editingExistingRule: boolean;
|
||||
}
|
||||
|
||||
export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
const { register, control, watch, errors, setValue } = useFormContext<RuleFormValues>();
|
||||
@@ -64,6 +68,7 @@ export const AlertTypeStep: FC = () => {
|
||||
</Field>
|
||||
<div className={styles.flexRow}>
|
||||
<Field
|
||||
disabled={editingExistingRule}
|
||||
label="Alert type"
|
||||
className={styles.formInput}
|
||||
error={errors.type?.message}
|
||||
@@ -91,30 +96,32 @@ export const AlertTypeStep: FC = () => {
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Select data source"
|
||||
error={errors.dataSourceName?.message}
|
||||
invalid={!!errors.dataSourceName?.message}
|
||||
>
|
||||
<InputControl
|
||||
as={DataSourcePicker as React.ComponentType<Omit<DataSourcePickerProps, 'current'>>}
|
||||
valueName="current"
|
||||
filter={dataSourceFilter}
|
||||
name="dataSourceName"
|
||||
noDefault={true}
|
||||
control={control}
|
||||
alerting={true}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a data source' },
|
||||
}}
|
||||
onChange={(ds: DataSourceInstanceSettings[]) => {
|
||||
// reset location if switching data sources, as differnet rules source will have different groups and namespaces
|
||||
setValue('location', undefined);
|
||||
return ds[0]?.name ?? null;
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
{ruleFormType === RuleFormType.system && (
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Select data source"
|
||||
error={errors.dataSourceName?.message}
|
||||
invalid={!!errors.dataSourceName?.message}
|
||||
>
|
||||
<InputControl
|
||||
as={(DataSourcePicker as unknown) as React.ComponentType<Omit<DataSourcePickerProps, 'current'>>}
|
||||
valueName="current"
|
||||
filter={dataSourceFilter}
|
||||
name="dataSourceName"
|
||||
noDefault={true}
|
||||
control={control}
|
||||
alerting={true}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a data source' },
|
||||
}}
|
||||
onChange={(ds: DataSourceInstanceSettings[]) => {
|
||||
// reset location if switching data sources, as differnet rules source will have different groups and namespaces
|
||||
setValue('location', undefined);
|
||||
return ds[0]?.name ?? null;
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
{ruleFormType === RuleFormType.system && (
|
||||
<Field
|
||||
|
||||
@@ -19,7 +19,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const AnnotationKeyInput: FC<Props> = ({ value, onChange, existingKeys, width, className }) => {
|
||||
const [isCustom, setIsCustom] = useState(false);
|
||||
const isCustomByDefault = !!value && !Object.keys(AnnotationOptions).includes(value); // custom by default if value does not match any of available options
|
||||
const [isCustom, setIsCustom] = useState(isCustomByDefault);
|
||||
|
||||
const annotationOptions = useMemo(
|
||||
(): SelectableValue[] => [
|
||||
|
||||
Reference in New Issue
Block a user