From 8360c054484ec1ede99505659d794ff21628abcb Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 28 May 2026 14:20:02 +0530 Subject: [PATCH] refactor(client): derive wizard step count from Zod field metadata --- client/src/Hooks/useMonitorForm.ts | 7 +++++ client/src/Pages/CreateMonitor/index.tsx | 13 ++++---- client/src/Validation/monitor.ts | 40 ++++++++++++++++++------ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index e22de4e31..50fa62ec3 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -16,6 +16,10 @@ const getBaseDefaults = (data?: Monitor | null) => ({ tags: data?.tags || [], statusWindowSize: data?.statusWindowSize || 5, statusWindowThreshold: data?.statusWindowThreshold || 60, +}); + +// Geo-check defaults — only http and ping support geo checks. +const getGeoCheckDefaults = (data?: Monitor | null) => ({ geoCheckEnabled: data?.geoCheckEnabled ?? false, geoCheckLocations: data?.geoCheckLocations || [], geoCheckInterval: data?.geoCheckInterval || 300000, @@ -33,6 +37,7 @@ export const getMonitorDefaults = ( case "http": defaults = { ...base, + ...getGeoCheckDefaults(data), type: "http", url: data?.url || "", ignoreTlsErrors: data?.ignoreTlsErrors || false, @@ -45,6 +50,7 @@ export const getMonitorDefaults = ( case "ping": defaults = { ...base, + ...getGeoCheckDefaults(data), type: "ping", url: data?.url || "", }; @@ -124,6 +130,7 @@ export const getMonitorDefaults = ( default: defaults = { ...base, + ...getGeoCheckDefaults(data), type: "http", url: "", ignoreTlsErrors: false, diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 9fd9b62cb..4f40f028f 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -47,7 +47,11 @@ import { } from "@/Types/Monitor"; import type { Notification } from "@/Types/Notification"; import type { Tag } from "@/Types/Tag"; -import { stepFieldsFor, type MonitorFormData } from "@/Validation/monitor"; +import { + stepFieldsFor, + monitorStepCount, + type MonitorFormData, +} from "@/Validation/monitor"; interface GeneralSettingsConfig { urlLabel: string; @@ -213,11 +217,6 @@ const getGeneralSettingsConfig = ( return configs[type] || configs.http; }; -// Whether the wizard's "Advanced" step (TLS / matching / geo) has any content -// for this monitor type. Types without it skip the step entirely. -const hasAdvancedStep = (type: MonitorType): boolean => - type === "http" || type === "grpc" || type === "websocket" || supportsGeoCheck(type); - const CreateMonitorPage = () => { const theme = useTheme(); const { t } = useTranslation(); @@ -276,7 +275,7 @@ const CreateMonitorPage = () => { const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean; // Steps without an advanced section drop it, so the last step is the form's. - const totalSteps = hasAdvancedStep(watchedType) ? 3 : 2; + const totalSteps = monitorStepCount(watchedType); const handleNext = async () => { const isValid = await trigger( diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index 1f7b92034..4b79d44e0 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -33,6 +33,12 @@ const baseSchema = z.object({ .min(1, "Incident percentage must be at least 1") .max(100, "Incident percentage must be at most 100") .register(monitorStepRegistry, { step: 1 }), +}); + +// Geo-distributed check fields. Only http and ping support geo checks +// (GeoCheckSupportedTypes), so these live on those schemas rather than the base +// — that way each type's step count is exactly what its own fields declare. +const geoCheckFields = { geoCheckEnabled: z.boolean().optional().register(monitorStepRegistry, { step: 2 }), geoCheckLocations: z .array(z.enum(GeoContinents)) @@ -43,7 +49,7 @@ const baseSchema = z.object({ .min(300000, "Interval must be at least 5 minutes") .optional() .register(monitorStepRegistry, { step: 2 }), -}); +}; // HTTP monitor schema const httpSchema = baseSchema.extend({ @@ -57,12 +63,14 @@ const httpSchema = baseSchema.extend({ .register(monitorStepRegistry, { step: 2 }), expectedValue: z.string().optional().register(monitorStepRegistry, { step: 2 }), jsonPath: z.string().optional().register(monitorStepRegistry, { step: 2 }), + ...geoCheckFields, }); // Ping monitor schema const pingSchema = baseSchema.extend({ type: z.literal("ping"), url: z.string().min(1, "Host is required"), + ...geoCheckFields, }); // Port monitor schema @@ -181,16 +189,28 @@ export const monitorSchema = z.discriminatedUnion("type", [ export type MonitorFormData = z.infer; -// The selected type's fields that belong to a given wizard step, read from the -// per-field `monitorStepRegistry` metadata (unannotated fields default to step -// 0). Derived from the schema so it can't drift from the field definitions. -export const stepFieldsFor = (type: MonitorType, step: number): string[] => { - const option = monitorSchema.options.find((o) => o.shape.type.value === type); - if (!option) return []; - return Object.entries(option.shape) - .filter(([, field]) => (monitorStepRegistry.get(field)?.step ?? 0) === step) +const optionForType = (type: MonitorType) => + monitorSchema.options.find((o) => o.shape.type.value === type); + +// The step each field of a type is validated on, from the per-field +// `monitorStepRegistry` metadata (unannotated fields default to step 0). +const fieldStepsFor = (type: MonitorType): [name: string, step: number][] => + Object.entries(optionForType(type)?.shape ?? {}).map(([name, field]) => [ + name, + monitorStepRegistry.get(field)?.step ?? 0, + ]); + +// The selected type's fields that belong to a given wizard step. Derived from +// the schema so it can't drift from the field definitions. +export const stepFieldsFor = (type: MonitorType, step: number): string[] => + fieldStepsFor(type) + .filter(([, fieldStep]) => fieldStep === step) .map(([name]) => name); -}; + +// Number of wizard steps a type has, derived from the highest step its fields +// declare. A type with no step-2 fields is a 2-step form, and so on. +export const monitorStepCount = (type: MonitorType): number => + Math.max(0, ...fieldStepsFor(type).map(([, step]) => step)) + 1; // Type-specific schemas exported for individual use export {