Add custom monitor interval options
This commit is contained in:
1 parent
f41e0f0619
commit
4bb366f5ab
5 files changed
+80
-13
No files matched your search
@@ -5,9 +5,10 @@ import {
|
||||
type FieldValues,
|
||||
} from "react-hook-form";
|
||||
import { Autocomplete } from "@/Components/inputs";
|
||||
import type { AutocompleteProps } from "@mui/material/Autocomplete";
|
||||
|
||||
export interface AutocompleteOption {
|
||||
id: string;
|
||||
id: string | number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -16,6 +17,8 @@ interface FormAutocompleteProps<T extends FieldValues, O extends AutocompleteOpt
|
||||
options: O[];
|
||||
fieldLabel?: string;
|
||||
placeholder?: string;
|
||||
filterOptions?: AutocompleteProps<O, false, false, false>["filterOptions"];
|
||||
getOptionDisabled?: AutocompleteProps<O, false, false, false>["getOptionDisabled"];
|
||||
}
|
||||
|
||||
export const FormAutocompleteField = <
|
||||
@@ -26,6 +29,8 @@ export const FormAutocompleteField = <
|
||||
options,
|
||||
fieldLabel,
|
||||
placeholder,
|
||||
filterOptions,
|
||||
getOptionDisabled,
|
||||
}: FormAutocompleteProps<T, O>) => {
|
||||
const { control } = useFormContext<T>();
|
||||
return (
|
||||
@@ -43,6 +48,8 @@ export const FormAutocompleteField = <
|
||||
}}
|
||||
fieldLabel={fieldLabel}
|
||||
placeholder={placeholder}
|
||||
filterOptions={filterOptions}
|
||||
getOptionDisabled={getOptionDisabled}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
PageSpeedStrategies,
|
||||
DnsRecordTypes,
|
||||
MonitorIntervalOptions,
|
||||
MIN_MONITOR_INTERVAL_MS,
|
||||
DefaultMonitorMatchMethod,
|
||||
MonitorMatchMethods,
|
||||
GeoCheckIntervalOptions,
|
||||
@@ -58,9 +59,14 @@ import { FormTextField } from "@/Components/inputs/forms/FormTextField";
|
||||
import { FormRadioGroup } from "@/Components/inputs/forms/FormRadioGroupField";
|
||||
import { FormMultiSelectField } from "@/Components/inputs/forms/FormMultiSelectField";
|
||||
import { FormSelectField } from "@/Components/inputs/forms/FormSelectField";
|
||||
import {
|
||||
FormAutocompleteField,
|
||||
type AutocompleteOption,
|
||||
} from "@/Components/inputs/forms/FormAutoCompleteField";
|
||||
import { FormSliderField } from "@/Components/inputs/forms/FormSliderField";
|
||||
import { FormSwitchField } from "@/Components/inputs/forms/FormSwitchField";
|
||||
import type { ProxyResponse } from "@/Types/Proxy";
|
||||
import { formatDuration } from "@/Utils/TimeUtils";
|
||||
|
||||
const httpMethodOptions = HttpMethods.map((method) => ({
|
||||
value: method,
|
||||
@@ -75,6 +81,36 @@ const maskedInputSx = {
|
||||
"&:not(.Mui-focused) textarea": { WebkitTextSecurity: "disc" },
|
||||
};
|
||||
|
||||
interface IntervalOption extends AutocompleteOption {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const INTERVAL_UNIT_MULTIPLIERS = [1000, 60000, 3600000, 86400000] as const;
|
||||
|
||||
const toIntervalOption = (intervalMs: number): IntervalOption => ({
|
||||
id: intervalMs,
|
||||
name: formatDuration(intervalMs, true),
|
||||
});
|
||||
|
||||
const filterIntervalOptions = (
|
||||
options: IntervalOption[],
|
||||
inputValue: string
|
||||
): IntervalOption[] => {
|
||||
const input = inputValue.trim();
|
||||
if (input === "" || options.some((option) => option.name === input)) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const amount = Number(input);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return INTERVAL_UNIT_MULTIPLIERS.map((multiplier) => amount * multiplier)
|
||||
.filter(Number.isSafeInteger)
|
||||
.map(toIntervalOption);
|
||||
};
|
||||
|
||||
interface GeneralSettingsConfig {
|
||||
urlLabel: string;
|
||||
urlPlaceholder: string;
|
||||
@@ -314,6 +350,7 @@ const CreateMonitorPage = () => {
|
||||
const watchedProxyMode = watch("proxyMode") as ProxyMode | undefined;
|
||||
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
|
||||
const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean;
|
||||
const watchedInterval = watch("interval");
|
||||
|
||||
// Steps without an advanced section drop it, so the last step is the form's.
|
||||
const totalSteps = monitorStepCount(watchedType);
|
||||
@@ -471,16 +508,16 @@ const CreateMonitorPage = () => {
|
||||
[t]
|
||||
);
|
||||
|
||||
const intervalOptions = useMemo(
|
||||
() =>
|
||||
MonitorIntervalOptions.map((option) => ({
|
||||
value: option.value,
|
||||
label: t(
|
||||
`pages.createMonitor.form.frequency.option.frequency.value.${option.labelKey}`
|
||||
),
|
||||
})),
|
||||
[t]
|
||||
);
|
||||
const intervalOptions = useMemo(() => {
|
||||
const options = MonitorIntervalOptions.map(({ value }) => toIntervalOption(value));
|
||||
if (
|
||||
typeof watchedInterval === "number" &&
|
||||
!options.some((option) => option.id === watchedInterval)
|
||||
) {
|
||||
options.push(toIntervalOption(watchedInterval));
|
||||
}
|
||||
return options;
|
||||
}, [watchedInterval]);
|
||||
|
||||
const matchMethodOptions = useMemo(
|
||||
() =>
|
||||
@@ -764,12 +801,16 @@ const CreateMonitorPage = () => {
|
||||
title={t("pages.createMonitor.form.frequency.title")}
|
||||
subtitle={t("pages.createMonitor.form.frequency.description")}
|
||||
rightContent={
|
||||
<FormSelectField
|
||||
<FormAutocompleteField
|
||||
name="interval"
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.frequency.option.frequency.label"
|
||||
)}
|
||||
options={intervalOptions}
|
||||
filterOptions={(options, { inputValue }) =>
|
||||
filterIntervalOptions(options, inputValue)
|
||||
}
|
||||
getOptionDisabled={(option) => option.id < MIN_MONITOR_INTERVAL_MS}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -60,6 +60,8 @@ export const monitorTypeLabelKey: Record<SelectableMonitorType, string> &
|
||||
pagespeed: "optionPagespeed",
|
||||
};
|
||||
|
||||
export const MIN_MONITOR_INTERVAL_MS = 10000;
|
||||
|
||||
export const MonitorIntervalOptions = [
|
||||
{ value: 15000, labelKey: "fifteenSeconds" },
|
||||
{ value: 30000, labelKey: "thirtySeconds" },
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
HttpMethods,
|
||||
PageSpeedStrategies,
|
||||
ProxyModes,
|
||||
MIN_MONITOR_INTERVAL_MS,
|
||||
type MonitorType,
|
||||
} from "@/Types/Monitor";
|
||||
import { ALL_HTTP_STATUS_CODES } from "@/Utils/statusCode";
|
||||
@@ -27,7 +28,8 @@ const baseSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
interval: z
|
||||
.number()
|
||||
.min(15000, "Interval must be at least 15 seconds")
|
||||
.int("Interval must be a whole number of milliseconds")
|
||||
.min(MIN_MONITOR_INTERVAL_MS, "Interval must be at least 10 seconds")
|
||||
.register(monitorStepRegistry, { step: 1 }),
|
||||
notifications: z.array(z.string()).register(monitorStepRegistry, { step: 1 }),
|
||||
tags: z.array(z.string()).register(monitorStepRegistry, { step: 1 }),
|
||||
|
||||
@@ -143,6 +143,21 @@ describe("JobScheduler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("addJob", () => {
|
||||
it("preserves an arbitrary monitor interval in the scheduled job", async () => {
|
||||
const { scheduler, mocks } = createScheduler();
|
||||
|
||||
await scheduler.addJob("m1", makeMonitor({ interval: 75000 }));
|
||||
|
||||
expect(mocks.jobsRepository.upsertJob).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "check:m1",
|
||||
intervalMs: 75000,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Regression: a heartbeat upsert racing shutdown's deleteById could resurrect the
|
||||
// registry row, leaving a ghost worker that inflates the alive count until TTL.
|
||||
// shutdown() must await the in-flight beat and issue the delete as the LAST write.
|
||||
|
||||
Reference in new issue
Block a user