From 7cab326ad379ddf7941994a3fdff942ba6efea20 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 6 Aug 2026 10:17:32 -0700 Subject: [PATCH] refactor selects --- .../inputs/forms/FormSelectField.tsx | 60 +++ client/src/Hooks/useMonitorForm.ts | 10 +- client/src/Pages/CreateMonitor/index.tsx | 344 +++++------------- client/src/Types/Monitor.ts | 21 +- 4 files changed, 180 insertions(+), 255 deletions(-) create mode 100644 client/src/Components/inputs/forms/FormSelectField.tsx diff --git a/client/src/Components/inputs/forms/FormSelectField.tsx b/client/src/Components/inputs/forms/FormSelectField.tsx new file mode 100644 index 000000000..78e42300a --- /dev/null +++ b/client/src/Components/inputs/forms/FormSelectField.tsx @@ -0,0 +1,60 @@ +import { + Controller, + useFormContext, + type FieldValues, + type FieldPath, +} from "react-hook-form"; +import MenuItem from "@mui/material/MenuItem"; +import { Select } from "@/Components/inputs"; + +export interface FormSelectOption { + value: string | number; + label: string; +} + +interface FormSelectProps extends Omit< + React.ComponentProps, + "name" | "error" | "value" | "onChange" +> { + name: FieldPath; + options: FormSelectOption[]; + fallbackValue?: string | number; // display value while the field is undefined + onValueChange?: (value: string | number) => void; // side effects, see Notes +} + +export const FormSelectField = ({ + name, + options, + fallbackValue, + onValueChange, + ...rest +}: FormSelectProps) => { + const { control } = useFormContext(); + return ( + ( + + )} + /> + ); +}; diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 9c6200d22..16314e8ca 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -1,6 +1,10 @@ import { useMemo } from "react"; import { monitorSchema, type MonitorFormData } from "@/Validation/monitor"; -import { DefaultHttpMethod, DefaultPageSpeedStrategy } from "@/Types/Monitor"; +import { + DefaultHttpMethod, + DefaultMonitorMatchMethod, + DefaultPageSpeedStrategy, +} from "@/Types/Monitor"; import type { Monitor, MonitorType } from "@/Types/Monitor"; interface UseMonitorFormOptions { @@ -43,7 +47,7 @@ export const getMonitorDefaults = ( method: data?.method ?? DefaultHttpMethod, ignoreTlsErrors: data?.ignoreTlsErrors || false, useAdvancedMatching: data?.useAdvancedMatching || false, - matchMethod: data?.matchMethod || "", + matchMethod: data?.matchMethod || DefaultMonitorMatchMethod, expectedValue: data?.expectedValue || "", jsonPath: data?.jsonPath || "", customUpCodes: data?.customUpCodes || [], @@ -138,7 +142,7 @@ export const getMonitorDefaults = ( method: DefaultHttpMethod, ignoreTlsErrors: false, useAdvancedMatching: false, - matchMethod: "", + matchMethod: DefaultMonitorMatchMethod, expectedValue: "", jsonPath: "", customUpCodes: [], diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index e0180095c..153e793f2 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -8,7 +8,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useTheme } from "@mui/material"; import Stack from "@mui/material/Stack"; import { Trans, useTranslation } from "react-i18next"; -import MenuItem from "@mui/material/MenuItem"; import Typography from "@mui/material/Typography"; import Link from "@mui/material/Link"; import { HeaderDeleteControls } from "@/Components/monitors"; @@ -23,7 +22,6 @@ import { } from "@/Components/design-elements"; import { Button, - Select, SwitchComponent as Switch, SliderWithLabel, Dialog, @@ -42,6 +40,12 @@ import { HttpMethods, SelectableMonitorTypes, monitorTypeLabelKey, + PageSpeedStrategies, + DnsRecordTypes, + MonitorIntervalOptions, + DefaultMonitorMatchMethod, + MonitorMatchMethods, + GeoCheckIntervalOptions, } from "@/Types/Monitor"; import type { Notification } from "@/Types/Notification"; import type { Tag } from "@/Types/Tag"; @@ -54,6 +58,7 @@ import { FormNumberField } from "@/Components/inputs/forms/FormNumberField"; 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"; interface GeneralSettingsConfig { urlLabel: string; @@ -444,26 +449,18 @@ const CreateMonitorPage = () => { {/* Strategy field - only for pagespeed type */} {generalSettingsConfig.showStrategy && ( - ( - + fieldLabel={t( + "pages.createMonitor.form.general.option.strategy.label" )} + options={PageSpeedStrategies.map((strategy) => ({ + value: strategy, + label: t( + `pages.createMonitor.form.general.option.strategy.${strategy}` + ), + }))} + fallbackValue={DefaultPageSpeedStrategy} /> )} @@ -480,34 +477,21 @@ const CreateMonitorPage = () => { {/* Game select - only for game type */} {generalSettingsConfig.showGameSelect && ( - ( - - )} + fieldLabel={t("pages.createMonitor.form.general.option.game.label")} + options={[ + { + value: "", + label: t( + "pages.createMonitor.form.general.option.game.placeholder" + ), + }, + ...Object.entries(games ?? {}).map(([key, game]) => ({ + value: key, + label: game.name, + })), + ]} /> )} @@ -546,51 +530,17 @@ const CreateMonitorPage = () => { /> )} {generalSettingsConfig.showDnsRecordType && ( - ( - + fieldLabel={t( + "pages.createMonitor.form.general.option.dnsRecordType.label" )} + options={DnsRecordTypes.map((recordType) => ({ + value: recordType, + label: t( + `pages.createMonitor.form.general.option.dnsRecordType.value.${recordType}` + ), + }))} /> )} { title={t("pages.createMonitor.form.frequency.title")} subtitle={t("pages.createMonitor.form.frequency.description")} rightContent={ - ( - + fieldLabel={t( + "pages.createMonitor.form.frequency.option.frequency.label" )} + options={MonitorIntervalOptions.map((option) => ({ + value: option.value, + label: t( + `pages.createMonitor.form.frequency.option.frequency.value.${option.labelKey}` + ), + }))} /> } /> @@ -881,50 +778,33 @@ const CreateMonitorPage = () => { subtitle={t("pages.createMonitor.form.advanced.description")} rightContent={ - ( - - - - {t( - `pages.createMonitor.form.advanced.option.method.description.${field.value ?? DefaultHttpMethod}` - )} - - - )} + fieldLabel={t("pages.createMonitor.form.advanced.option.method.label")} + fallbackValue={DefaultHttpMethod} + onValueChange={(value) => { + // HEAD has no response body, reset advanced matching fields if selected + if (value === "HEAD") { + setValue("useAdvancedMatching", false); + setValue("matchMethod", DefaultMonitorMatchMethod); + setValue("expectedValue", ""); + setValue("jsonPath", ""); + } + }} + options={HttpMethods.map((method) => ({ + value: method, + label: method, + }))} /> + + {t( + `pages.createMonitor.form.advanced.option.method.description.${watchedMethod}` + )} + { )} {watchedUseAdvancedMatching && watchedMethod !== "HEAD" && ( - ( - + fieldLabel={t( + "pages.createMonitor.form.advanced.option.matchMethod.label" )} + fallbackValue={DefaultMonitorMatchMethod} + options={MonitorMatchMethods.map((method) => ({ + value: method, + label: t( + `pages.createMonitor.form.advanced.option.matchMethod.${method}` + ), + }))} /> { "pages.createMonitor.form.geoChecks.option.locations.label" )} /> - ( - + fieldLabel={t( + "pages.createMonitor.form.geoChecks.option.interval.label" )} + options={GeoCheckIntervalOptions.map((option) => ({ + value: option.value, + label: t( + `pages.createMonitor.form.geoChecks.option.interval.value.${option.labelKey}` + ), + }))} /> )} diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 1f3a59b1f..bf5e4f170 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -48,6 +48,23 @@ export const monitorTypeLabelKey: Record = { dns: "optionDns", }; +export const MonitorIntervalOptions = [ + { value: 15000, labelKey: "fifteenSeconds" }, + { value: 30000, labelKey: "thirtySeconds" }, + { value: 60000, labelKey: "oneMinute" }, + { value: 120000, labelKey: "twoMinutes" }, + { value: 180000, labelKey: "threeMinutes" }, + { value: 240000, labelKey: "fourMinutes" }, + { value: 300000, labelKey: "fiveMinutes" }, + { value: 600000, labelKey: "tenMinutes" }, + { value: 900000, labelKey: "fifteenMinutes" }, + { value: 1800000, labelKey: "thirtyMinutes" }, +] as const; + +export const GeoCheckIntervalOptions = MonitorIntervalOptions.filter( + ({ value }) => value >= 300000 +); + export const DnsRecordTypes = ["A", "AAAA", "CNAME", "MX", "TXT", "NS"] as const; export type DnsRecordType = (typeof DnsRecordTypes)[number]; @@ -70,7 +87,9 @@ export const MonitorStatuses = [ ] as const; export type MonitorStatus = (typeof MonitorStatuses)[number]; -export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; +export const MonitorMatchMethods = ["equal", "include", "regex"] as const; +export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | ""; +export const DefaultMonitorMatchMethod: MonitorMatchMethod = "equal"; export const PageSpeedStrategies = ["desktop", "mobile"] as const; export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];