diff --git a/client/src/Components/design-elements/StepProgress.tsx b/client/src/Components/design-elements/StepProgress.tsx index fe7fb2880..00c5064ab 100644 --- a/client/src/Components/design-elements/StepProgress.tsx +++ b/client/src/Components/design-elements/StepProgress.tsx @@ -14,29 +14,30 @@ export const StepProgress = ({ steps, current }: StepProgressProps) => { const activeBg = `linear-gradient(135deg, ${theme.palette.primary.light} 0%, ${theme.palette.primary.main} 100%)`; const completedBg = `linear-gradient(135deg, ${alpha(theme.palette.primary.light, 0.5)} 0%, ${alpha(theme.palette.primary.main, 0.5)} 100%)`; + const getSegmentBackground = (index: number) => { + if (index < current) return completedBg; + if (index === current) return activeBg; + return theme.palette.divider; + }; + return ( - {Array.from({ length: steps }).map((_, index) => { - const background = - index < current - ? completedBg - : index === current - ? activeBg - : theme.palette.divider; - return ( - - ); - })} + {Array.from({ length: steps }).map((_, index) => ( + + ))} ); }; diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index eeb5c138e..e22de4e31 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -21,121 +21,128 @@ const getBaseDefaults = (data?: Monitor | null) => ({ geoCheckInterval: data?.geoCheckInterval || 300000, }); +export const getMonitorDefaults = ( + type: MonitorType, + data: Monitor | null = null +): MonitorFormData => { + const base = getBaseDefaults(data); + + let defaults: MonitorFormData; + + switch (type) { + case "http": + defaults = { + ...base, + type: "http", + url: data?.url || "", + ignoreTlsErrors: data?.ignoreTlsErrors || false, + useAdvancedMatching: data?.useAdvancedMatching || false, + matchMethod: data?.matchMethod || "", + expectedValue: data?.expectedValue || "", + jsonPath: data?.jsonPath || "", + }; + break; + case "ping": + defaults = { + ...base, + type: "ping", + url: data?.url || "", + }; + break; + case "port": + defaults = { + ...base, + type: "port", + url: data?.url || "", + port: data?.port || 80, + }; + break; + case "docker": + defaults = { + ...base, + type: "docker", + url: data?.url || "", + }; + break; + case "game": + defaults = { + ...base, + type: "game", + url: data?.url || "", + port: data?.port || 27015, + gameId: data?.gameId || "", + }; + break; + case "grpc": + defaults = { + ...base, + type: "grpc", + url: data?.url || "", + port: data?.port || 50051, + grpcServiceName: data?.grpcServiceName || "", + ignoreTlsErrors: data?.ignoreTlsErrors || false, + }; + break; + case "pagespeed": + defaults = { + ...base, + type: "pagespeed", + url: data?.url || "", + strategy: data?.strategy ?? DefaultPageSpeedStrategy, + }; + break; + case "hardware": + defaults = { + ...base, + type: "hardware", + url: data?.url || "", + secret: data?.secret || "", + cpuAlertThreshold: data?.cpuAlertThreshold ?? 100, + memoryAlertThreshold: data?.memoryAlertThreshold ?? 100, + diskAlertThreshold: data?.diskAlertThreshold ?? 100, + tempAlertThreshold: data?.tempAlertThreshold ?? 100, + selectedDisks: data?.selectedDisks || [], + }; + break; + case "websocket": + defaults = { + ...base, + type: "websocket", + url: data?.url || "", + ignoreTlsErrors: data?.ignoreTlsErrors || false, + }; + break; + case "dns": + defaults = { + ...base, + type: "dns", + url: data?.url || "", + dnsServer: data?.dnsServer || "", + dnsRecordType: data?.dnsRecordType || "A", + }; + break; + default: + defaults = { + ...base, + type: "http", + url: "", + ignoreTlsErrors: false, + useAdvancedMatching: false, + matchMethod: "", + expectedValue: "", + jsonPath: "", + }; + } + + return defaults; +}; + export const useMonitorForm = ({ data = null, defaultType = "http", }: UseMonitorFormOptions = {}) => { return useMemo(() => { const type = data?.type || defaultType; - const base = getBaseDefaults(data); - - let defaults: MonitorFormData; - - switch (type) { - case "http": - defaults = { - ...base, - type: "http", - url: data?.url || "", - ignoreTlsErrors: data?.ignoreTlsErrors || false, - useAdvancedMatching: data?.useAdvancedMatching || false, - matchMethod: data?.matchMethod || "", - expectedValue: data?.expectedValue || "", - jsonPath: data?.jsonPath || "", - }; - break; - case "ping": - defaults = { - ...base, - type: "ping", - url: data?.url || "", - }; - break; - case "port": - defaults = { - ...base, - type: "port", - url: data?.url || "", - port: data?.port || 80, - }; - break; - case "docker": - defaults = { - ...base, - type: "docker", - url: data?.url || "", - }; - break; - case "game": - defaults = { - ...base, - type: "game", - url: data?.url || "", - port: data?.port || 27015, - gameId: data?.gameId || "", - }; - break; - case "grpc": - defaults = { - ...base, - type: "grpc", - url: data?.url || "", - port: data?.port || 50051, - grpcServiceName: data?.grpcServiceName || "", - ignoreTlsErrors: data?.ignoreTlsErrors || false, - }; - break; - case "pagespeed": - defaults = { - ...base, - type: "pagespeed", - url: data?.url || "", - strategy: data?.strategy ?? DefaultPageSpeedStrategy, - }; - break; - case "hardware": - defaults = { - ...base, - type: "hardware", - url: data?.url || "", - secret: data?.secret || "", - cpuAlertThreshold: data?.cpuAlertThreshold ?? 100, - memoryAlertThreshold: data?.memoryAlertThreshold ?? 100, - diskAlertThreshold: data?.diskAlertThreshold ?? 100, - tempAlertThreshold: data?.tempAlertThreshold ?? 100, - selectedDisks: data?.selectedDisks || [], - }; - break; - case "websocket": - defaults = { - ...base, - type: "websocket", - url: data?.url || "", - ignoreTlsErrors: data?.ignoreTlsErrors || false, - }; - break; - case "dns": - defaults = { - ...base, - type: "dns", - url: data?.url || "", - dnsServer: data?.dnsServer || "", - dnsRecordType: data?.dnsRecordType || "A", - }; - break; - default: - defaults = { - ...base, - type: "http", - url: "", - ignoreTlsErrors: false, - useAdvancedMatching: false, - matchMethod: "", - expectedValue: "", - jsonPath: "", - }; - } - - return { schema: monitorSchema, defaults }; + return { schema: monitorSchema, defaults: getMonitorDefaults(type, data) }; }, [data, defaultType]); }; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 1240d27a0..dcd0fd97f 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -37,7 +37,7 @@ import { } from "@/Components/inputs"; import { SPACING, LAYOUT } from "@/Utils/Theme/constants"; import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; -import { useMonitorForm } from "@/Hooks/useMonitorForm"; +import { useMonitorForm, getMonitorDefaults } from "@/Hooks/useMonitorForm"; import { type Monitor, type MonitorType, @@ -213,7 +213,10 @@ const getGeneralSettingsConfig = ( return configs[type] || configs.http; }; -const TOTAL_STEPS = 3; +// 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); // Fields validated before advancing past each wizard step (uptime create only). const STEP_FIELDS: FieldPath[][] = [ @@ -282,33 +285,41 @@ const CreateMonitorPage = () => { resolver: zodResolver(schema), defaultValues: defaults, }); - const { control, watch, handleSubmit, clearErrors, trigger } = form; + const { control, watch, handleSubmit, clearErrors, trigger, reset } = form; useEffect(() => { - form.reset(defaults); - }, [defaults, form]); + reset(defaults); + }, [defaults, reset]); - // Multi-step wizard (uptime create only). Other flows keep the flat form. - const isWizard = showTypeSelector; + // Multi-step wizard, uptime create only (showTypeSelector). Other flows + // keep the flat form, where showStep() is always true. const [currentStep, setCurrentStep] = useState(0); - const showStep = (step: number) => !isWizard || currentStep === step; + const showStep = (step: number) => !showTypeSelector || currentStep === step; + + const watchedType = watch("type") as MonitorType; + const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean; + 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 handleNext = async () => { const isValid = await trigger(STEP_FIELDS[currentStep]); if (isValid) { - setCurrentStep((step) => Math.min(step + 1, TOTAL_STEPS - 1)); + setCurrentStep((step) => step + 1); } }; - const handleBack = () => setCurrentStep((step) => Math.max(step - 1, 0)); - - const watchedType = watch("type") as MonitorType; - - const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean; - const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean; + const handleBack = () => setCurrentStep((step) => step - 1); + // Reset the form when the monitor type changes so stale type-specific fields + // can't produce an invalid monitor. Type can only be changed on step 0. useEffect(() => { - clearErrors(); - }, [watchedType, clearErrors]); + if (showTypeSelector) { + reset(getMonitorDefaults(watchedType)); + } else { + clearErrors(); + } + }, [watchedType, showTypeSelector, reset, clearErrors]); const generalSettingsConfig = useMemo( () => getGeneralSettingsConfig(watchedType, t), @@ -378,9 +389,9 @@ const CreateMonitorPage = () => { refetch={refetchMonitor} onDelete={handleDeleteClick} /> - {isWizard && ( + {showTypeSelector && ( )} @@ -1351,9 +1362,9 @@ const CreateMonitorPage = () => { - {isWizard && ( + {showTypeSelector && ( )} - {isWizard && currentStep < TOTAL_STEPS - 1 ? ( + {showTypeSelector && currentStep < totalSteps - 1 ? (