Skip to content

Commit b2e1be4

Browse files
VikaCepckbedwell
andauthored
feat: improve new alerts UI (#1064)
* feat: add descriptions for new alerts * fix: add legacy legend to old alerts * feat: display alert query link in tooltip * fix: only show query on existing checks. Use input threshold * fix: display alert sections using Tabs * fix: change way we display ProbeFailedExecutions alert * feat: allow to choose pending period * feat: filter out pending periods that don't make sense for the check Also, moving the threshold input to a separate component * fix: implement threshold unit selector and adjust validation * refactor: allow to choose number of executions instead of percentage * fix: only show ProbeFailedExecutionsTooHigh and HTTPTargetCertificateCloseToExpiring alerts * feat: add alerting period as a form prop - Use it to calculate the approx. number for test executions along with frequency - Send it in the POST request * fix: adjust tooltip text * fix: updates based on feedback - Adjust allowed periods defined in sync notes doc - Text updates * fix: move tls certificate alert to new component * fix: tests * fix: remove unneeded code - As backend support has been removed - See grafana/synthetic-monitoring#214 (comment) * fix: change naming for alerting tabs * fix: tests * fix: remove heading in legacy alerts for consistency * fix: set default period value when not set * fix: test updates * fix: use pluralize for alert text and change default * fix: alert text when no probe is selected * fix: move FailedExecutionsAlert description to constants file * fix: improve alerts validation * fix: remove unneeded alert type references * fix: add validation on threshold not being higher than total checks * fix: improve error messages visualization * feat: show alert evaluation info * fix: clear validation when unselecting an alert * fix: only show the message if all props are set * fix: change query from to be now-3h and remove unneeded params * fix: make schemas source of truth for alerts. improve error validation * fix: change alerts endpoint to use PUT * fix: improve alert evaluation message and frequency display * fix: tweak text * fix: added revalidate hook, fixed error message display, added color to 'of y' * fix: remove 1min and 2min periods 1m and 2m periods are no longer supported * fix: change alerts endpoint method to PUT in mocks * fix: update ProbeFailedExecutionsTooHigh query * fix: change ProbeFailedExecutionsTooHigh query to compare against user-set threshold --------- Co-authored-by: Chris Bedwell <christopher.bedwell@grafana.com>
1 parent 8f82b93 commit b2e1be4

32 files changed

+772
-341
lines changed

‎package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"@hookform/resolvers": "3.3.4",
110110
"@tanstack/react-query": "5.8.4",
111111
"@tanstack/react-query-devtools": "5.8.4",
112+
"@types/pluralize": "^0.0.33",
112113
"acorn": "8.12.1",
113114
"acorn-walk": "8.3.3",
114115
"constrained-editor-plugin": "^1.3.0",
@@ -118,6 +119,7 @@
118119
"js-base64": "^3.7.7",
119120
"lodash": "4.17.21",
120121
"ol": "8.2.0",
122+
"pluralize": "^8.0.0",
121123
"prismjs": "1.29.0",
122124
"punycode": "^2.1.1",
123125
"rc-slider": "10.5.0",

‎src/checkUsageCalc.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export const getTotalChecksPerMonth = (probeCount: number, frequencySeconds: num
2626
return checksPerMonth * probeCount;
2727
};
2828

29+
export const getTotalChecksPerPeriod = (probeCount: number, frequencySeconds: number, periodInSeconds: number) => {
30+
const checksPerMinute = 60 / frequencySeconds;
31+
const checksPerPeriod = checksPerMinute * (periodInSeconds / 60) * probeCount;
32+
return Math.round(checksPerPeriod);
33+
};
34+
2935
const getLogsGbPerMonth = (probeCount: number, frequencySeconds: number) => {
3036
const gbPerCheck = 0.0008;
3137
const checksPerMonth = getChecksPerMonth(frequencySeconds);

‎src/components/CheckEditor/ProbeOptions.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Field } from '@grafana/ui';
44

55
import { CheckFormValues, CheckType, ProbeWithMetadata } from 'types';
66
import { useProbesWithMetadata } from 'data/useProbes';
7+
import { useRevalidateForm } from 'hooks/useRevalidateForm';
78
import { SliderInput } from 'components/SliderInput';
89

910
import { CheckProbes } from './CheckProbes/CheckProbes';
@@ -21,6 +22,12 @@ export const ProbeOptions = ({ checkType, disabled }: ProbeOptionsProps) => {
2122
} = useFormContext<CheckFormValues>();
2223
const { minFrequency, maxFrequency } = getFrequencyBounds(checkType);
2324

25+
const revalidateForm = useRevalidateForm();
26+
27+
const handleChangingFrequency = () => {
28+
revalidateForm<CheckFormValues>(`alerts`);
29+
};
30+
2431
return (
2532
<div>
2633
<Controller
@@ -47,7 +54,13 @@ export const ProbeOptions = ({ checkType, disabled }: ProbeOptionsProps) => {
4754
invalid={Boolean(errors.frequency)}
4855
error={errors.frequency?.message}
4956
>
50-
<SliderInput disabled={disabled} name="frequency" min={minFrequency} max={maxFrequency} />
57+
<SliderInput
58+
disabled={disabled}
59+
name="frequency"
60+
min={minFrequency}
61+
max={maxFrequency}
62+
onChange={handleChangingFrequency}
63+
/>
5164
</Field>
5265
</div>
5366
);

‎src/components/CheckEditor/transformations/toFormValues.ping.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import { CheckFormValuesPing, CheckType, PingCheck, PingSettingsFormValues } from 'types';
2-
import {
3-
getBaseFormValuesFromCheck,
4-
predefinedAlertsToFormValues,
5-
} from 'components/CheckEditor/transformations/toFormValues.utils';
6-
import { PING_PREDEFINED_ALERTS } from 'components/CheckForm/AlertsPerCheck/AlertsPerCheck.constants';
2+
import { getBaseFormValuesFromCheck } from 'components/CheckEditor/transformations/toFormValues.utils';
73
import { FALLBACK_CHECK_PING } from 'components/constants';
84

95
export function getPingCheckFormValues(check: PingCheck): CheckFormValuesPing {
@@ -17,7 +13,6 @@ export function getPingCheckFormValues(check: PingCheck): CheckFormValuesPing {
1713
},
1814
alerts: {
1915
...base.alerts,
20-
...predefinedAlertsToFormValues(PING_PREDEFINED_ALERTS),
2116
},
2217
};
2318
}

‎src/components/CheckEditor/transformations/toFormValues.utils.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,7 @@ export function predefinedAlertsToFormValues(predefinedAlerts: PredefinedAlertIn
6161
return Object.values(predefinedAlerts).reduce((acc, alert) => {
6262
return {
6363
...acc,
64-
[alert.type]: {
65-
threshold: alert.default,
66-
isSelected: false,
67-
},
64+
[alert.type]: alert.defaultValues,
6865
};
6966
}, {});
7067
}

‎src/components/CheckEditor/transformations/toPayload.alerts.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export function getAlertsPayload(formValues?: CheckAlertFormRecord, checkId?: nu
99
if (alert.isSelected) {
1010
alerts.push({
1111
name: alertType as CheckAlertType,
12-
threshold: alert.threshold!!,
12+
threshold: alert.threshold!,
13+
period: alert.period ? alert.period : undefined,
1314
});
1415
}
1516
return alerts;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
import { GrafanaTheme2 } from '@grafana/data';
3+
import { PopoverContent, Text, Tooltip, useStyles2 } from '@grafana/ui';
4+
import { css } from '@emotion/css';
5+
import pluralize from 'pluralize';
6+
7+
import { secondsToDuration } from 'utils';
8+
9+
interface TooltipWrapperProps {
10+
content: PopoverContent;
11+
}
12+
13+
const TooltipWrapper: React.FC<React.PropsWithChildren<TooltipWrapperProps>> = ({ content, children }) => {
14+
const styles = useStyles2(getStyles);
15+
16+
return (
17+
<Tooltip content={content} interactive={true}>
18+
<strong className={styles.tooltipText}>{children}</strong>
19+
</Tooltip>
20+
);
21+
};
22+
23+
interface AlertEvaluationInfoProps {
24+
testExecutionsPerPeriod: number;
25+
checkFrequency: number;
26+
probesNumber: number;
27+
period: string;
28+
}
29+
30+
export const AlertEvaluationInfo: React.FC<AlertEvaluationInfoProps> = ({
31+
testExecutionsPerPeriod,
32+
checkFrequency,
33+
probesNumber,
34+
period,
35+
}) => {
36+
const frequency = secondsToDuration(checkFrequency);
37+
const tooltipData = [
38+
{
39+
label: `frequency`,
40+
content: frequency,
41+
},
42+
{
43+
label: 'probes',
44+
content: `${probesNumber} ${pluralize('probe', probesNumber)} selected`,
45+
},
46+
{
47+
label: 'period',
48+
content: period,
49+
},
50+
];
51+
52+
return (
53+
<Text variant="bodySmall" color="warning">
54+
{`This alert will evaluate against a total of [${testExecutionsPerPeriod}] ${pluralize(
55+
'execution',
56+
testExecutionsPerPeriod
57+
)} based on your current settings: `}
58+
{tooltipData.map((tooltip, index) => (
59+
<React.Fragment key={tooltip.label}>
60+
<TooltipWrapper content={tooltip.content}>{tooltip.label}</TooltipWrapper>
61+
{index < tooltipData.length - 2 && `, `}
62+
{index === tooltipData.length - 2 && `, and `}
63+
</React.Fragment>
64+
))}
65+
{`.`}
66+
</Text>
67+
);
68+
};
69+
70+
const getStyles = (theme: GrafanaTheme2) => ({
71+
tooltipText: css({
72+
textDecoration: 'underline',
73+
}),
74+
});
Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import React from 'react';
2-
import { Controller, useFormContext } from 'react-hook-form';
3-
import { GrafanaTheme2 } from '@grafana/data';
4-
import { Checkbox, Field, Input, Label, useStyles2 } from '@grafana/ui';
2+
import { useFormContext } from 'react-hook-form';
3+
import { GrafanaTheme2, urlUtil } from '@grafana/data';
4+
import { TextLink, useStyles2 } from '@grafana/ui';
55
import { css } from '@emotion/css';
66

77
import { CheckAlertType, CheckFormValues } from 'types';
8+
import { useMetricsDS } from 'hooks/useMetricsDS';
89

9-
import { useCheckFormContext } from '../CheckFormContext/CheckFormContext';
1010
import { PredefinedAlertInterface } from './AlertsPerCheck.constants';
11+
import { FailedExecutionsAlert } from './FailedExecutionsAlert';
12+
import { HTTPTargetCertificateCloseToExpiringAlert } from './HTTPTargetCertificateCloseToExpiringAlert';
13+
14+
function createExploreLink(dataSourceName: string, query: string) {
15+
return urlUtil.renderUrl(`/explore`, {
16+
left: JSON.stringify(['now-3h', 'now', dataSourceName, { datasource: dataSourceName, expr: query }]),
17+
});
18+
}
1119

1220
export const AlertItem = ({
1321
alert,
@@ -18,83 +26,84 @@ export const AlertItem = ({
1826
selected: boolean;
1927
onSelectionChange: (type: CheckAlertType) => void;
2028
}) => {
21-
const styles = useStyles2(getStyles);
29+
const styles = useStyles2(getAlertItemStyles);
2230

23-
const { control, formState } = useFormContext<CheckFormValues>();
24-
const { isFormDisabled } = useCheckFormContext();
31+
const { getValues } = useFormContext<CheckFormValues>();
2532

2633
const handleToggleAlert = (type: CheckAlertType) => {
2734
onSelectionChange(type);
2835
};
2936

30-
const thresholdError = formState.errors?.alerts?.[alert.type]?.threshold?.message;
37+
const ds = useMetricsDS();
38+
39+
const job = getValues('job');
40+
const instance = getValues('target');
41+
const threshold = getValues(`alerts.${alert.type}.threshold`);
42+
const period = getValues(`alerts.${alert.type}.period`);
43+
44+
const query = alert.query
45+
.replace(/\$instance/g, instance)
46+
.replace(/\$job/g, job)
47+
.replace(/\$threshold/g, threshold)
48+
.replace(/\$period/g, period);
49+
50+
const exploreLink = ds && getValues('id') && threshold && createExploreLink(ds.name, query);
51+
const tooltipContent = (
52+
<div>
53+
{alert.description.replace(/\$threshold/g, threshold)}{' '}
54+
{exploreLink && (
55+
<div>
56+
<TextLink href={exploreLink} external={true} variant="bodySmall">
57+
Explore query
58+
</TextLink>
59+
</div>
60+
)}
61+
</div>
62+
);
3163

3264
return (
3365
<div key={alert.type} className={styles.item}>
34-
<div className={styles.itemInfo}>
35-
<Checkbox id={`alert-${alert.type}`} onClick={() => handleToggleAlert(alert.type)} checked={selected} />
36-
<Label htmlFor={`alert-${alert.type}`} className={styles.columnLabel}>
37-
{alert.name}
38-
</Label>
39-
</div>
40-
<div className={styles.thresholdInput}>
41-
<Field
42-
label="Threshold"
43-
htmlFor={`alert-threshold-${alert.type}`}
44-
invalid={!!thresholdError}
45-
error={thresholdError}
46-
>
47-
<Controller
48-
name={`alerts.${alert.type}.threshold`}
49-
control={control}
50-
render={({ field }) => {
51-
return (
52-
<Input
53-
{...field}
54-
aria-disabled={!selected}
55-
suffix={alert.unit}
56-
type="number"
57-
step="any"
58-
id={`alert-threshold-${alert.type}`}
59-
onChange={(e) => {
60-
const value = e.currentTarget.value;
61-
return field.onChange(value !== '' ? Number(value) : '');
62-
}}
63-
width={10}
64-
disabled={!selected || isFormDisabled}
65-
/>
66-
);
67-
}}
68-
/>
69-
</Field>
70-
</div>
66+
{alert.type === CheckAlertType.ProbeFailedExecutionsTooHigh && (
67+
<FailedExecutionsAlert
68+
alert={alert}
69+
selected={selected}
70+
onSelectionChange={handleToggleAlert}
71+
tooltipContent={tooltipContent}
72+
/>
73+
)}
74+
75+
{alert.type === CheckAlertType.HTTPTargetCertificateCloseToExpiring && (
76+
<HTTPTargetCertificateCloseToExpiringAlert
77+
alert={alert}
78+
selected={selected}
79+
onSelectionChange={handleToggleAlert}
80+
tooltipContent={tooltipContent}
81+
/>
82+
)}
7183
</div>
7284
);
7385
};
7486

75-
const getStyles = (theme: GrafanaTheme2) => ({
87+
export const getAlertItemStyles = (theme: GrafanaTheme2) => ({
7688
item: css({
7789
display: `flex`,
7890
gap: theme.spacing(1),
7991
marginLeft: theme.spacing(1),
92+
minHeight: '40px',
93+
paddingTop: theme.spacing(1),
8094
}),
8195

82-
itemInfo: css({
83-
display: 'flex',
84-
alignItems: 'center',
96+
alertRow: css({
8597
gap: theme.spacing(1),
86-
width: '50%',
87-
textWrap: 'wrap',
98+
alignItems: 'flex-start',
99+
'& > *': {
100+
marginTop: theme.spacing(0.5),
101+
},
88102
}),
89-
90-
columnLabel: css({
91-
fontWeight: theme.typography.fontWeightLight,
92-
fontSize: theme.typography.h6.fontSize,
93-
lineHeight: theme.typography.body.lineHeight,
94-
marginBottom: '0',
103+
alertCheckbox: css({
104+
marginTop: theme.spacing(0.75),
95105
}),
96-
97-
thresholdInput: css({
98-
marginLeft: '22px',
106+
alertTooltip: css({
107+
marginTop: theme.spacing(1),
99108
}),
100109
});

‎src/components/CheckForm/AlertsPerCheck/AlertsList.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
5858
list: css({
5959
display: 'flex',
6060
flexDirection: 'column',
61-
whiteSpace: 'nowrap',
6261
overflowY: 'auto',
6362
}),
6463

0 commit comments

Comments
 (0)