Skip to content

Commit 89ab9b1

Browse files
authored
feat: Add traceroute (#245)
1 parent 7d3b4ca commit 89ab9b1

27 files changed

+1809
-90
lines changed

‎src/components/AlertRuleForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export const AlertRuleForm = ({ rule, onSubmit }: Props) => {
224224
<span className={styles.inlineText}>will fire an alert if less than </span>
225225
<Field
226226
invalid={Boolean(errors?.probePercentage)}
227-
error={errors?.probePercentage?.message}
227+
error={errors?.probePercentage?.message?.toString()}
228228
className={styles.noMargin}
229229
>
230230
<Input
@@ -238,7 +238,7 @@ export const AlertRuleForm = ({ rule, onSubmit }: Props) => {
238238
<span className={styles.inlineText}>% of probes report connection success for</span>
239239
<Field
240240
invalid={Boolean(errors?.timeCount)}
241-
error={errors?.timeCount?.message}
241+
error={errors?.timeCount?.message?.toString()}
242242
className={styles.noMargin}
243243
>
244244
<Input

‎src/components/CheckEditor/CheckEditor.test.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import { CheckEditor } from './CheckEditor';
1515
import { getInstanceMock } from '../../datasource/__mocks__/DataSource';
1616
import userEvent from '@testing-library/user-event';
1717
import { InstanceContext } from 'contexts/InstanceContext';
18-
import { AppPluginMeta, DataSourceSettings } from '@grafana/data';
18+
import { AppPluginMeta, DataSourceSettings, FeatureToggles } from '@grafana/data';
1919
import { DNS_RESPONSE_MATCH_OPTIONS } from 'components/constants';
20+
import { FeatureFlagProvider } from 'components/FeatureFlagProvider';
2021
jest.setTimeout(60000);
2122

2223
// Mock useAlerts hook
@@ -147,10 +148,14 @@ const renderCheckEditor = async ({ check = defaultCheck, withAlerting = true } =
147148
alertRuler: withAlerting ? ({} as DataSourceSettings) : undefined,
148149
};
149150
const meta = {} as AppPluginMeta<GlobalSettings>;
151+
const featureToggles = ({ traceroute: true } as unknown) as FeatureToggles;
152+
const isFeatureEnabled = jest.fn(() => true);
150153
render(
151-
<InstanceContext.Provider value={{ instance, loading: false, meta }}>
152-
<CheckEditor check={check} onReturn={onReturn} />
153-
</InstanceContext.Provider>
154+
<FeatureFlagProvider overrides={{ featureToggles, isFeatureEnabled }}>
155+
<InstanceContext.Provider value={{ instance, loading: false, meta }}>
156+
<CheckEditor check={check} onReturn={onReturn} />
157+
</InstanceContext.Provider>
158+
</FeatureFlagProvider>
154159
);
155160
await waitFor(() => expect(screen.getByText('Check Details')).toBeInTheDocument());
156161
return instance;

‎src/components/CheckEditor/CheckEditor.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useState, useMemo, useContext } from 'react';
22
import { css } from '@emotion/css';
33
import { Button, ConfirmModal, Field, Input, HorizontalGroup, Select, Legend, Alert, useStyles } from '@grafana/ui';
44
import { useAsyncCallback } from 'react-async-hook';
5-
import { Check, CheckType, OrgRole, CheckFormValues, SubmissionError } from 'types';
5+
import { Check, CheckType, OrgRole, CheckFormValues, SubmissionErrorWrapper, FeatureName } from 'types';
66
import { hasRole } from 'utils';
77
import { getDefaultValuesFromCheck, getCheckFromFormValues } from './checkFormTransformations';
88
import { validateJob, validateTarget } from 'validation';
@@ -17,6 +17,7 @@ import { GrafanaTheme } from '@grafana/data';
1717
import { CheckUsage } from '../CheckUsage';
1818
import { CheckFormAlert } from 'components/CheckFormAlert';
1919
import { InstanceContext } from 'contexts/InstanceContext';
20+
import { useFeatureFlag } from 'hooks/useFeatureFlag';
2021
import { trackEvent, trackException } from 'analytics';
2122

2223
interface Props {
@@ -52,6 +53,8 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
5253
const [showDeleteModal, setShowDeleteModal] = useState(false);
5354
const styles = useStyles(getStyles);
5455
const defaultValues = useMemo(() => getDefaultValuesFromCheck(check), [check]);
56+
const { isEnabled: tracerouteEnabled } = useFeatureFlag(FeatureName.Traceroute);
57+
5558
const formMethods = useForm<CheckFormValues>({ defaultValues, mode: 'onChange' });
5659
const selectedCheckType = formMethods.watch('checkType').value ?? CheckType.PING;
5760

@@ -73,7 +76,7 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
7376
onReturn(true);
7477
});
7578

76-
const submissionError = error as SubmissionError;
79+
const submissionError = (error as unknown) as SubmissionErrorWrapper;
7780
if (error) {
7881
trackException(`addNewCheckSubmitException: ${error}`);
7982
}
@@ -101,7 +104,11 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
101104
<Select
102105
{...field}
103106
placeholder="Check type"
104-
options={CHECK_TYPE_OPTIONS}
107+
options={
108+
tracerouteEnabled
109+
? CHECK_TYPE_OPTIONS
110+
: CHECK_TYPE_OPTIONS.filter(({ value }) => value !== CheckType.Traceroute)
111+
}
105112
width={30}
106113
disabled={check?.id ? true : false}
107114
/>
@@ -138,7 +145,12 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
138145
control={formMethods.control}
139146
rules={{
140147
required: true,
141-
validate: (target) => validateTarget(selectedCheckType, target),
148+
validate: (target) => {
149+
// We have to get refetch the check type value from form state in the validation because the value will be stale if we rely on the the .watch method in the render
150+
const targetFormValue = formMethods.getValues().checkType;
151+
const selectedCheckType = targetFormValue.value as CheckType;
152+
return validateTarget(selectedCheckType, target);
153+
},
142154
}}
143155
render={({ field }) => (
144156
<CheckTarget
@@ -189,7 +201,7 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
189201
{submissionError && (
190202
<div className={styles.submissionError}>
191203
<Alert title="Save failed" severity="error">
192-
{`${submissionError.status}: ${submissionError.message ?? submissionError.msg ?? 'Something went wrong'}`}
204+
{`${submissionError.status}: ${submissionError.data?.msg ?? 'Something went wrong'}`}
193205
</Alert>
194206
</div>
195207
)}

‎src/components/CheckEditor/CheckSettings.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PingSettingsForm } from 'components/PingSettings';
44
import { HttpSettingsForm } from 'components/http/HttpSettings';
55
import DnsSettingsForm from 'components/DnsSettings';
66
import { TcpSettingsForm } from 'components/TcpSettings';
7+
import { TracerouteSettingsForm } from 'components/TracerouteSettingsForm';
78

89
interface Props {
910
isEditor: boolean;
@@ -24,5 +25,8 @@ export const CheckSettings: FC<Props> = ({ isEditor, typeOfCheck }) => {
2425
case CheckType.TCP: {
2526
return <TcpSettingsForm isEditor={isEditor} />;
2627
}
28+
case CheckType.Traceroute: {
29+
return <TracerouteSettingsForm isEditor={isEditor} />;
30+
}
2731
}
2832
};

‎src/components/CheckEditor/ProbeOptions.tsx

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React, { useState, useEffect, useContext } from 'react';
2-
import { Field } from '@grafana/ui';
2+
import { Field, Input } from '@grafana/ui';
33
import CheckProbes from './CheckProbes';
44
import { InstanceContext } from 'contexts/InstanceContext';
5-
import { Probe } from 'types';
5+
import { Probe, CheckType } from 'types';
66
import { SliderInput } from 'components/SliderInput';
77
import { Subheader } from 'components/Subheader';
88
import { useFormContext, Controller } from 'react-hook-form';
@@ -19,10 +19,13 @@ export const ProbeOptions = ({ frequency, timeout, isEditor, probes }: Props) =>
1919
const [availableProbes, setAvailableProbes] = useState<Probe[]>([]);
2020
const {
2121
control,
22+
watch,
2223
formState: { errors },
2324
} = useFormContext();
2425
const { instance } = useContext(InstanceContext);
2526

27+
const checkType = watch('checkType').value;
28+
2629
useEffect(() => {
2730
const abortController = new AbortController();
2831
const fetchProbes = async () => {
@@ -58,37 +61,47 @@ export const ProbeOptions = ({ frequency, timeout, isEditor, probes }: Props) =>
5861
<Field
5962
label="Frequency"
6063
description="How frequently the check should run."
61-
disabled={!isEditor}
64+
disabled={!isEditor || checkType === CheckType.Traceroute}
6265
invalid={Boolean(errors.frequency)}
6366
error={errors.frequency?.message}
6467
>
65-
<SliderInput
66-
validate={validateFrequency}
67-
name="frequency"
68-
prefixLabel={'Every'}
69-
suffixLabel={'seconds'}
70-
min={10.0}
71-
max={120.0}
72-
defaultValue={frequency / 1000}
73-
/>
68+
{checkType === CheckType.Traceroute ? (
69+
// This is just a placeholder for now, the frequency for traceroute checks is hardcoded in the submit
70+
<Input value={120} prefix="Every" suffix="seconds" width={20} />
71+
) : (
72+
<SliderInput
73+
validate={(value) => validateFrequency(value, checkType)}
74+
name="frequency"
75+
prefixLabel={'Every'}
76+
suffixLabel={'seconds'}
77+
min={checkType === CheckType.Traceroute ? 60.0 : 10.0}
78+
max={checkType === CheckType.Traceroute ? 240.0 : 120.0}
79+
defaultValue={checkType === CheckType.Traceroute ? 120 : frequency / 1000}
80+
/>
81+
)}
7482
</Field>
7583
<Field
7684
label="Timeout"
7785
description="Maximum execution time for a check"
78-
disabled={!isEditor}
86+
disabled={!isEditor || checkType === CheckType.Traceroute}
7987
invalid={Boolean(errors.timeout)}
8088
error={errors.timeout?.message}
8189
>
82-
<SliderInput
83-
name="timeout"
84-
validate={validateTimeout}
85-
defaultValue={timeout / 1000}
86-
max={10.0}
87-
min={1.0}
88-
step={0.5}
89-
suffixLabel="seconds"
90-
prefixLabel="After"
91-
/>
90+
{checkType === CheckType.Traceroute ? (
91+
// This is just a placeholder for now, the timeout for traceroute checks is hardcoded in the submit
92+
<Input value={30} prefix="Every" suffix="seconds" width={20} />
93+
) : (
94+
<SliderInput
95+
name="timeout"
96+
validate={(value) => validateTimeout(value, checkType)}
97+
defaultValue={checkType === CheckType.Traceroute ? 30 : timeout / 1000}
98+
max={checkType === CheckType.Traceroute ? 30.0 : 10.0}
99+
min={1.0}
100+
step={0.5}
101+
suffixLabel="seconds"
102+
prefixLabel="After"
103+
/>
104+
)}
92105
</Field>
93106
</div>
94107
);

‎src/components/CheckEditor/checkFormTransformations.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
AlertSensitivity,
2525
TCPQueryResponse,
2626
TLSConfig,
27+
TracerouteSettings,
28+
TracerouteSettingsFormValues,
2729
} from 'types';
2830

2931
import {
@@ -243,6 +245,17 @@ const getDnsSettingsFormValues = (settings: Settings): DnsSettingsFormValues =>
243245
};
244246
};
245247

248+
const getTracerouteSettingsFormValues = (settings: Settings): TracerouteSettingsFormValues => {
249+
const tracerouteSettings = settings.traceroute ?? (fallbackSettings(CheckType.Traceroute) as TracerouteSettings);
250+
251+
return {
252+
firstHop: String(tracerouteSettings.firstHop ?? 1),
253+
maxHops: String(tracerouteSettings.maxHops),
254+
retries: String(tracerouteSettings.retries ?? 0),
255+
maxUnknownHops: String(tracerouteSettings.maxUnknownHops),
256+
};
257+
};
258+
246259
const getFormSettingsForCheck = (settings: Settings): SettingsFormValues => {
247260
const type = checkType(settings);
248261
switch (type) {
@@ -252,6 +265,8 @@ const getFormSettingsForCheck = (settings: Settings): SettingsFormValues => {
252265
return { tcp: getTcpSettingsFormValues(settings) };
253266
case CheckType.DNS:
254267
return { dns: getDnsSettingsFormValues(settings) };
268+
case CheckType.Traceroute:
269+
return { traceroute: getTracerouteSettingsFormValues(settings) };
255270
case CheckType.PING:
256271
default:
257272
return { ping: getPingSettingsFormValues(settings) };
@@ -264,6 +279,7 @@ const getAllFormSettingsForCheck = (): SettingsFormValues => {
264279
tcp: getTcpSettingsFormValues(fallbackSettings(CheckType.TCP)),
265280
dns: getDnsSettingsFormValues(fallbackSettings(CheckType.DNS)),
266281
ping: getPingSettingsFormValues(fallbackSettings(CheckType.PING)),
282+
traceroute: getTracerouteSettingsFormValues(fallbackSettings(CheckType.Traceroute)),
267283
};
268284
};
269285

@@ -530,6 +546,20 @@ const getDnsSettings = (
530546
};
531547
};
532548

549+
const getTracerouteSettings = (
550+
settings: TracerouteSettingsFormValues | undefined,
551+
defaultSettings: TracerouteSettingsFormValues | undefined
552+
): TracerouteSettings => {
553+
const fallbackValues = fallbackSettings(CheckType.Traceroute).traceroute as TracerouteSettings;
554+
const updatedSettings = settings ?? defaultSettings ?? fallbackValues;
555+
return {
556+
firstHop: parseInt(String(updatedSettings.firstHop), 10),
557+
maxHops: parseInt(String(updatedSettings.maxHops), 10),
558+
retries: 0,
559+
maxUnknownHops: parseInt(String(updatedSettings.maxUnknownHops), 10),
560+
};
561+
};
562+
533563
const getSettingsFromFormValues = (formValues: Partial<CheckFormValues>, defaultValues: CheckFormValues): Settings => {
534564
const checkType = getValueFromSelectable(formValues.checkType ?? defaultValues.checkType);
535565
switch (checkType) {
@@ -541,11 +571,31 @@ const getSettingsFromFormValues = (formValues: Partial<CheckFormValues>, default
541571
return { dns: getDnsSettings(formValues.settings?.dns, defaultValues.settings.dns) };
542572
case CheckType.PING:
543573
return { ping: getPingSettings(formValues.settings?.ping, defaultValues.settings.ping) };
574+
case CheckType.Traceroute:
575+
return {
576+
traceroute: {
577+
...getTracerouteSettings(formValues.settings?.traceroute, defaultValues.settings.traceroute),
578+
},
579+
};
544580
default:
545581
throw new Error(`Check type of ${checkType} is invalid`);
546582
}
547583
};
548584

585+
const getTimeoutFromFormValue = (timeout: number, checkType?: CheckType): number => {
586+
if (checkType === CheckType.Traceroute) {
587+
return 30000;
588+
}
589+
return timeout * 1000;
590+
};
591+
592+
const getFrequencyFromFormValue = (frequency: number, checkType?: CheckType): number => {
593+
if (checkType === CheckType.Traceroute) {
594+
return 120000;
595+
}
596+
return frequency * 1000;
597+
};
598+
549599
export const getCheckFromFormValues = (
550600
formValues: Omit<CheckFormValues, 'alert'>,
551601
defaultValues: CheckFormValues
@@ -556,8 +606,8 @@ export const getCheckFromFormValues = (
556606
enabled: formValues.enabled,
557607
labels: formValues.labels ?? [],
558608
probes: formValues.probes,
559-
timeout: formValues.timeout * 1000,
560-
frequency: formValues.frequency * 1000,
609+
timeout: getTimeoutFromFormValue(formValues.timeout, getValueFromSelectable(formValues.checkType)),
610+
frequency: getFrequencyFromFormValue(formValues.frequency, getValueFromSelectable(formValues.checkType)),
561611
alertSensitivity: getValueFromSelectable(formValues.alertSensitivity) ?? AlertSensitivity.None,
562612
settings: getSettingsFromFormValues(formValues, defaultValues),
563613
basicMetricsOnly: !formValues.publishAdvancedMetrics,

‎src/components/CheckTarget.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ const getTargetHelpText = (typeOfCheck: CheckType | undefined): TargetHelpInfo =
5454
};
5555
break;
5656
}
57+
case CheckType.Traceroute: {
58+
resp = {
59+
text: 'Hostname to send traceroute',
60+
example: 'grafana.com',
61+
};
62+
break;
63+
}
5764
}
5865
return resp;
5966
};

0 commit comments

Comments
 (0)