{fields.map((field, index) => {
- const localPath = `object_matchers[${index}]`;
return (
@@ -134,7 +131,7 @@ export const AmRoutesExpandedForm = ({
error={errors.object_matchers?.[index]?.value?.message}
>
@@ -225,38 +222,11 @@ export const AmRoutesExpandedForm = ({
invalid={!!errors.groupWaitValue}
error={errors.groupWaitValue?.message}
>
- <>
-
- (
-
- )}
- control={control}
- name="groupWaitValue"
- rules={{
- validate: optionalPositiveInteger,
- }}
- />
- (
-
- >
+
- <>
-
- (
-
- )}
- control={control}
- name="groupIntervalValue"
- rules={{
- validate: optionalPositiveInteger,
- }}
- />
- (
-
- >
+
- <>
-
- (
-
- )}
- control={control}
- name="repeatIntervalValue"
- rules={{
- validate: optionalPositiveInteger,
- }}
- />
- (
-
- >
+
>
)}
diff --git a/public/app/features/alerting/unified/components/notification-policies/Filters.tsx b/public/app/features/alerting/unified/components/notification-policies/Filters.tsx
index 7965cca4a81..33ee37d8042 100644
--- a/public/app/features/alerting/unified/components/notification-policies/Filters.tsx
+++ b/public/app/features/alerting/unified/components/notification-policies/Filters.tsx
@@ -89,6 +89,7 @@ const NotificationPoliciesFilter = ({
)}
@@ -417,12 +419,6 @@ const MuteTimings: FC<{ timings: string[]; alertManagerSourceName: string }> = (
);
};
-const TIMING_OPTIONS_DEFAULTS = {
- group_wait: '30s',
- group_interval: '5m',
- repeat_interval: '4h',
-};
-
const TimingOptionsMeta: FC<{ timingOptions: TimingOptions }> = ({ timingOptions }) => {
const groupWait = timingOptions.group_wait ?? TIMING_OPTIONS_DEFAULTS.group_wait;
const groupInterval = timingOptions.group_interval ?? TIMING_OPTIONS_DEFAULTS.group_interval;
diff --git a/public/app/features/alerting/unified/components/notification-policies/PromDurationDocs.tsx b/public/app/features/alerting/unified/components/notification-policies/PromDurationDocs.tsx
new file mode 100644
index 00000000000..ca3f0488fd3
--- /dev/null
+++ b/public/app/features/alerting/unified/components/notification-policies/PromDurationDocs.tsx
@@ -0,0 +1,68 @@
+import { css } from '@emotion/css';
+import React from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
+
+import { TimeOptions } from '../../types/time';
+
+export function PromDurationDocs() {
+ const styles = useStyles2(getPromDurationStyles);
+ return (
+
+ Prometheus duration format consist of a number followed by a time unit.
+
+ Different units can be combined for more granularity.
+
+
+
+
Symbol
+
Time unit
+
Example
+
+
+
+
+
+
+
+
Multiple units combined
+
1m30s, 2h30m20s, 1w2d
+
+
+
+ );
+}
+
+function PromDurationDocsTimeUnit({ unit, name, example }: { unit: TimeOptions; name: string; example: string }) {
+ const styles = useStyles2(getPromDurationStyles);
+
+ return (
+ <>
+
{unit}
+
{name}
+
{example}
+ >
+ );
+}
+
+const getPromDurationStyles = (theme: GrafanaTheme2) => ({
+ unit: css`
+ font-weight: ${theme.typography.fontWeightBold};
+ `,
+ list: css`
+ display: grid;
+ grid-template-columns: max-content 1fr 2fr;
+ gap: ${theme.spacing(1, 3)};
+ `,
+ header: css`
+ display: contents;
+ font-weight: ${theme.typography.fontWeightBold};
+ `,
+ examples: css`
+ display: contents;
+ & > div {
+ grid-column: 1 / span 2;
+ }
+ `,
+});
diff --git a/public/app/features/alerting/unified/components/notification-policies/PromDurationInput.tsx b/public/app/features/alerting/unified/components/notification-policies/PromDurationInput.tsx
new file mode 100644
index 00000000000..5c873ee6563
--- /dev/null
+++ b/public/app/features/alerting/unified/components/notification-policies/PromDurationInput.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import { Icon, Input } from '@grafana/ui';
+
+import { HoverCard } from '../HoverCard';
+
+import { PromDurationDocs } from './PromDurationDocs';
+
+export const PromDurationInput = React.forwardRef
>(
+ (props, ref) => {
+ return (
+ } disabled={false}>
+
+
+ }
+ {...props}
+ ref={ref}
+ />
+ );
+ }
+);
+
+PromDurationInput.displayName = 'PromDurationInput';
diff --git a/public/app/features/alerting/unified/components/notification-policies/formStyles.ts b/public/app/features/alerting/unified/components/notification-policies/formStyles.ts
index f61a525c080..7cb793af79d 100644
--- a/public/app/features/alerting/unified/components/notification-policies/formStyles.ts
+++ b/public/app/features/alerting/unified/components/notification-policies/formStyles.ts
@@ -16,11 +16,11 @@ export const getFormStyles = (theme: GrafanaTheme2) => {
input: css`
flex: 1;
`,
- timingContainer: css`
- max-width: ${theme.spacing(33)};
+ promDurationInput: css`
+ max-width: ${theme.spacing(32)};
`,
- smallInput: css`
- width: ${theme.spacing(6.5)};
+ timingFormContainer: css`
+ padding: ${theme.spacing(1)};
`,
linkText: css`
text-decoration: underline;
diff --git a/public/app/features/alerting/unified/components/notification-policies/timingOptions.ts b/public/app/features/alerting/unified/components/notification-policies/timingOptions.ts
new file mode 100644
index 00000000000..62376d582ee
--- /dev/null
+++ b/public/app/features/alerting/unified/components/notification-policies/timingOptions.ts
@@ -0,0 +1,11 @@
+export type TimingOptions = {
+ group_wait?: string;
+ group_interval?: string;
+ repeat_interval?: string;
+};
+
+export const TIMING_OPTIONS_DEFAULTS: Required = {
+ group_wait: '30s',
+ group_interval: '5m',
+ repeat_interval: '4h',
+};
diff --git a/public/app/features/alerting/unified/types/amroutes.ts b/public/app/features/alerting/unified/types/amroutes.ts
index 59c70e68983..f412a0e3c7f 100644
--- a/public/app/features/alerting/unified/types/amroutes.ts
+++ b/public/app/features/alerting/unified/types/amroutes.ts
@@ -9,11 +9,8 @@ export interface FormAmRoute {
groupBy: string[];
overrideTimings: boolean;
groupWaitValue: string;
- groupWaitValueType: string;
groupIntervalValue: string;
- groupIntervalValueType: string;
repeatIntervalValue: string;
- repeatIntervalValueType: string;
muteTimeIntervals: string[];
routes: FormAmRoute[];
}
diff --git a/public/app/features/alerting/unified/utils/amroutes.ts b/public/app/features/alerting/unified/utils/amroutes.ts
index 2019de7ed92..0c39d0d2139 100644
--- a/public/app/features/alerting/unified/utils/amroutes.ts
+++ b/public/app/features/alerting/unified/utils/amroutes.ts
@@ -1,5 +1,4 @@
import { uniqueId } from 'lodash';
-import { Validate } from 'react-hook-form';
import { SelectableValue } from '@grafana/data';
import { MatcherOperator, ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
@@ -10,9 +9,7 @@ import { MatcherFieldValue } from '../types/silence-form';
import { matcherToMatcherField, parseMatcher } from './alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
import { findExistingRoute } from './routeTree';
-import { parseInterval, timeOptions } from './time';
-
-const defaultValueAndType: [string, string] = ['', ''];
+import { isValidPrometheusDuration } from './time';
const matchersToArrayFieldMatchers = (
matchers: Record | undefined,
@@ -30,25 +27,6 @@ const matchersToArrayFieldMatchers = (
[] as MatcherFieldValue[]
);
-const intervalToValueAndType = (
- strValue: string | undefined,
- defaultValue?: typeof defaultValueAndType
-): [string, string] => {
- if (!strValue) {
- return defaultValue ?? defaultValueAndType;
- }
-
- const [value, valueType] = strValue ? parseInterval(strValue) : [undefined, undefined];
-
- const timeOption = timeOptions.find((opt) => opt.value === valueType);
-
- if (!value || !timeOption) {
- return defaultValueAndType;
- }
-
- return [String(value), timeOption.value];
-};
-
const selectableValueToString = (selectableValue: SelectableValue): string => selectableValue.value!;
const selectableValuesToStrings = (arr: Array> | undefined): string[] =>
@@ -80,11 +58,8 @@ export const emptyRoute: FormAmRoute = {
receiver: '',
overrideTimings: false,
groupWaitValue: '',
- groupWaitValueType: timeOptions[0].value,
groupIntervalValue: '',
- groupIntervalValueType: timeOptions[0].value,
repeatIntervalValue: '',
- repeatIntervalValueType: timeOptions[0].value,
muteTimeIntervals: [],
};
@@ -168,10 +143,6 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo
route.object_matchers?.map((matcher) => ({ name: matcher[0], operator: matcher[1], value: matcher[2] })) ?? [];
const matchers = route.matchers?.map((matcher) => matcherToMatcherField(parseMatcher(matcher))) ?? [];
- const [groupWaitValue, groupWaitValueType] = intervalToValueAndType(route.group_wait, ['', 's']);
- const [groupIntervalValue, groupIntervalValueType] = intervalToValueAndType(route.group_interval, ['', 'm']);
- const [repeatIntervalValue, repeatIntervalValueType] = intervalToValueAndType(route.repeat_interval, ['', 'h']);
-
return {
id,
// Frontend migration to use object_matchers instead of matchers, match, and match_re
@@ -185,13 +156,10 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo
receiver: route.receiver ?? '',
overrideGrouping: Array.isArray(route.group_by) && route.group_by.length !== 0,
groupBy: route.group_by ?? [],
- overrideTimings: [groupWaitValue, groupIntervalValue, repeatIntervalValue].some(Boolean),
- groupWaitValue,
- groupWaitValueType,
- groupIntervalValue,
- groupIntervalValueType,
- repeatIntervalValue,
- repeatIntervalValueType,
+ overrideTimings: [route.group_wait, route.group_interval, route.repeat_interval].some(Boolean),
+ groupWaitValue: route.group_wait ?? '',
+ groupIntervalValue: route.group_interval ?? '',
+ repeatIntervalValue: route.repeat_interval ?? '',
routes: formRoutes,
muteTimeIntervals: route.mute_time_intervals ?? [],
};
@@ -210,24 +178,21 @@ export const formAmRouteToAmRoute = (
groupBy,
overrideTimings,
groupWaitValue,
- groupWaitValueType,
groupIntervalValue,
- groupIntervalValueType,
repeatIntervalValue,
- repeatIntervalValueType,
receiver,
} = formAmRoute;
const group_by = overrideGrouping && groupBy ? groupBy : [];
const overrideGroupWait = overrideTimings && groupWaitValue;
- const group_wait = overrideGroupWait ? `${groupWaitValue}${groupWaitValueType}` : undefined;
+ const group_wait = overrideGroupWait ? groupWaitValue : undefined;
const overrideGroupInterval = overrideTimings && groupIntervalValue;
- const group_interval = overrideGroupInterval ? `${groupIntervalValue}${groupIntervalValueType}` : undefined;
+ const group_interval = overrideGroupInterval ? groupIntervalValue : undefined;
const overrideRepeatInterval = overrideTimings && repeatIntervalValue;
- const repeat_interval = overrideRepeatInterval ? `${repeatIntervalValue}${repeatIntervalValueType}` : undefined;
+ const repeat_interval = overrideRepeatInterval ? repeatIntervalValue : undefined;
const object_matchers = formAmRoute.object_matchers
?.filter((route) => route.name && route.value && route.operator)
.map(({ name, operator, value }) => [name, operator, value] as ObjectMatcher);
@@ -300,10 +265,10 @@ export const mapMultiSelectValueToStrings = (
return selectableValuesToStrings(selectableValues);
};
-export const optionalPositiveInteger: Validate = (value) => {
- if (!value) {
- return undefined;
+export function promDurationValidator(duration: string) {
+ if (duration.length === 0) {
+ return true;
}
- return !/^\d+$/.test(value) ? 'Must be a positive integer.' : undefined;
-};
+ return isValidPrometheusDuration(duration) || 'Invalid duration format. Must be {number}{time_unit}';
+}
diff --git a/public/app/features/alerting/unified/utils/time.ts b/public/app/features/alerting/unified/utils/time.ts
index 491c5b790ee..9a96fa1f4aa 100644
--- a/public/app/features/alerting/unified/utils/time.ts
+++ b/public/app/features/alerting/unified/utils/time.ts
@@ -1,4 +1,3 @@
-import { durationToMilliseconds, parseDuration } from '@grafana/data';
import { describeInterval } from '@grafana/data/src/datetime/rangeutil';
import { TimeOptions } from '../types/time';
@@ -28,10 +27,6 @@ export const timeOptions = Object.entries(TimeOptions).map(([key, value]) => ({
value: value,
}));
-export function parseDurationToMilliseconds(duration: string) {
- return durationToMilliseconds(parseDuration(duration));
-}
-
export function isValidPrometheusDuration(duration: string): boolean {
try {
parsePrometheusDuration(duration);