Merge pull request #3225 from bluewave-labs/feat/v2-create-monitors

feat: V2 create monitors
This commit is contained in:
Alexander Holliday
2026-01-30 13:27:52 -08:00
committed by GitHub
54 changed files with 1587 additions and 3744 deletions
@@ -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 (
<FormControl
sx={{ minWidth: "10%" }}
size="small"
>
<Select
multiple={multiple}
IconComponent={(props) => (
<Icon
{...props}
name="PlusCircle"
size={18}
/>
)}
displayEmpty
value={controlledValue}
onChange={onChange}
renderValue={(selected) => {
if (!selected?.length) {
return header;
}
return (
header +
" | " +
selected
.map((value) => options.find((option) => option.value === value)?.label)
.filter(Boolean)
.join(", ")
);
}}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
}}
>
{options.map((option) => (
<MenuItem
key={option.value}
value={option.value}
sx={{
height: theme.spacing(17),
padding: 0,
}}
>
<Checkbox
checked={controlledValue.includes(option.value)}
size="small"
/>
<ListItemText primary={option.label} />
</MenuItem>
))}
</Select>
</FormControl>
);
};
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;
@@ -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 (
<React.Fragment>
<ButtonGroup
variant="contained"
color="accent"
ref={anchorRef}
aria-label="Monitor actions"
disabled={isLoading || isExporting}
>
<Button onClick={handleClick}>{options[selectedIndex]}</Button>
<Button
size="small"
aria-controls={open ? "split-button-menu" : undefined}
aria-expanded={open ? "true" : undefined}
aria-label="select monitor action"
aria-haspopup="menu"
onClick={handleToggle}
>
<Icon
name="ChevronDown"
size={20}
/>
</Button>
</ButtonGroup>
<Popper
sx={{ zIndex: 1 }}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === "bottom" ? "center top" : "center bottom",
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList
id="split-button-menu"
autoFocusItem
>
{options.map((option, index) => (
<MenuItem
key={option}
selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</React.Fragment>
);
};
export default MonitorActions;
@@ -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 <SkeletonLayout />;
if (monitorCount === 1) {
heading = "monitor";
}
return (
<Stack
direction="row"
alignItems="center"
display="flex"
width="fit-content"
height={theme.spacing(18)}
gap={theme.spacing(2)}
mt={theme.spacing(2)}
px={theme.spacing(4)}
pt={theme.spacing(2)}
pb={theme.spacing(3)}
borderRadius={theme.spacing(1)}
sx={{
...sx,
backgroundColor: theme.palette.secondary.main,
}}
>
{monitorCount} <Typography component="h2">{heading}</Typography>
{children}
</Stack>
);
};
MonitorCountHeader.propTypes = {
isLoading: PropTypes.bool,
monitorCount: PropTypes.number,
heading: PropTypes.string,
children: PropTypes.node,
sx: PropTypes.object,
};
export default MonitorCountHeader;
@@ -1,26 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Skeleton
variant="text"
width={100}
height={32}
/>
<Skeleton
variant="circular"
width={40}
height={40}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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();
@@ -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 <Skeleton />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Status monitor={monitor} />
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Tooltip
key={monitor?.id}
placement="bottom"
title={tooltipTitle}
>
<span>
<Button
variant="contained"
color="secondary"
loading={isSending}
startIcon={
<Icon
name="Mail"
size={18}
/>
}
disabled={isTestNotificationsDisabled}
onClick={() => {
testAllNotifications({ monitorId: monitor?.id });
}}
sx={{
whiteSpace: "nowrap",
}}
>
{t("sendTestNotifications")}
</Button>
</span>
</Tooltip>
<Button
variant="contained"
color="secondary"
onClick={(e) => {
navigate(`/incidents/${monitor?.id}`);
}}
>
{t("menu.incidents")}
</Button>
{isAdmin && (
<Button
variant="contained"
color="secondary"
loading={isPausing}
startIcon={
monitor?.isActive ? (
<Icon
name="Pause"
size={18}
/>
) : (
<Icon
name="Play"
size={18}
/>
)
}
onClick={() => {
pauseMonitor({
monitorId: monitor?.id,
triggerUpdate,
});
}}
>
{monitor?.isActive ? t("pause") : t("resume")}
</Button>
)}
{isAdmin && (
<Button
variant="contained"
color="secondary"
startIcon={
<Icon
name="Settings"
size={18}
/>
}
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
>
Configure
</Button>
)}
</Stack>
</Stack>
);
};
MonitorDetailsControlHeader.propTypes = {
path: PropTypes.string,
isLoading: PropTypes.bool,
isAdmin: PropTypes.bool,
monitor: PropTypes.object,
triggerUpdate: PropTypes.func,
};
export default MonitorDetailsControlHeader;
@@ -1,23 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 (
<Stack>
<Typography variant="monitorName">{monitor?.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography variant="monitorUrl">{formatMonitorUrl(monitor?.url)}</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Stack>
);
};
Status.propTypes = {
monitor: PropTypes.object,
};
export default Status;
@@ -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 (
<Stack gap={theme.spacing(6)}>
<Search
type="notifications"
label="Notifications"
options={notifications}
filteredBy="notificationName"
multiple={true}
value={selectedNotifications}
inputValue={notificationsSearch}
handleInputChange={setNotificationsSearch}
handleChange={(value) => {
handleSearch(value);
}}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
...sx,
}}
/>
<Stack
flex={1}
width="100%"
>
{selectedNotifications.map((notification, index) => (
<Stack
direction="row"
alignItems="center"
key={notification.id}
width="100%"
>
<Typography
flexGrow={1} // <-- This will take up all available horizontal space
>
{notification.notificationName}
</Typography>
<Icon
name="Trash2"
size={20}
onClick={() => {
handleDelete(notification.id);
}}
style={{ cursor: "pointer" }}
/>
{index < selectedNotifications.length - 1 && <Divider />}
</Stack>
))}
</Stack>
</Stack>
);
};
NotificationConfig.propTypes = {
notifications: PropTypes.array,
setMonitor: PropTypes.func,
setNotifications: PropTypes.array,
};
export default NotificationConfig;
-1
View File
@@ -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";
+88
View File
@@ -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 (
<Radio
{...props}
icon={
<Circle
size={16}
strokeWidth={1.5}
/>
}
checkedIcon={
<CircleDot
size={14}
strokeWidth={1.5}
/>
}
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 (
<FormControlLabel
control={<RadioInput {...props} />}
label={
<>
<Typography component="p">{label}</Typography>
<Typography
component="h6"
color={theme.palette.text.secondary}
>
{description}
</Typography>
</>
}
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),
},
}}
/>
);
};
+14 -12
View File
@@ -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<T> extends Omit<SelectProps<T>, "label"> {
placeholderColor?: string;
}
export const SelectInput = <T,>({
fieldLabel,
required,
placeholder,
placeholderColor,
...props
}: SelectInputProps<T>) => {
const SelectInputInner = <T,>(
{ fieldLabel, required, placeholder, placeholderColor, ...props }: SelectInputProps<T>,
ref: React.ForwardedRef<HTMLDivElement>
) => {
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 = <T,>({
const capitalized = items.map(
(item) => item.charAt(0).toUpperCase() + item.slice(1)
);
return (<Typography>{capitalized.join(" | ")}</Typography>) as any;
return <Typography>{capitalized.join(" | ")}</Typography>;
}
const nodes = React.Children.toArray(props.children as React.ReactNode);
@@ -59,6 +56,7 @@ export const SelectInput = <T,>({
const select = (
<Select<T>
{...props}
ref={ref}
displayEmpty
renderValue={renderValue}
inputProps={{
@@ -100,3 +98,7 @@ export const SelectInput = <T,>({
return select;
};
export const SelectInput = forwardRef(SelectInputInner) as <T>(
props: SelectInputProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> }
) => React.ReactElement;
@@ -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<HTMLSpanElement, SliderInputProps>(
({ sx, showValue = false, ...props }, ref) => {
const theme = useTheme();
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
return (
<Stack
gap={theme.spacing(8)}
direction={"row"}
alignItems={"center"}
>
{showValue && <Typography>{props.value}</Typography>}
<Slider
{...props}
ref={ref}
sx={[
{
"& .MuiSlider-track": {
backgroundColor: theme.palette.primary.main,
border: "none",
},
"& .MuiSlider-rail": {
backgroundColor: theme.palette.grey[300],
opacity: 1,
},
"& .MuiSlider-thumb": {
backgroundColor: "#fff",
"&:hover, &.Mui-focusVisible": {
boxShadow: `0 0 0 8px ${theme.palette.primary.main}20`,
},
},
"& .MuiSlider-valueLabel": {
backgroundColor: theme.palette.primary.main,
},
},
...additionalSx,
]}
/>
</Stack>
);
}
);
interface SliderWithLabelProps extends SliderProps {
fieldLabel?: string;
required?: boolean;
showValue?: boolean;
sliderMaxWidth?: ResponsiveStyleValue<number | string>;
}
export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>(
(
{ fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props },
ref
) => {
const theme = useTheme();
const labelText = fieldLabel;
return (
<Stack spacing={theme.spacing(2)}>
{fieldLabel && <FieldLabel required={required}>{labelText}</FieldLabel>}
<Box maxWidth={sliderMaxWidth}>
<SliderInput
{...props}
showValue={showValue}
value={value}
ref={ref}
/>
</Box>
</Stack>
);
}
);
@@ -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<HTMLInputElement, SwitchProps>(
function SwitchComponent({ sx, ...props }, ref) {
const theme = useTheme();
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
return (
<Switch
{...props}
slotProps={{
input: {
ref: ref,
},
}}
sx={[
{
"& .MuiSwitch-switchBase": {
"&.Mui-checked": {
color: "#E0E0E0",
"& + .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
border: 0,
},
},
},
},
...additionalSx,
]}
/>
);
}
);
@@ -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";
@@ -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"];
@@ -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<BaseHeaderProps>) => {
const theme = useTheme();
return (
<Stack
spacing={{ xs: theme.spacing(8), md: 0 }}
direction={{ xs: "column", md: "row" }}
alignItems={"center"}
justifyContent={"space-between"}
>
<MonitorStatus monitor={monitor} />
{children}
</Stack>
);
};
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 (
<Stack
spacing={{ xs: theme.spacing(8), md: 0 }}
direction={{ xs: "column", md: "row" }}
alignItems={"center"}
justifyContent={"space-between"}
>
<MonitorStatus monitor={monitor} />
<BaseHeader monitor={monitor}>
<Stack
width={{ xs: "100%", md: "auto" }}
direction={{ xs: "column", md: "row" }}
@@ -97,6 +109,68 @@ export const HeaderMonitorControls = ({
</Button>
)}
</Stack>
</Stack>
</BaseHeader>
);
};
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<any, Monitor>();
if (!monitor) {
return null;
}
return (
<BaseHeader monitor={monitor}>
<Stack
width={{ xs: "100%", md: "auto" }}
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(2)}
>
{isAdmin && (
<Button
variant="contained"
color="secondary"
loading={isPosting}
startIcon={monitor?.isActive ? <Icon icon={Pause} /> : <Icon icon={Play} />}
onClick={async () => {
await post(`/monitors/pause/${monitor.id}`, {});
await refetch();
}}
>
{monitor?.isActive ? t("pause") : t("resume")}
</Button>
)}
{isAdmin && (
<Button
variant="contained"
color="error"
startIcon={<Icon icon={Trash} />}
onClick={() => {
onDelete?.();
}}
>
{t("common.buttons.delete")}
</Button>
)}
</Stack>
</BaseHeader>
);
};
@@ -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";
+37 -1
View File
@@ -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 = <B = any, R = any>() => {
return { patch: patchFn, loading, error };
};
export const usePut = <B = any, R = any>() => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { toastError, toastSuccess } = useToast();
const putFn = async (
endpoint: string,
body?: B,
config?: AxiosRequestConfig
): Promise<ApiResponse<R> | null> => {
setLoading(true);
setError(null);
try {
const res = await put<ApiResponse<R>>(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 = <R = any>() => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
+19 -403
View File
@@ -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 () => {
+108
View File
@@ -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]);
};
+721
View File
@@ -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<string, GeneralSettingsConfig> = {
http: {
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
},
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<Monitor>(
isEditMode ? `/monitors/${monitorId}` : null
);
// Fetch notifications for the team
const { data: notifications } = useGet<Notification[]>("/notifications/team");
// Fetch games for game type monitors
const { data: games } = useGet<GamesMap>("/monitors/games");
const { schema, defaults } = useMonitorForm({
data: existingMonitor ?? null,
defaultType,
});
const form = useForm<MonitorFormData>({
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<MonitorFormData, Monitor>();
const { patch, loading: isUpdating } = usePatch<MonitorFormData, Monitor>();
const isSubmitting = isCreating || isUpdating;
// Delete functionality
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const { deleteFn, loading: isDeleting } = useDelete();
const handleDeleteClick = () => {
setIsDeleteDialogOpen(true);
};
const handleDeleteConfirm = async () => {
if (!monitorId) return;
await deleteFn(`/monitors/${monitorId}`);
setIsDeleteDialogOpen(false);
// Navigate based on page type
if (pageType === "pagespeed") {
navigate("/pagespeed");
} else if (pageType === "hardware") {
navigate("/infrastructure");
} else {
navigate("/uptime");
}
};
const handleDeleteCancel = () => {
setIsDeleteDialogOpen(false);
};
const onSubmit = async (data: MonitorFormData) => {
let result;
if (isEditMode && monitorId) {
result = await patch(`/monitors/${monitorId}`, data);
} else {
result = await post("/monitors", data);
}
if (result?.success) {
// 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 (
<BasePage
component="form"
onSubmit={handleSubmit(onSubmit, onError)}
>
<HeaderDeleteControls
monitor={existingMonitor}
isAdmin={true}
refetch={refetchMonitor}
onDelete={handleDeleteClick}
/>
{/* Monitor Type Selection - only shown for uptime monitors */}
{showTypeSelector && (
<ConfigBox
title={t("pages.createMonitor.form.type.title")}
subtitle={t("pages.createMonitor.form.type.description")}
rightContent={
<Controller
name="type"
control={control}
render={({ field, fieldState }) => (
<FormControl error={!!fieldState.error}>
<RadioGroup
{...field}
sx={{ gap: theme.spacing(6) }}
>
<RadioWithDescription
value="http"
label={t("pages.createMonitor.form.type.optionHttp")}
description={t(
"pages.createMonitor.form.type.optionHttpDescription"
)}
/>
<RadioWithDescription
value="ping"
label={t("pages.createMonitor.form.type.optionPing")}
description={t(
"pages.createMonitor.form.type.optionPingDescription"
)}
/>
<RadioWithDescription
value="docker"
label={t("pages.createMonitor.form.type.optionDocker")}
description={t(
"pages.createMonitor.form.type.optionDockerDescription"
)}
/>
<RadioWithDescription
value="port"
label={t("pages.createMonitor.form.type.optionPort")}
description={t(
"pages.createMonitor.form.type.optionPortDescription"
)}
/>
<RadioWithDescription
value="game"
label={t("pages.createMonitor.form.type.optionGame")}
description={t(
"pages.createMonitor.form.type.optionGameDescription"
)}
/>
</RadioGroup>
</FormControl>
)}
/>
}
/>
)}
{/* General Settings - Dynamic based on type */}
<ConfigBox
title={t("pages.createMonitor.form.general.title")}
subtitle={t(`pages.createMonitor.form.general.description.${watchedType}`)}
rightContent={
<Stack spacing={theme.spacing(8)}>
{/* URL/Host/Container field - not shown for hardware */}
{generalSettingsConfig.showUrl && (
<Controller
name="url"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={generalSettingsConfig.urlLabel}
placeholder={generalSettingsConfig.urlPlaceholder}
fullWidth
disabled={isEditMode}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
)}
{/* Port field - only for port and game types */}
{generalSettingsConfig.showPort && (
<Controller
name="port"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
onChange={(e) => 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 && (
<Controller
name="gameId"
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? ""}
fieldLabel={t("chooseGame")}
error={!!fieldState.error}
>
<MenuItem value="">Select a game</MenuItem>
{games &&
Object.entries(games).map(([key, game]) => (
<MenuItem
key={key}
value={key}
>
{game.name}
</MenuItem>
))}
</Select>
)}
/>
)}
{/* Secret field - only for hardware type */}
{generalSettingsConfig.showSecret && (
<Controller
name="secret"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
type="text"
fieldLabel={t("pages.createMonitor.form.general.option.secret.label")}
placeholder={t(
"pages.createMonitor.form.general.option.secret.placeholder"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
)}
{/* Display name field - common to all types */}
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
type="text"
fieldLabel={t("pages.createMonitor.form.general.option.name.label")}
placeholder={generalSettingsConfig.namePlaceholder}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
{/* Frequency ConfigBox */}
<ConfigBox
title={t("pages.createMonitor.form.frequency.title")}
subtitle={t("pages.createMonitor.form.frequency.description")}
rightContent={
<Controller
name="interval"
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value ?? 60000}
fieldLabel={t(
"pages.createMonitor.form.frequency.option.frequency.label"
)}
error={!!fieldState.error}
>
<MenuItem value={15000}>{t("time.fifteenSeconds")}</MenuItem>
<MenuItem value={30000}>{t("time.thirtySeconds")}</MenuItem>
<MenuItem value={60000}>{t("time.oneMinute")}</MenuItem>
<MenuItem value={120000}>{t("time.twoMinutes")}</MenuItem>
<MenuItem value={180000}>{t("time.threeMinutes")}</MenuItem>
<MenuItem value={240000}>{t("time.fourMinutes")}</MenuItem>
<MenuItem value={300000}>{t("time.fiveMinutes")}</MenuItem>
<MenuItem value={600000}>{t("time.tenMinutes")}</MenuItem>
<MenuItem value={900000}>{t("time.fifteenMinutes")}</MenuItem>
<MenuItem value={1800000}>{t("time.thirtyMinutes")}</MenuItem>
</Select>
)}
/>
}
/>
{/* Incidents ConfigBox */}
<ConfigBox
title={t("pages.createMonitor.form.incidents.title")}
subtitle={t("pages.createMonitor.form.incidents.description")}
rightContent={
<Stack spacing={theme.spacing(12)}>
<Controller
name="statusWindowSize"
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
fieldLabel={t("pages.createMonitor.form.incidents.option.checks.label")}
min={1}
max={25}
valueLabelDisplay="auto"
/>
)}
/>
<Controller
name="statusWindowThreshold"
control={control}
render={({ field }) => (
<SliderWithLabel
{...field}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
fieldLabel={t(
"pages.createMonitor.form.incidents.option.percentage.label"
)}
min={1}
max={100}
valueLabelDisplay="auto"
/>
)}
/>
</Stack>
}
/>
{/* Notifications ConfigBox */}
<ConfigBox
title={t("pages.createMonitor.form.notifications.title")}
subtitle={t("pages.createMonitor.form.notifications.description")}
rightContent={
<Controller
name="notifications"
control={control}
render={({ field }) => {
// Map notifications to have 'name' property for v2 Autocomplete
const notificationOptions = (notifications ?? []).map((n) => ({
...n,
name: n.notificationName,
}));
const selectedNotifications = notificationOptions.filter((n) =>
(field.value ?? []).includes(n.id)
);
return (
<Stack spacing={theme.spacing(4)}>
<Autocomplete
multiple
options={notificationOptions}
value={selectedNotifications}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: typeof notificationOptions) => {
field.onChange(newValue.map((n) => n.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
/>
{selectedNotifications.length > 0 && (
<Stack
flex={1}
width="100%"
>
{selectedNotifications.map((notification, index) => (
<Stack
direction="row"
alignItems="center"
key={notification.id}
width="100%"
>
<Typography flexGrow={1}>
{notification.notificationName}
</Typography>
<IconButton
size="small"
onClick={() => {
field.onChange(
(field.value ?? []).filter(
(id: string) => id !== notification.id
)
);
}}
aria-label="Remove notification"
>
<Trash2 size={16} />
</IconButton>
{index < selectedNotifications.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
);
}}
/>
}
/>
{/* TLS/SSL ConfigBox - only for HTTP monitors */}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.ignoreTls.title")}
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
rightContent={
<Controller
name="ignoreTlsErrors"
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(2)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t("pages.createMonitor.form.ignoreTls.option.tls.label")}
</Typography>
</Stack>
)}
/>
}
/>
)}
{/* Advanced Settings ConfigBox - only for HTTP monitors */}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.advanced.title")}
subtitle={t("pages.createMonitor.form.advanced.description")}
rightContent={
<Stack spacing={theme.spacing(8)}>
<Controller
name="useAdvancedMatching"
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(2)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t(
"pages.createMonitor.form.advanced.option.advancedMatching.label"
)}
</Typography>
</Stack>
)}
/>
{watchedUseAdvancedMatching && (
<Stack spacing={theme.spacing(8)}>
<Controller
name="matchMethod"
control={control}
render={({ field }) => (
<Select
{...field}
value={field.value ?? "equal"}
fieldLabel={t(
"pages.createMonitor.form.advanced.option.matchMethod.label"
)}
>
<MenuItem value="equal">{t("matchMethodOptions.equal")}</MenuItem>
<MenuItem value="include">
{t("matchMethodOptions.include")}
</MenuItem>
<MenuItem value="regex">{t("matchMethodOptions.regex")}</MenuItem>
</Select>
)}
/>
<Controller
name="expectedValue"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.createMonitor.form.advanced.option.expectedValue.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="jsonPath"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.createMonitor.form.advanced.option.jsonPath.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Typography
component="span"
color="text.secondary"
sx={{ opacity: 0.8 }}
>
<Trans
i18nKey="pages.createMonitor.form.advanced.option.jsonPath.description"
components={{
jmesLink: (
<Link
href="https://jmespath.org/"
target="_blank"
rel="noopener noreferrer"
/>
),
}}
/>
</Typography>
</Stack>
)}
</Stack>
}
/>
)}
{/* Submit Button */}
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
loading={isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{t("common.buttons.save")}
</Button>
</Stack>
<Dialog
open={isDeleteDialogOpen}
title={t("common.dialogs.delete.title")}
content={t("common.dialogs.delete.description")}
onConfirm={handleDeleteConfirm}
onCancel={handleDeleteCancel}
loading={isDeleting}
/>
</BasePage>
);
};
export default CreateMonitorPage;
@@ -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 (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("infrastructureCustomizeAlerts")}
</Typography>
<Typography component="p">
{t("infrastructureAlertNotificationDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu" ? capitalizeFirstLetter(metric) : metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
onFieldChange={onChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{getAlertError(errors)}
</Typography>
)}
</Stack>
</ConfigBox>
);
};
CustomAlertsSection.propTypes = {
errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
infrastructureMonitor: PropTypes.object.isRequired,
handleCheckboxChange: PropTypes.func.isRequired,
};
export default CustomAlertsSection;
@@ -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
* <CustomThreshold
* checkboxId="cpu-threshold"
* checkboxName="cpu_threshold"
* checkboxLabel="Enable CPU Threshold"
* isChecked={true}
* onCheckboxChange={handleCheckboxToggle}
* fieldId="cpu-threshold-value"
* fieldName="cpu_threshold_value"
* fieldValue="80"
* onFieldChange={handleFieldChange}
* onFieldBlur={handleFieldBlur}
* alertUnit="%"
* errors={{}}
* infrastructureMonitor={monitorConfig}
* />
*/
export const CustomThreshold = ({
checkboxId,
checkboxName,
checkboxLabel,
onCheckboxChange,
isChecked,
fieldId,
fieldName,
fieldValue,
onFieldChange,
onFieldBlur,
alertUnit,
errors,
}) => {
const theme = useTheme();
return (
<Stack
direction={{ sm: "column", md: "row" }}
spacing={theme.spacing(2)}
>
<Box
sx={{
width: { md: "45%", lg: "25%", xl: "20%" },
}}
justifyContent="flex-start"
>
<Checkbox
id={checkboxId}
name={checkboxName}
label={checkboxLabel}
isChecked={isChecked}
onChange={onCheckboxChange}
/>
</Box>
<Stack
direction={"row"}
sx={{
justifyContent: "flex-start",
}}
alignItems="center"
spacing={theme.spacing(4)}
>
<TextInput
maxWidth="var(--env-var-width-4)"
type="number"
id={fieldId}
name={fieldName}
value={fieldValue}
onBlur={onFieldBlur}
onChange={onFieldChange}
error={errors[fieldId] ? true : false}
disabled={!isChecked}
/>
<Typography
component="p"
m={theme.spacing(3)}
>
{alertUnit}
</Typography>
</Stack>
</Stack>
);
};
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,
};
@@ -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 (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("v1.infrastructure.disk_selection_title")}
</Typography>
<Typography component="p">
{t("v1.infrastructure.disk_selection_description")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{!availableDisks || availableDisks.length === 0 ? (
<Typography
variant="body2"
sx={{ fontStyle: "italic", opacity: 0.8 }}
>
{t("v1.infrastructure.disk_selection_info")}
</Typography>
) : (
availableDisks.map((disk) => {
const identifier = disk.mountpoint || disk.device;
return (
<Stack
key={identifier}
direction={{ sm: "column", md: "row" }}
spacing={theme.spacing(2)}
>
<Box
sx={{
width: "100%",
}}
justifyContent="flex-start"
>
<Checkbox
id={`disk-${identifier}`}
name={identifier}
label={identifier}
isChecked={selectedDisks.includes(identifier)}
onChange={(e) => handleDiskChange(e, identifier)}
/>
</Box>
</Stack>
);
})
)}
</Stack>
</ConfigBox>
);
};
DiskSelection.propTypes = {
availableDisks: PropTypes.arrayOf(
PropTypes.shape({
mountpoint: PropTypes.string.isRequired,
})
),
selectedDisks: PropTypes.arrayOf(PropTypes.string).isRequired,
onChange: PropTypes.func.isRequired,
};
export default DiskSelection;
@@ -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 (
<Box
alignSelf="flex-end"
ml="auto"
>
<Button
onClick={handlePause}
loading={isBusy}
variant="contained"
color="secondary"
sx={{
pl: theme.spacing(4),
pr: theme.spacing(6),
"& svg": {
mr: theme.spacing(2),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 0.1,
},
},
}}
>
{monitor?.isActive ? (
<>
<Icon
name="PauseCircle"
size={20}
/>
{t("pause")}
</>
) : (
<>
<Icon
name="PlayCircle"
size={20}
/>
{t("resume")}
</>
)}
</Button>
<Button
loading={isBusy}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ ml: theme.spacing(6) }}
>
{t("remove")}
</Button>
<Dialog
open={isOpen}
theme={theme}
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
/>
</Box>
);
};
MonitorActionButtons.propTypes = {
monitor: PropTypes.object.isRequired,
isBusy: PropTypes.bool.isRequired,
handlePause: PropTypes.func.isRequired,
handleRemove: PropTypes.func.isRequired,
};
export default MonitorActionButtons;
@@ -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 (
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={pagespeedStatusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: { offset: [0, -8] },
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="monitorUrl"
>
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
ml={theme.spacing(6)}
mt={theme.spacing(1)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: theme.spacing(2),
height: theme.spacing(2),
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: theme.spacing(-5),
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{t("editing")}
</Typography>
</Stack>
);
};
MonitorStatusHeader.propTypes = {
monitor: PropTypes.object.isRequired,
infrastructureMonitor: PropTypes.object.isRequired,
};
export default MonitorStatusHeader;
@@ -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;
@@ -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;
@@ -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;
@@ -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 (
<Box className="create-infrastructure-monitor">
<Breadcrumbs list={CRUMBS} />
<Stack
component="form"
onSubmit={onSubmit}
noValidate
spellCheck="false"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
color={
!isCreate ? theme.palette.primary.contrastTextSecondary : undefined
}
>
{!isCreate ? infrastructureMonitor.name : t("createYour") + " "}
</Typography>
{isCreate ? (
<Typography
component="span"
fontSize="inherit"
fontWeight="inherit"
color={theme.palette.primary.contrastTextSecondary}
>
{t("monitor")}
</Typography>
) : (
<></>
)}
</Typography>
{!isCreate && monitor && (
<MonitorStatusHeader
monitor={monitor}
infrastructureMonitor={infrastructureMonitor}
/>
)}
</Box>
{!isCreate && monitor && (
<MonitorActionButtons
monitor={monitor}
isBusy={isBusy}
handlePause={handlePause}
handleRemove={handleRemove}
/>
)}
</Stack>
<ConfigBox>
<Stack>
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">
{t("infrastructureCreateGeneralSettingsDescription")}
</Typography>
<Typography component="p">
{t("infrastructureServerRequirement")}{" "}
<Link
level="primary"
url="https://github.com/bluewave-labs/checkmate-agent"
label={t("common.monitoringAgentName")}
/>
</Typography>
</Stack>
<Stack gap={theme.spacing(8)}>
<TextInput
type="url"
id="url"
name="url"
startAdornment={<HttpAdornment https={https} />}
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 && (
<FieldWrapper
label={t("infrastructureProtocol")}
labelVariant="p"
>
<ButtonGroup>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
</FieldWrapper>
)}
<TextInput
type="text"
id="name"
name="name"
label={t("infrastructureDisplayNameLabel")}
placeholder="Google"
isOptional={true}
value={infrastructureMonitor.name}
onChange={onChange}
error={errors["name"]}
/>
<TextInput
type="text"
id="secret"
name="secret"
label={t("infrastructureAuthorizationSecretLabel")}
value={infrastructureMonitor.secret}
onChange={onChange}
error={errors["secret"] ? true : false}
helperText={errors["secret"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("notificationConfig.title")}
</Typography>
<Typography component="p">{t("notificationConfig.description")}</Typography>
</Box>
<NotificationsConfig
notifications={notifications}
setMonitor={setInfrastructureMonitor}
setNotifications={infrastructureMonitor.notifications}
/>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("createMonitorPage.incidentConfigTitle")}
</Typography>
<Typography component="p">
{t("createMonitorPage.incidentConfigDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="statusWindowSize"
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
type="number"
value={infrastructureMonitor.statusWindowSize}
onChange={onChange}
error={errors["statusWindowSize"] ? true : false}
helperText={errors["statusWindowSize"]}
/>
<TextInput
name="statusWindowThreshold"
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
type="number"
value={infrastructureMonitor.statusWindowThreshold}
onChange={onChange}
error={errors["statusWindowThreshold"] ? true : false}
helperText={errors["statusWindowThreshold"]}
/>
</Stack>
</ConfigBox>
<CustomAlertsSection
errors={errors}
onChange={onChange}
infrastructureMonitor={infrastructureMonitor}
handleCheckboxChange={handleCheckboxChange}
/>
{monitorId && (
<DiskSelection
availableDisks={availableDisks}
selectedDisks={infrastructureMonitor.selectedDisks}
onChange={(newSelectedDisks) =>
onChangeForm("selectedDisks", newSelectedDisks)
}
/>
)}
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Select
id="interval"
name="interval"
label="Check frequency"
value={infrastructureMonitor.interval || 15}
onChange={onChange}
items={FREQUENCIES}
/>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
type="submit"
variant="contained"
color="accent"
loading={isBusy}
>
{t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")}
</Button>
</Stack>
</Stack>
</Box>
);
};
export default CreateInfrastructureMonitor;
@@ -1,6 +1,6 @@
import { BasePage, Tab, Tabs } from "@/Components/v2/design-elements";
import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common";
import { MonitorStatBoxes } from "@/Components/v2/monitors";
import { HeaderTimeRange } from "@/Components/v2/common";
import { MonitorStatBoxes, HeaderMonitorControls } from "@/Components/v2/monitors";
import { TabNetwork } from "@/Pages/Infrastructure/Details/Components/TabNetwork";
import { TabOverview } from "@/Pages/Infrastructure/Details/Components/TabOverview";
-514
View File
@@ -1,514 +0,0 @@
// Components
import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Icon from "@/Components/v1/Icon";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import PulseDot from "@/Components/v1/Animated/PulseDot.jsx";
import SkeletonLayout from "./skeleton.jsx";
import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx";
import Dialog from "@/Components/v1/Dialog/index.jsx";
import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
import Radio from "@/Components/v1/Inputs/Radio/index.jsx";
// Utils
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { monitorValidation } from "../../../Validation/validation.js";
import { parseDomainName } from "../../../Utils/monitorUtilsLegacy.js";
import { useTranslation } from "react-i18next";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js";
import { useTheme } from "@emotion/react";
import { createToast } from "../../../Utils/toastUtils.jsx";
import { useParams } from "react-router";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
import {
useCreateMonitor,
useFetchMonitorById,
useDeleteMonitor,
useUpdateMonitor,
usePauseMonitor,
} from "../../../Hooks/monitorHooks.js";
const PageSpeedSetup = () => {
const { monitorId } = useParams();
const isCreate = typeof monitorId === "undefined";
const CRUMBS = [
{ name: "pagespeed", path: "/pagespeed" },
...(isCreate
? [{ name: "create", path: `/pagespeed/create` }]
: [
{ name: "details", path: `/pagespeed/${monitorId}` },
{ name: "configure", path: `/pagespeed/configure/${monitorId}` },
]),
];
// States
const [monitor, setMonitor] = useState(
isCreate
? {
url: "",
name: "",
type: "pagespeed",
notifications: [],
interval: 180000,
}
: {}
);
const [https, setHttps] = useState(true);
const [errors, setErrors] = useState({});
const [isOpen, setIsOpen] = useState(false);
const [updateTrigger, setUpdateTrigger] = useState(false);
// Setup
const { t } = useTranslation();
const theme = useTheme();
// Constants
const MS_PER_MINUTE = 60000;
const FREQUENCIES = [
{ _id: 3, name: t("time.threeMinutes") },
{ _id: 5, name: t("time.fiveMinutes") },
{ _id: 10, name: t("time.tenMinutes") },
{ _id: 20, name: t("time.twentyMinutes") },
{ _id: 60, name: t("time.oneHour") },
{ _id: 1440, name: t("time.oneDay") },
{ _id: 10080, name: t("time.oneWeek") },
];
const { user } = useSelector((state) => state.auth);
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
const [notifications, notificationsAreLoading, notificationsError] =
useGetNotificationsByTeamId();
// Hooks for API actions
const [isLoading] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger });
const [createMonitor, isCreating] = useCreateMonitor();
const [deleteMonitor, isDeleting] = useDeleteMonitor();
const [updateMonitor, isUpdating] = useUpdateMonitor();
const [pauseMonitor, isPausing] = usePauseMonitor();
// Handlers
const onSubmit = async (event) => {
event.preventDefault();
if (isCreate) {
let form = {
url: `http${https ? "s" : ""}://` + monitor.url,
name: monitor.name === "" ? monitor.url : monitor.name,
type: monitor.type,
interval: monitor.interval,
};
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: form.name,
notifications: monitor.notifications,
};
await createMonitor({ monitor: form, redirect: "/pagespeed" });
} else {
const monitorParams = {
url: monitor.url,
name: monitor.name === "" ? monitor.url : monitor.name,
type: monitor.type,
interval: monitor.interval,
};
const { error } = monitorValidation.validate(monitorParams, { abortEarly: false });
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({ body: t("checkFormError") });
return;
}
await updateMonitor({ monitor, redirect: "/pagespeed" });
}
};
const handleChange = (event) => {
let { value, name } = event.target;
if (name === "interval") {
value = value * MS_PER_MINUTE;
}
setMonitor({
...monitor,
[name]: value,
});
const { error } = monitorValidation.validate(
{ [name]: value, type: monitor.type },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const handleBlur = (event) => {
const { name, value } = event.target;
if (name === "url" && monitor.name === "") {
setMonitor((prev) => ({ ...prev, name: parseDomainName(value) }));
}
};
const triggerUpdate = () => {
setUpdateTrigger(!updateTrigger);
};
const handlePause = async () => {
await pauseMonitor({ monitorId, triggerUpdate });
};
const handleRemove = async (event) => {
event.preventDefault();
await deleteMonitor({ monitor: { id: monitorId }, redirect: "/pagespeed" });
};
const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing;
if (Object.keys(monitor).length === 0) {
return <SkeletonLayout />;
}
return (
<Box
sx={{
"& h1": { color: theme.palette.primary.contrastText },
}}
>
<Breadcrumbs list={CRUMBS} />
<Stack
component="form"
onSubmit={onSubmit}
noValidate
spellCheck="false"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
color={
!isCreate ? theme.palette.primary.contrastTextSecondary : undefined
}
>
{!isCreate ? monitor.name : t("createYour") + " "}
</Typography>
{isCreate ? (
<Typography
component="span"
fontSize="inherit"
fontWeight="inherit"
color={theme.palette.primary.contrastTextSecondary}
>
{t("pageSpeedMonitor")}
</Typography>
) : (
<></>
)}
</Typography>
{!isCreate && (
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={pagespeedStatusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: { offset: [0, -8] },
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="monitorUrl"
>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
ml={theme.spacing(6)}
mt={theme.spacing(1)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: theme.spacing(2),
height: theme.spacing(2),
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: theme.spacing(-5),
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{t("editing")}
</Typography>
</Stack>
)}
</Box>
{!isCreate && (
<Box
alignSelf="flex-end"
ml="auto"
>
<Button
onClick={handlePause}
loading={isBusy}
variant="contained"
color="secondary"
sx={{
pl: theme.spacing(4),
pr: theme.spacing(6),
"& svg": {
mr: theme.spacing(2),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 0.1,
},
},
}}
>
{monitor?.isActive ? (
<>
<Icon
name="PauseCircle"
size={20}
/>
{t("pause")}
</>
) : (
<>
<Icon
name="PlayCircle"
size={20}
/>
{t("resume")}
</>
)}
</Button>
<Button
loading={isBusy}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ ml: theme.spacing(6) }}
>
{t("remove")}
</Button>
</Box>
)}
</Stack>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">
{t("pageSpeedConfigureSettingsDescription")}
</Typography>
</Box>
<Stack
gap={!isCreate ? theme.spacing(20) : theme.spacing(15)}
sx={{
".MuiInputBase-root:has(> .Mui-disabled)": {
backgroundColor: theme.palette.tertiary.main,
},
}}
>
<TextInput
type={"url"}
name="url"
id="monitor-url"
label={!isCreate ? t("url") : t("urlMonitor")}
startAdornment={isCreate ? <HttpAdornment https={https} /> : undefined}
placeholder="random.website.com"
value={monitor.url || ""}
onChange={handleChange}
onBlur={isCreate ? handleBlur : undefined}
error={!!errors["url"]}
helperText={errors["url"]}
disabled={!isCreate}
/>
<TextInput
type="text"
id="monitor-name"
name="name"
label={t("monitorDisplayName")}
isOptional={true}
placeholder="Google"
value={monitor.name || ""}
onChange={handleChange}
error={!!errors["name"]}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
{isCreate && (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateChecks")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateChecksDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
title="PageSpeed"
desc={t("pageSpeedLighthouseAPI")}
size="small"
value="http"
checked={monitor.type === "pagespeed"}
/>
<ButtonGroup sx={{ ml: "32px" }}>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group" // Why does this work?
filled={(!https).toString()} // There's nothing in the docs about this either
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
</Stack>
{errors["type"] ? (
<Box>
<Typography
component="p"
color={theme.palette.error.contrastText}
>
{errors["type"]}
</Typography>
</Box>
) : (
""
)}
</Stack>
</ConfigBox>
)}
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("notificationConfig.title")}
</Typography>
<Typography component="p">{t("notificationConfig.description")}</Typography>
</Box>
<NotificationsConfig
notifications={notifications}
setMonitor={setMonitor}
setNotifications={isCreate ? undefined : monitor.notifications}
/>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(isCreate ? 12 : 20)}>
<Select
id="monitor-interval"
name="interval"
label={t("checkFrequency")}
value={monitor?.interval / MS_PER_MINUTE || 3}
onChange={handleChange}
items={FREQUENCIES}
/>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
mt={isCreate ? undefined : "auto"}
>
<Button
type="submit"
variant="contained"
color="accent"
loading={isBusy}
disabled={!Object.values(errors).every((value) => value === undefined)}
sx={isCreate ? undefined : { px: theme.spacing(12) }}
>
{isCreate ? t("createMonitor") : t("settingsSave")}
</Button>
</Stack>
</Stack>
{!isCreate && (
<Dialog
open={isOpen}
theme={theme}
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
/>
)}
</Box>
);
};
export default PageSpeedSetup;
@@ -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 (
<>
<Skeleton
variant="rounded"
width="15%"
height={34}
/>
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(6)}
maxWidth="1000px"
>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.spacing(4) }}
/>
<Skeleton
variant="rounded"
width="50%"
height={18}
/>
</Box>
<Stack
direction="row"
gap={theme.spacing(6)}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<Skeleton
variant="rounded"
width={100}
height={34}
/>
<Skeleton
variant="rounded"
width={100}
height={34}
/>
</Stack>
</Stack>
<Skeleton
variant="rounded"
width="100%"
height={500}
/>
<Stack
direction="row"
justifyContent="flex-end"
>
<Skeleton
variant="rounded"
width="15%"
height={34}
/>
</Stack>
</Stack>
</>
);
};
export default SkeletonLayout;
+1 -1
View File
@@ -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";
-826
View File
@@ -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 <SkeletonLayout />;
}
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={CRUMBS} />
<Stack
component="form"
onSubmit={onSubmit}
noValidate
spellCheck="false"
gap={theme.spacing(12)}
flex={1}
>
<Stack
direction="row"
gap={theme.spacing(12)}
>
<Box>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
color={
!isCreate ? theme.palette.primary.contrastTextSecondary : undefined
}
>
{!isCreate ? monitor.name : t("createYour") + " "}
</Typography>
{isCreate && (
<Typography
component="span"
fontSize="inherit"
fontWeight="inherit"
color={theme.palette.primary.contrastTextSecondary}
>
{t("monitor")}
</Typography>
)}
</Typography>
{!isCreate && (
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={t(`statusMsg.${[determineState(monitor)]}`)}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -8],
},
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="monitorUrl"
>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
ml={theme.spacing(6)}
mt={theme.spacing(1)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: theme.spacing(2),
height: theme.spacing(2),
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: theme.spacing(-5),
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{t("editing")}
</Typography>
</Stack>
)}
</Box>
{!isCreate && (
<Box
justifyContent="space-between"
sx={{
alignSelf: "flex-end",
ml: "auto",
display: "flex",
gap: theme.spacing(2),
}}
>
<Button
variant="contained"
color="secondary"
loading={isBusy}
startIcon={
monitor?.isActive ? (
<Icon
name="Pause"
size={18}
/>
) : (
<Icon
name="Play"
size={18}
/>
)
}
onClick={() => {
pauseMonitor({
monitorId: monitor?.id,
triggerUpdate,
});
}}
>
{monitor?.isActive ? t("pause") : t("resume")}
</Button>
<Button
loading={isBusy}
variant="contained"
color="error"
sx={{ px: theme.spacing(8) }}
onClick={() => setIsOpen(true)}
>
{t("remove")}
</Button>
</Box>
)}
</Stack>
{isCreate && (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateChecks")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateChecksDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<Radio
name="type"
title={t("websiteMonitoring")}
desc={t("websiteMonitoringDescription")}
size="small"
value="http"
checked={monitor.type === "http"}
onChange={onChange}
/>
{monitor.type === "http" ? (
<ButtonGroup sx={{ ml: theme.spacing(16) }}>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
) : (
""
)}
</Stack>
<Radio
name="type"
title={t("pingMonitoring")}
desc={t("pingMonitoringDescription")}
size="small"
value="ping"
checked={monitor.type === "ping"}
onChange={onChange}
/>
<Radio
name="type"
title={t("dockerContainerMonitoring")}
desc={t("dockerContainerMonitoringDescription")}
size="small"
value="docker"
checked={monitor.type === "docker"}
onChange={onChange}
/>
<Radio
name="type"
title={t("portMonitoring")}
desc={t("portMonitoringDescription")}
size="small"
value="port"
checked={monitor.type === "port"}
onChange={onChange}
/>
<Radio
name="type"
title={t("gameServerMonitoring")}
desc={t("gameServerMonitoringDescription")}
size="small"
value="game"
checked={monitor.type === "game"}
onChange={onChange}
/>
{errors["type"] ? (
<Box className="error-container">
<Typography
component="p"
className="input-error"
color={theme.palette.error.contrastText}
>
{errors["type"]}
</Typography>
</Box>
) : (
""
)}
</Stack>
</ConfigBox>
)}
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">
{isCreate
? t(`uptimeGeneralInstructions.${monitor.type}`)
: t("distributedUptimeCreateSelectURL")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="monitor-url"
name="url"
type={monitor?.type === "http" ? "url" : "text"}
label={
(monitor.type === "http" || monitor.type === "port") && !isCreate
? t("url")
: monitorTypeMaps[monitor.type].label || t("urlMonitor")
}
placeholder={monitorTypeMaps[monitor.type].placeholder || ""}
value={parsedUrl?.host + parsedUrl?.pathname || monitor?.url || ""}
https={isCreate ? https : protocol === "https"}
startAdornment={
monitor?.type === "http" && (
<HttpAdornment https={isCreate ? https : protocol === "https"} />
)
}
helperText={errors["url"]}
onChange={onChange}
disabled={!isCreate}
/>
<TextInput
name="port"
type="number"
label={t("portToMonitor")}
placeholder="5173"
value={monitor.port || ""}
onChange={onChange}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port" && monitor.type !== "game"}
/>
{monitor.type === "game" && (
<Select
name="gameId"
label={t("chooseGame")}
value={monitor.gameId || ""}
placeholder={t("chooseGame")}
onChange={onChange}
items={GAMELIST}
error={errors["gameId"] ? true : false}
/>
)}
<TextInput
name="name"
type="text"
label={t("displayName")}
isOptional={true}
placeholder={monitorTypeMaps[monitor.type].namePlaceholder}
value={monitor.name || ""}
onChange={onChange}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("createMonitorPage.incidentConfigTitle")}
</Typography>
<Typography component="p">
{t("createMonitorPage.incidentConfigDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="statusWindowSize"
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
type="number"
value={monitor.statusWindowSize}
onChange={onChange}
error={errors["statusWindowSize"] ? true : false}
helperText={errors["statusWindowSize"]}
/>
<TextInput
name="statusWindowThreshold"
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
type="number"
value={monitor.statusWindowThreshold}
onChange={onChange}
error={errors["statusWindowThreshold"] ? true : false}
helperText={errors["statusWindowThreshold"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("notificationConfig.title")}
</Typography>
<Typography component="p">{t("notificationConfig.description")}</Typography>
</Box>
<NotificationsConfig
notifications={notifications}
setMonitor={setMonitor}
setNotifications={isCreate ? null : monitor.notifications}
/>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("ignoreTLSError")}
</Typography>
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
</Box>
<Stack>
<FormControlLabel
sx={{ marginLeft: theme.spacing(0) }}
control={
<Switch
name="ignoreTlsErrors"
checked={monitor.ignoreTlsErrors}
onChange={onChange}
sx={{ mr: theme.spacing(2) }}
/>
}
label={t("tlsErrorIgnored")}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
name="interval"
label={t("checkFrequency")}
value={displayInterval}
onChange={onChange}
items={FREQUENCIES}
/>
{monitor.type === "http" && (
<Checkbox
name="useAdvancedMatching"
label={t("advancedMatching")}
isChecked={useAdvancedMatching}
onChange={onChange}
/>
)}
{monitor.type === "http" && useAdvancedMatching && (
<>
<Select
name="matchMethod"
label={t("matchMethod")}
value={monitor.matchMethod || "equal"}
onChange={onChange}
items={matchMethodOptions}
/>
<Stack>
<TextInput
type="text"
name="expectedValue"
label={t("expectedValue")}
isOptional={true}
placeholder={
expectedValuePlaceholders[monitor.matchMethod || "equal"]
}
value={monitor.expectedValue}
onChange={onChange}
error={errors["expectedValue"] ? true : false}
helperText={errors["expectedValue"]}
/>
<Typography
component="span"
color={theme.palette.primary.contrastTextTertiary}
opacity={0.8}
>
{t("uptimeCreate")}
</Typography>
</Stack>
<Stack>
<TextInput
name="jsonPath"
type="text"
label={t("uptimeAdvancedMatching.jsonPath")}
isOptional={true}
placeholder="data.status"
value={monitor.jsonPath}
onChange={onChange}
error={errors["jsonPath"] ? true : false}
helperText={errors["jsonPath"]}
/>
<Typography
component="span"
color={theme.palette.primary.contrastTextTertiary}
opacity={0.8}
>
{t("uptimeCreateJsonPath") + " "}
<Typography
component="a"
href="https://jmespath.org/"
target="_blank"
color="info"
>
jmespath.org
</Typography>
{" " + t("uptimeCreateJsonPathQuery")}
</Typography>
</Stack>
</>
)}
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
type="submit"
variant="contained"
color="accent"
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isBusy}
sx={{ px: theme.spacing(12) }}
>
{t("settingsSave")}
</Button>
</Stack>
</Stack>
{!isCreate && (
<Dialog
open={isOpen}
theme={theme}
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
isLoading={isLoading}
/>
)}
</Stack>
);
};
UptimeCreate.propTypes = {
isClone: PropTypes.bool,
};
export default UptimeCreate;
@@ -1,90 +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 (
<>
<Skeleton
variant="rounded"
width="15%"
height={34}
/>
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(6)}
>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.spacing(4) }}
/>
<Skeleton
variant="rounded"
width="50%"
height={18}
/>
</Box>
<Stack
direction="row"
gap={theme.spacing(6)}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<Skeleton
variant="rounded"
width={150}
height={34}
/>
</Stack>
</Stack>
<Skeleton
variant="rounded"
width="100%"
height={200}
/>
<Skeleton
variant="rounded"
width="100%"
height={200}
/>
<Skeleton
variant="rounded"
width="100%"
height={200}
/>
<Stack
direction="row"
justifyContent="flex-end"
>
<Skeleton
variant="rounded"
width="15%"
height={34}
/>
</Stack>
</Stack>
</>
);
};
export default SkeletonLayout;
+2 -1
View File
@@ -1,10 +1,11 @@
import { BasePage } from "@/Components/v2/design-elements";
import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common";
import { HeaderTimeRange } from "@/Components/v2/common";
import Stack from "@mui/material/Stack";
import {
HistogramStatus,
RadialAvgResponse,
HistogramDetails,
HeaderMonitorControls,
} from "@/Components/v2/monitors";
import { TrendingUp, AlertTriangle } from "lucide-react";
import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable";
+1 -1
View File
@@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => {
// Default to all types when none selected
const effectiveTypes =
selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port"];
selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port", "game"];
// Build URL for monitors with checks
const monitorsWithChecksUrl = useMemo(() => {
+44 -13
View File
@@ -17,16 +17,13 @@ import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx";
// Uptime
import Uptime from "../Pages/Uptime/Monitors";
import UptimeDetails from "../Pages/Uptime/Details";
import UptimeCreate from "../Pages/Uptime/Create/index.jsx";
// PageSpeed
import PageSpeed from "../Pages/PageSpeed/Monitors/index";
import PageSpeedDetails from "../Pages/PageSpeed/Details/";
import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx";
// Infrastructure
import Infrastructure from "../Pages/Infrastructure/Monitors";
import InfrastructureCreate from "../Pages/Infrastructure/Create/index.jsx";
import InfrastructureDetails from "../Pages/Infrastructure/Details/index";
// Server Status
@@ -60,6 +57,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);
@@ -96,11 +95,13 @@ const Routes = () => {
<Route
path="/uptime/create"
element={<UptimeCreate />}
/>
<Route
path="/uptime/create/:monitorId"
element={<UptimeCreate isClone={true} />}
element={
<>
<ThemeProvider theme={v2theme}>
<CreateMonitor />
</ThemeProvider>
</>
}
/>
<Route
path="/uptime/:monitorId/"
@@ -114,7 +115,13 @@ const Routes = () => {
/>
<Route
path="/uptime/configure/:monitorId/"
element={<UptimeCreate />}
element={
<>
<ThemeProvider theme={v2theme}>
<CreateMonitor />
</ThemeProvider>
</>
}
/>
<Route
@@ -129,7 +136,13 @@ const Routes = () => {
/>
<Route
path="pagespeed/create"
element={<PageSpeedCreate />}
element={
<>
<ThemeProvider theme={v2theme}>
<CreateMonitor />
</ThemeProvider>
</>
}
/>
<Route
path="pagespeed/:monitorId"
@@ -143,7 +156,13 @@ const Routes = () => {
/>
<Route
path="pagespeed/configure/:monitorId"
element={<PageSpeedCreate />}
element={
<>
<ThemeProvider theme={v2theme}>
<CreateMonitor />
</ThemeProvider>
</>
}
/>
<Route
path="infrastructure"
@@ -157,11 +176,23 @@ const Routes = () => {
/>
<Route
path="infrastructure/create"
element={<InfrastructureCreate />}
element={
<>
<ThemeProvider theme={v2theme}>
<CreateMonitor />
</ThemeProvider>
</>
}
/>
<Route
path="/infrastructure/configure/:monitorId"
element={<InfrastructureCreate />}
element={
<>
<ThemeProvider theme={v2theme}>
<CreateMonitor />
</ThemeProvider>
</>
}
/>
<Route
path="infrastructure/:monitorId"
+10
View File
@@ -35,6 +35,7 @@ export interface Monitor {
statusWindowThreshold: number;
type: MonitorType;
ignoreTlsErrors: boolean;
useAdvancedMatching: boolean;
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
@@ -155,3 +156,12 @@ export interface HardwareDetailsResponse {
stats: HardwareStats;
monitorStats: MonitorStats | null;
}
export interface Game {
name: string;
options?: {
port?: number;
};
}
export type GamesMap = Record<string, Game>;
+6
View File
@@ -67,6 +67,12 @@ export const patch = <T>(
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> => api.patch<T>(url, data, config);
export const put = <T>(
url: string,
data: unknown,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> => api.put<T>(url, data, config);
export const deleteOp = <T>(
url: string,
config: AxiosRequestConfig = {}
+121
View File
@@ -0,0 +1,121 @@
import { z } from "zod";
// URL schema with custom error message
const urlSchema = z.url({ message: "Please enter a valid URL" });
// Common base schema for all monitor types
const baseSchema = z.object({
name: z
.string()
.min(1, "Monitor name is required")
.max(50, "Monitor name must be at most 50 characters"),
description: z.string().optional(),
interval: z.number().min(15000, "Interval must be at least 15 seconds"),
notifications: z.array(z.string()),
statusWindowSize: z
.number({ message: "Status window size is required" })
.min(1, "Status window size must be at least 1")
.max(25, "Status window size must be at most 25"),
statusWindowThreshold: z
.number({ message: "Threshold percentage is required" })
.min(1, "Incident percentage must be at least 1")
.max(100, "Incident percentage must be at most 100"),
});
// HTTP monitor schema
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(),
});
// Ping monitor schema
const pingSchema = baseSchema.extend({
type: z.literal("ping"),
url: z.string().min(1, "Host is required"),
});
// Port monitor schema
const portSchema = baseSchema.extend({
type: z.literal("port"),
url: z.string().min(1, "Host is required"),
port: z
.number()
.min(1, "Port must be at least 1")
.max(65535, "Port must be at most 65535"),
});
// Docker monitor schema
const dockerSchema = baseSchema.extend({
type: z.literal("docker"),
url: z.string().min(1, "Container ID is required"),
});
// Game server monitor schema
const gameSchema = baseSchema.extend({
type: z.literal("game"),
url: z.string().min(1, "Host is required"),
port: z
.number()
.min(1, "Port must be at least 1")
.max(65535, "Port must be at most 65535"),
gameId: z.string().min(1, "Game type is required"),
});
// PageSpeed monitor schema
const pagespeedSchema = baseSchema.extend({
type: z.literal("pagespeed"),
url: urlSchema,
});
// Hardware/Infrastructure monitor schema
const hardwareSchema = baseSchema.extend({
type: z.literal("hardware"),
url: urlSchema,
secret: z.string({ message: "Secret is required" }).min(1, "Secret is required"),
cpuAlertThreshold: z
.number()
.min(0, "CPU threshold must be at least 0")
.max(100, "CPU threshold must be at most 100"),
memoryAlertThreshold: z
.number()
.min(0, "Memory threshold must be at least 0")
.max(100, "Memory threshold must be at most 100"),
diskAlertThreshold: z
.number()
.min(0, "Disk threshold must be at least 0")
.max(100, "Disk threshold must be at most 100"),
tempAlertThreshold: z
.number()
.min(0, "Temperature threshold must be at least 0")
.max(150, "Temperature threshold must be at most 150"),
selectedDisks: z.array(z.string()),
});
// Discriminated union of all monitor types
export const monitorSchema = z.discriminatedUnion("type", [
httpSchema,
pingSchema,
portSchema,
dockerSchema,
gameSchema,
pagespeedSchema,
hardwareSchema,
]);
export type MonitorFormData = z.infer<typeof monitorSchema>;
// Type-specific schemas exported for individual use
export {
httpSchema,
pingSchema,
portSchema,
dockerSchema,
gameSchema,
pagespeedSchema,
hardwareSchema,
};
+135 -2
View File
@@ -2,6 +2,8 @@
"common": {
"appName": "Checkmate",
"monitoringAgentName": "Capture",
"see": "See",
"forDocumentation": "for query language documentation.",
"breadcrumbs": {
"home": "Home",
"details": "Details"
@@ -20,7 +22,8 @@
"cancel": "Cancel",
"confirm": "Confirm",
"save": "Save",
"test": "Test"
"test": "Test",
"configure": "Configure"
},
"alerts": {
"pageSpeedApiKey": {
@@ -220,6 +223,117 @@
}
}
},
"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.",
"optionPagespeed": "PageSpeed",
"optionPagespeedDescription": "Analyze page performance using Google PageSpeed Insights.",
"optionHardware": "Hardware",
"optionHardwareDescription": "Monitor server hardware metrics like CPU, memory, and disk."
},
"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": "Enter the URL of the page to analyze with Google PageSpeed and add a clear display name that appears on the dashboard.",
"hardware": "Enter the URL of your infrastructure agent, the authorization secret, and a clear display name that appears on the dashboard."
},
"option": {
"url": {
"label": "URL",
"placeholder": "https://www.google.com"
},
"name": {
"label": "Display name",
"placeholder": "e.g. My Website"
},
"host": {
"label": "Host",
"placeholder": "192.168.1.100 or example.com"
},
"container": {
"label": "Container name/ID",
"placeholder": "my-app or abcd1234"
},
"secret": {
"label": "Authorization secret",
"placeholder": "Enter your secret key"
}
}
},
"frequency": {
"title": "Check frequency",
"description": "How often do you want to check the status of this monitor?",
"option": {
"frequency": {
"label": "Check frequency"
}
}
},
"incidents": {
"title": "Incidents",
"description": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.",
"option": {
"checks": {
"label": "Number of checks in sliding window"
},
"percentage": {
"label": "What percentage of checks in the sliding window fail/succeed before monitor status changes?"
}
}
},
"notifications": {
"title": "Notifications",
"description": "Select the notification channels you want to use"
},
"ignoreTls": {
"title": "TLS/SSL settings",
"description": "Configure TLS/SSL certificate validation for HTTPS connections.",
"option": {
"tls": {
"label": "Ignore TLS/SSL errors"
}
}
},
"advanced": {
"title": "Advanced settings",
"description": "Optional settings for advanced use cases",
"option": {
"advancedMatching": {
"label": "Use advanced matching"
},
"matchMethod": {
"label": "Match method"
},
"expectedValue": {
"label": "Expected value",
"description": "The expected value is used to match against response result, and the match determines the status."
},
"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. See <jmesLink>jmespath.org</jmesLink> for query language documentation."
}
}
}
}
},
"uptime": {
"table": {
"headers": {
@@ -262,6 +376,15 @@
}
},
"pageSpeed": {
"fallback": {
"actionButton": "Create a monitor!",
"checks": [
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved"
],
"title": "A PageSpeed monitor is used to:"
},
"table": {
"headers": {
"pageSpeedScore": "PageSpeed score"
@@ -285,6 +408,15 @@
}
},
"infrastructure": {
"fallback": {
"actionButton": "Create a monitor!",
"checks": [
"Track the performance of your servers",
"Identify bottlenecks and optimize usage",
"Ensure reliability with real-time monitoring"
],
"title": "An infrastructure monitor is used to:"
},
"table": {
"headers": {
"cpu": "CPU",
@@ -991,7 +1123,8 @@
},
"notificationConfig": {
"title": "Notifications",
"description": "Select the notifications channels you want to use"
"description": "Select the notifications channels you want to use",
"placeholder": "Search notifications..."
},
"monitorStatus": {
"checkingEvery": "Checking every {{interval}}",
+5 -3
View File
@@ -28,18 +28,20 @@ export const createApp = ({
}) => {
const allowedOrigin = envSettings.clientHost;
const app = express();
app.use(generalApiLimiter);
// Static files
app.use(express.static(frontendPath));
app.use(
cors({
origin: allowedOrigin,
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
allowedHeaders: ["Content-Type", "Authorization"],
allowedHeaders: ["Content-Type", "Authorization", "Accept-Language"],
credentials: true,
})
);
app.use(express.static(frontendPath));
app.use(express.json());
app.use(cookieParser());
+4
View File
@@ -92,6 +92,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
type: Boolean,
default: false,
},
useAdvancedMatching: {
type: Boolean,
default: false,
},
jsonPath: {
type: String,
},
@@ -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,
+1 -1
View File
@@ -50,7 +50,7 @@ class MonitorRoutes {
// Individual monitor CRUD routes
this.router.get("/:monitorId", this.monitorController.getMonitorById);
this.router.put("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor);
this.router.patch("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor);
this.router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.deleteMonitor);
}
@@ -6,6 +6,7 @@ import type {
UptimeDetailsResult,
HardwareDetailsResult,
PageSpeedDetailsResult,
GamesMap,
} from "@/types/monitor.js";
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js";
import fs from "fs";
@@ -51,7 +52,7 @@ export interface IMonitorService {
order?: "asc" | "desc";
explain?: boolean;
}): Promise<MonitorsWithChecksByTeamIdResult>;
getAllGames(): any;
getAllGames(): GamesMap;
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
// update
@@ -373,7 +374,7 @@ export class MonitorService implements IMonitorService {
return { summary: summary ?? null, count, monitors: monitorsWithChecks };
};
getAllGames = (): any => {
getAllGames = (): GamesMap => {
return this.games;
};
@@ -255,7 +255,7 @@ class NetworkService implements INetworkService {
}
private async requestHttp(monitor: Monitor): Promise<MonitorStatusResponse> {
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;
}
+16
View File
@@ -26,6 +26,7 @@ export interface Monitor {
statusWindowThreshold: number;
type: MonitorType;
ignoreTlsErrors: boolean;
useAdvancedMatching: boolean;
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
@@ -129,3 +130,18 @@ export interface PageSpeedDetailsResult {
};
monitorStats: import("./monitorStats.js").MonitorStats | null;
}
export interface Game {
name: string;
release_year?: number;
options?: {
port?: number;
port_query?: number;
protocol?: string;
};
extra?: {
old_id?: string;
};
}
export type GamesMap = Record<string, Game>;
+29 -25
View File
@@ -152,12 +152,13 @@ const getCertificateParamValidation = joi.object({
const createMonitorBodyValidation = joi.object({
_id: joi.string(),
name: joi.string().required(),
description: joi.string().required(),
description: joi.string().allow(null, ""),
type: joi.string().required(),
statusWindowSize: joi.number().min(1).max(20).default(5),
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(),
@@ -171,7 +172,7 @@ const createMonitorBodyValidation = joi.object({
secret: joi.string(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string(),
matchMethod: joi.string().allow(null, ""),
gameId: joi.string().allow(""),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
@@ -184,29 +185,32 @@ const createMonitorsBodyValidation = joi.array().items(
})
);
const editMonitorBodyValidation = joi.object({
name: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
description: joi.string(),
interval: joi.number(),
notifications: joi.array().items(joi.string()),
secret: joi.string(),
ignoreTlsErrors: joi.boolean(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string().allow(null, ""),
port: joi.number().min(1).max(65535),
thresholds: joi.object().keys({
usage_cpu: joi.number(),
usage_memory: joi.number(),
usage_disk: joi.number(),
usage_temperature: joi.number(),
}),
gameId: joi.string(),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
});
const editMonitorBodyValidation = joi
.object({
name: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
description: joi.string().allow(null, ""),
interval: joi.number(),
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, ""),
port: joi.number().min(1).max(65535),
thresholds: joi.object().keys({
usage_cpu: joi.number(),
usage_memory: joi.number(),
usage_disk: joi.number(),
usage_temperature: joi.number(),
}),
gameId: joi.string().allow(""),
selectedDisks: joi.array().items(joi.string()).optional(),
group: joi.string().max(50).trim().allow(null, "").optional(),
})
.options({ stripUnknown: true });
const pauseMonitorParamValidation = joi.object({
monitorId: joi.string().required(),