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

This commit is contained in:
Br0wnHammer committed 2026-05-27 01:07:48 +05:30
1 parent 1702fd1c5a
commit 967e415b0a
3 files changed
+168 -149

No files matched your search

@@ -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 (
<Stack
direction="row"
gap={theme.spacing(LAYOUT.XS)}
width="100%"
>
{Array.from({ length: steps }).map((_, index) => {
const background =
index < current
? completedBg
: index === current
? activeBg
: theme.palette.divider;
return (
<Box
key={index}
flex={1}
height={theme.spacing(LAYOUT.XXS)}
borderRadius={theme.shape.borderRadius}
sx={{ background, transition: "background 0.2s ease" }}
/>
);
})}
{Array.from({ length: steps }).map((_, index) => (
<Box
key={index}
flex={1}
height={theme.spacing(LAYOUT.XXS)}
borderRadius={theme.shape.borderRadius}
sx={{
background: getSegmentBackground(index),
transition: "background 0.2s ease",
}}
/>
))}
</Stack>
);
};
+117 -110
View File
@@ -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]);
};
+33 -22
View File
@@ -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<MonitorFormData>[][] = [
@@ -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 && (
<StepProgress
steps={TOTAL_STEPS}
steps={totalSteps}
current={currentStep}
/>
)}
@@ -1351,9 +1362,9 @@ const CreateMonitorPage = () => {
<Stack
direction="row"
justifyContent={isWizard ? "space-between" : "flex-end"}
justifyContent={showTypeSelector ? "space-between" : "flex-end"}
>
{isWizard && (
{showTypeSelector && (
<Button
type="button"
variant="outlined"
@@ -1364,7 +1375,7 @@ const CreateMonitorPage = () => {
{t("common.buttons.back")}
</Button>
)}
{isWizard && currentStep < TOTAL_STEPS - 1 ? (
{showTypeSelector && currentStep < totalSteps - 1 ? (
<Button
key="wizard-next"
type="button"