import { useMemo, useState } from "react"; import { useEffect } from "react"; import { logger } from "@/Utils/logger"; import { ALL_HTTP_STATUS_CODES } from "@/Utils/statusCode"; import { useParams, useLocation, useNavigate } from "react-router"; import { useForm, FormProvider } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTheme } from "@mui/material"; import Stack from "@mui/material/Stack"; import { Trans, useTranslation } from "react-i18next"; import Typography from "@mui/material/Typography"; import Link from "@mui/material/Link"; import { HeaderDeleteControls } from "@/Components/monitors"; import { GeoContinents } from "@/Types/GeoCheck"; import Box from "@mui/material/Box"; import { BasePage, ColoredLabel, ConfigBox, StepProgress, } from "@/Components/design-elements"; import { Button, Dialog } from "@/Components/inputs"; import { 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, type HttpMethod, supportsGeoCheck, DefaultPageSpeedStrategy, DefaultHttpMethod, HttpMethods, SelectableMonitorTypes, monitorTypeLabelKey, PageSpeedStrategies, DnsRecordTypes, MonitorIntervalOptions, DefaultMonitorMatchMethod, MonitorMatchMethods, GeoCheckIntervalOptions, ProxyModes, type ProxyMode, } from "@/Types/Monitor"; import type { Notification } from "@/Types/Notification"; import type { Tag } from "@/Types/Tag"; import { stepFieldsFor, monitorStepCount, type MonitorFormData, } from "@/Validation/monitor"; import { FormNumberField } from "@/Components/inputs/forms/FormNumberField"; import { FormTextField } from "@/Components/inputs/forms/FormTextField"; import { FormRadioGroup } from "@/Components/inputs/forms/FormRadioGroupField"; import { FormMultiSelectField } from "@/Components/inputs/forms/FormMultiSelectField"; import { FormSelectField } from "@/Components/inputs/forms/FormSelectField"; import { FormSliderField } from "@/Components/inputs/forms/FormSliderField"; import { FormSwitchField } from "@/Components/inputs/forms/FormSwitchField"; import type { ProxyResponse } from "@/Types/Proxy"; const httpMethodOptions = HttpMethods.map((method) => ({ value: method, label: method, })); interface GeneralSettingsConfig { urlLabel: string; urlPlaceholder: string; namePlaceholder: string; showUrl: boolean; showProxy: 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 = { 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, showProxy: 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, showProxy: false, 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, showProxy: false, 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, showProxy: false, 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, showProxy: false, 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, showProxy: false, 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, showProxy: false, 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, showProxy: false, 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, showProxy: false, 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, showProxy: false, 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( isEditMode ? `/monitors/${monitorId}` : null ); const { data: notifications } = useGet("/notifications/team"); const { data: games } = useGet("/monitors/games"); const { data: tags } = useGet("/tags/team"); const { data: proxies } = useGet("/proxies/team"); const { data: appSettings } = useGet<{ globalProxyEnabled?: boolean; globalProxyId?: string | null; }>("/settings"); const { schema, defaults } = useMonitorForm({ data: existingMonitor ?? null, defaultType, }); const form = useForm({ resolver: zodResolver(schema), defaultValues: defaults, }); const { watch, handleSubmit, clearErrors, trigger, reset, setValue } = 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 watchedMethod = watch("method") as HttpMethod | undefined; const watchedProxyMode = watch("proxyMode") as ProxyMode | undefined; 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 isWizardStep = !isEditMode && currentStep < totalSteps - 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(); const { patch, loading: isUpdating } = usePatch(); 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); }; const handleFormSubmit = isWizardStep ? (event: React.FormEvent) => { event.preventDefault(); void handleNext(); } : handleSubmit(onSubmit, onError); const notificationOptions = useMemo(() => { return (notifications ?? []).map((n) => ({ ...n, name: n.notificationName, })); }, [notifications]); const locationOptions = useMemo( () => GeoContinents.map((continent) => ({ id: continent, name: t( `pages.createMonitor.form.geoChecks.option.locations.options.${continent}` ), })), [t] ); const typeOptions = useMemo( () => SelectableMonitorTypes.map((type) => ({ value: type, label: t(`pages.common.monitors.monitorTypes.${monitorTypeLabelKey[type]}`), description: t( `pages.createMonitor.form.type.${monitorTypeLabelKey[type]}Description` ), })), [t] ); const strategyOptions = useMemo( () => PageSpeedStrategies.map((strategy) => ({ value: strategy, label: t(`pages.createMonitor.form.general.option.strategy.${strategy}`), })), [t] ); const gameOptions = useMemo( () => [ { value: "", label: t("pages.createMonitor.form.general.option.game.placeholder"), }, ...Object.entries(games ?? {}).map(([key, game]) => ({ value: key, label: game.name, })), ], [games, t] ); const dnsRecordTypeOptions = useMemo( () => DnsRecordTypes.map((recordType) => ({ value: recordType, label: t( `pages.createMonitor.form.general.option.dnsRecordType.value.${recordType}` ), })), [t] ); const intervalOptions = useMemo( () => MonitorIntervalOptions.map((option) => ({ value: option.value, label: t( `pages.createMonitor.form.frequency.option.frequency.value.${option.labelKey}` ), })), [t] ); const matchMethodOptions = useMemo( () => MonitorMatchMethods.map((method) => ({ value: method, label: t(`pages.createMonitor.form.advanced.option.matchMethod.${method}`), })), [t] ); const geoCheckIntervalOptions = useMemo( () => GeoCheckIntervalOptions.map((option) => ({ value: option.value, label: t( `pages.createMonitor.form.geoChecks.option.interval.value.${option.labelKey}` ), })), [t] ); const proxyModeOptions = useMemo( () => ProxyModes.map((mode) => ({ value: mode, label: t(`pages.createMonitor.form.general.option.proxyMode.value.${mode}`), })), [t] ); const proxyOptions = useMemo( () => (proxies ?? []).map((proxy) => ({ value: proxy.id, label: `${proxy.name} (${proxy.host}:${proxy.port})`, })), [proxies] ); const globalProxyName = appSettings?.globalProxyEnabled === true ? proxies?.find((proxy) => proxy.id === appSettings?.globalProxyId)?.name : undefined; return ( {!isEditMode && ( )} {/* Monitor Type Selection - only shown for uptime monitors */} {showTypeSelector && showStep(0) && ( } /> )} {showStep(0) && ( ), }} /> } rightContent={ {/* URL/Host/Container field - not shown for hardware */} {generalSettingsConfig.showUrl && ( )} {/* Proxy fields - only shown for HTTP */} {generalSettingsConfig.showProxy && ( <> {watchedProxyMode === "inherit" && ( {globalProxyName ? t( "pages.createMonitor.form.general.option.proxyMode.effect.global", { name: globalProxyName } ) : t( "pages.createMonitor.form.general.option.proxyMode.effect.direct" )} )} {watchedProxyMode === "custom" && ( )} )} {/* Strategy field - only for pagespeed type */} {generalSettingsConfig.showStrategy && ( )} {/* Port field - only for port and game types */} {generalSettingsConfig.showPort && ( )} {/* Game select - only for game type */} {generalSettingsConfig.showGameSelect && ( )} {/* gRPC Service Name field - only for grpc type */} {generalSettingsConfig.showGrpcServiceName && ( )} {/* Secret field - only for hardware type */} {generalSettingsConfig.showSecret && ( )} {generalSettingsConfig.showDnsServer && ( )} {generalSettingsConfig.showDnsRecordType && ( )} } /> )} {showStep(1) && ( } /> )} {/* Alert Thresholds - only for hardware type */} {watchedType === "hardware" && showStep(1) && ( `${value}%`} /> `${value}%`} /> `${value}%`} /> `${value}°C`} /> } /> )} {showStep(1) && ( } /> )} {showStep(1) && ( } /> )} {showStep(1) && ( ( )} renderOptionContent={(option) => ( )} /> } /> )} {showStep(2) && (watchedType === "http" || watchedType === "grpc" || watchedType === "websocket") && ( } /> )} {showStep(2) && watchedType === "http" && ( { // HEAD has no response body, reset advanced matching fields if selected if (value === "HEAD") { setValue("useAdvancedMatching", false); setValue("matchMethod", DefaultMonitorMatchMethod); setValue("expectedValue", ""); setValue("jsonPath", ""); } }} options={httpMethodOptions} /> {t( `pages.createMonitor.form.advanced.option.method.description.${watchedMethod}` )} {watchedMethod !== "HEAD" && ( )} {watchedUseAdvancedMatching && watchedMethod !== "HEAD" && ( ), }} /> )} } /> )} {showStep(2) && supportsGeoCheck(watchedType) && ( {watchGeoCheckEnabled && ( )} } /> )} {!isEditMode && ( )} {!isEditMode && currentStep < totalSteps - 1 ? ( ) : ( )} ); }; export default CreateMonitorPage;