//Components import { Box, Button, ButtonGroup, FormControlLabel, Stack, Switch, Tooltip, Typography, } from "@mui/material"; import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; import Select from "@/Components/v1/Inputs/Select/index.jsx"; import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; import Dialog from "@/Components/v1/Dialog/index.jsx"; import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; import SkeletonLayout from "./skeleton.jsx"; // Utils import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { monitorValidation } from "../../../Validation/validation.js"; import { createToast } from "../../../Utils/toastUtils.jsx"; import Icon from "@/Components/v1/Icon"; import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; import { useParams } from "react-router-dom"; import { useCreateMonitor, useDeleteMonitor, useUpdateMonitor, usePauseMonitor, useFetchMonitorById, useFetchMonitorGames, } from "../../../Hooks/monitorHooks.js"; /** * Create page renders monitor creation or configuration views. * @component */ const UptimeCreate = ({ isClone = false }) => { const { monitorId } = useParams(); const isCreate = typeof monitorId === "undefined" || isClone; // States const [monitor, setMonitor] = useState({ type: "http", statusWindowSize: 5, statusWindowThreshold: 60, matchMethod: "equal", expectedValue: "", jsonPath: "", notifications: [], interval: 60000, ignoreTlsErrors: false, ...(isCreate ? { url: "", name: "" } : { port: undefined }), }); const [errors, setErrors] = useState({}); const [https, setHttps] = useState(true); const [isOpen, setIsOpen] = useState(false); const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false); const [games, setGames] = useState({}); const triggerUpdate = () => { setUpdateTrigger(!updateTrigger); }; // Hooks const [notifications, notificationsAreLoading, notificationsError] = useGetNotificationsByTeamId(); const { determineState, statusColor } = useMonitorUtils(); // Fetch monitor details const [isFetchingMonitor] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger: true, }); // Fetch games const [isFetchingGames] = useFetchMonitorGames({ setGames, triggerUpdate: true, }); // Combine the loading states const isLoading = isFetchingMonitor || isFetchingGames; const [createMonitor, isCreating] = useCreateMonitor(); const [pauseMonitor, isPausing] = usePauseMonitor({}); const [deleteMonitor, isDeleting] = useDeleteMonitor(); const [updateMonitor, isUpdating] = useUpdateMonitor(); // Setup const theme = useTheme(); const { t } = useTranslation(); // Constants const MS_PER_MINUTE = 60000; const FREQUENCIES = [ { _id: 0.25, name: t("time.fifteenSeconds") }, { _id: 0.5, name: t("time.thirtySeconds") }, { _id: 1, name: t("time.oneMinute") }, { _id: 2, name: t("time.twoMinutes") }, { _id: 3, name: t("time.threeMinutes") }, { _id: 4, name: t("time.fourMinutes") }, { _id: 5, name: t("time.fiveMinutes") }, { _id: 10, name: t("time.tenMinutes") }, { _id: 15, name: t("time.fifteenMinutes") }, { _id: 30, name: t("time.thirtyMinutes") }, ]; const GAMELIST = Object.entries(games).map(([key, value]) => ({ _id: key, name: value.name, })); const CRUMBS = [ { name: "uptime", path: "/uptime" }, ...(isCreate ? [{ name: "create", path: `/uptime/create` }] : [ { name: "details", path: `/uptime/${monitorId}` }, { name: "configure", path: `/uptime/configure/${monitorId}` }, ]), ]; const matchMethodOptions = [ { _id: "equal", name: t("matchMethodOptions.equal") }, { _id: "include", name: t("matchMethodOptions.include") }, { _id: "regex", name: t("matchMethodOptions.regex") }, ]; const expectedValuePlaceholders = { regex: t("matchMethodOptions.regexPlaceholder"), equal: t("matchMethodOptions.equalPlaceholder"), include: t("matchMethodOptions.includePlaceholder"), }; const monitorTypeMaps = { http: { label: t("monitorType.http.label"), placeholder: t("monitorType.http.placeholder"), namePlaceholder: t("monitorType.http.namePlaceholder"), }, ping: { label: t("monitorType.ping.label"), placeholder: t("monitorType.ping.placeholder"), namePlaceholder: t("monitorType.ping.namePlaceholder"), }, docker: { label: t("monitorType.docker.label"), placeholder: t("monitorType.docker.placeholder"), namePlaceholder: t("monitorType.docker.namePlaceholder"), }, port: { label: t("monitorType.port.label"), placeholder: t("monitorType.port.placeholder"), namePlaceholder: t("monitorType.port.namePlaceholder"), }, game: { label: t("monitorType.game.label"), placeholder: t("monitorType.game.placeholder"), namePlaceholder: t("monitorType.game.namePlaceholder"), }, }; // Handlers const onSubmit = async (event) => { event.preventDefault(); const { notifications, ...rest } = monitor; let form = {}; if (isCreate) { form = { url: monitor.type === "http" && !isClone ? `http${https ? "s" : ""}://` + monitor.url : monitor.url, name: monitor.name || monitor.url.substring(0, 50), statusWindowSize: monitor.statusWindowSize, statusWindowThreshold: monitor.statusWindowThreshold, type: monitor.type, port: monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, interval: monitor.interval, matchMethod: monitor.matchMethod, expectedValue: monitor.expectedValue, jsonPath: monitor.jsonPath, ignoreTlsErrors: monitor.ignoreTlsErrors, gameId: monitor.gameId || undefined, }; } else { form = { id: monitor.id, url: monitor.url, name: monitor.name || monitor.url.substring(0, 50), statusWindowSize: monitor.statusWindowSize, statusWindowThreshold: monitor.statusWindowThreshold, type: monitor.type, matchMethod: monitor.matchMethod, expectedValue: monitor.expectedValue, jsonPath: monitor.jsonPath, interval: monitor.interval, teamId: monitor.teamId, userId: monitor.userId, port: monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, ignoreTlsErrors: monitor.ignoreTlsErrors, gameId: monitor.gameId || undefined, }; } if (!useAdvancedMatching) { form.matchMethod = isCreate ? undefined : ""; form.expectedValue = isCreate ? undefined : ""; form.jsonPath = isCreate ? undefined : ""; } const { error } = monitorValidation.validate(form, { abortEarly: false, }); if (error) { const newErrors = {}; error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); setErrors(newErrors); createToast({ body: t("checkFormError") }); return; } form = { ...form, description: monitor.name || monitor.url, notifications: monitor.notifications, }; if (isCreate) { await createMonitor({ monitor: form, redirect: "/uptime" }); } else { await updateMonitor({ monitor: form, redirect: "/uptime" }); } }; const onChange = (event) => { let { name, value, checked } = event.target; if (name === "ignoreTlsErrors") { value = checked; } if (name === "useAdvancedMatching") { setUseAdvancedMatching(checked); return; } if (name === "interval") { value = value * MS_PER_MINUTE; } setMonitor((prev) => ({ ...prev, [name]: value })); if (name === "type") { setErrors({}); return; } const { error } = monitorValidation.validate( { type: monitor.type, [name]: value }, { abortEarly: false } ); setErrors((prev) => ({ ...prev, ...(error && error.details[0].path[0] === name ? { [name]: error.details[0].message } : { [name]: undefined }), })); }; const handlePause = async () => { await pauseMonitor({ monitorId, triggerUpdate }); }; const handleRemove = async (event) => { event.preventDefault(); const TEMP_MONITOR = { id: monitor.id }; await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" }); }; const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; const displayInterval = monitor?.interval / MS_PER_MINUTE || 1; const parsedUrl = monitor?.url; const protocol = parsedUrl?.protocol?.replace(":", "") || ""; useEffect(() => { if (!isCreate || isClone) { if (monitor.matchMethod) { setUseAdvancedMatching(true); } else { setUseAdvancedMatching(false); } } }, [monitor, isCreate]); if (Object.keys(monitor).length === 0) { return ; } return ( {!isCreate ? monitor.name : t("createYour") + " "} {isCreate && ( {t("monitor")} )} {!isCreate && ( {monitor.url?.replace(/^https?:\/\//, "") || "..."} {t("editing")} )} {!isCreate && ( )} {isCreate && ( {t("distributedUptimeCreateChecks")} {t("distributedUptimeCreateChecksDescription")} {monitor.type === "http" ? ( ) : ( "" )} {errors["type"] ? ( {errors["type"]} ) : ( "" )} )} {t("settingsGeneralSettings")} {isCreate ? t(`uptimeGeneralInstructions.${monitor.type}`) : t("distributedUptimeCreateSelectURL")} ) } helperText={errors["url"]} onChange={onChange} disabled={!isCreate} />