diff --git a/client/src/Components/v1/FilterHeader/index.jsx b/client/src/Components/v1/FilterHeader/index.jsx deleted file mode 100644 index de2c7f0e9..000000000 --- a/client/src/Components/v1/FilterHeader/index.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; -import Icon from "../Icon"; - -/** - * A reusable filter header component that displays a dropdown menu with selectable options. - * - * @component - * @param {Object} props - The component props. - * @param {string} props.header - The header text to display when no options are selected. - * @param {Array} props.options - An array of options to display in the dropdown menu. Each option should have a `value` and `label`. - * @param {Array} [props.value] - The currently selected values. - * @param {Function} props.onChange - The callback function to handle changes in the selected values. - * @param {boolean} [props.multiple=true] - Whether multiple options can be selected. - * @returns {JSX.Element} The rendered FilterHeader component. - */ - -const FilterHeader = ({ header, options, value, onChange, multiple = true }) => { - const theme = useTheme(); - - const controlledValue = value === undefined ? [] : value; // Ensure value is always treated as an array for controlled component purposes - - return ( - - - - ); -}; - -FilterHeader.propTypes = { - header: PropTypes.string.isRequired, - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - }) - ).isRequired, - value: PropTypes.arrayOf(PropTypes.string), - onChange: PropTypes.func.isRequired, - multiple: PropTypes.bool, -}; - -export default FilterHeader; diff --git a/client/src/Components/v1/MonitorActions/index.jsx b/client/src/Components/v1/MonitorActions/index.jsx deleted file mode 100644 index f63fc23fe..000000000 --- a/client/src/Components/v1/MonitorActions/index.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import * as React from "react"; -import Button from "@mui/material/Button"; -import ButtonGroup from "@mui/material/ButtonGroup"; -import Icon from "../Icon"; -import ClickAwayListener from "@mui/material/ClickAwayListener"; -import Grow from "@mui/material/Grow"; -import Paper from "@mui/material/Paper"; -import Popper from "@mui/material/Popper"; -import MenuItem from "@mui/material/MenuItem"; -import MenuList from "@mui/material/MenuList"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { useExportMonitors } from "../../../Hooks/monitorHooks.js"; - -const MonitorActions = ({ isLoading }) => { - const [open, setOpen] = React.useState(false); - const anchorRef = React.useRef(null); - const [selectedIndex, setSelectedIndex] = React.useState(0); - const navigate = useNavigate(); - const { t } = useTranslation(); - const [exportMonitors, isExporting] = useExportMonitors(); - - const options = [t("monitorActions.import"), t("monitorActions.export")]; - - const handleClick = async () => { - if (selectedIndex === 0) { - // Import - navigate("/uptime/bulk-import"); - } else { - // Export - const [success, error] = await exportMonitors(); - if (!success) { - createToast({ body: error || t("export.failed") }); - } - } - }; - - const handleMenuItemClick = (event, index) => { - setSelectedIndex(index); - setOpen(false); - }; - - const handleToggle = () => { - setOpen((prevOpen) => !prevOpen); - }; - - const handleClose = (event) => { - if (anchorRef.current && anchorRef.current.contains(event.target)) { - return; - } - setOpen(false); - }; - - return ( - - - - - - - {({ TransitionProps, placement }) => ( - - - - - {options.map((option, index) => ( - handleMenuItemClick(event, index)} - > - {option} - - ))} - - - - - )} - - - ); -}; - -export default MonitorActions; diff --git a/client/src/Components/v1/MonitorCountHeader/index.jsx b/client/src/Components/v1/MonitorCountHeader/index.jsx deleted file mode 100644 index eba644d26..000000000 --- a/client/src/Components/v1/MonitorCountHeader/index.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import SkeletonLayout from "./skeleton.jsx"; - -const MonitorCountHeader = ({ - isLoading = false, - monitorCount, - heading = "monitors", - sx, - children, -}) => { - const theme = useTheme(); - if (isLoading) return ; - - if (monitorCount === 1) { - heading = "monitor"; - } - - return ( - - {monitorCount} {heading} - {children} - - ); -}; - -MonitorCountHeader.propTypes = { - isLoading: PropTypes.bool, - monitorCount: PropTypes.number, - heading: PropTypes.string, - children: PropTypes.node, - sx: PropTypes.object, -}; - -export default MonitorCountHeader; diff --git a/client/src/Components/v1/MonitorCountHeader/skeleton.jsx b/client/src/Components/v1/MonitorCountHeader/skeleton.jsx deleted file mode 100644 index 9b4971c32..000000000 --- a/client/src/Components/v1/MonitorCountHeader/skeleton.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Components/v1/MonitorCreateHeader/index.jsx b/client/src/Components/v1/MonitorCreateHeader/index.jsx index 90c12f5ec..c3a979eb9 100644 --- a/client/src/Components/v1/MonitorCreateHeader/index.jsx +++ b/client/src/Components/v1/MonitorCreateHeader/index.jsx @@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; -import MonitorActions from "../MonitorActions/index.jsx"; const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => { const navigate = useNavigate(); diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx deleted file mode 100644 index 0793e7149..000000000 --- a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx +++ /dev/null @@ -1,158 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Status from "./status.jsx"; -import Skeleton from "./skeleton.jsx"; -import Button from "@mui/material/Button"; -import { Tooltip } from "@mui/material"; -import Icon from "../Icon"; - -// Utils -import PropTypes from "prop-types"; -import { useNavigate } from "react-router-dom"; -import { useTheme } from "@mui/material/styles"; -import { usePauseMonitor } from "../../../Hooks/monitorHooks.js"; -import { useSendTestEmail } from "../../../Hooks/useSendTestEmail.js"; -import { useTranslation } from "react-i18next"; -import { useTestAllNotifications } from "../../../Hooks/useNotifications.js"; -/** - * MonitorDetailsControlHeader component displays the control header for monitor details. - * It includes status display, pause/resume button, and a configure button for admins. - * - * @component - * @param {Object} props - Component props - * @param {string} props.path - The base path for navigation - * @param {boolean} [props.isLoading=false] - Flag indicating if the data is loading - * @param {boolean} [props.isAdmin=false] - Flag indicating if the user is an admin - * @param {Object} props.monitor - The monitor object containing details - * @param {Function} props.triggerUpdate - Function to trigger an update - * @returns {JSX.Element} The rendered component - */ -const MonitorDetailsControlHeader = ({ - path, - isLoading = false, - isAdmin = false, - monitor, - triggerUpdate, -}) => { - const navigate = useNavigate(); - const theme = useTheme(); - const { t } = useTranslation(); - const [pauseMonitor, isPausing, error] = usePauseMonitor(); - - const isTestNotificationsDisabled = monitor?.notifications?.length === 0; - - const tooltipTitle = isTestNotificationsDisabled ? t("testNotificationsDisabled") : ""; - - // const [isSending, emailError, sendTestEmail] = useSendTestEmail(); - - const [testAllNotifications, isSending, errorAllNotifications] = - useTestAllNotifications(); - - if (isLoading) { - return ; - } - - return ( - - - - - - - - - - - {isAdmin && ( - - )} - {isAdmin && ( - - )} - - - ); -}; - -MonitorDetailsControlHeader.propTypes = { - path: PropTypes.string, - isLoading: PropTypes.bool, - isAdmin: PropTypes.bool, - monitor: PropTypes.object, - triggerUpdate: PropTypes.func, -}; - -export default MonitorDetailsControlHeader; diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/skeleton.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/skeleton.jsx deleted file mode 100644 index 64dc0547f..000000000 --- a/client/src/Components/v1/MonitorDetailsControlHeader/skeleton.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx deleted file mode 100644 index c6a3cfcfa..000000000 --- a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx +++ /dev/null @@ -1,51 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import PulseDot from "../Animated/PulseDot.jsx"; -import Typography from "@mui/material/Typography"; -import Dot from "../Dot/index.jsx"; -// Utils -import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; -import { formatMonitorUrl } from "../../../Utils/utils.js"; -/** - * Status component displays the status information of a monitor. - * It includes the monitor's name, URL, and check interval. - * - * @component - * @param {Object} props - Component props - * @param {Object} props.monitor - The monitor object containing details - * @param {string} props.monitor.name - The name of the monitor - * @param {string} props.monitor.url - The URL of the monitor - * @param {number} props.monitor.interval - The interval at which the monitor checks - * @returns {JSX.Element} The rendered component - */ -const Status = ({ monitor }) => { - const theme = useTheme(); - const { statusColor, determineState } = useMonitorUtils(); - - return ( - - {monitor?.name} - - - {formatMonitorUrl(monitor?.url)} - - - Checking every {formatDurationRounded(monitor?.interval)}. - - - - ); -}; - -Status.propTypes = { - monitor: PropTypes.object, -}; - -export default Status; diff --git a/client/src/Components/v1/NotificationConfig/index.jsx b/client/src/Components/v1/NotificationConfig/index.jsx deleted file mode 100644 index f1ed30ad1..000000000 --- a/client/src/Components/v1/NotificationConfig/index.jsx +++ /dev/null @@ -1,127 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Divider from "@mui/material/Divider"; -import Icon from "../Icon"; -import Search from "../Inputs/Search/index.jsx"; - -// Utils -import { useState, useEffect } from "react"; -import { useTheme } from "@mui/material/styles"; -import PropTypes from "prop-types"; - -const NotificationConfig = ({ - notifications, - setMonitor, - setNotifications, - //FieldWrapper's props - gap, - labelMb, - labelFontWeight, - labelVariant, - labelSx = {}, - sx = {}, -}) => { - // Local state - const [notificationsSearch, setNotificationsSearch] = useState(""); - const [selectedNotifications, setSelectedNotifications] = useState([]); - - const handleSearch = (value) => { - setSelectedNotifications(value); - setMonitor((prev) => { - return { - ...prev, - notifications: value.map((notification) => notification.id), - }; - }); - }; - - // Handlers - const handleDelete = (id) => { - const updatedNotifications = selectedNotifications.filter( - (notification) => notification.id !== id - ); - - setSelectedNotifications(updatedNotifications); - setMonitor((prev) => { - return { - ...prev, - notifications: updatedNotifications.map((notification) => notification.id), - }; - }); - }; - - // Setup - const theme = useTheme(); - - useEffect(() => { - if (setNotifications) { - const toSet = setNotifications.map((notification) => { - return notifications.find((n) => n.id === notification); - }); - setSelectedNotifications(toSet); - } - }, [setNotifications, notifications]); - - return ( - - { - handleSearch(value); - }} - labelMb={labelMb} - labelVariant={labelVariant} - labelFontWeight={labelFontWeight} - labelSx={labelSx} - gap={gap} - sx={{ - ...sx, - }} - /> - - {selectedNotifications.map((notification, index) => ( - - - {notification.notificationName} - - { - handleDelete(notification.id); - }} - style={{ cursor: "pointer" }} - /> - {index < selectedNotifications.length - 1 && } - - ))} - - - ); -}; - -NotificationConfig.propTypes = { - notifications: PropTypes.array, - setMonitor: PropTypes.func, - setNotifications: PropTypes.array, -}; - -export default NotificationConfig; diff --git a/client/src/Components/v2/common/index.ts b/client/src/Components/v2/common/index.ts index d0d1262b7..2ea3bf3f7 100644 --- a/client/src/Components/v2/common/index.ts +++ b/client/src/Components/v2/common/index.ts @@ -2,5 +2,4 @@ export * from "../common/charts/HistogramResponseTime"; export * from "../common/charts/HeatmapResponseTime"; export * from "../common/charts/HeatmapResponseTimeTooltip"; export * from "../common/controls/HeaderCreate"; -export * from "../common/controls/HeaderMonitorControls"; export * from "../common/controls/HeaderTimeRange"; 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/Select.tsx b/client/src/Components/v2/inputs/Select.tsx index 057b0e72c..b9675acd7 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,15 @@ 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 +36,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 +56,7 @@ export const SelectInput = ({ const select = ( {...props} + ref={ref} displayEmpty renderValue={renderValue} inputProps={{ @@ -100,3 +98,7 @@ export const SelectInput = ({ return select; }; + +export const SelectInput = forwardRef(SelectInputInner) as ( + props: SelectInputProps & { ref?: React.ForwardedRef } +) => React.ReactElement; diff --git a/client/src/Components/v2/inputs/Slider.tsx b/client/src/Components/v2/inputs/Slider.tsx new file mode 100644 index 000000000..97a16ac2c --- /dev/null +++ b/client/src/Components/v2/inputs/Slider.tsx @@ -0,0 +1,89 @@ +import Typography from "@mui/material/Typography"; +import { forwardRef } from "react"; +import Slider from "@mui/material/Slider"; +import type { SliderProps } from "@mui/material/Slider"; +import { useTheme } from "@mui/material/styles"; +import Stack from "@mui/material/Stack"; +import { FieldLabel } from "./FieldLabel"; +import Box from "@mui/material/Box"; +import type { ResponsiveStyleValue } from "@mui/system"; + +interface SliderInputProps extends SliderProps { + sx?: SliderProps["sx"]; + showValue?: boolean; +} + +export const SliderInput = forwardRef( + ({ sx, showValue = false, ...props }, ref) => { + const theme = useTheme(); + const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + + return ( + + {showValue && {props.value}} + + + ); + } +); + +interface SliderWithLabelProps extends SliderProps { + fieldLabel?: string; + required?: boolean; + showValue?: boolean; + sliderMaxWidth?: ResponsiveStyleValue; +} + +export const SliderWithLabel = forwardRef( + ( + { fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props }, + ref + ) => { + const theme = useTheme(); + + const labelText = fieldLabel; + + return ( + + {fieldLabel && {labelText}} + + + + + ); + } +); 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 70e740dbd..1618daf76 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -9,3 +9,6 @@ export { ToggleButtonGroupInput as ToggleButtonGroup, } from "./ToggleButton"; export { DialogInput as Dialog } from "./Dialog"; +export * from "./Radio"; +export * from "./Switch"; +export * from "./Slider"; diff --git a/client/src/Components/v2/monitors/ControlsFilter.tsx b/client/src/Components/v2/monitors/ControlsFilter.tsx index 34eedd190..01802ee3f 100644 --- a/client/src/Components/v2/monitors/ControlsFilter.tsx +++ b/client/src/Components/v2/monitors/ControlsFilter.tsx @@ -6,7 +6,7 @@ import useMediaQuery from "@mui/material/useMediaQuery"; import type { MonitorType } from "@/Types/Monitor"; import { Typography, useTheme } from "@mui/material"; -const types = ["http", "ping", "port", "docker"]; +const types = ["http", "ping", "port", "docker", "game"]; const statuses = ["up", "down"]; const states = ["active", "paused"]; diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/monitors/HeaderMonitorControls.tsx similarity index 59% rename from client/src/Components/v2/common/controls/HeaderMonitorControls.tsx rename to client/src/Components/v2/monitors/HeaderMonitorControls.tsx index 17576a8b2..d1a0d078d 100644 --- a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/monitors/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; } @@ -24,8 +42,8 @@ export const HeaderMonitorControls = ({ refetch, }: HeaderMonitorControlsProps) => { const { t } = useTranslation(); - const navigate = useNavigate(); const theme = useTheme(); + const navigate = useNavigate(); const { post, loading: isPosting, @@ -36,13 +54,7 @@ export const HeaderMonitorControls = ({ return null; } return ( - - + )} - + + ); +}; + +interface HeaderDeleteControlsProps { + monitor?: Monitor | null; + isAdmin: boolean; + refetch: Function; + onDelete?: () => void; +} + +export const HeaderDeleteControls = ({ + monitor, + isAdmin, + refetch, + onDelete, +}: HeaderDeleteControlsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { + post, + loading: isPosting, + // error: postError, + } = usePost(); + + if (!monitor) { + return null; + } + return ( + + + {isAdmin && ( + + )} + {isAdmin && ( + + )} + + ); }; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index ce90f1b8c..e2856d6a9 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -1,5 +1,6 @@ export * from "./ControlsFilter"; export * from "./MonitorStatBoxes"; +export * from "./HeaderMonitorControls"; export * from "./charts/HistogramStatus"; export * from "./charts/RadialAvgResponse"; export * from "./charts/HistogramDetails"; diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts index 08ef9a9a5..dfd256bdd 100644 --- a/client/src/Hooks/UseApi.ts +++ b/client/src/Hooks/UseApi.ts @@ -1,7 +1,7 @@ import useSWR from "swr"; import type { SWRConfiguration } from "swr"; import type { AxiosRequestConfig } from "axios"; -import { get, patch, post, deleteOp } from "@/Utils/ApiClient"; +import { get, patch, post, put, deleteOp } from "@/Utils/ApiClient"; import { useState } from "react"; import { useToast } from "@/Hooks/UseToast"; @@ -109,6 +109,42 @@ export const usePatch = () => { return { patch: patchFn, loading, error }; }; +export const usePut = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError, toastSuccess } = useToast(); + + const putFn = async ( + endpoint: string, + body?: B, + config?: AxiosRequestConfig + ): Promise | null> => { + setLoading(true); + setError(null); + + try { + const res = await put>(endpoint, body, { + ...config, + headers: { + ...config?.headers, + }, + }); + toastSuccess(res.data?.msg || "Operation successful"); + return res.data; + } catch (err: any) { + console.error(err); + const errMsg = err?.response?.data?.msg || err.message || "An error occurred"; + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { put: putFn, loading, error }; +}; + export const useDelete = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 9d46e9365..006c5b76e 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -1,107 +1,8 @@ import { useEffect, useState } from "react"; import { networkService } from "../main.jsx"; import { createToast } from "../Utils/toastUtils.jsx"; -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "./useMonitorUtils.js"; -import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -export const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => { - const [isLoading, setIsLoading] = useState(false); - const [monitors, setMonitors] = useState(undefined); - const [monitorsSummary, setMonitorsSummary] = useState(undefined); - const [networkError, setNetworkError] = useState(false); - - useEffect(() => { - const fetchMonitors = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorsWithSummaryByTeamId({ - types, - }); - const { monitors, summary } = res?.data?.data ?? {}; - setMonitors(monitors); - setMonitorsSummary(summary); - } catch (error) { - console.error(error); - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [types, monitorUpdateTrigger]); - return [monitors, monitorsSummary, isLoading, networkError]; -}; - -export const useFetchMonitorsWithChecks = ({ - types, - limit, - page, - rowsPerPage, - filter, - field, - order, - monitorUpdateTrigger, -}) => { - const [isLoading, setIsLoading] = useState(false); - const [count, setCount] = useState(undefined); - const [monitors, setMonitors] = useState(undefined); - const [summary, setSummary] = useState(undefined); - const [networkError, setNetworkError] = useState(false); - - const theme = useTheme(); - const { getMonitorWithPercentage } = useMonitorUtils(); - useEffect(() => { - const fetchMonitors = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorsWithChecksByTeamId({ - limit, - types, - page, - rowsPerPage, - filter, - field, - order, - }); - - const { count, monitors, summary } = res?.data?.data ?? {}; - const mappedMonitors = monitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - setSummary(summary); - setMonitors(mappedMonitors); - setCount(count || 0); - } catch (error) { - console.error(error); - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [ - field, - filter, - getMonitorWithPercentage, - limit, - order, - page, - rowsPerPage, - theme, - types, - monitorUpdateTrigger, - ]); - return [summary, monitors, count, isLoading, networkError]; -}; - export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { const [isLoading, setIsLoading] = useState(false); const [monitors, setMonitors] = useState(undefined); @@ -132,225 +33,17 @@ export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { return [monitors, isLoading, networkError]; }; -export const useFetchStatsByMonitorId = ({ - monitorId, - sortOrder, - limit, - dateRange, - numToDisplay, - normalize, - updateTrigger, -}) => { - const [monitor, setMonitor] = useState(undefined); - const [audits, setAudits] = useState(undefined); - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - useEffect(() => { - const fetchMonitor = async () => { - try { - setIsLoading(true); - const res = await networkService.getStatsByMonitorId({ - monitorId: monitorId, - sortOrder, - limit, - dateRange, - numToDisplay, - normalize, - }); - setMonitor(res?.data?.data ?? undefined); - setAudits(res?.data?.data?.checks?.[0]?.audits ?? undefined); - } catch (error) { - setNetworkError(true); - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitor(); - }, [monitorId, dateRange, numToDisplay, normalize, sortOrder, limit, updateTrigger]); - return [monitor, audits, isLoading, networkError]; -}; - -export const useFetchMonitorGames = ({ setGames, updateTrigger }) => { - const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - const fetchGames = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorGames(); - setGames(res.data.data); - } catch (error) { - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchGames(); - }, [setGames, updateTrigger]); - return [isLoading]; -}; - -export const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { - const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - if (typeof monitorId === "undefined") { - setIsLoading(false); - return; - } - const fetchMonitor = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorById({ monitorId: monitorId }); - setMonitor(res.data.data); - } catch (error) { - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitor(); - }, [monitorId, setMonitor, updateTrigger]); - return [isLoading]; -}; - -export const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => { - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [monitor, setMonitor] = useState(undefined); - - useEffect(() => { - const fetchMonitor = async () => { - try { - if (!monitorId) { - return { monitor: undefined, isLoading: false, networkError: undefined }; - } - const response = await networkService.getHardwareDetailsByMonitorId({ - monitorId: monitorId, - dateRange: dateRange, - }); - setMonitor(response.data.data); - } catch (error) { - setNetworkError(true); - } finally { - setIsLoading(false); - } - }; - fetchMonitor(); - }, [monitorId, dateRange, updateTrigger]); - return [monitor, isLoading, networkError]; -}; -export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigger }) => { - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [monitor, setMonitor] = useState(undefined); - const [monitorStats, setMonitorStats] = useState(undefined); - - useEffect(() => { - const fetchMonitor = async () => { - try { - if (!monitorId) { - return { monitor: undefined, isLoading: false, networkError: undefined }; - } - const response = await networkService.getPageSpeedDetailsByMonitorId({ - monitorId: monitorId, - dateRange: dateRange, - }); - setMonitor(response.data.data.monitor); - setMonitorStats(response.data.data.monitorStats); - } catch (error) { - setNetworkError(true); - } finally { - setIsLoading(false); - } - }; - fetchMonitor(); - }, [monitorId, dateRange, updateTrigger]); - return [monitor, monitorStats, isLoading, networkError]; -}; - -export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { - const [networkError, setNetworkError] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [monitor, setMonitor] = useState(undefined); - const [monitorStats, setMonitorStats] = useState(undefined); - useEffect(() => { - const fetchMonitors = async () => { - try { - const res = await networkService.getUptimeDetailsById({ - monitorId: monitorId, - dateRange: dateRange, - normalize: true, - }); - const { monitorData, monitorStats } = res?.data?.data ?? {}; - setMonitor(monitorData); - setMonitorStats(monitorStats); - } catch (error) { - setNetworkError(true); - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [dateRange, monitorId, trigger]); - return [monitor, monitorStats, isLoading, networkError]; -}; - -export const useCreateMonitor = () => { - const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate(); - const createMonitor = async ({ monitor, redirect }) => { - try { - setIsLoading(true); - await networkService.createMonitor({ monitor }); - createToast({ body: "Monitor created successfully!" }); - if (redirect) { - navigate(redirect); - } - } catch (error) { - createToast({ body: "Failed to create monitor." }); - } finally { - setIsLoading(false); - } - }; - return [createMonitor, isLoading]; -}; - -export const useFetchGlobalSettings = () => { - const [isLoading, setIsLoading] = useState(true); - const [globalSettings, setGlobalSettings] = useState(undefined); - useEffect(() => { - const fetchGlobalSettings = async () => { - try { - const res = await networkService.getAppSettings(); - setGlobalSettings(res?.data); - } catch (error) { - console.error("Failed to fetch global settings:", error); - createToast({ body: "Failed to load global settings" }); - } finally { - setIsLoading(false); - } - }; - - fetchGlobalSettings(); - }, []); - - return [globalSettings, isLoading]; -}; - export const useDeleteMonitor = () => { const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate(); - const deleteMonitor = async ({ monitor, redirect }) => { + const { t } = useTranslation(); + const deleteMonitor = async (monitorId, successCallback = () => {}) => { try { setIsLoading(true); - await networkService.deleteMonitorById({ monitorId: monitor.id }); - createToast({ body: "Monitor deleted successfully!" }); - if (redirect) { - navigate(redirect); - } + await networkService.deleteMonitorById({ monitorId }); + successCallback(); + createToast({ body: t("monitorDeleted") }); } catch (error) { - createToast({ body: "Failed to delete monitor." }); + createToast({ body: t("failedDeleteMonitor") }); } finally { setIsLoading(false); } @@ -358,68 +51,24 @@ export const useDeleteMonitor = () => { return [deleteMonitor, isLoading]; }; -export const useUpdateMonitor = () => { - const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate(); - const updateMonitor = async ({ monitor, redirect }) => { - try { - setIsLoading(true); - const updatedFields = { - name: monitor.name, - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - description: monitor.description, - interval: monitor.interval, - notifications: monitor.notifications, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - ignoreTlsErrors: monitor.ignoreTlsErrors, - jsonPath: monitor.jsonPath, - ...((monitor.type === "port" || monitor.type === "game") && { - port: monitor.port, - }), - ...(monitor.type == "game" && { - gameId: monitor.gameId, - }), - ...(monitor.type === "hardware" && { - thresholds: monitor.thresholds, - secret: monitor.secret, - selectedDisks: monitor.selectedDisks, - }), - }; - await networkService.updateMonitor({ - monitorId: monitor.id, - updatedFields, - }); - - createToast({ body: "Monitor updated successfully!" }); - if (redirect) { - navigate(redirect); - } - } catch (error) { - createToast({ body: "Failed to update monitor." }); - } finally { - setIsLoading(false); - } - }; - return [updateMonitor, isLoading]; -}; - export const usePauseMonitor = () => { const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - const pauseMonitor = async ({ monitorId, triggerUpdate }) => { + const [error, setError] = useState(null); + const { t } = useTranslation(); + + const pauseMonitor = async (monitorId, successCallback = () => {}) => { try { setIsLoading(true); const res = await networkService.pauseMonitorById({ monitorId }); - createToast({ - body: res.data.data.isActive - ? "Monitor resumed successfully" - : "Monitor paused successfully", - }); - triggerUpdate(); + successCallback(); + if (res.data.data.isActive === false) { + createToast({ body: t("monitorPaused") }); + } else { + createToast({ body: t("monitorResumed") }); + } } catch (error) { - setError(error); + setError(error.message); + createToast({ body: t("failedPauseMonitor") }); } finally { setIsLoading(false); } @@ -491,7 +140,7 @@ export const useCreateBulkMonitors = () => { try { const response = await networkService.createBulkMonitors(formData); - return [true, response.data, null]; // [success, data, error] + return [true, response.data, null]; } catch (err) { const errorMessage = err?.response?.data?.msg || err.message; return [false, null, errorMessage]; @@ -503,39 +152,6 @@ export const useCreateBulkMonitors = () => { return [createBulkMonitors, isLoading]; }; -export const useExportMonitors = () => { - const [isLoading, setIsLoading] = useState(false); - const { t } = useTranslation(); - - const exportMonitors = async () => { - setIsLoading(true); - try { - const response = await networkService.exportMonitors(); - - // Create a download link - const url = window.URL.createObjectURL(response.data); - const link = document.createElement("a"); - link.href = url; - link.setAttribute("download", "monitors.csv"); - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(url); - - createToast({ body: t("export.success") }); - return [true, null]; - } catch (err) { - const errorMessage = err?.response?.data?.msg || err.message; - createToast({ body: errorMessage || t("export.failed") }); - return [false, errorMessage]; - } finally { - setIsLoading(false); - } - }; - - return [exportMonitors, isLoading]; -}; - export const useFetchJson = () => { const [isLoading, setIsLoading] = useState(false); const fetchJson = async () => { diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts new file mode 100644 index 000000000..153cf9f19 --- /dev/null +++ b/client/src/Hooks/useMonitorForm.ts @@ -0,0 +1,108 @@ +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 || "", + description: data?.description || "", + 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, + useAdvancedMatching: data?.useAdvancedMatching || false, + matchMethod: data?.matchMethod || "", + expectedValue: data?.expectedValue || "", + jsonPath: data?.jsonPath || "", + }; + break; + case "ping": + defaults = { + ...base, + type: "ping", + url: data?.url || "", + }; + break; + case "port": + defaults = { + ...base, + type: "port", + url: data?.url || "", + port: data?.port || 80, + }; + break; + case "docker": + defaults = { + ...base, + type: "docker", + url: data?.url || "", + }; + break; + case "game": + defaults = { + ...base, + type: "game", + url: data?.url || "", + port: data?.port || 27015, + gameId: data?.gameId || "", + }; + break; + case "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, + useAdvancedMatching: 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..58ba49dcd --- /dev/null +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -0,0 +1,721 @@ +import { useMemo, 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"; +import { useTheme } from "@mui/material"; +import Stack from "@mui/material/Stack"; +import RadioGroup from "@mui/material/RadioGroup"; +import FormControl from "@mui/material/FormControl"; +import { Trans, useTranslation } from "react-i18next"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import Link from "@mui/material/Link"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import { Trash2 } from "lucide-react"; +import { HeaderDeleteControls } from "@/Components/v2/monitors"; + +import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; +import { + RadioWithDescription, + Button, + TextField, + Select, + Autocomplete, + SwitchComponent as Switch, + SliderWithLabel, + Dialog, +} from "@/Components/v2/inputs"; +import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; +import { useMonitorForm } from "@/Hooks/useMonitorForm"; +import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor"; +import type { Notification } from "@/Types/Notification"; +import type { MonitorFormData } from "@/Validation/monitor"; + +interface GeneralSettingsConfig { + urlLabel: string; + urlPlaceholder: string; + namePlaceholder: string; + showUrl: boolean; + showPort: boolean; + showGameSelect: boolean; + showSecret: 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, + showPort: false, + showGameSelect: false, + showSecret: false, + }, + ping: { + urlLabel: t("pages.createMonitor.form.general.option.host.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.host.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: false, + }, + docker: { + urlLabel: t("pages.createMonitor.form.general.option.container.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.container.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: false, + showGameSelect: false, + showSecret: false, + }, + port: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: true, + showGameSelect: false, + showSecret: false, + }, + game: { + urlLabel: t("pages.createMonitor.form.general.option.url.label"), + urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"), + namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"), + showUrl: true, + showPort: true, + showGameSelect: true, + showSecret: false, + }, + 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; +}; + +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 + ); + + // Fetch notifications for the team + const { data: notifications } = useGet("/notifications/team"); + // Fetch games for game type monitors + const { data: games } = useGet("/monitors/games"); + + const { schema, defaults } = useMonitorForm({ + data: existingMonitor ?? null, + defaultType, + }); + + 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; + + const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean; + + useEffect(() => { + clearErrors(); + }, [watchedType, 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) { + // Navigate based on page type + if (pageType === "pagespeed") { + navigate("/pagespeed"); + } else if (pageType === "hardware") { + navigate("/infrastructure"); + } else { + navigate("/uptime"); + } + } + }; + + const onError = (errors: unknown) => { + console.log(errors); + }; + + return ( + + + {/* Monitor Type Selection - only shown for uptime monitors */} + {showTypeSelector && ( + ( + + + + + + + + + + )} + /> + } + /> + )} + + {/* General Settings - Dynamic based on type */} + + {/* URL/Host/Container field - not shown for hardware */} + {generalSettingsConfig.showUrl && ( + ( + + )} + /> + )} + + {/* 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 && ( + ( + + )} + /> + )} + + {/* Secret field - only for hardware type */} + {generalSettingsConfig.showSecret && ( + ( + + )} + /> + )} + + {/* Display name field - common to all types */} + ( + + )} + /> + + } + /> + + {/* Frequency ConfigBox */} + ( + + )} + /> + } + /> + + {/* Incidents ConfigBox */} + + ( + + )} + /> + ( + + )} + /> + + } + /> + + {/* 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 && } + + ))} + + )} + + ); + }} + /> + } + /> + + {/* TLS/SSL ConfigBox - only for HTTP monitors */} + {watchedType === "http" && ( + ( + + field.onChange(e.target.checked)} + /> + + {t("pages.createMonitor.form.ignoreTls.option.tls.label")} + + + )} + /> + } + /> + )} + + {/* Advanced Settings ConfigBox - only for HTTP monitors */} + {watchedType === "http" && ( + + ( + + field.onChange(e.target.checked)} + /> + + {t( + "pages.createMonitor.form.advanced.option.advancedMatching.label" + )} + + + )} + /> + {watchedUseAdvancedMatching && ( + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + ), + }} + /> + + + )} + + } + /> + )} + + {/* Submit Button */} + + + + + + ); +}; + +export default CreateMonitorPage; 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/PageSpeed/Details/index.tsx b/client/src/Pages/PageSpeed/Details/index.tsx index f1e4df4ba..3be3a245b 100644 --- a/client/src/Pages/PageSpeed/Details/index.tsx +++ b/client/src/Pages/PageSpeed/Details/index.tsx @@ -1,12 +1,12 @@ import { BasePage } from "@/Components/v2/design-elements"; import type { PageSpeedDetailsResponse } from "@/Types/Monitor"; -import { HeaderMonitorControls } from "@/Components/v2/common"; import Stack from "@mui/material/Stack"; import { HistogramPageSpeedDetails, PiePageSpeed, PiePageSpeedLegend, MonitorStatBoxes, + HeaderMonitorControls, } from "@/Components/v2/monitors"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; 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} - /> -