refactor(client): derive wizard step count from Zod field metadata

This commit is contained in:
Br0wnHammer committed 2026-05-28 14:20:02 +05:30
1 parent 3cce1aa842
commit 8360c05448
3 files changed
+43 -17

No files matched your search

+7
View File
@@ -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,
+6 -7
View File
@@ -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(
+30 -10
View File
@@ -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<typeof monitorSchema>;
// 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 {