feat(client): split uptime monitor creation into a 3-step wizard
This commit is contained in:
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
@@ -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]);
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user