1import React, { FC, useState } from 'react';
2import { css, cx } from '@emotion/css';
3import { GrafanaTheme2 } from '@grafana/data';
4import {
5  Button,
6  Field,
7  FieldArray,
8  Form,
9  HorizontalGroup,
10  IconButton,
11  Input,
12  InputControl,
13  MultiSelect,
14  Select,
15  Switch,
16  useStyles2,
17} from '@grafana/ui';
18import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
19import {
20  emptyArrayFieldMatcher,
21  mapMultiSelectValueToStrings,
22  mapSelectValueToString,
23  optionalPositiveInteger,
24  stringToSelectableValue,
25  stringsToSelectableValues,
26} from '../../utils/amroutes';
27import { timeOptions } from '../../utils/time';
28import { getFormStyles } from './formStyles';
29import { matcherFieldOptions } from '../../utils/alertmanager';
30
31export interface AmRoutesExpandedFormProps {
32  onCancel: () => void;
33  onSave: (data: FormAmRoute) => void;
34  receivers: AmRouteReceiver[];
35  routes: FormAmRoute;
36}
37
38export const AmRoutesExpandedForm: FC<AmRoutesExpandedFormProps> = ({ onCancel, onSave, receivers, routes }) => {
39  const styles = useStyles2(getStyles);
40  const formStyles = useStyles2(getFormStyles);
41  const [overrideGrouping, setOverrideGrouping] = useState(routes.groupBy.length > 0);
42  const [overrideTimings, setOverrideTimings] = useState(
43    !!routes.groupWaitValue || !!routes.groupIntervalValue || !!routes.repeatIntervalValue
44  );
45  const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(routes.groupBy));
46
47  return (
48    <Form defaultValues={routes} onSubmit={onSave}>
49      {({ control, register, errors, setValue }) => (
50        <>
51          {/* @ts-ignore-check: react-hook-form made me do this */}
52          <input type="hidden" {...register('id')} />
53          {/* @ts-ignore-check: react-hook-form made me do this */}
54          <FieldArray name="object_matchers" control={control}>
55            {({ fields, append, remove }) => (
56              <>
57                <div>Matching labels</div>
58                <div className={styles.matchersContainer}>
59                  {fields.map((field, index) => {
60                    const localPath = `object_matchers[${index}]`;
61                    return (
62                      <HorizontalGroup key={field.id} align="flex-start">
63                        <Field
64                          label="Label"
65                          invalid={!!errors.object_matchers?.[index]?.name}
66                          error={errors.object_matchers?.[index]?.name?.message}
67                        >
68                          <Input
69                            {...register(`${localPath}.name`, { required: 'Field is required' })}
70                            defaultValue={field.name}
71                            placeholder="label"
72                          />
73                        </Field>
74                        <Field label={'Operator'}>
75                          <InputControl
76                            render={({ field: { onChange, ref, ...field } }) => (
77                              <Select
78                                {...field}
79                                className={styles.matchersOperator}
80                                onChange={(value) => onChange(value?.value)}
81                                options={matcherFieldOptions}
82                                aria-label="Operator"
83                              />
84                            )}
85                            defaultValue={field.operator}
86                            control={control}
87                            name={`${localPath}.operator` as const}
88                            rules={{ required: { value: true, message: 'Required.' } }}
89                          />
90                        </Field>
91                        <Field
92                          label="Value"
93                          invalid={!!errors.object_matchers?.[index]?.value}
94                          error={errors.object_matchers?.[index]?.value?.message}
95                        >
96                          <Input
97                            {...register(`${localPath}.value`, { required: 'Field is required' })}
98                            defaultValue={field.value}
99                            placeholder="value"
100                          />
101                        </Field>
102                        <IconButton
103                          className={styles.removeButton}
104                          tooltip="Remove matcher"
105                          name={'trash-alt'}
106                          onClick={() => remove(index)}
107                        >
108                          Remove
109                        </IconButton>
110                      </HorizontalGroup>
111                    );
112                  })}
113                </div>
114                <Button
115                  className={styles.addMatcherBtn}
116                  icon="plus"
117                  onClick={() => append(emptyArrayFieldMatcher)}
118                  variant="secondary"
119                  type="button"
120                >
121                  Add matcher
122                </Button>
123              </>
124            )}
125          </FieldArray>
126          <Field label="Contact point">
127            {/* @ts-ignore-check: react-hook-form made me do this */}
128            <InputControl
129              render={({ field: { onChange, ref, ...field } }) => (
130                <Select
131                  aria-label="Contact point"
132                  {...field}
133                  className={formStyles.input}
134                  onChange={(value) => onChange(mapSelectValueToString(value))}
135                  options={receivers}
136                  menuShouldPortal
137                />
138              )}
139              control={control}
140              name="receiver"
141            />
142          </Field>
143          <Field label="Continue matching subsequent sibling nodes">
144            <Switch id="continue-toggle" {...register('continue')} />
145          </Field>
146          <Field label="Override grouping">
147            <Switch
148              id="override-grouping-toggle"
149              value={overrideGrouping}
150              onChange={() => setOverrideGrouping((overrideGrouping) => !overrideGrouping)}
151            />
152          </Field>
153          {overrideGrouping && (
154            <Field label="Group by" description="Group alerts when you receive a notification based on labels.">
155              <InputControl
156                render={({ field: { onChange, ref, ...field } }) => (
157                  <MultiSelect
158                    aria-label="Group by"
159                    menuShouldPortal
160                    {...field}
161                    allowCustomValue
162                    className={formStyles.input}
163                    onCreateOption={(opt: string) => {
164                      setGroupByOptions((opts) => [...opts, stringToSelectableValue(opt)]);
165
166                      // @ts-ignore-check: react-hook-form made me do this
167                      setValue('groupBy', [...field.value, opt]);
168                    }}
169                    onChange={(value) => onChange(mapMultiSelectValueToStrings(value))}
170                    options={groupByOptions}
171                  />
172                )}
173                control={control}
174                name="groupBy"
175              />
176            </Field>
177          )}
178          <Field label="Override general timings">
179            <Switch
180              id="override-timings-toggle"
181              value={overrideTimings}
182              onChange={() => setOverrideTimings((overrideTimings) => !overrideTimings)}
183            />
184          </Field>
185          {overrideTimings && (
186            <>
187              <Field
188                label="Group wait"
189                description="The waiting time until the initial notification is sent for a new group created by an incoming alert."
190                invalid={!!errors.groupWaitValue}
191                error={errors.groupWaitValue?.message}
192              >
193                <>
194                  <div className={cx(formStyles.container, formStyles.timingContainer)}>
195                    <InputControl
196                      render={({ field, fieldState: { invalid } }) => (
197                        <Input
198                          {...field}
199                          className={formStyles.smallInput}
200                          invalid={invalid}
201                          placeholder="Time"
202                          aria-label="Group wait value"
203                        />
204                      )}
205                      control={control}
206                      name="groupWaitValue"
207                      rules={{
208                        validate: optionalPositiveInteger,
209                      }}
210                    />
211                    <InputControl
212                      render={({ field: { onChange, ref, ...field } }) => (
213                        <Select
214                          menuShouldPortal
215                          {...field}
216                          className={formStyles.input}
217                          onChange={(value) => onChange(mapSelectValueToString(value))}
218                          options={timeOptions}
219                          aria-label="Group wait type"
220                        />
221                      )}
222                      control={control}
223                      name="groupWaitValueType"
224                    />
225                  </div>
226                </>
227              </Field>
228              <Field
229                label="Group interval"
230                description="The waiting time to send a batch of new alerts for that group after the first notification was sent."
231                invalid={!!errors.groupIntervalValue}
232                error={errors.groupIntervalValue?.message}
233              >
234                <>
235                  <div className={cx(formStyles.container, formStyles.timingContainer)}>
236                    <InputControl
237                      render={({ field, fieldState: { invalid } }) => (
238                        <Input
239                          {...field}
240                          className={formStyles.smallInput}
241                          invalid={invalid}
242                          placeholder="Time"
243                          aria-label="Group interval value"
244                        />
245                      )}
246                      control={control}
247                      name="groupIntervalValue"
248                      rules={{
249                        validate: optionalPositiveInteger,
250                      }}
251                    />
252                    <InputControl
253                      render={({ field: { onChange, ref, ...field } }) => (
254                        <Select
255                          menuShouldPortal
256                          {...field}
257                          className={formStyles.input}
258                          onChange={(value) => onChange(mapSelectValueToString(value))}
259                          options={timeOptions}
260                          aria-label="Group interval type"
261                        />
262                      )}
263                      control={control}
264                      name="groupIntervalValueType"
265                    />
266                  </div>
267                </>
268              </Field>
269              <Field
270                label="Repeat interval"
271                description="The waiting time to resend an alert after they have successfully been sent."
272                invalid={!!errors.repeatIntervalValue}
273                error={errors.repeatIntervalValue?.message}
274              >
275                <>
276                  <div className={cx(formStyles.container, formStyles.timingContainer)}>
277                    <InputControl
278                      render={({ field, fieldState: { invalid } }) => (
279                        <Input
280                          {...field}
281                          className={formStyles.smallInput}
282                          invalid={invalid}
283                          placeholder="Time"
284                          aria-label="Repeat interval value"
285                        />
286                      )}
287                      control={control}
288                      name="repeatIntervalValue"
289                      rules={{
290                        validate: optionalPositiveInteger,
291                      }}
292                    />
293                    <InputControl
294                      render={({ field: { onChange, ref, ...field } }) => (
295                        <Select
296                          menuShouldPortal
297                          {...field}
298                          className={formStyles.input}
299                          menuPlacement="top"
300                          onChange={(value) => onChange(mapSelectValueToString(value))}
301                          options={timeOptions}
302                          aria-label="Repeat interval type"
303                        />
304                      )}
305                      control={control}
306                      name="repeatIntervalValueType"
307                    />
308                  </div>
309                </>
310              </Field>
311            </>
312          )}
313          <div className={styles.buttonGroup}>
314            <Button type="submit">Save policy</Button>
315            <Button onClick={onCancel} fill="outline" type="button" variant="secondary">
316              Cancel
317            </Button>
318          </div>
319        </>
320      )}
321    </Form>
322  );
323};
324
325const getStyles = (theme: GrafanaTheme2) => {
326  const commonSpacing = theme.spacing(3.5);
327
328  return {
329    addMatcherBtn: css`
330      margin-bottom: ${commonSpacing};
331    `,
332    matchersContainer: css`
333      background-color: ${theme.colors.background.secondary};
334      margin: ${theme.spacing(1, 0)};
335      padding: ${theme.spacing(1, 4.6, 1, 1.5)};
336      width: fit-content;
337    `,
338    matchersOperator: css`
339      min-width: 140px;
340    `,
341    nestedPolicies: css`
342      margin-top: ${commonSpacing};
343    `,
344    removeButton: css`
345      margin-left: ${theme.spacing(1)};
346      margin-top: ${theme.spacing(2.5)};
347    `,
348    buttonGroup: css`
349      margin: ${theme.spacing(6)} 0 ${commonSpacing};
350
351      & > * + * {
352        margin-left: ${theme.spacing(1.5)};
353      }
354    `,
355  };
356};
357