feat(client): split uptime monitor creation into a 3-step wizard

This commit is contained in:
Br0wnHammer committed 2026-05-28 00:21:02 +05:30
1 parent 37dfc67e33
commit 3cce1aa842
2 files changed
+46 -52

No files matched your search

+4 -28
View File
@@ -47,7 +47,7 @@ import {
} from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { Tag } from "@/Types/Tag";
import { monitorFieldsByType, type MonitorFormData } from "@/Validation/monitor";
import { stepFieldsFor, type MonitorFormData } from "@/Validation/monitor";
interface GeneralSettingsConfig {
urlLabel: string;
@@ -218,32 +218,6 @@ const getGeneralSettingsConfig = (
const hasAdvancedStep = (type: MonitorType): boolean =>
type === "http" || type === "grpc" || type === "websocket" || supportsGeoCheck(type);
// The wizard step on which each field is validated. Fields not listed default
// to step 0, so a field added to the schema is validated by default rather than
// silently skipped. Step membership is the only thing hardcoded here; the field
// set itself is derived from the schema via `monitorFieldsByType`.
const FIELD_STEP: Record<string, number> = {
interval: 1,
statusWindowSize: 1,
statusWindowThreshold: 1,
notifications: 1,
tags: 1,
ignoreTlsErrors: 2,
useAdvancedMatching: 2,
matchMethod: 2,
expectedValue: 2,
jsonPath: 2,
geoCheckEnabled: 2,
geoCheckLocations: 2,
geoCheckInterval: 2,
};
// Exactly the selected type's fields that belong to the given step.
const stepFieldsFor = (type: MonitorType, step: number): FieldPath<MonitorFormData>[] =>
(monitorFieldsByType[type] ?? []).filter(
(field) => (FIELD_STEP[field] ?? 0) === step
) as FieldPath<MonitorFormData>[];
const CreateMonitorPage = () => {
const theme = useTheme();
const { t } = useTranslation();
@@ -305,7 +279,9 @@ const CreateMonitorPage = () => {
const totalSteps = hasAdvancedStep(watchedType) ? 3 : 2;
const handleNext = async () => {
const isValid = await trigger(stepFieldsFor(watchedType, currentStep));
const isValid = await trigger(
stepFieldsFor(watchedType, currentStep) as FieldPath<MonitorFormData>[]
);
if (isValid) {
setCurrentStep((step) => step + 1);
}
+42 -24
View File
@@ -2,6 +2,11 @@ import { z } from "zod";
import { GeoContinents } from "@/Types/GeoCheck";
import { DnsRecordTypes, PageSpeedStrategies, type MonitorType } from "@/Types/Monitor";
// Wizard step a field is validated on. Attached inline to each field below so
// the grouping lives next to the field definition; unannotated fields default
// to step 0. Read via `stepFieldsFor`.
export const monitorStepRegistry = z.registry<{ step: number }>();
// URL schema with custom error message
const urlSchema = z.url({ message: "Please enter a valid URL" });
@@ -12,34 +17,46 @@ const baseSchema = z.object({
.min(1, "Monitor name is required")
.max(50, "Monitor name must be at most 50 characters"),
description: z.string().optional(),
interval: z.number().min(15000, "Interval must be at least 15 seconds"),
notifications: z.array(z.string()),
tags: z.array(z.string()),
interval: z
.number()
.min(15000, "Interval must be at least 15 seconds")
.register(monitorStepRegistry, { step: 1 }),
notifications: z.array(z.string()).register(monitorStepRegistry, { step: 1 }),
tags: z.array(z.string()).register(monitorStepRegistry, { step: 1 }),
statusWindowSize: z
.number({ message: "Status window size is required" })
.min(1, "Status window size must be at least 1")
.max(25, "Status window size must be at most 25"),
.max(25, "Status window size must be at most 25")
.register(monitorStepRegistry, { step: 1 }),
statusWindowThreshold: z
.number({ message: "Threshold percentage is required" })
.min(1, "Incident percentage must be at least 1")
.max(100, "Incident percentage must be at most 100"),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
.max(100, "Incident percentage must be at most 100")
.register(monitorStepRegistry, { step: 1 }),
geoCheckEnabled: z.boolean().optional().register(monitorStepRegistry, { step: 2 }),
geoCheckLocations: z
.array(z.enum(GeoContinents))
.optional()
.register(monitorStepRegistry, { step: 2 }),
geoCheckInterval: z
.number()
.min(300000, "Interval must be at least 5 minutes")
.optional(),
.optional()
.register(monitorStepRegistry, { step: 2 }),
});
// HTTP monitor schema
const httpSchema = baseSchema.extend({
type: z.literal("http"),
url: urlSchema,
ignoreTlsErrors: z.boolean(),
useAdvancedMatching: z.boolean(),
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(),
jsonPath: z.string().optional(),
ignoreTlsErrors: z.boolean().register(monitorStepRegistry, { step: 2 }),
useAdvancedMatching: z.boolean().register(monitorStepRegistry, { step: 2 }),
matchMethod: z
.enum(["equal", "include", "regex", ""])
.optional()
.register(monitorStepRegistry, { step: 2 }),
expectedValue: z.string().optional().register(monitorStepRegistry, { step: 2 }),
jsonPath: z.string().optional().register(monitorStepRegistry, { step: 2 }),
});
// Ping monitor schema
@@ -84,7 +101,7 @@ const grpcSchema = baseSchema.extend({
.min(1, "Port must be at least 1")
.max(65535, "Port must be at most 65535"),
grpcServiceName: z.string().optional(),
ignoreTlsErrors: z.boolean(),
ignoreTlsErrors: z.boolean().register(monitorStepRegistry, { step: 2 }),
});
// PageSpeed monitor schema
@@ -122,7 +139,7 @@ const hardwareSchema = baseSchema.extend({
const websocketSchema = baseSchema.extend({
type: z.literal("websocket"),
url: z.string().min(1, "WebSocket URL is required"),
ignoreTlsErrors: z.boolean(),
ignoreTlsErrors: z.boolean().register(monitorStepRegistry, { step: 2 }),
});
// Hostname (FQDN) — labels of 1-63 alphanumerics/hyphens separated by dots,
@@ -164,15 +181,16 @@ export const monitorSchema = z.discriminatedUnion("type", [
export type MonitorFormData = z.infer<typeof monitorSchema>;
// Field names present in each monitor type's schema, derived from the schema
// itself. Consumers (e.g. the create wizard's per-step validation) should read
// this rather than hardcoding field lists, so renames/additions stay in sync.
export const monitorFieldsByType = Object.fromEntries(
monitorSchema.options.map((option) => [
option.shape.type.value,
Object.keys(option.shape),
])
) as Record<MonitorType, string[]>;
// 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)
.map(([name]) => name);
};
// Type-specific schemas exported for individual use
export {