diff --git a/client/src/hooks/forms/UseInitMonitorFrom.tsx b/client/src/hooks/forms/UseInitMonitorFrom.tsx index b57a5df16..e7b12da65 100644 --- a/client/src/hooks/forms/UseInitMonitorFrom.tsx +++ b/client/src/hooks/forms/UseInitMonitorFrom.tsx @@ -1,32 +1,56 @@ -import { monitorSchema } from "@/validation/zod"; -import ms from "ms"; import { useMemo } from "react"; +import humanInterval from "human-interval"; +import type { FormValues, SubmitValues } from "@/pages/uptime/UptimeForm"; +import type { IMonitor } from "@/types/monitor"; + +import ms from "ms"; -import { z } from "zod"; export const useInitForm = ({ initialData, }: { - initialData: Partial> | undefined; + initialData: IMonitor | undefined; }) => { return useMemo(() => { - let humanInterval = "1 minute"; - if (initialData?.interval) { - const parsed = Number(initialData.interval); - if (!isNaN(parsed)) { - humanInterval = ms(parsed, { long: true }); - } - } - - const defaults: z.infer = { - type: initialData?.type || "https", - url: initialData?.url || "", - port: initialData?.port || 80, - n: initialData?.n || 3, - notificationChannels: initialData?.notificationChannels || [], - name: initialData?.name || "", - interval: humanInterval, - rejectUnauthorized: initialData?.rejectUnauthorized ?? true, + const apiToForm = (apiData: IMonitor): FormValues => { + return { + type: apiData.type, + url: apiData.url, + port: apiData.port, + n: apiData.n, + notificationChannels: apiData.notificationChannels, + name: apiData.name, + interval: ms(apiData.interval, { long: true }), + rejectUnauthorized: apiData.rejectUnauthorized, + }; }; - return { defaults }; + + const formToApi = (formData: FormValues): SubmitValues => { + const submitData: SubmitValues = { + type: formData.type, + url: formData.url, + port: formData.type === "port" ? formData.port : undefined, + n: formData.n, + notificationChannels: formData.notificationChannels, + name: formData.name, + interval: humanInterval(formData.interval), + rejectUnauthorized: formData.rejectUnauthorized, + }; + + return submitData; + }; + + const defaults: FormValues = initialData + ? apiToForm(initialData) + : { + type: "https", + url: "", + port: 80, + n: 3, + notificationChannels: [], + name: "", + interval: "1 minute", + rejectUnauthorized: true, + }; + return { defaults, apiToForm, formToApi }; }, [initialData]); }; diff --git a/client/src/pages/pagespeed/PageSpeedCreate.tsx b/client/src/pages/pagespeed/PageSpeedCreate.tsx index e5c395565..744daae7a 100644 --- a/client/src/pages/pagespeed/PageSpeedCreate.tsx +++ b/client/src/pages/pagespeed/PageSpeedCreate.tsx @@ -2,7 +2,7 @@ import { PageSpeedForm } from "@/pages/pagespeed/PageSpeedForm"; import humanInterval from "human-interval"; import { z } from "zod"; -import { monitorSchema } from "@/validation/zod"; +import { monitorSchema } from "@/validation"; import { useGet, usePost } from "@/hooks/UseApi"; import type { ApiResponse } from "@/types/api"; import { useNavigate } from "react-router"; diff --git a/client/src/pages/uptime/UptimeConfigure.tsx b/client/src/pages/uptime/UptimeConfigure.tsx index 95d04fbf5..2fc411aca 100644 --- a/client/src/pages/uptime/UptimeConfigure.tsx +++ b/client/src/pages/uptime/UptimeConfigure.tsx @@ -1,21 +1,14 @@ -import { monitorSchema } from "@/validation/zod"; import { useGet, usePatch } from "@/hooks/UseApi"; import { UptimeForm } from "@/pages/uptime/UptimeForm"; import { useParams } from "react-router"; import type { ApiResponse } from "@/types/api"; -import humanInterval from "human-interval"; -import { z } from "zod"; import { useNavigate } from "react-router"; import type { INotificationChannel } from "@/types/notification-channel"; import type { IMonitor } from "@/types/monitor"; +import type { SubmitValues } from "@/pages/uptime/UptimeForm"; const UptimeConfigurePage = () => { - type FormValues = z.infer; - type SubmitValues = Omit & { - interval: number | undefined; - }; - const { id } = useParams(); const navigate = useNavigate(); const { response } = useGet>( @@ -24,23 +17,17 @@ const UptimeConfigurePage = () => { const { response: monitorResponse } = useGet>( `/monitors/${id}` ); - const monitor = monitorResponse?.data || {}; + const monitor = monitorResponse?.data; const notificationOptions = response?.data ?? []; const { patch, loading, error } = usePatch, IMonitor>(); - const onSubmit = async (data: FormValues) => { - let interval = humanInterval(data.interval); - if (!interval) interval = 60000; - const submitData = { - type: data.type, - name: data.name, - n: data.n, - notificationChannels: data.notificationChannels, - interval, - rejectUnauthorized: data.rejectUnauthorized, - }; - const result = await patch(`/monitors/${id}`, submitData); + if (!monitor) { + return null; + } + + const onSubmit = async (data: SubmitValues) => { + const result = await patch(`/monitors/${id}`, data); if (result) { navigate(`/uptime/${id}`); } else { @@ -50,9 +37,7 @@ const UptimeConfigurePage = () => { return ( { - type FormValues = z.infer; - type SubmitValues = Omit & { - interval: number | undefined; - }; const navigate = useNavigate(); const { response } = useGet>( "/notification-channels" ); const { post, loading, error } = usePost(); - const onSubmit = async (data: FormValues) => { - let interval = humanInterval(data.interval); - if (!interval) interval = 60000; - const submitData = { ...data, interval }; - const result = await post("/monitors", submitData); + const onSubmit = async (data: SubmitValues) => { + const result = await post("/monitors", data); if (result) { navigate("/uptime"); } else { diff --git a/client/src/pages/uptime/UptimeForm.tsx b/client/src/pages/uptime/UptimeForm.tsx index 3179bf3a2..0904b68c2 100644 --- a/client/src/pages/uptime/UptimeForm.tsx +++ b/client/src/pages/uptime/UptimeForm.tsx @@ -14,7 +14,7 @@ import { Typography } from "@mui/material"; import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; -import { monitorSchema } from "@/validation/zod"; +import { monitorSchema } from "@/validation"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { @@ -23,10 +23,14 @@ import { useWatch, type SubmitHandler, } from "react-hook-form"; +import type { IMonitor } from "@/types/monitor"; import { useTheme } from "@mui/material/styles"; import { useInitForm } from "@/hooks/forms/UseInitMonitorFrom"; -type FormValues = z.infer; +export type FormValues = z.infer; +export type SubmitValues = Omit, "interval"> & { + interval: number | undefined; +}; export const UptimeForm = ({ mode = "create", @@ -36,14 +40,14 @@ export const UptimeForm = ({ loading, }: { mode?: "create" | "configure"; - initialData?: Partial; - onSubmit: SubmitHandler; + initialData?: IMonitor; + onSubmit: SubmitHandler; notificationOptions: any[]; loading: boolean; }) => { const { t } = useTranslation(); const theme = useTheme(); - const { defaults } = useInitForm({ initialData: initialData }); + const { defaults, formToApi } = useInitForm({ initialData: initialData }); const urlInputRef = useRef(null); const { @@ -73,6 +77,10 @@ export const UptimeForm = ({ name: "notificationChannels", }); + const submitForm = (formData: FormValues) => { + onSubmit(formToApi(formData)); + }; + useEffect(() => { if (!selectedType) return; @@ -108,7 +116,7 @@ export const UptimeForm = ({ }, [selectedType]); return ( - + {mode === "create" && ( { + // if (!val || val.trim() === "") return; + const ms = humanInterval(val); + + if (!ms || isNaN(ms)) { + ctx.addIssue({ + code: "custom", + message: "Invalid duration format", + }); + } else if (ms < 10000) { + ctx.addIssue({ + code: "custom", + message: "Minimum duration is 10 seconds", + }); + } +}); + +export const monitorSchema = z + .object({ + type: z.string().min(1, "You must select an option"), + url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"), + n: z.coerce + .number({ message: "Number required" }) + .min(1, "Minimum value is 1") + .max(25, "Maximum value is 25"), + port: z.coerce.number().optional(), + notificationChannels: z.array(z.string()).optional().default([]), + name: z.string().min(1, "Display name is required"), + interval: durationSchema, + rejectUnauthorized: z.boolean().default(true), + }) + .superRefine((data, ctx) => { + if (data.type === "port") { + if (!data.port) { + ctx.addIssue({ + code: "custom", + message: "Port is required for port monitoring", + path: ["port"], + }); + } + if (isNaN(Number(data.port))) { + ctx.addIssue({ + code: "custom", + message: "Port must be a number", + path: ["port"], + }); + } + if (Number(data.port) < 1 || Number(data.port) > 65535) { + ctx.addIssue({ + code: "custom", + message: "Port must be between 1 and 65535", + path: ["port"], + }); + } + } + }); diff --git a/client/src/validation/zod.ts b/client/src/validation/zod.ts index ce90c5a6a..2a5cd5afb 100644 --- a/client/src/validation/zod.ts +++ b/client/src/validation/zod.ts @@ -71,51 +71,7 @@ const optionalString = (schema: T) => schema.optional() ); -export const monitorSchema = z - .object({ - type: z.string().min(1, "You must select an option"), - url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"), - n: z.coerce - .number({ message: "Number required" }) - .min(1, "Minimum value is 1") - .max(25, "Maximum value is 25"), - port: z.coerce.number().optional(), - notificationChannels: z.array(z.string()).optional().default([]), - name: z.string().min(1, "Display name is required"), - interval: durationSchema, - rejectUnauthorized: z.boolean().default(true), - }) - .superRefine((data, ctx) => { - if (data.type === "port") { - if (!data.port) { - ctx.addIssue({ - code: "custom", - message: "Port is required for port monitoring", - path: ["port"], - }); - } - if (isNaN(Number(data.port))) { - ctx.addIssue({ - code: "custom", - message: "Port must be a number", - path: ["port"], - }); - } - if (Number(data.port) < 1 || Number(data.port) > 65535) { - ctx.addIssue({ - code: "custom", - message: "Port must be between 1 and 65535", - path: ["port"], - }); - } - } - }) - .transform((data) => { - if (data.type !== "port") { - return { ...data, port: undefined }; - } - return data; - }); +// moved to client/src/validation/monitor.ts export const monitorSchemaPageSpeed = z.object({ type: z.string().min(1, "You must select an option"),