import { css, cx } from '@emotion/css'; import { concat, uniq, upperFirst, without } from 'lodash'; import { useEffect, useState } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, Field, FieldSet, Icon, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { MuteTimingFields } from '../../types/mute-timing-form'; import { DAYS_OF_THE_WEEK, MONTHS, defaultTimeInterval, validateArrayField } from '../../utils/mute-timings'; import { MuteTimingTimeRange } from './MuteTimingTimeRange'; import { TimezoneSelect } from './timezones'; export const MuteTimingTimeInterval = () => { const styles = useStyles2(getStyles); const { formState, register, setValue } = useFormContext(); const { fields: timeIntervals, append: addTimeInterval, remove: removeTimeInterval, } = useFieldArray({ name: 'time_intervals', }); const { isGrafanaAlertmanager } = useAlertmanager(); return (
<>

A time interval is a definition for a moment in time. All fields are lists, and at least one list element must be satisfied to match the field. If a field is left blank, any moment of time will match the field. For an instant of time to match a complete time interval, all fields must match. A mute timing can contain multiple time intervals.

{timeIntervals.map((timeInterval, timeIntervalIndex) => { const errors = formState.errors; // manually register the "location" field, react-hook-form doesn't handle nested field arrays well and will refuse to set // the default value for the field when using "useFieldArray" register(`time_intervals.${timeIntervalIndex}.location`); return (
} width={50} onChange={(selectedTimezone) => { setValue(`time_intervals.${timeIntervalIndex}.location`, selectedTimezone.value); }} // @ts-ignore react-hook-form doesn't handle nested field arrays well defaultValue={{ label: timeInterval.location, value: timeInterval.location }} data-testid="mute-timing-location" /> { setValue(`time_intervals.${timeIntervalIndex}.weekdays`, daysOfWeek); }} // @ts-ignore react-hook-form doesn't handle nested field arrays well defaultValue={timeInterval.weekdays} /> validateArrayField( value, (day) => { const parsedDay = parseInt(day, 10); return (parsedDay > -31 && parsedDay < 0) || (parsedDay > 0 && parsedDay < 32); }, 'Invalid day' ), })} width={50} // @ts-ignore react-hook-form doesn't handle nested field arrays well defaultValue={timeInterval.days_of_month} placeholder="Example: 1, 14:16, -1" data-testid="mute-timing-days" /> validateArrayField( value, (month) => MONTHS.includes(month) || (parseInt(month, 10) < 13 && parseInt(month, 10) > 0), 'Invalid month' ), })} width={50} placeholder="Example: 1:3, may:august, december" // @ts-ignore react-hook-form doesn't handle nested field arrays well defaultValue={timeInterval.months} data-testid="mute-timing-months" /> validateArrayField(value, (year) => /^\d{4}$/.test(year), 'Invalid year'), })} width={50} placeholder="Example: 2021:2022, 2030" // @ts-ignore react-hook-form doesn't handle nested field arrays well defaultValue={timeInterval.years} data-testid="mute-timing-years" /> {/* This switch is only available for Grafana Alertmanager, as for now, Grafana alert manager doesn't support this feature It hanldes empty list as undefined making impossible the use of an empty list for disabling time interval */} {!isGrafanaAlertmanager && ( )}
); })}
); }; interface DaysOfTheWeekProps { defaultValue?: string; onChange: (input: string) => void; } const parseDays = (input: string): string[] => { const parsedDays = input .split(',') .map((day) => day.trim()) // each "day" could still be a range of days, so we parse the range .flatMap((day) => (day.includes(':') ? parseWeekdayRange(day) : day)) .map((day) => day.toLowerCase()) // remove invalid weekdays .filter((day) => DAYS_OF_THE_WEEK.includes(day)); return uniq(parsedDays); }; // parse monday:wednesday to ["monday", "tuesday", "wednesday"] function parseWeekdayRange(input: string): string[] { const [start = '', end = ''] = input.split(':'); const startIndex = DAYS_OF_THE_WEEK.indexOf(start); const endIndex = DAYS_OF_THE_WEEK.indexOf(end); return DAYS_OF_THE_WEEK.slice(startIndex, endIndex + 1); } const DaysOfTheWeek = ({ defaultValue = '', onChange }: DaysOfTheWeekProps) => { const styles = useStyles2(getStyles); const defaultValues = parseDays(defaultValue); const [selectedDays, setSelectedDays] = useState(defaultValues); const toggleDay = (day: string) => { selectedDays.includes(day) ? setSelectedDays((selectedDays) => without(selectedDays, day)) : setSelectedDays((selectedDays) => concat(selectedDays, day)); }; useEffect(() => { onChange(selectedDays.join(', ')); }, [selectedDays, onChange]); return (
{DAYS_OF_THE_WEEK.map((day) => { const style = cx(styles.dayOfTheWeek, selectedDays.includes(day) && 'selected'); const abbreviated = day.slice(0, 3); return ( ); })}
); }; const getStyles = (theme: GrafanaTheme2) => ({ input: css({ width: '400px', }), timeIntervalSection: css({ backgroundColor: theme.colors.background.secondary, padding: theme.spacing(2), }), removeTimeIntervalButton: css({ marginTop: theme.spacing(2), }), dayOfTheWeek: css({ cursor: 'pointer', userSelect: 'none', padding: `${theme.spacing(1)} ${theme.spacing(3)}`, border: `solid 1px ${theme.colors.border.medium}`, background: 'none', borderRadius: theme.shape.radius.default, color: theme.colors.text.secondary, '&.selected': { fontWeight: theme.typography.fontWeightBold, color: theme.colors.primary.text, borderColor: theme.colors.primary.border, background: theme.colors.primary.transparent, }, }), });