Chore: Remove Form usage from notification policies (#81758)

* Chore: Replace Form component usage in EditDefaultPolicyForm.tsx

* Chore: Replace Form component usage in EditNotificationPolicyForm.tsx

* Remove ts-ignore
This commit is contained in:
Alex Khomenko 2024-02-21 17:14:49 +01:00 committed by GitHub
parent 5460d75e74
commit 5132828a2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 329 additions and 320 deletions

View File

@ -1,6 +1,7 @@
import React, { ReactNode, useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Collapse, Field, Form, InputControl, Link, MultiSelect, Select, useStyles2 } from '@grafana/ui';
import { Collapse, Field, Link, MultiSelect, Select, useStyles2 } from '@grafana/ui';
import { RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { FormAmRoute } from '../../types/amroutes';
@ -41,125 +42,131 @@ export const AmRootRouteForm = ({
const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(route.group_by));
const defaultValues = amRouteToFormAmRoute(route);
const {
handleSubmit,
register,
control,
formState: { errors },
setValue,
getValues,
} = useForm<FormAmRoute>({
defaultValues: {
...defaultValues,
overrideTimings: true,
overrideGrouping: true,
},
});
return (
<Form defaultValues={{ ...defaultValues, overrideTimings: true, overrideGrouping: true }} onSubmit={onSubmit}>
{({ register, control, errors, setValue, getValues }) => (
<form onSubmit={handleSubmit(onSubmit)}>
<Field label="Default contact point" invalid={!!errors.receiver} error={errors.receiver?.message}>
<>
<Field label="Default contact point" invalid={!!errors.receiver} error={errors.receiver?.message}>
<>
<div className={styles.container} data-testid="am-receiver-select">
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
aria-label="Default contact point"
{...field}
className={styles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={receivers}
/>
)}
control={control}
name="receiver"
rules={{ required: { value: true, message: 'Required.' } }}
/>
<span>or</span>
<Link
className={styles.linkText}
href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}
>
Create a contact point
</Link>
</div>
</>
</Field>
<Field
label="Group by"
description="Group alerts when you receive a notification based on labels."
data-testid="am-group-select"
>
{/* @ts-ignore-check: react-hook-form made me do this */}
<InputControl
<div className={styles.container} data-testid="am-receiver-select">
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<MultiSelect
aria-label="Group by"
<Select
aria-label="Default contact point"
{...field}
allowCustomValue
className={styles.input}
onCreateOption={(opt: string) => {
setGroupByOptions((opts) => [...opts, stringToSelectableValue(opt)]);
// @ts-ignore-check: react-hook-form made me do this
setValue('groupBy', [...field.value, opt]);
}}
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
options={[...commonGroupByOptions, ...groupByOptions]}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={receivers}
/>
)}
control={control}
name="groupBy"
name="receiver"
rules={{ required: { value: true, message: 'Required.' } }}
/>
<span>or</span>
<Link
className={styles.linkText}
href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}
>
Create a contact point
</Link>
</div>
</>
</Field>
<Field
label="Group by"
description="Group alerts when you receive a notification based on labels."
data-testid="am-group-select"
>
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<MultiSelect
aria-label="Group by"
{...field}
allowCustomValue
className={styles.input}
onCreateOption={(opt: string) => {
setGroupByOptions((opts) => [...opts, stringToSelectableValue(opt)]);
setValue('groupBy', [...(field.value || []), opt]);
}}
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
options={[...commonGroupByOptions, ...groupByOptions]}
/>
)}
control={control}
name="groupBy"
/>
</Field>
<Collapse
collapsible
className={styles.collapse}
isOpen={isTimingOptionsExpanded}
label="Timing options"
onToggle={setIsTimingOptionsExpanded}
>
<div className={styles.timingFormContainer}>
<Field
label="Group wait"
description="The waiting time until the initial notification is sent for a new group created by an incoming alert. Default 30 seconds."
invalid={!!errors.groupWaitValue}
error={errors.groupWaitValue?.message}
data-testid="am-group-wait"
>
<PromDurationInput
{...register('groupWaitValue', { validate: promDurationValidator })}
placeholder={TIMING_OPTIONS_DEFAULTS.group_wait}
className={styles.promDurationInput}
aria-label="Group wait"
/>
</Field>
<Collapse
collapsible
className={styles.collapse}
isOpen={isTimingOptionsExpanded}
label="Timing options"
onToggle={setIsTimingOptionsExpanded}
<Field
label="Group interval"
description="The waiting time to send a batch of new alerts for that group after the first notification was sent. Default 5 minutes."
invalid={!!errors.groupIntervalValue}
error={errors.groupIntervalValue?.message}
data-testid="am-group-interval"
>
<div className={styles.timingFormContainer}>
<Field
label="Group wait"
description="The waiting time until the initial notification is sent for a new group created by an incoming alert. Default 30 seconds."
invalid={!!errors.groupWaitValue}
error={errors.groupWaitValue?.message}
data-testid="am-group-wait"
>
<PromDurationInput
{...register('groupWaitValue', { validate: promDurationValidator })}
placeholder={TIMING_OPTIONS_DEFAULTS.group_wait}
className={styles.promDurationInput}
aria-label="Group wait"
/>
</Field>
<Field
label="Group interval"
description="The waiting time to send a batch of new alerts for that group after the first notification was sent. Default 5 minutes."
invalid={!!errors.groupIntervalValue}
error={errors.groupIntervalValue?.message}
data-testid="am-group-interval"
>
<PromDurationInput
{...register('groupIntervalValue', { validate: promDurationValidator })}
placeholder={TIMING_OPTIONS_DEFAULTS.group_interval}
className={styles.promDurationInput}
aria-label="Group interval"
/>
</Field>
<Field
label="Repeat interval"
description="The waiting time to resend an alert after they have successfully been sent. Default 4 hours. Should be a multiple of Group interval."
invalid={!!errors.repeatIntervalValue}
error={errors.repeatIntervalValue?.message}
data-testid="am-repeat-interval"
>
<PromDurationInput
{...register('repeatIntervalValue', {
validate: (value: string) => {
const groupInterval = getValues('groupIntervalValue');
return repeatIntervalValidator(value, groupInterval);
},
})}
placeholder={TIMING_OPTIONS_DEFAULTS.repeat_interval}
className={styles.promDurationInput}
aria-label="Repeat interval"
/>
</Field>
</div>
</Collapse>
<div className={styles.container}>{actionButtons}</div>
</>
)}
</Form>
<PromDurationInput
{...register('groupIntervalValue', { validate: promDurationValidator })}
placeholder={TIMING_OPTIONS_DEFAULTS.group_interval}
className={styles.promDurationInput}
aria-label="Group interval"
/>
</Field>
<Field
label="Repeat interval"
description="The waiting time to resend an alert after they have successfully been sent. Default 4 hours. Should be a multiple of Group interval."
invalid={!!errors.repeatIntervalValue}
error={errors.repeatIntervalValue?.message}
data-testid="am-repeat-interval"
>
<PromDurationInput
{...register('repeatIntervalValue', {
validate: (value: string) => {
const groupInterval = getValues('groupIntervalValue');
return repeatIntervalValidator(value, groupInterval);
},
})}
placeholder={TIMING_OPTIONS_DEFAULTS.repeat_interval}
className={styles.promDurationInput}
aria-label="Repeat interval"
/>
</Field>
</div>
</Collapse>
<div className={styles.container}>{actionButtons}</div>
</form>
);
};

View File

@ -1,17 +1,15 @@
import { css } from '@emotion/css';
import React, { ReactNode, useState } from 'react';
import { useForm, Controller, useFieldArray } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import {
Badge,
Button,
Field,
FieldArray,
FieldValidationMessage,
Form,
IconButton,
Input,
InputControl,
MultiSelect,
Select,
Stack,
@ -75,224 +73,228 @@ export const AmRoutesExpandedForm = ({
object_matchers: route ? formAmRoute.object_matchers : emptyMatcher,
};
const {
handleSubmit,
control,
register,
formState: { errors },
setValue,
watch,
getValues,
} = useForm<FormAmRoute>({
defaultValues,
});
const { fields, append, remove } = useFieldArray({
control,
name: 'object_matchers',
});
return (
<Form defaultValues={defaultValues} onSubmit={onSubmit} maxWidth="none">
{({ control, register, errors, setValue, watch, getValues }) => (
<>
<input type="hidden" {...register('id')} />
{/* @ts-ignore-check: react-hook-form made me do this */}
<FieldArray name="object_matchers" control={control}>
{({ fields, append, remove }) => (
<>
<Stack direction="column" alignItems="flex-start">
<div>Matching labels</div>
{fields.length === 0 && (
<Badge
color="orange"
className={styles.noMatchersWarning}
icon="exclamation-triangle"
text="If no matchers are specified, this notification policy will handle all alert instances."
/>
)}
{fields.length > 0 && (
<div className={styles.matchersContainer}>
{fields.map((field, index) => {
return (
<Stack direction="row" key={field.id} alignItems="center">
<Field
label="Label"
invalid={!!errors.object_matchers?.[index]?.name}
error={errors.object_matchers?.[index]?.name?.message}
>
<Input
{...register(`object_matchers.${index}.name`, { required: 'Field is required' })}
defaultValue={field.name}
placeholder="label"
autoFocus
/>
</Field>
<Field label={'Operator'}>
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
className={styles.matchersOperator}
onChange={(value) => onChange(value?.value)}
options={matcherFieldOptions}
aria-label="Operator"
/>
)}
defaultValue={field.operator}
control={control}
name={`object_matchers.${index}.operator`}
rules={{ required: { value: true, message: 'Required.' } }}
/>
</Field>
<Field
label="Value"
invalid={!!errors.object_matchers?.[index]?.value}
error={errors.object_matchers?.[index]?.value?.message}
>
<Input
{...register(`object_matchers.${index}.value`)}
defaultValue={field.value}
placeholder="value"
/>
</Field>
<IconButton tooltip="Remove matcher" name={'trash-alt'} onClick={() => remove(index)}>
Remove
</IconButton>
</Stack>
);
})}
</div>
)}
<Button
className={styles.addMatcherBtn}
icon="plus"
onClick={() => append(emptyArrayFieldMatcher)}
variant="secondary"
type="button"
<form onSubmit={handleSubmit(onSubmit)}>
<input type="hidden" {...register('id')} />
<Stack direction="column" alignItems="flex-start">
<div>Matching labels</div>
{fields.length === 0 && (
<Badge
color="orange"
className={styles.noMatchersWarning}
icon="exclamation-triangle"
text="If no matchers are specified, this notification policy will handle all alert instances."
/>
)}
{fields.length > 0 && (
<div className={styles.matchersContainer}>
{fields.map((field, index) => {
return (
<Stack direction="row" key={field.id} alignItems="center">
<Field
label="Label"
invalid={!!errors.object_matchers?.[index]?.name}
error={errors.object_matchers?.[index]?.name?.message}
>
Add matcher
</Button>
<Input
{...register(`object_matchers.${index}.name`, { required: 'Field is required' })}
defaultValue={field.name}
placeholder="label"
autoFocus
/>
</Field>
<Field label={'Operator'}>
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
className={styles.matchersOperator}
onChange={(value) => onChange(value?.value)}
options={matcherFieldOptions}
aria-label="Operator"
/>
)}
defaultValue={field.operator}
control={control}
name={`object_matchers.${index}.operator`}
rules={{ required: { value: true, message: 'Required.' } }}
/>
</Field>
<Field
label="Value"
invalid={!!errors.object_matchers?.[index]?.value}
error={errors.object_matchers?.[index]?.value?.message}
>
<Input
{...register(`object_matchers.${index}.value`)}
defaultValue={field.value}
placeholder="value"
/>
</Field>
<IconButton tooltip="Remove matcher" name={'trash-alt'} onClick={() => remove(index)}>
Remove
</IconButton>
</Stack>
);
})}
</div>
)}
<Button
className={styles.addMatcherBtn}
icon="plus"
onClick={() => append(emptyArrayFieldMatcher)}
variant="secondary"
type="button"
>
Add matcher
</Button>
</Stack>
<Field label="Contact point">
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<Select
aria-label="Contact point"
{...field}
className={formStyles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={receiversWithOnCallOnTop}
isClearable
/>
)}
control={control}
name="receiver"
/>
</Field>
<Field label="Continue matching subsequent sibling nodes">
<Switch id="continue-toggle" {...register('continue')} />
</Field>
<Field label="Override grouping">
<Switch id="override-grouping-toggle" {...register('overrideGrouping')} />
</Field>
{watch().overrideGrouping && (
<Field
label="Group by"
description="Group alerts when you receive a notification based on labels. If empty it will be inherited from the parent policy."
>
<Controller
rules={{
validate: (value) => {
if (!value || value.length === 0) {
return 'At least one group by option is required.';
}
return true;
},
}}
render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
<>
<MultiSelect
aria-label="Group by"
{...field}
invalid={Boolean(error)}
allowCustomValue
className={formStyles.input}
onCreateOption={(opt: string) => {
setGroupByOptions((opts) => [...opts, stringToSelectableValue(opt)]);
setValue('groupBy', [...(field.value || []), opt]);
}}
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
options={[...commonGroupByOptions, ...groupByOptions]}
/>
{error && <FieldValidationMessage>{error.message}</FieldValidationMessage>}
</>
)}
</FieldArray>
<Field label="Contact point">
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<Select
aria-label="Contact point"
{...field}
className={formStyles.input}
onChange={(value) => onChange(mapSelectValueToString(value))}
options={receiversWithOnCallOnTop}
isClearable
/>
)}
control={control}
name="receiver"
/>
</Field>
<Field label="Continue matching subsequent sibling nodes">
<Switch id="continue-toggle" {...register('continue')} />
</Field>
<Field label="Override grouping">
<Switch id="override-grouping-toggle" {...register('overrideGrouping')} />
</Field>
{watch().overrideGrouping && (
<Field
label="Group by"
description="Group alerts when you receive a notification based on labels. If empty it will be inherited from the parent policy."
>
<InputControl
rules={{
validate: (value) => {
if (!value || value.length === 0) {
return 'At least one group by option is required.';
}
return true;
},
}}
render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
<>
<MultiSelect
aria-label="Group by"
{...field}
invalid={Boolean(error)}
allowCustomValue
className={formStyles.input}
onCreateOption={(opt: string) => {
setGroupByOptions((opts) => [...opts, stringToSelectableValue(opt)]);
// @ts-ignore-check: react-hook-form made me do this
setValue('groupBy', [...field.value, opt]);
}}
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
options={[...commonGroupByOptions, ...groupByOptions]}
/>
{error && <FieldValidationMessage>{error.message}</FieldValidationMessage>}
</>
)}
control={control}
name="groupBy"
/>
</Field>
)}
<Field label="Override general timings">
<Switch id="override-timings-toggle" {...register('overrideTimings')} />
</Field>
{watch().overrideTimings && (
<>
<Field
label={routeTimingsFields.groupWait.label}
description={routeTimingsFields.groupWait.description}
invalid={!!errors.groupWaitValue}
error={errors.groupWaitValue?.message}
>
<PromDurationInput
{...register('groupWaitValue', { validate: promDurationValidator })}
aria-label={routeTimingsFields.groupWait.ariaLabel}
className={formStyles.promDurationInput}
/>
</Field>
<Field
label={routeTimingsFields.groupInterval.label}
description={routeTimingsFields.groupInterval.description}
invalid={!!errors.groupIntervalValue}
error={errors.groupIntervalValue?.message}
>
<PromDurationInput
{...register('groupIntervalValue', { validate: promDurationValidator })}
aria-label={routeTimingsFields.groupInterval.ariaLabel}
className={formStyles.promDurationInput}
/>
</Field>
<Field
label={routeTimingsFields.repeatInterval.label}
description={routeTimingsFields.repeatInterval.description}
invalid={!!errors.repeatIntervalValue}
error={errors.repeatIntervalValue?.message}
>
<PromDurationInput
{...register('repeatIntervalValue', {
validate: (value = '') => {
const groupInterval = getValues('groupIntervalValue');
return repeatIntervalValidator(value, groupInterval);
},
})}
aria-label={routeTimingsFields.repeatInterval.ariaLabel}
className={formStyles.promDurationInput}
/>
</Field>
</>
)}
control={control}
name="groupBy"
/>
</Field>
)}
<Field label="Override general timings">
<Switch id="override-timings-toggle" {...register('overrideTimings')} />
</Field>
{watch().overrideTimings && (
<>
<Field
label="Mute timings"
data-testid="am-mute-timing-select"
description="Add mute timing to policy"
invalid={!!errors.muteTimeIntervals}
label={routeTimingsFields.groupWait.label}
description={routeTimingsFields.groupWait.description}
invalid={!!errors.groupWaitValue}
error={errors.groupWaitValue?.message}
>
<InputControl
render={({ field: { onChange, ref, ...field } }) => (
<MultiSelect
aria-label="Mute timings"
{...field}
className={formStyles.input}
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
options={muteTimingOptions}
/>
)}
control={control}
name="muteTimeIntervals"
<PromDurationInput
{...register('groupWaitValue', { validate: promDurationValidator })}
aria-label={routeTimingsFields.groupWait.ariaLabel}
className={formStyles.promDurationInput}
/>
</Field>
<Field
label={routeTimingsFields.groupInterval.label}
description={routeTimingsFields.groupInterval.description}
invalid={!!errors.groupIntervalValue}
error={errors.groupIntervalValue?.message}
>
<PromDurationInput
{...register('groupIntervalValue', { validate: promDurationValidator })}
aria-label={routeTimingsFields.groupInterval.ariaLabel}
className={formStyles.promDurationInput}
/>
</Field>
<Field
label={routeTimingsFields.repeatInterval.label}
description={routeTimingsFields.repeatInterval.description}
invalid={!!errors.repeatIntervalValue}
error={errors.repeatIntervalValue?.message}
>
<PromDurationInput
{...register('repeatIntervalValue', {
validate: (value = '') => {
const groupInterval = getValues('groupIntervalValue');
return repeatIntervalValidator(value, groupInterval);
},
})}
aria-label={routeTimingsFields.repeatInterval.ariaLabel}
className={formStyles.promDurationInput}
/>
</Field>
{actionButtons}
</>
)}
</Form>
<Field
label="Mute timings"
data-testid="am-mute-timing-select"
description="Add mute timing to policy"
invalid={!!errors.muteTimeIntervals}
>
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<MultiSelect
aria-label="Mute timings"
{...field}
className={formStyles.input}
onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
options={muteTimingOptions}
/>
)}
control={control}
name="muteTimeIntervals"
/>
</Field>
{actionButtons}
</form>
);
};