From 4b92dbd19b763d36a1a6a80461e34cfc441ca52f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 22:17:56 +0000 Subject: [PATCH 01/23] initial commit --- client/src/Components/v2/inputs/Radio.tsx | 88 +++++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Hooks/useMonitorForm.ts | 105 +++++++++ client/src/Pages/CreateMonitor/index.tsx | 266 ++++++++++++++++++++++ client/src/Routes/index.jsx | 12 + client/src/Validation/monitor.ts | 122 ++++++++++ client/src/locales/en.json | 31 +++ 7 files changed, 625 insertions(+) create mode 100644 client/src/Components/v2/inputs/Radio.tsx create mode 100644 client/src/Hooks/useMonitorForm.ts create mode 100644 client/src/Pages/CreateMonitor/index.tsx create mode 100644 client/src/Validation/monitor.ts diff --git a/client/src/Components/v2/inputs/Radio.tsx b/client/src/Components/v2/inputs/Radio.tsx new file mode 100644 index 000000000..ad8dc3374 --- /dev/null +++ b/client/src/Components/v2/inputs/Radio.tsx @@ -0,0 +1,88 @@ +import Radio from "@mui/material/Radio"; +import type { RadioProps } from "@mui/material/Radio"; +import { useTheme } from "@mui/material/styles"; +import { Circle, CircleDot } from "lucide-react"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Typography from "@mui/material/Typography"; + +interface RadioInputProps extends RadioProps {} + +export const RadioInput = ({ ...props }: RadioInputProps) => { + const theme = useTheme(); + return ( + + } + checkedIcon={ + + } + sx={{ + padding: 0, + mt: theme.spacing(0.5), + color: theme.palette.text.secondary, + "&.Mui-checked": { + color: theme.palette.primary.main, + "& svg circle": { + fill: theme.palette.primary.main, + }, + }, + "& .MuiSvgIcon-root": { + fontSize: 16, + }, + "& svg": { + stroke: "currentColor", + }, + "& svg path, & svg line, & svg polyline, & svg rect, & svg circle": { + stroke: "currentColor", + fill: "none", + }, + }} + /> + ); +}; + +export const RadioWithDescription = ({ + label, + description, + ...props +}: RadioInputProps & { label: string; description: string }) => { + const theme = useTheme(); + return ( + } + label={ + <> + {label} + + {description} + + + } + sx={{ + alignItems: "flex-start", + p: theme.spacing(2.5), + m: theme.spacing(-2.5), + borderRadius: theme.shape.borderRadius, + + "&:hover": { + backgroundColor: theme.palette.background.paper, + }, + "& .MuiButtonBase-root": { + p: 0, + mr: theme.spacing(6), + }, + }} + /> + ); +}; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 70e740dbd..a0646d394 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -9,3 +9,4 @@ export { ToggleButtonGroupInput as ToggleButtonGroup, } from "./ToggleButton"; export { DialogInput as Dialog } from "./Dialog"; +export * from "./Radio"; diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts new file mode 100644 index 000000000..2d7df97ab --- /dev/null +++ b/client/src/Hooks/useMonitorForm.ts @@ -0,0 +1,105 @@ +import { useMemo } from "react"; +import { monitorSchema, type MonitorFormData } from "@/Validation/monitor"; +import type { Monitor, MonitorType } from "@/Types/Monitor"; + +interface UseMonitorFormOptions { + data?: Monitor | null; + defaultType?: MonitorType; +} + +const getBaseDefaults = (data?: Monitor | null) => ({ + name: data?.name || "", + interval: data?.interval || 60000, + notifications: data?.notifications || [], + statusWindowSize: data?.statusWindowSize || 5, + statusWindowThreshold: data?.statusWindowThreshold || 60, +}); + +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, + 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 "pagespeed": + defaults = { + ...base, + type: "pagespeed", + url: data?.url || "", + }; + break; + case "hardware": + defaults = { + ...base, + type: "hardware", + url: data?.url || "", + secret: data?.secret || "", + cpuAlertThreshold: data?.cpuAlertThreshold ?? 80, + memoryAlertThreshold: data?.memoryAlertThreshold ?? 80, + diskAlertThreshold: data?.diskAlertThreshold ?? 80, + tempAlertThreshold: data?.tempAlertThreshold ?? 80, + selectedDisks: data?.selectedDisks || [], + }; + break; + default: + defaults = { + ...base, + type: "http", + url: "", + ignoreTlsErrors: false, + matchMethod: "", + expectedValue: "", + jsonPath: "", + }; + } + + return { schema: monitorSchema, defaults }; + }, [data, defaultType]); +}; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx new file mode 100644 index 000000000..80f818407 --- /dev/null +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -0,0 +1,266 @@ +import { useMemo } from "react"; +import { useEffect } from "react"; +import { useParams } 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 { useTranslation } from "react-i18next"; + +import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; +import { RadioWithDescription, Button, TextField, Select } from "@/Components/v2/inputs"; +import { useGet } from "@/Hooks/UseApi"; +import { useMonitorForm } from "@/Hooks/useMonitorForm"; +import type { Monitor, MonitorType } from "@/Types/Monitor"; +import type { MonitorFormData } from "@/Validation/monitor"; +import MenuItem from "@mui/material/MenuItem"; + +interface GeneralSettingsConfig { + urlLabel: string; + urlPlaceholder: string; + namePlaceholder: string; + showPort: boolean; + showGameSelect: boolean; +} + +const getGeneralSettingsConfig = ( + type: MonitorType, + t: (key: string) => string +): GeneralSettingsConfig => { + const configs: Record = { + http: { + urlLabel: t("monitorType.http.label"), + urlPlaceholder: t("monitorType.http.placeholder"), + namePlaceholder: t("monitorType.http.namePlaceholder"), + showPort: false, + showGameSelect: false, + }, + ping: { + urlLabel: t("monitorType.ping.label"), + urlPlaceholder: t("monitorType.ping.placeholder"), + namePlaceholder: t("monitorType.ping.namePlaceholder"), + showPort: false, + showGameSelect: false, + }, + docker: { + urlLabel: t("monitorType.docker.label"), + urlPlaceholder: t("monitorType.docker.placeholder"), + namePlaceholder: t("monitorType.docker.namePlaceholder"), + showPort: false, + showGameSelect: false, + }, + port: { + urlLabel: t("monitorType.port.label"), + urlPlaceholder: t("monitorType.port.placeholder"), + namePlaceholder: t("monitorType.port.namePlaceholder"), + showPort: true, + showGameSelect: false, + }, + game: { + urlLabel: t("monitorType.game.label"), + urlPlaceholder: t("monitorType.game.placeholder"), + namePlaceholder: t("monitorType.game.namePlaceholder"), + showPort: true, + showGameSelect: true, + }, + }; + return configs[type] || configs.http; +}; + +const CreateMonitorPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const { monitorId } = useParams(); + const isEditMode = Boolean(monitorId); + + const { data: existingMonitor } = useGet( + isEditMode ? `/monitors/${monitorId}` : null + ); + + const { schema, defaults } = useMonitorForm({ data: existingMonitor ?? null }); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + const { control, watch, handleSubmit, clearErrors } = form; + + useEffect(() => { + form.reset(defaults); + }, [defaults, form]); + + const watchedType = watch("type") as MonitorType; + + useEffect(() => { + clearErrors(); + }, [watchedType, clearErrors]); + + const generalSettingsConfig = useMemo( + () => getGeneralSettingsConfig(watchedType, t), + [watchedType, t] + ); + + const onSubmit = async (data: MonitorFormData) => { + console.log(data); + }; + + const onError = (errors: unknown) => { + console.log(errors); + }; + + return ( + + {/* Monitor Type Selection */} + ( + + + + + + + + + + )} + /> + } + /> + + {/* General Settings - Dynamic based on type */} + + {/* URL/Host/Container field */} + ( + + )} + /> + + {/* Port field - only for port and game types */} + {generalSettingsConfig.showPort && ( + ( + field.onChange(Number(e.target.value) || 0)} + type="number" + fieldLabel={t("portToMonitor")} + placeholder="5173" + fullWidth + error={!!fieldState.error} + helperText={fieldState.error?.message ?? ""} + /> + )} + /> + )} + + {/* Game select - only for game type */} + {generalSettingsConfig.showGameSelect && ( + ( + + )} + /> + )} + + {/* Display name field - common to all types */} + ( + + )} + /> + + } + /> + + {/* Submit Button */} + + + + + ); +}; + +export default CreateMonitorPage; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 498be4ba7..c2613a16c 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -60,6 +60,8 @@ import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; import BulkImport from "../Pages/Uptime/BulkImport/index.jsx"; import Logs from "../Pages/Logs/index.jsx"; +import CreateMonitor from "@/Pages/CreateMonitor"; + const Routes = () => { const mode = useSelector((state) => state.ui.mode); const AdminCheckedRegister = withAdminCheck(AuthRegister); @@ -78,6 +80,16 @@ const Routes = () => { path="/" element={} /> + + + + + + } + /> ; + +// Type-specific schemas exported for individual use +export { + httpSchema, + pingSchema, + portSchema, + dockerSchema, + gameSchema, + pagespeedSchema, + hardwareSchema, +}; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 80da1835c..d9f2e7ac6 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -220,6 +220,37 @@ } } }, + "createMonitor": { + "form": { + "type": { + "description": "Select the type of check to perform", + "optionType": "Type", + "title": "Type", + "optionHttp": "HTTP(S)", + "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.", + "optionPing": "Ping", + "optionPingDescription": "Use ICMP Ping to monitor if a server is online.", + "optionDocker": "Docker", + "optionDockerDescription": "Use Docker to monitor if a container is running.", + "optionPort": "Port", + "optionPortDescription": "Monitor if a specific port on a server is open.", + "optionGame": "Game", + "optionGameDescription": "Monitor if a specific game server is online." + }, + "general": { + "title": "General settings", + "description": { + "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", + "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", + "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).", + "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", + "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", + "pagespeed": "Here you can select the URL of the host, together with the type of monitor.", + "infrastructure": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent." + } + } + } + }, "uptime": { "table": { "headers": { From 84e4ea7b649802dbbef7738ba472c79d4aab82a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 22:20:50 +0000 Subject: [PATCH 02/23] forward ref --- client/src/Components/v2/inputs/Select.tsx | 32 ++++++++++++++-------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/client/src/Components/v2/inputs/Select.tsx b/client/src/Components/v2/inputs/Select.tsx index 057b0e72c..8be20897d 100644 --- a/client/src/Components/v2/inputs/Select.tsx +++ b/client/src/Components/v2/inputs/Select.tsx @@ -1,7 +1,7 @@ import Select from "@mui/material/Select"; -import React from "react"; -import { useTheme } from "@mui/material/styles"; import type { SelectProps } from "@mui/material/Select"; +import React, { forwardRef } from "react"; +import { useTheme } from "@mui/material/styles"; import Stack from "@mui/material/Stack"; import { FieldLabel } from "./FieldLabel"; import { ChevronDown } from "lucide-react"; @@ -14,18 +14,21 @@ interface SelectInputProps extends Omit, "label"> { placeholderColor?: string; } -export const SelectInput = ({ - fieldLabel, - required, - placeholder, - placeholderColor, - ...props -}: SelectInputProps) => { +const SelectInputInner = ( + { + fieldLabel, + required, + placeholder, + placeholderColor, + ...props + }: SelectInputProps, + ref: React.ForwardedRef +) => { const theme = useTheme(); const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled; - const renderValue = (selected: any) => { - const isMultiple = Boolean((props as any).multiple); + const renderValue = (selected: unknown) => { + const isMultiple = Boolean((props as { multiple?: boolean }).multiple); const isEmpty = isMultiple ? !Array.isArray(selected) || selected.length === 0 : selected === undefined || selected === null || selected === ""; @@ -39,7 +42,7 @@ export const SelectInput = ({ const capitalized = items.map( (item) => item.charAt(0).toUpperCase() + item.slice(1) ); - return ({capitalized.join(" | ")}) as any; + return {capitalized.join(" | ")}; } const nodes = React.Children.toArray(props.children as React.ReactNode); @@ -59,6 +62,7 @@ export const SelectInput = ({ const select = ( {...props} + ref={ref} displayEmpty renderValue={renderValue} inputProps={{ @@ -100,3 +104,7 @@ export const SelectInput = ({ return select; }; + +export const SelectInput = forwardRef(SelectInputInner) as ( + props: SelectInputProps & { ref?: React.ForwardedRef } +) => React.ReactElement; From 841d25ce3e0db0ce9ad15b8564061a7e75d5d63f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 Jan 2026 22:47:29 +0000 Subject: [PATCH 03/23] general --- client/src/Pages/CreateMonitor/index.tsx | 134 +++++++++++++++++------ client/src/Validation/monitor.ts | 4 +- client/src/locales/en.json | 32 +++++- 3 files changed, 132 insertions(+), 38 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 80f818407..e0b5e2930 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -8,6 +8,7 @@ import Stack from "@mui/material/Stack"; import RadioGroup from "@mui/material/RadioGroup"; import FormControl from "@mui/material/FormControl"; import { useTranslation } from "react-i18next"; +import MenuItem from "@mui/material/MenuItem"; import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; import { RadioWithDescription, Button, TextField, Select } from "@/Components/v2/inputs"; @@ -15,14 +16,15 @@ import { useGet } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; import type { Monitor, MonitorType } from "@/Types/Monitor"; import type { MonitorFormData } from "@/Validation/monitor"; -import MenuItem from "@mui/material/MenuItem"; interface GeneralSettingsConfig { urlLabel: string; urlPlaceholder: string; namePlaceholder: string; + showUrl: boolean; showPort: boolean; showGameSelect: boolean; + showSecret: boolean; } const getGeneralSettingsConfig = ( @@ -31,39 +33,67 @@ const getGeneralSettingsConfig = ( ): GeneralSettingsConfig => { const configs: Record = { http: { - urlLabel: t("monitorType.http.label"), - urlPlaceholder: t("monitorType.http.placeholder"), - namePlaceholder: t("monitorType.http.namePlaceholder"), + 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, }, ping: { - urlLabel: t("monitorType.ping.label"), - urlPlaceholder: t("monitorType.ping.placeholder"), - namePlaceholder: t("monitorType.ping.namePlaceholder"), + 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, }, docker: { - urlLabel: t("monitorType.docker.label"), - urlPlaceholder: t("monitorType.docker.placeholder"), - namePlaceholder: t("monitorType.docker.namePlaceholder"), + 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, }, port: { - urlLabel: t("monitorType.port.label"), - urlPlaceholder: t("monitorType.port.placeholder"), - namePlaceholder: t("monitorType.port.namePlaceholder"), + 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, }, game: { - urlLabel: t("monitorType.game.label"), - urlPlaceholder: t("monitorType.game.placeholder"), - namePlaceholder: t("monitorType.game.namePlaceholder"), + 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, + }, + 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, + }, + hardware: { + 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: true, }, }; return configs[type] || configs.http; @@ -156,6 +186,20 @@ const CreateMonitorPage = () => { label={t("pages.createMonitor.form.type.optionGame")} description={t("pages.createMonitor.form.type.optionGameDescription")} /> + + )} @@ -169,22 +213,24 @@ const CreateMonitorPage = () => { subtitle={t(`pages.createMonitor.form.general.description.${watchedType}`)} rightContent={ - {/* URL/Host/Container field */} - ( - - )} - /> + {/* URL/Host/Container field - not shown for hardware */} + {generalSettingsConfig.showUrl && ( + ( + + )} + /> + )} {/* Port field - only for port and game types */} {generalSettingsConfig.showPort && ( @@ -225,6 +271,28 @@ const CreateMonitorPage = () => { /> )} + {/* Secret field - only for hardware type */} + {generalSettingsConfig.showSecret && ( + ( + + )} + /> + )} + {/* Display name field - common to all types */} { Date: Thu, 29 Jan 2026 23:04:40 +0000 Subject: [PATCH 04/23] notifications --- client/src/Pages/CreateMonitor/index.tsx | 85 +++++++++++++++++++++++- client/src/locales/en.json | 7 +- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index e0b5e2930..d5e10d97b 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -9,12 +9,23 @@ import RadioGroup from "@mui/material/RadioGroup"; import FormControl from "@mui/material/FormControl"; import { useTranslation } from "react-i18next"; import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import { Trash2 } from "lucide-react"; import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; -import { RadioWithDescription, Button, TextField, Select } from "@/Components/v2/inputs"; +import { + RadioWithDescription, + Button, + TextField, + Select, + Autocomplete, +} from "@/Components/v2/inputs"; import { useGet } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; import type { Monitor, MonitorType } from "@/Types/Monitor"; +import type { Notification } from "@/Types/Notification"; import type { MonitorFormData } from "@/Validation/monitor"; interface GeneralSettingsConfig { @@ -109,6 +120,9 @@ const CreateMonitorPage = () => { isEditMode ? `/monitors/${monitorId}` : null ); + // Fetch notifications for the team + const { data: notifications } = useGet("/notifications/team"); + const { schema, defaults } = useMonitorForm({ data: existingMonitor ?? null }); const form = useForm({ @@ -313,6 +327,75 @@ const CreateMonitorPage = () => { } /> + {/* Notifications ConfigBox */} + { + // Map notifications to have 'name' property for v2 Autocomplete + const notificationOptions = (notifications ?? []).map((n) => ({ + ...n, + name: n.notificationName, + })); + const selectedNotifications = notificationOptions.filter((n) => + (field.value ?? []).includes(n.id) + ); + return ( + + option.name} + onChange={(_: unknown, newValue: typeof notificationOptions) => { + field.onChange(newValue.map((n) => n.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + /> + {selectedNotifications.length > 0 && ( + + {selectedNotifications.map((notification, index) => ( + + + {notification.notificationName} + + { + field.onChange( + (field.value ?? []).filter( + (id: string) => id !== notification.id + ) + ); + }} + aria-label="Remove notification" + > + + + {index < selectedNotifications.length - 1 && } + + ))} + + )} + + ); + }} + /> + } + /> + {/* Submit Button */} Date: Thu, 29 Jan 2026 23:10:34 +0000 Subject: [PATCH 05/23] TLS/SSL --- client/src/Components/v2/inputs/Switch.tsx | 37 ++++++++++++++++++++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Pages/CreateMonitor/index.tsx | 30 ++++++++++++++++++ client/src/locales/en.json | 9 ++++++ 4 files changed, 77 insertions(+) create mode 100644 client/src/Components/v2/inputs/Switch.tsx diff --git a/client/src/Components/v2/inputs/Switch.tsx b/client/src/Components/v2/inputs/Switch.tsx new file mode 100644 index 000000000..b8ae8c1ec --- /dev/null +++ b/client/src/Components/v2/inputs/Switch.tsx @@ -0,0 +1,37 @@ +import { forwardRef } from "react"; +import Switch from "@mui/material/Switch"; +import type { SwitchProps } from "@mui/material/Switch"; +import { useTheme } from "@mui/material/styles"; + +export const SwitchComponent = forwardRef( + function SwitchComponent({ sx, ...props }, ref) { + const theme = useTheme(); + const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + + return ( + + ); + } +); diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index a0646d394..4bfd62947 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -10,3 +10,4 @@ export { } from "./ToggleButton"; export { DialogInput as Dialog } from "./Dialog"; export * from "./Radio"; +export * from "./Switch"; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index d5e10d97b..93539e10c 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -21,6 +21,7 @@ import { TextField, Select, Autocomplete, + SwitchComponent as Switch, } from "@/Components/v2/inputs"; import { useGet } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; @@ -396,6 +397,35 @@ const CreateMonitorPage = () => { } /> + {/* TLS/SSL ConfigBox - only for HTTP monitors */} + {watchedType === "http" && ( + ( + + field.onChange(e.target.checked)} + /> + + {t("pages.createMonitor.form.ignoreTls.option.tls.label")} + + + )} + /> + } + /> + )} + {/* Submit Button */} Date: Thu, 29 Jan 2026 23:17:08 +0000 Subject: [PATCH 06/23] check frequency --- client/src/Pages/CreateMonitor/index.tsx | 31 ++++++++++++++++++++++++ client/src/locales/en.json | 9 +++++++ 2 files changed, 40 insertions(+) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 93539e10c..591f423fa 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -328,6 +328,37 @@ const CreateMonitorPage = () => { } /> + {/* Frequency ConfigBox */} + ( + + )} + /> + } + /> + {/* Notifications ConfigBox */} Date: Thu, 29 Jan 2026 23:39:14 +0000 Subject: [PATCH 07/23] add incidents config box --- client/src/Pages/CreateMonitor/index.tsx | 48 ++++++++++++++++++++++++ client/src/Validation/monitor.ts | 6 +-- client/src/locales/en.json | 12 ++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 591f423fa..c0740393b 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -359,6 +359,54 @@ const CreateMonitorPage = () => { } /> + {/* Incidents ConfigBox */} + + ( + { + const val = e.target.value; + field.onChange(val === "" ? undefined : Number(val)); + }} + onBlur={field.onBlur} + type="number" + fieldLabel={t("pages.createMonitor.form.incidents.option.checks.label")} + fullWidth + error={!!fieldState.error} + helperText={fieldState.error?.message ?? ""} + /> + )} + /> + ( + { + const val = e.target.value; + field.onChange(val === "" ? undefined : Number(val)); + }} + onBlur={field.onBlur} + type="number" + fieldLabel={t("pages.createMonitor.form.incidents.option.percentage.label")} + fullWidth + error={!!fieldState.error} + helperText={fieldState.error?.message ?? ""} + /> + )} + /> + + } + /> + {/* Notifications ConfigBox */} Date: Thu, 29 Jan 2026 23:52:21 +0000 Subject: [PATCH 08/23] sliders --- client/src/Components/v2/inputs/Slider.tsx | 41 +++++++++++++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Pages/CreateMonitor/index.tsx | 61 +++++++++++----------- 3 files changed, 73 insertions(+), 30 deletions(-) create mode 100644 client/src/Components/v2/inputs/Slider.tsx diff --git a/client/src/Components/v2/inputs/Slider.tsx b/client/src/Components/v2/inputs/Slider.tsx new file mode 100644 index 000000000..11b3e8db8 --- /dev/null +++ b/client/src/Components/v2/inputs/Slider.tsx @@ -0,0 +1,41 @@ +import { forwardRef } from "react"; +import Slider from "@mui/material/Slider"; +import type { SliderProps } from "@mui/material/Slider"; +import { useTheme } from "@mui/material/styles"; + +export const SliderInput = forwardRef( + function SliderInput({ sx, ...props }, ref) { + const theme = useTheme(); + const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + + return ( + + ); + } +); diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 4bfd62947..1618daf76 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -11,3 +11,4 @@ export { export { DialogInput as Dialog } from "./Dialog"; export * from "./Radio"; export * from "./Switch"; +export * from "./Slider"; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index c0740393b..215cdab5f 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -22,6 +22,7 @@ import { Select, Autocomplete, SwitchComponent as Switch, + SliderInput as Slider, } from "@/Components/v2/inputs"; import { useGet } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; @@ -340,7 +341,9 @@ const CreateMonitorPage = () => { + {t("matchMethodOptions.equal")} + {t("matchMethodOptions.include")} + {t("matchMethodOptions.regex")} + + )} + /> + ( + + )} + /> + ( + + )} + /> + + )} + + } + /> + )} + {/* Submit Button */} Date: Fri, 30 Jan 2026 20:21:31 +0000 Subject: [PATCH 14/23] useAdvncedMatching --- client/src/Hooks/useMonitorForm.ts | 2 + client/src/Pages/CreateMonitor/index.tsx | 63 ++++++++++++++----- client/src/Types/Monitor.ts | 1 + client/src/Validation/monitor.ts | 1 + client/src/locales/en.json | 4 +- server/src/db/models/Monitor.ts | 4 ++ .../monitors/MongoMonitorsRepository.ts | 2 + .../service/infrastructure/networkService.ts | 4 +- server/src/types/monitor.ts | 1 + server/src/validation/joi.ts | 2 + 10 files changed, 66 insertions(+), 18 deletions(-) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 50dc7b920..153cf9f19 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -33,6 +33,7 @@ export const useMonitorForm = ({ type: "http", url: data?.url || "", ignoreTlsErrors: data?.ignoreTlsErrors || false, + useAdvancedMatching: data?.useAdvancedMatching || false, matchMethod: data?.matchMethod || "", expectedValue: data?.expectedValue || "", jsonPath: data?.jsonPath || "", @@ -95,6 +96,7 @@ export const useMonitorForm = ({ type: "http", url: "", ignoreTlsErrors: false, + useAdvancedMatching: false, matchMethod: "", expectedValue: "", jsonPath: "", diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 313b744cd..d7d9a645e 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useParams, useLocation, useNavigate } from "react-router"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -7,9 +7,10 @@ import { useTheme } from "@mui/material"; import Stack from "@mui/material/Stack"; import RadioGroup from "@mui/material/RadioGroup"; import FormControl from "@mui/material/FormControl"; -import { useTranslation } from "react-i18next"; +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 FormControlLabel from "@mui/material/FormControlLabel"; @@ -163,7 +164,7 @@ const CreateMonitorPage = () => { const watchedType = watch("type") as MonitorType; - const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean; useEffect(() => { clearErrors(); @@ -550,16 +551,22 @@ const CreateMonitorPage = () => { subtitle={t("pages.createMonitor.form.advanced.description")} rightContent={ - setShowAdvancedSettings(e.target.checked)} + ( + field.onChange(e.target.checked)} + /> + } + label={t("advancedMatching")} /> - } - label={t("advancedMatching")} + )} /> - {showAdvancedSettings && ( + {watchedUseAdvancedMatching && ( { )} @@ -583,7 +594,9 @@ const CreateMonitorPage = () => { { )} /> + + + ), + }} + /> + )} diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 94b3d83d7..ddf44e56f 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -35,6 +35,7 @@ export interface Monitor { statusWindowThreshold: number; type: MonitorType; ignoreTlsErrors: boolean; + useAdvancedMatching: boolean; jsonPath?: string; expectedValue?: string; matchMethod?: MonitorMatchMethod; diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index 39fccafbd..68a15fbec 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -27,6 +27,7 @@ const httpSchema = baseSchema.extend({ type: z.literal("http"), url: urlSchema, ignoreTlsErrors: z.boolean(), + useAdvancedMatching: z.boolean(), matchMethod: z.enum(["equal", "include", "regex", ""]).optional(), expectedValue: z.string().optional(), jsonPath: z.string().optional(), diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 80cb2e206..123a1f146 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -2,6 +2,8 @@ "common": { "appName": "Checkmate", "monitoringAgentName": "Capture", + "see": "See", + "forDocumentation": "for query language documentation.", "breadcrumbs": { "home": "Home", "details": "Details" @@ -322,7 +324,7 @@ }, "jsonPath": { "label": "JSONPath expression", - "description": "This expression will be evaluated against the response JSON data and the result will be used to match against the expected value." + "description": "This expression will be evaluated against the response JSON data and the result will be used to match against the expected value. See jmespath.org for query language documentation." } } } diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index 24c8ef0bf..d4cc9a872 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -92,6 +92,10 @@ const MonitorSchema = new Schema( type: Boolean, default: false, }, + useAdvancedMatching: { + type: Boolean, + default: false, + }, jsonPath: { type: String, }, diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 9cb519303..9b30e5ac2 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -290,6 +290,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { statusWindowThreshold: doc.statusWindowThreshold, type: doc.type, ignoreTlsErrors: doc.ignoreTlsErrors, + useAdvancedMatching: doc.useAdvancedMatching ?? false, jsonPath: doc.jsonPath ?? undefined, expectedValue: doc.expectedValue ?? undefined, matchMethod: doc.matchMethod ?? undefined, @@ -374,6 +375,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { statusWindowThreshold: doc.statusWindowThreshold, type: doc.type, ignoreTlsErrors: doc.ignoreTlsErrors, + useAdvancedMatching: doc.useAdvancedMatching ?? false, jsonPath: doc.jsonPath ?? undefined, expectedValue: doc.expectedValue ?? undefined, matchMethod: doc.matchMethod ?? undefined, diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 7db8fc6e2..92d269808 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -255,7 +255,7 @@ class NetworkService implements INetworkService { } private async requestHttp(monitor: Monitor): Promise { - const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; + const { url, secret, id, teamId, type, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor; const httpResponse = this.buildStatusResponse({ monitor, overrides: { @@ -305,7 +305,7 @@ class NetworkService implements INetworkService { timings: response.timings, }); - if (!expectedValue && !jsonPath) { + if (!useAdvancedMatching) { return httpResponse; } diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 62ced091d..fe6ac8c8a 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -26,6 +26,7 @@ export interface Monitor { statusWindowThreshold: number; type: MonitorType; ignoreTlsErrors: boolean; + useAdvancedMatching: boolean; jsonPath?: string; expectedValue?: string; matchMethod?: MonitorMatchMethod; diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index 581d2afd2..926d3b0fe 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -158,6 +158,7 @@ const createMonitorBodyValidation = joi.object({ statusWindowThreshold: joi.number().min(1).max(100).default(60), url: joi.string().required(), ignoreTlsErrors: joi.boolean().default(false), + useAdvancedMatching: joi.boolean().default(false), port: joi.number(), isActive: joi.boolean(), interval: joi.number(), @@ -193,6 +194,7 @@ const editMonitorBodyValidation = joi.object({ notifications: joi.array().items(joi.string()), secret: joi.string(), ignoreTlsErrors: joi.boolean(), + useAdvancedMatching: joi.boolean(), jsonPath: joi.string().allow(""), expectedValue: joi.string().allow(""), matchMethod: joi.string().allow(null, ""), From 443ad46fa79bc202a9a2c05ee943a8d5fdae403c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 30 Jan 2026 20:38:05 +0000 Subject: [PATCH 15/23] update strings --- client/src/Pages/CreateMonitor/index.tsx | 22 +++++++++++----------- client/src/locales/en.json | 3 +++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index d7d9a645e..c38f4eb05 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -13,7 +13,6 @@ 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 FormControlLabel from "@mui/material/FormControlLabel"; import { Trash2 } from "lucide-react"; import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; @@ -25,7 +24,6 @@ import { Autocomplete, SwitchComponent as Switch, SliderWithLabel, - Checkbox, } from "@/Components/v2/inputs"; import { useGet, usePost, usePatch } from "@/Hooks/UseApi"; import { useMonitorForm } from "@/Hooks/useMonitorForm"; @@ -555,15 +553,17 @@ const CreateMonitorPage = () => { name="useAdvancedMatching" control={control} render={({ field }) => ( - field.onChange(e.target.checked)} - /> - } - label={t("advancedMatching")} - /> + + field.onChange(e.target.checked)} + /> + {t("pages.createMonitor.form.advanced.option.advancedMatching.label")} + )} /> {watchedUseAdvancedMatching && ( diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 123a1f146..7cdfe215a 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -315,6 +315,9 @@ "title": "Advanced settings", "description": "Optional settings for advanced use cases", "option": { + "advancedMatching": { + "label": "Use advanced matching" + }, "matchMethod": { "label": "Match method" }, From 91bb60f2ff9b923cf214cbb8bd29f6764714825b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 30 Jan 2026 20:55:24 +0000 Subject: [PATCH 16/23] delte header --- .../common/controls/HeaderMonitorControls.tsx | 93 +- client/src/Pages/CreateMonitor/index.tsx | 14 +- .../Create/Components/CustomAlertsSection.jsx | 87 -- .../Components/CustomThreshold/index.jsx | 127 --- .../Create/Components/DiskSelection.jsx | 90 -- .../Components/MonitorActionButtons.jsx | 83 -- .../Create/Components/MonitorStatusHeader.jsx | 73 -- .../hooks/useInfrastructureMonitorForm.jsx | 99 --- .../Create/hooks/useInfrastructureSubmit.jsx | 84 -- .../hooks/useValidateInfrastructureForm.jsx | 37 - .../src/Pages/Infrastructure/Create/index.jsx | 409 --------- client/src/Pages/PageSpeed/Create/index.css | 0 client/src/Pages/PageSpeed/Create/index.jsx | 514 ----------- .../src/Pages/PageSpeed/Create/skeleton.jsx | 86 -- client/src/Pages/Uptime/Create/index.css | 0 client/src/Pages/Uptime/Create/index.jsx | 826 ------------------ client/src/Pages/Uptime/Create/skeleton.jsx | 90 -- client/src/Routes/index.jsx | 7 - client/src/locales/en.json | 3 +- 19 files changed, 97 insertions(+), 2625 deletions(-) delete mode 100644 client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx delete mode 100644 client/src/Pages/Infrastructure/Create/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Create/index.css delete mode 100644 client/src/Pages/PageSpeed/Create/index.jsx delete mode 100644 client/src/Pages/PageSpeed/Create/skeleton.jsx delete mode 100644 client/src/Pages/Uptime/Create/index.css delete mode 100644 client/src/Pages/Uptime/Create/index.jsx delete mode 100644 client/src/Pages/Uptime/Create/skeleton.jsx diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx index 17576a8b2..0db84940b 100644 --- a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -1,7 +1,7 @@ import Stack from "@mui/material/Stack"; import { Icon, MonitorStatus } from "@/Components/v2/design-elements"; import { Button } from "@/Components/v2/inputs"; -import { Settings, Pause, Play, Mail, Bug } from "lucide-react"; +import { Settings, Pause, Play, Mail, Bug, Trash } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -9,10 +9,28 @@ import { useTheme } from "@mui/material/styles"; import { usePost } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor.js"; +interface BaseHeaderProps { + monitor: Monitor; +} + +const BaseHeader = ({ monitor, children }: React.PropsWithChildren) => { + const theme = useTheme(); + return ( + + + {children} + + ); +}; interface HeaderMonitorControlsProps { path: string; - monitor?: Monitor; + monitor?: Monitor | null; isAdmin: boolean; refetch: Function; } @@ -36,13 +54,7 @@ export const HeaderMonitorControls = ({ return null; } return ( - - + )} - + + ); +}; + +interface HeaderDeleteControlsProps { + monitor?: Monitor | null; + isAdmin: boolean; + refetch: Function; +} + +export const HeaderDeleteControls = ({ + monitor, + isAdmin, + refetch, +}: HeaderDeleteControlsProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const theme = useTheme(); + const { + post, + loading: isPosting, + // error: postError, + } = usePost(); + + if (!monitor) { + return null; + } + return ( + + + {isAdmin && ( + + )} + {isAdmin && ( + + )} + + ); }; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index c38f4eb05..37741746b 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -14,6 +14,7 @@ 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/v2/common"; import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; import { @@ -138,7 +139,7 @@ const CreateMonitorPage = () => { ? "hardware" : "http"; - const { data: existingMonitor } = useGet( + const { data: existingMonitor, refetch: refetchMonitor } = useGet( isEditMode ? `/monitors/${monitorId}` : null ); @@ -206,6 +207,11 @@ const CreateMonitorPage = () => { component="form" onSubmit={handleSubmit(onSubmit, onError)} > + {/* Monitor Type Selection - only shown for uptime monitors */} {showTypeSelector && ( { checked={field.value ?? false} onChange={(e) => field.onChange(e.target.checked)} /> - {t("pages.createMonitor.form.advanced.option.advancedMatching.label")} + + {t( + "pages.createMonitor.form.advanced.option.advancedMatching.label" + )} + )} /> diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx deleted file mode 100644 index 64f82cec2..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import { Box, Stack, Typography } from "@mui/material"; -import { CustomThreshold } from "./CustomThreshold/index.jsx"; -import { capitalizeFirstLetter } from "../../../../Utils/stringUtils.js"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; -const CustomAlertsSection = ({ - errors, - onChange, - infrastructureMonitor, - handleCheckboxChange, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const METRICS = ["cpu", "memory", "disk", "temperature"]; - const METRIC_PREFIX = "usage_"; - const hasAlertError = (errors) => { - return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; - }; - const getAlertError = (errors) => { - const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)); - return errorKey ? errors[errorKey] : null; - }; - return ( - - - - {t("infrastructureCustomizeAlerts")} - - - {t("infrastructureAlertNotificationDescription")} - - - - {METRICS.map((metric) => { - return ( - - ); - })} - - {hasAlertError(errors) && ( - - {getAlertError(errors)} - - )} - - - ); -}; - -CustomAlertsSection.propTypes = { - errors: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, - handleCheckboxChange: PropTypes.func.isRequired, -}; - -export default CustomAlertsSection; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx deleted file mode 100644 index d783f7985..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Box, Stack, Typography } from "@mui/material"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; - -/** - * CustomThreshold Component - * - * A reusable component that renders a checkbox with an associated numeric input field - * and an optional unit label. The input field can be enabled/disabled based on checkbox state. - * - * @component - * @param {Object} props - Component properties - * @param {string} [props.checkboxId] - Optional unique identifier for the checkbox - * @param {string} [props.checkboxName] - Optional name attribute for the checkbox - * @param {string} props.checkboxLabel - Label text for the checkbox - * @param {boolean} props.isChecked - Current checked state of the checkbox - * @param {Function} props.onCheckboxChange - Callback function when checkbox is toggled - * @param {string} props.fieldId - Unique identifier for the input field - * @param {string} [props.fieldName] - Optional name attribute for the input field - * @param {string} props.fieldValue - Current value of the input field - * @param {Function} props.onFieldChange - Callback function when input field value changes - * @param {Function} props.onFieldBlur - Callback function when input field loses focus - * @param {string} props.alertUnit - Unit label displayed next to the input field - * @param {Object} props.errors - Object containing validation errors for the field - * @param {Object} props.infrastructureMonitor - Infrastructure monitor configuration object - * - * @returns {React.ReactElement} Rendered CustomThreshold component - * - * @example - * - */ -export const CustomThreshold = ({ - checkboxId, - checkboxName, - checkboxLabel, - onCheckboxChange, - isChecked, - fieldId, - fieldName, - fieldValue, - onFieldChange, - onFieldBlur, - alertUnit, - errors, -}) => { - const theme = useTheme(); - return ( - - - - - - - - - {alertUnit} - - - - ); -}; - -CustomThreshold.propTypes = { - checkboxId: PropTypes.string, - checkboxName: PropTypes.string, - checkboxLabel: PropTypes.string.isRequired, - isChecked: PropTypes.bool.isRequired, - onCheckboxChange: PropTypes.func.isRequired, - fieldId: PropTypes.string.isRequired, - fieldName: PropTypes.string, - fieldValue: PropTypes.string.isRequired, - onFieldChange: PropTypes.func.isRequired, - onFieldBlur: PropTypes.func, - alertUnit: PropTypes.string.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, -}; diff --git a/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx b/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx deleted file mode 100644 index 50c4ed980..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import { Box, Stack, Typography } from "@mui/material"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -const DiskSelection = ({ availableDisks, selectedDisks, onChange }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const handleDiskChange = (event, mountpoint) => { - const isChecked = event.target.checked; - let newSelectedDisks = []; - - if (isChecked) { - newSelectedDisks = [...selectedDisks, mountpoint]; - } else { - newSelectedDisks = selectedDisks.filter((disk) => disk !== mountpoint); - } - - onChange(newSelectedDisks); - }; - - return ( - - - - {t("v1.infrastructure.disk_selection_title")} - - - {t("v1.infrastructure.disk_selection_description")} - - - - - {!availableDisks || availableDisks.length === 0 ? ( - - {t("v1.infrastructure.disk_selection_info")} - - ) : ( - availableDisks.map((disk) => { - const identifier = disk.mountpoint || disk.device; - return ( - - - handleDiskChange(e, identifier)} - /> - - - ); - }) - )} - - - ); -}; - -DiskSelection.propTypes = { - availableDisks: PropTypes.arrayOf( - PropTypes.shape({ - mountpoint: PropTypes.string.isRequired, - }) - ), - selectedDisks: PropTypes.arrayOf(PropTypes.string).isRequired, - onChange: PropTypes.func.isRequired, -}; - -export default DiskSelection; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx deleted file mode 100644 index 2bde5f321..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useState } from "react"; -import { Box, Button } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import Icon from "@/Components/v1/Icon"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import PropTypes from "prop-types"; - -const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const [isOpen, setIsOpen] = useState(false); - - return ( - - - - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - /> - - ); -}; - -MonitorActionButtons.propTypes = { - monitor: PropTypes.object.isRequired, - isBusy: PropTypes.bool.isRequired, - handlePause: PropTypes.func.isRequired, - handleRemove: PropTypes.func.isRequired, -}; - -export default MonitorActionButtons; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx deleted file mode 100644 index 8fa6f4ff1..000000000 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; -import PropTypes from "prop-types"; -const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); - return ( - - - - - - - - {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - ); -}; - -MonitorStatusHeader.propTypes = { - monitor: PropTypes.object.isRequired, - infrastructureMonitor: PropTypes.object.isRequired, -}; - -export default MonitorStatusHeader; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx deleted file mode 100644 index a38ac9fc6..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState, useCallback } from "react"; -const useInfrastructureMonitorForm = () => { - const [infrastructureMonitor, setInfrastructureMonitor] = useState({ - url: "", - name: "", - notifications: [], - notify_email: false, - interval: 0.25, - statusWindowSize: 5, - statusWindowThreshold: 60, - cpu: false, - usage_cpu: "", - memory: false, - usage_memory: "", - disk: false, - usage_disk: "", - temperature: false, - usage_temperature: "", - secret: "", - selectedDisks: [], - }); - - const onChangeForm = (name, value) => { - setInfrastructureMonitor({ - ...infrastructureMonitor, - [name]: value, - }); - }; - const handleCheckboxChange = (event) => { - setInfrastructureMonitor({ - ...infrastructureMonitor, - [event.target.name]: event.target.checked, - }); - }; - const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => { - const gt = globalSettings?.data?.settings?.globalThresholds || {}; - setInfrastructureMonitor((prev) => ({ - ...prev, - url: "", - name: "", - notifications: [], - interval: 0.25, - cpu: gt.cpu !== undefined, - usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "", - memory: gt.memory !== undefined, - usage_memory: gt.memory !== undefined ? gt.memory.toString() : "", - disk: gt.disk !== undefined, - usage_disk: gt.disk !== undefined ? gt.disk.toString() : "", - temperature: gt.temperature !== undefined, - usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", - secret: "", - selectedDisks: [], - })); - }, []); - - const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => { - const MS_PER_MINUTE = 60000; - const { thresholds = {} } = monitor; - setInfrastructureMonitor((prev) => ({ - ...prev, - url: monitor.url.replace(/^https?:\/\//, ""), - name: monitor.name || "", - notifications: monitor.notifications || [], - interval: monitor.interval / MS_PER_MINUTE, - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - cpu: thresholds.usage_cpu !== undefined, - usage_cpu: - thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "", - memory: thresholds.usage_memory !== undefined, - usage_memory: - thresholds.usage_memory !== undefined - ? (thresholds.usage_memory * 100).toString() - : "", - disk: thresholds.usage_disk !== undefined, - usage_disk: - thresholds.usage_disk !== undefined - ? (thresholds.usage_disk * 100).toString() - : "", - temperature: thresholds.usage_temperature !== undefined, - usage_temperature: - thresholds.usage_temperature !== undefined - ? (thresholds.usage_temperature * 100).toString() - : "", - secret: monitor.secret || "", - selectedDisks: monitor.selectedDisks || [], - })); - }, []); - return { - infrastructureMonitor, - setInfrastructureMonitor, - onChangeForm, - handleCheckboxChange, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - }; -}; - -export default useInfrastructureMonitorForm; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx deleted file mode 100644 index 04279cac4..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks.js"; -const useInfrastructureSubmit = () => { - const [createMonitor, isCreating] = useCreateMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - const buildForm = (infrastructureMonitor, https) => { - const MS_PER_MINUTE = 60000; - - let form = { - url: `http${https ? "s" : ""}://` + infrastructureMonitor.url, - name: - infrastructureMonitor.name === "" - ? infrastructureMonitor.url - : infrastructureMonitor.name, - interval: infrastructureMonitor.interval * MS_PER_MINUTE, - statusWindowSize: infrastructureMonitor.statusWindowSize, - statusWindowThreshold: infrastructureMonitor.statusWindowThreshold, - cpu: infrastructureMonitor.cpu, - ...(infrastructureMonitor.cpu - ? { usage_cpu: infrastructureMonitor.usage_cpu } - : {}), - memory: infrastructureMonitor.memory, - ...(infrastructureMonitor.memory - ? { usage_memory: infrastructureMonitor.usage_memory } - : {}), - disk: infrastructureMonitor.disk, - ...(infrastructureMonitor.disk - ? { usage_disk: infrastructureMonitor.usage_disk } - : {}), - ...(infrastructureMonitor.temperature - ? { usage_temperature: infrastructureMonitor.usage_temperature } - : {}), - secret: infrastructureMonitor.secret, - selectedDisks: infrastructureMonitor.selectedDisks, - }; - return form; - }; - const submitInfrastructureForm = async ( - infrastructureMonitor, - form, - isCreate, - monitorId - ) => { - const { - cpu, - usage_cpu, - memory, - usage_memory, - disk, - usage_disk, - temperature, - usage_temperature, - selectedDisks, - ...rest - } = form; - - const thresholds = { - ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}), - ...(memory ? { usage_memory: usage_memory / 100 } : {}), - ...(disk ? { usage_disk: usage_disk / 100 } : {}), - ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}), - }; - - const finalForm = { - ...(isCreate ? {} : { id: monitorId }), - ...rest, - description: form.name, - type: "hardware", - notifications: infrastructureMonitor.notifications, - selectedDisks, - thresholds, - }; - // Handle create or update - isCreate - ? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" }) - : await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" }); - }; - return { - buildForm, - submitInfrastructureForm, - isCreating, - isUpdating, - }; -}; -export default useInfrastructureSubmit; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx deleted file mode 100644 index f7665e65f..000000000 --- a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useState } from "react"; -import { infrastructureMonitorValidation } from "../../../../Validation/validation.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -const useValidateInfrastructureForm = () => { - const [errors, setErrors] = useState({}); - - const validateField = (name, value) => { - const { error } = infrastructureMonitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - const validateForm = (form) => { - const { error } = infrastructureMonitorValidation.validate(form, { - abortEarly: false, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - console.log(newErrors); - setErrors(newErrors); - createToast({ body: "Please check the form for errors." }); - return error; - } - return null; - }; - return { errors, validateField, validateForm }; -}; -export default useValidateInfrastructureForm; diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx deleted file mode 100644 index 936d87ab0..000000000 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ /dev/null @@ -1,409 +0,0 @@ -//Components -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx"; -import Link from "@/Components/v1/Link/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx"; -import MonitorActionButtons from "./Components/MonitorActionButtons.jsx"; -import CustomAlertsSection from "./Components/CustomAlertsSection.jsx"; -import DiskSelection from "./Components/DiskSelection.jsx"; -// Utils -import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; -import { networkService } from "../../../Utils/NetworkService.js"; -import { useParams } from "react-router-dom"; -import { useState, useEffect } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { - useDeleteMonitor, - useFetchGlobalSettings, - useFetchMonitorById, - usePauseMonitor, -} from "@/Hooks/monitorHooks.js"; -import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx"; -import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx"; -import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; - -const CreateInfrastructureMonitor = () => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined"; - - const theme = useTheme(); - const { t } = useTranslation(); - - // State - const [monitor, setMonitor] = useState(null); - const [https, setHttps] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [availableDisks, setAvailableDisks] = useState([]); - - // Fetch monitor details if editing - const [isLoading] = useFetchMonitorById({ - monitorId, - setMonitor, - updateTrigger: true, - }); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); - const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId(); - const [pauseMonitor, isPausing] = usePauseMonitor(); - const { - infrastructureMonitor, - setInfrastructureMonitor, - onChangeForm, - handleCheckboxChange, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - } = useInfrastructureMonitorForm(); - const { errors, validateField, validateForm } = useValidateInfrastructureForm(); - const { buildForm, submitInfrastructureForm, isCreating, isUpdating } = - useInfrastructureSubmit(); - - 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: 5, name: t("time.fiveMinutes") }, - { _id: 10, name: t("time.tenMinutes") }, - ]; - const CRUMBS = [ - { name: "Infrastructure monitors", path: "/infrastructure" }, - ...(isCreate - ? [{ name: "Create", path: "/infrastructure/create" }] - : [ - { name: "Details", path: `/infrastructure/${monitorId}` }, - { name: "Configure", path: `/infrastructure/configure/${monitorId}` }, - ]), - ]; - // Populate form fields if editing - useEffect(() => { - if (isCreate) { - if (globalSettingsLoading) return; - setHttps(false); - initializeInfrastructureMonitorForCreate(globalSettings); - } else if (monitor) { - setHttps(monitor.url.startsWith("https")); - initializeInfrastructureMonitorForUpdate(monitor); - const fetchLastCheck = async () => { - try { - let disks = monitor?.recentChecks?.[0]?.disk || []; - - if (disks.length > 0) { - setAvailableDisks(disks); - return; - } - - const response = await networkService.getChecksByMonitor({ - monitorId, - dateRange: "all", - rowsPerPage: 1, - sortOrder: "desc", - }); - disks = response?.data?.data?.checks?.[0]?.disk || []; - setAvailableDisks(disks); - } catch (error) { - setAvailableDisks([]); - } - }; - - fetchLastCheck(); - } - }, [ - isCreate, - monitor, - globalSettings, - globalSettingsLoading, - initializeInfrastructureMonitorForCreate, - initializeInfrastructureMonitorForUpdate, - ]); - - // Handlers - const onSubmit = async (event) => { - event.preventDefault(); - const form = buildForm(infrastructureMonitor, https); - // When editing, exclude URL from validation since it's disabled and can't be changed - const formToValidate = isCreate ? form : { ...form, url: monitor.url }; - const error = validateForm(formToValidate); - if (error) { - return; - } - submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId); - }; - - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const onChange = (event) => { - const { value, name } = event.target; - onChangeForm(name, value); - validateField(name, value); - }; - - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/infrastructure" }); - }; - - const isBusy = - isLoading || - isUpdating || - isCreating || - isDeleting || - isPausing || - notificationsAreLoading; - - return ( - - - - - - - - {!isCreate ? infrastructureMonitor.name : t("createYour") + " "} - - {isCreate ? ( - - {t("monitor")} - - ) : ( - <> - )} - - {!isCreate && monitor && ( - - )} - - {!isCreate && monitor && ( - - )} - - - - - {t("settingsGeneralSettings")} - - - {t("infrastructureCreateGeneralSettingsDescription")} - - - {t("infrastructureServerRequirement")}{" "} - - - - - } - placeholder={"localhost:59232/api/v1/metrics"} - label={t("infrastructureServerUrlLabel")} - https={https} - value={infrastructureMonitor.url} - onChange={onChange} - error={errors["url"] ? true : false} - helperText={errors["url"]} - disabled={!isCreate} - /> - {isCreate && ( - - - - - - - )} - - - - - - - - {t("notificationConfig.title")} - - {t("notificationConfig.description")} - - - - - - - {t("createMonitorPage.incidentConfigTitle")} - - - {t("createMonitorPage.incidentConfigDescription")} - - - - - - - - - - {monitorId && ( - - onChangeForm("selectedDisks", newSelectedDisks) - } - /> - )} - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - - - - - - - - {!isCreate && ( - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - /> - )} - - ); -}; - -export default PageSpeedSetup; diff --git a/client/src/Pages/PageSpeed/Create/skeleton.jsx b/client/src/Pages/PageSpeed/Create/skeleton.jsx deleted file mode 100644 index 347fcaa65..000000000 --- a/client/src/Pages/PageSpeed/Create/skeleton.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Box, Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Create/index.css b/client/src/Pages/Uptime/Create/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx deleted file mode 100644 index 01fc9eb7a..000000000 --- a/client/src/Pages/Uptime/Create/index.jsx +++ /dev/null @@ -1,826 +0,0 @@ -//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, - }); - - // Fetch games - const [isFetchingGames] = useFetchMonitorGames({ - setGames, - updateTrigger, - }); - - // 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 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} - /> -