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 ? (