Files
Checkmate/client/src/Pages/CreateMonitor/index.tsx
T

1386 lines
41 KiB
TypeScript

import { useMemo, useState } from "react";
import { useEffect } from "react";
import { logger } from "@/Utils/logger";
import { useParams, useLocation, useNavigate } from "react-router";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTheme } from "@mui/material";
import Stack from "@mui/material/Stack";
import RadioGroup from "@mui/material/RadioGroup";
import FormControl from "@mui/material/FormControl";
import { Trans, useTranslation } from "react-i18next";
import MenuItem from "@mui/material/MenuItem";
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton";
import { Trash2 } from "lucide-react";
import { HeaderDeleteControls } from "@/Components/monitors";
import { GeoContinents } from "@/Types/GeoCheck";
import {
BasePage,
ColoredLabel,
ConfigBox,
StepProgress,
} from "@/Components/design-elements";
import {
RadioWithDescription,
Button,
TextField,
Select,
Autocomplete,
SwitchComponent as Switch,
SliderWithLabel,
Dialog,
} from "@/Components/inputs";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
import { useMonitorForm, getMonitorDefaults } from "@/Hooks/useMonitorForm";
import {
type Monitor,
type MonitorType,
type GamesMap,
supportsGeoCheck,
DefaultPageSpeedStrategy,
} from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { Tag } from "@/Types/Tag";
import {
stepFieldsFor,
monitorStepCount,
type MonitorFormData,
} from "@/Validation/monitor";
interface GeneralSettingsConfig {
urlLabel: string;
urlPlaceholder: string;
namePlaceholder: string;
showUrl: boolean;
showPort: boolean;
showGameSelect: boolean;
showSecret: boolean;
showGrpcServiceName: boolean;
showIgnoreTls: boolean;
showStrategy: boolean;
showDnsServer: boolean;
showDnsRecordType: boolean;
}
const getGeneralSettingsConfig = (
type: MonitorType,
t: (key: string) => string
): GeneralSettingsConfig => {
const configs: Record<string, GeneralSettingsConfig> = {
http: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
ping: {
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.host.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
docker: {
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.container.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
port: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: true,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
game: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: true,
showGameSelect: true,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
grpc: {
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.host.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: true,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: true,
showIgnoreTls: true,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
pagespeed: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: true,
showDnsServer: false,
showDnsRecordType: false,
},
hardware: {
urlLabel: t("pages.createMonitor.form.general.option.captureUrl.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.captureUrl.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: true,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
websocket: {
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.wsUrl.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: true,
showStrategy: false,
showDnsServer: false,
showDnsRecordType: false,
},
dns: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: false,
showStrategy: false,
showDnsServer: true,
showDnsRecordType: true,
},
};
return configs[type] || configs.http;
};
const CreateMonitorPage = () => {
const theme = useTheme();
const { t } = useTranslation();
const { monitorId } = useParams();
const location = useLocation();
const navigate = useNavigate();
const isEditMode = Boolean(monitorId);
// Extract page type from URL path (e.g., /pagespeed/create -> pagespeed)
const pageType = useMemo(() => {
const pathSegments = location.pathname.split("/").filter(Boolean);
const firstSegment = pathSegments[0];
if (firstSegment === "pagespeed") return "pagespeed";
if (firstSegment === "infrastructure") return "hardware";
return "uptime";
}, [location.pathname]);
const showTypeSelector = pageType === "uptime" && !isEditMode;
const defaultType: MonitorType =
pageType === "pagespeed"
? "pagespeed"
: pageType === "hardware"
? "hardware"
: "http";
const { data: existingMonitor, refetch: refetchMonitor } = useGet<Monitor>(
isEditMode ? `/monitors/${monitorId}` : null
);
const { data: notifications } = useGet<Notification[]>("/notifications/team");
const { data: games } = useGet<GamesMap>("/monitors/games");
const { data: tags } = useGet<Tag[]>("/tags/team");
const { schema, defaults } = useMonitorForm({
data: existingMonitor ?? null,
defaultType,
});
const form = useForm<MonitorFormData>({
resolver: zodResolver(schema),
defaultValues: defaults,
});
const { control, watch, handleSubmit, clearErrors, trigger, reset } = form;
useEffect(() => {
reset(defaults);
}, [defaults, reset]);
// Multi-step wizard on every create flow; edit mode keeps the flat form
// where showStep() is always true.
const [currentStep, setCurrentStep] = useState(0);
const showStep = (step: number) => isEditMode || 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 = monitorStepCount(watchedType);
const handleNext = async () => {
const isValid = await trigger(stepFieldsFor(watchedType, currentStep));
if (isValid) {
setCurrentStep((step) => step + 1);
}
};
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(() => {
if (showTypeSelector) {
reset(getMonitorDefaults(watchedType));
} else {
clearErrors();
}
}, [watchedType, showTypeSelector, reset, clearErrors]);
const generalSettingsConfig = useMemo(
() => getGeneralSettingsConfig(watchedType, t),
[watchedType, t]
);
const { post, loading: isCreating } = usePost<MonitorFormData, Monitor>();
const { patch, loading: isUpdating } = usePatch<MonitorFormData, Monitor>();
const isSubmitting = isCreating || isUpdating;
// Delete functionality
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const { deleteFn, loading: isDeleting } = useDelete();
const handleDeleteClick = () => {
setIsDeleteDialogOpen(true);
};
const handleDeleteConfirm = async () => {
if (!monitorId) return;
await deleteFn(`/monitors/${monitorId}`);
setIsDeleteDialogOpen(false);
// Navigate based on page type
if (pageType === "pagespeed") {
navigate("/pagespeed");
} else if (pageType === "hardware") {
navigate("/infrastructure");
} else {
navigate("/uptime");
}
};
const handleDeleteCancel = () => {
setIsDeleteDialogOpen(false);
};
const onSubmit = async (data: MonitorFormData) => {
let result;
if (isEditMode && monitorId) {
result = await patch(`/monitors/${monitorId}`, data);
} else {
result = await post("/monitors", data);
}
if (result?.success) {
if (pageType === "pagespeed") {
navigate("/pagespeed");
} else if (pageType === "hardware") {
navigate("/infrastructure");
} else {
navigate("/uptime");
}
}
};
const onError = (errors: unknown) => {
logger.debug("Monitor creation validation errors", errors);
};
return (
<BasePage
component="form"
onSubmit={handleSubmit(onSubmit, onError)}
>
<HeaderDeleteControls
monitor={existingMonitor}
isAdmin={true}
refetch={refetchMonitor}
onDelete={handleDeleteClick}
/>
{!isEditMode && (
<StepProgress
steps={totalSteps}
current={currentStep}
/>
)}
{/* Monitor Type Selection - only shown for uptime monitors */}
{showTypeSelector && showStep(0) && (
<ConfigBox
title={t("pages.createMonitor.form.type.title")}
subtitle={t("pages.createMonitor.form.type.description")}
rightContent={
<Controller
name="type"
control={control}
render={({ field, fieldState }) => (
<FormControl error={!!fieldState.error}>
<RadioGroup
{...field}
sx={{ gap: theme.spacing(LAYOUT.MD) }}
>
<RadioWithDescription
value="http"
label={t("pages.common.monitors.monitorTypes.optionHttp")}
description={t(
"pages.createMonitor.form.type.optionHttpDescription"
)}
/>
<RadioWithDescription
value="ping"
label={t("pages.common.monitors.monitorTypes.optionPing")}
description={t(
"pages.createMonitor.form.type.optionPingDescription"
)}
/>
<RadioWithDescription
value="docker"
label={t("pages.common.monitors.monitorTypes.optionDocker")}
description={t(
"pages.createMonitor.form.type.optionDockerDescription"
)}
/>
<RadioWithDescription
value="port"
label={t("pages.common.monitors.monitorTypes.optionPort")}
description={t(
"pages.createMonitor.form.type.optionPortDescription"
)}
/>
<RadioWithDescription
value="game"
label={t("pages.common.monitors.monitorTypes.optionGame")}
description={t(
"pages.createMonitor.form.type.optionGameDescription"
)}
/>
<RadioWithDescription
value="grpc"
label={t("pages.common.monitors.monitorTypes.optionGrpc")}
description={t(
"pages.createMonitor.form.type.optionGrpcDescription"
)}
/>
<RadioWithDescription
value="websocket"
label={t("pages.common.monitors.monitorTypes.optionWebSocket")}
description={t(
"pages.createMonitor.form.type.optionWebSocketDescription"
)}
/>
<RadioWithDescription
value="dns"
label={t("pages.common.monitors.monitorTypes.optionDns")}
description={t(
"pages.createMonitor.form.type.optionDnsDescription"
)}
/>
</RadioGroup>
</FormControl>
)}
/>
}
/>
)}
{showStep(0) && (
<ConfigBox
title={t("pages.createMonitor.form.general.title")}
subtitle={
<Trans
i18nKey={`pages.createMonitor.form.general.description.${watchedType}`}
components={{
gamedigLink: (
<Link
href="https://github.com/gamedig/node-gamedig/blob/master/GAMES_LIST.md"
target="_blank"
rel="noopener noreferrer"
/>
),
}}
/>
}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
{/* URL/Host/Container field - not shown for hardware */}
{generalSettingsConfig.showUrl && (
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={generalSettingsConfig.urlLabel}
placeholder={generalSettingsConfig.urlPlaceholder}
fullWidth
disabled={isEditMode}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
)}
{/* Strategy field - only for pagespeed type */}
{generalSettingsConfig.showStrategy && (
<Controller
name="strategy"
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? DefaultPageSpeedStrategy}
fieldLabel={t(
"pages.createMonitor.form.general.option.strategy.label"
)}
error={!!fieldState.error}
>
<MenuItem value="desktop">
{t("pages.createMonitor.form.general.option.strategy.desktop")}
</MenuItem>
<MenuItem value="mobile">
{t("pages.createMonitor.form.general.option.strategy.mobile")}
</MenuItem>
</Select>
)}
/>
)}
{/* Port field - only for port and game types */}
{generalSettingsConfig.showPort && (
<Controller
name="port"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value === 0 ? "" : field.value}
onChange={(e) => {
const val = e.target.value;
field.onChange(val === "" ? 0 : Number(val));
}}
type="number"
fieldLabel={t("pages.createMonitor.form.general.option.port.label")}
placeholder={t(
"pages.createMonitor.form.general.option.port.placeholder"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
)}
{/* Game select - only for game type */}
{generalSettingsConfig.showGameSelect && (
<Controller
name="gameId"
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? ""}
fieldLabel={t("pages.createMonitor.form.general.option.game.label")}
error={!!fieldState.error}
>
<MenuItem value="">
{t(
"pages.createMonitor.form.general.option.game.placeholder"
)}{" "}
</MenuItem>
{games &&
Object.entries(games).map(([key, game]) => (
<MenuItem
key={key}
value={key}
>
{game.name}
</MenuItem>
))}
</Select>
)}
/>
)}
{/* gRPC Service Name field - only for grpc type */}
{generalSettingsConfig.showGrpcServiceName && (
<Controller
name="grpcServiceName"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
type="text"
fieldLabel={t(
"pages.createMonitor.form.general.option.grpcServiceName.label"
)}
placeholder={t(
"pages.createMonitor.form.general.option.grpcServiceName.placeholder"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
)}
{/* Secret field - only for hardware type */}
{generalSettingsConfig.showSecret && (
<Controller
name="secret"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
type="text"
fieldLabel={t(
"pages.createMonitor.form.general.option.secret.label"
)}
placeholder={t(
"pages.createMonitor.form.general.option.secret.placeholder"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
)}
{generalSettingsConfig.showDnsServer && (
<Controller
name="dnsServer"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
type="text"
fieldLabel={t(
"pages.createMonitor.form.general.option.dnsServer.label"
)}
placeholder={t(
"pages.createMonitor.form.general.option.dnsServer.placeholder"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
)}
{generalSettingsConfig.showDnsRecordType && (
<Controller
name="dnsRecordType"
control={control}
defaultValue="A"
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? "A"}
fieldLabel={t(
"pages.createMonitor.form.general.option.dnsRecordType.label"
)}
error={!!fieldState.error}
>
<MenuItem value={"A"}>
{t(
"pages.createMonitor.form.general.option.dnsRecordType.value.A"
)}
</MenuItem>
<MenuItem value={"AAAA"}>
{t(
"pages.createMonitor.form.general.option.dnsRecordType.value.AAAA"
)}
</MenuItem>
<MenuItem value={"CNAME"}>
{t(
"pages.createMonitor.form.general.option.dnsRecordType.value.CNAME"
)}
</MenuItem>
<MenuItem value={"MX"}>
{t(
"pages.createMonitor.form.general.option.dnsRecordType.value.MX"
)}
</MenuItem>
<MenuItem value={"NS"}>
{t(
"pages.createMonitor.form.general.option.dnsRecordType.value.NS"
)}
</MenuItem>
<MenuItem value={"TXT"}>
{t(
"pages.createMonitor.form.general.option.dnsRecordType.value.TXT"
)}
</MenuItem>
</Select>
)}
/>
)}
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.createMonitor.form.general.option.name.label")}
placeholder={generalSettingsConfig.namePlaceholder}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
)}
{showStep(1) && (
<ConfigBox
title={t("pages.createMonitor.form.frequency.title")}
subtitle={t("pages.createMonitor.form.frequency.description")}
rightContent={
<Controller
name="interval"
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? 60000}
fieldLabel={t(
"pages.createMonitor.form.frequency.option.frequency.label"
)}
error={!!fieldState.error}
>
<MenuItem value={15000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.fifteenSeconds"
)}
</MenuItem>
<MenuItem value={30000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.thirtySeconds"
)}
</MenuItem>
<MenuItem value={60000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.oneMinute"
)}
</MenuItem>
<MenuItem value={120000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.twoMinutes"
)}
</MenuItem>
<MenuItem value={180000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.threeMinutes"
)}
</MenuItem>
<MenuItem value={240000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.fourMinutes"
)}
</MenuItem>
<MenuItem value={300000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.fiveMinutes"
)}
</MenuItem>
<MenuItem value={600000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.tenMinutes"
)}
</MenuItem>
<MenuItem value={900000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.fifteenMinutes"
)}
</MenuItem>
<MenuItem value={1800000}>
{t(
"pages.createMonitor.form.frequency.option.frequency.value.thirtyMinutes"
)}
</MenuItem>
</Select>
)}
/>
}
/>
)}
{/* Alert Thresholds - only for hardware type */}
{generalSettingsConfig.showSecret && showStep(1) && (
<ConfigBox
title={t("pages.createMonitor.form.thresholds.title")}
subtitle={t("pages.createMonitor.form.thresholds.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="cpuAlertThreshold"
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
fieldLabel={t(
"pages.createMonitor.form.thresholds.option.cpuThreshold.label"
)}
min={0}
max={100}
step={1}
valueLabelDisplay="auto"
valueLabelFormat={(value) => `${value}%`}
/>
)}
/>
<Controller
name="memoryAlertThreshold"
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
fieldLabel={t(
"pages.createMonitor.form.thresholds.option.memoryThreshold.label"
)}
min={0}
max={100}
step={1}
valueLabelDisplay="auto"
valueLabelFormat={(value) => `${value}%`}
/>
)}
/>
<Controller
name="diskAlertThreshold"
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
fieldLabel={t(
"pages.createMonitor.form.thresholds.option.diskThreshold.label"
)}
min={0}
max={100}
step={1}
valueLabelDisplay="auto"
valueLabelFormat={(value) => `${value}%`}
/>
)}
/>
<Controller
name="tempAlertThreshold"
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
fieldLabel={t(
"pages.createMonitor.form.thresholds.option.tempThreshold.label"
)}
min={0}
max={100}
step={1}
valueLabelDisplay="auto"
valueLabelFormat={(value) => `${value}°C`}
/>
)}
/>
</Stack>
}
/>
)}
{showStep(1) && (
<ConfigBox
title={t("pages.createMonitor.form.incidents.title")}
subtitle={t("pages.createMonitor.form.incidents.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="statusWindowSize"
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
fieldLabel={t(
"pages.createMonitor.form.incidents.option.checks.label"
)}
min={1}
max={25}
valueLabelDisplay="auto"
/>
)}
/>
<Controller
name="statusWindowThreshold"
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
fieldLabel={t(
"pages.createMonitor.form.incidents.option.percentage.label"
)}
min={1}
max={100}
valueLabelDisplay="auto"
/>
)}
/>
</Stack>
}
/>
)}
{showStep(1) && (
<ConfigBox
title={t("pages.createMonitor.form.notifications.title")}
subtitle={t("pages.createMonitor.form.notifications.description")}
rightContent={
<Controller
name="notifications"
control={control}
render={({ field }) => {
// Map notifications to have 'name' property for Autocomplete
const notificationOptions = (notifications ?? []).map((n) => ({
...n,
name: n.notificationName,
}));
const selectedNotifications = notificationOptions.filter((n) =>
(field.value ?? []).includes(n.id)
);
return (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Autocomplete
multiple
options={notificationOptions}
value={selectedNotifications}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: typeof notificationOptions) => {
field.onChange(newValue.map((n) => n.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
/>
{selectedNotifications.length > 0 && (
<Stack
flex={1}
width="100%"
>
{selectedNotifications.map((notification, index) => (
<Stack
direction="row"
alignItems="center"
key={notification.id}
width="100%"
>
<Typography flexGrow={1}>
{notification.notificationName}
</Typography>
<IconButton
size="small"
onClick={() => {
field.onChange(
(field.value ?? []).filter(
(id: string) => id !== notification.id
)
);
}}
aria-label="Remove notification"
>
<Trash2 size={16} />
</IconButton>
{index < selectedNotifications.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
);
}}
/>
}
/>
)}
{showStep(1) && (
<ConfigBox
title={t("pages.createMonitor.form.tags.title")}
subtitle={t("pages.createMonitor.form.tags.description")}
rightContent={
<Controller
name="tags"
control={control}
render={({ field }) => {
const tagOptions = tags ?? [];
const selectedTags = tagOptions.filter((tag) =>
(field.value ?? []).includes(tag.id)
);
return (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Autocomplete
multiple
options={tagOptions}
value={selectedTags}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: typeof tagOptions) => {
field.onChange(newValue.map((tag) => tag.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderOptionContent={(option) => (
<ColoredLabel
text={option.name}
color={option.color}
/>
)}
/>
{selectedTags.length > 0 && (
<Stack
flex={1}
gap={theme.spacing(SPACING.XL)}
width="100%"
>
{selectedTags.map((tag, index) => (
<Stack
direction="row"
justifyContent={"space-between"}
alignItems="center"
key={tag.id}
width="100%"
>
<ColoredLabel
text={tag.name}
color={tag.color}
/>
<div style={{ flexGrow: 1 }} />
<IconButton
size="small"
onClick={() => {
field.onChange(
(field.value ?? []).filter(
(id: string) => id !== tag.id
)
);
}}
aria-label="Remove tag"
>
<Trash2 size={16} />
</IconButton>
{index < selectedTags.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
);
}}
/>
}
/>
)}
{showStep(2) &&
(watchedType === "http" ||
watchedType === "grpc" ||
watchedType === "websocket") && (
<ConfigBox
title={t("pages.createMonitor.form.ignoreTls.title")}
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
rightContent={
<Controller
name="ignoreTlsErrors"
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(SPACING.LG)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t("pages.createMonitor.form.ignoreTls.option.tls.label")}
</Typography>
</Stack>
)}
/>
}
/>
)}
{showStep(2) && watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.advanced.title")}
subtitle={t("pages.createMonitor.form.advanced.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="useAdvancedMatching"
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(SPACING.LG)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t(
"pages.createMonitor.form.advanced.option.advancedMatching.label"
)}
</Typography>
</Stack>
)}
/>
{watchedUseAdvancedMatching && (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="matchMethod"
control={control}
render={({ field }) => (
<Select
{...field}
value={field.value ?? "equal"}
fieldLabel={t(
"pages.createMonitor.form.advanced.option.matchMethod.label"
)}
>
<MenuItem value="equal">
{t(
"pages.createMonitor.form.advanced.option.matchMethod.equal"
)}
</MenuItem>
<MenuItem value="include">
{t(
"pages.createMonitor.form.advanced.option.matchMethod.include"
)}
</MenuItem>
<MenuItem value="regex">
{t(
"pages.createMonitor.form.advanced.option.matchMethod.regex"
)}
</MenuItem>
</Select>
)}
/>
<Controller
name="expectedValue"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.createMonitor.form.advanced.option.expectedValue.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="jsonPath"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.createMonitor.form.advanced.option.jsonPath.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Typography
component="span"
color={theme.palette.text.secondary}
sx={{ opacity: 0.8 }}
>
<Trans
i18nKey="pages.createMonitor.form.advanced.option.jsonPath.description"
components={{
jmesLink: (
<Link
href="https://jmespath.org/"
target="_blank"
rel="noopener noreferrer"
/>
),
}}
/>
</Typography>
</Stack>
)}
</Stack>
}
/>
)}
{showStep(2) && supportsGeoCheck(watchedType) && (
<ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="geoCheckEnabled"
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(SPACING.LG)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t("pages.createMonitor.form.geoChecks.option.enabled.label")}
</Typography>
</Stack>
)}
/>
{watchGeoCheckEnabled && (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="geoCheckLocations"
control={control}
render={({ field }) => {
// Map continents to have 'name' property for Autocomplete
const locationOptions = GeoContinents.map((continent) => ({
id: continent,
name: t(
`pages.createMonitor.form.geoChecks.option.locations.options.${continent}`
),
}));
const selectedLocations = locationOptions.filter((loc) =>
(field.value ?? []).includes(loc.id)
);
return (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Autocomplete
multiple
options={locationOptions}
value={selectedLocations}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: typeof locationOptions) => {
field.onChange(newValue.map((loc) => loc.id));
}}
isOptionEqualToValue={(option, value) =>
option.id === value.id
}
fieldLabel={t(
"pages.createMonitor.form.geoChecks.option.locations.label"
)}
/>
{selectedLocations.length > 0 && (
<Stack
flex={1}
width="100%"
>
{selectedLocations.map((location, index) => (
<Stack
direction="row"
alignItems="center"
key={location.id}
width="100%"
>
<Typography flexGrow={1}>{location.name}</Typography>
<IconButton
size="small"
onClick={() => {
field.onChange(
(field.value ?? []).filter(
(id: string) => id !== location.id
)
);
}}
aria-label="Remove location"
>
<Trash2 size={16} />
</IconButton>
{index < selectedLocations.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
);
}}
/>
<Controller
name="geoCheckInterval"
control={control}
render={({ field }) => (
<Select
{...field}
value={field.value ?? 300000}
fieldLabel={t(
"pages.createMonitor.form.geoChecks.option.interval.label"
)}
>
<MenuItem value={300000}>
{t(
"pages.createMonitor.form.geoChecks.option.interval.value.fiveMinutes"
)}
</MenuItem>
<MenuItem value={600000}>
{t(
"pages.createMonitor.form.geoChecks.option.interval.value.tenMinutes"
)}
</MenuItem>
<MenuItem value={900000}>
{t(
"pages.createMonitor.form.geoChecks.option.interval.value.fifteenMinutes"
)}
</MenuItem>
<MenuItem value={1800000}>
{t(
"pages.createMonitor.form.geoChecks.option.interval.value.thirtyMinutes"
)}
</MenuItem>
</Select>
)}
/>
</Stack>
)}
</Stack>
}
/>
)}
<Stack
direction="row"
justifyContent={!isEditMode ? "space-between" : "flex-end"}
>
{!isEditMode && (
<Button
type="button"
variant="outlined"
color="secondary"
disabled={currentStep === 0}
onClick={handleBack}
>
{t("common.buttons.back")}
</Button>
)}
{!isEditMode && currentStep < totalSteps - 1 ? (
<Button
key="wizard-next"
type="button"
variant="contained"
color="primary"
onClick={handleNext}
>
{t("common.buttons.next")}
</Button>
) : (
<Button
key="wizard-save"
loading={isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{t("common.buttons.save")}
</Button>
)}
</Stack>
<Dialog
open={isDeleteDialogOpen}
title={t("common.dialogs.delete.title")}
content={t("common.dialogs.delete.description")}
onConfirm={handleDeleteConfirm}
onCancel={handleDeleteCancel}
loading={isDeleting}
/>
</BasePage>
);
};
export default CreateMonitorPage;