54 files changed
+1838
-1079
No files matched your search
@@ -72,14 +72,20 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
||||
|
||||
## 💚 Questions & Ideas
|
||||
|
||||
If you have any questions, suggestions or comments, please use our [Discord channel](https://discord.gg/NAb6H3UTjK). We've also launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you!
|
||||
If you have any questions, suggestions or comments, you have several options:
|
||||
|
||||
- [Discord channel](https://discord.gg/NAb6H3UTjK)
|
||||
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions)
|
||||
- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/)
|
||||
|
||||
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
|
||||
## 🧩 Features
|
||||
|
||||
- Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5)
|
||||
- Website monitoring
|
||||
- Page speed monitoring
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
|
||||
- Docker monitoring
|
||||
- Ping monitoring
|
||||
- SSL monitoring
|
||||
@@ -134,7 +140,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
|
||||
We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
|
||||
|
||||
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 6000+ stars and attracted 80+ contributors from around the globe.
|
||||
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 7000+ stars and attracted 90+ contributors from around the globe.
|
||||
|
||||
Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us!
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@ metadata:
|
||||
name: checkmate-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- range $key, $value := := $secrets }}
|
||||
{{- range $key, $value := $secrets }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
@@ -16,6 +18,8 @@ const MAXIMUM_VALUE = 100;
|
||||
* @param {number} [props.radius=60] - Radius of the gauge circle
|
||||
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
|
||||
* @param {number} [props.threshold=50] - Threshold for color change
|
||||
* @param {number} [props.precision=1] - Precision of the progress percentage
|
||||
* @param {string} [props.unit="%"] - Unit of progress
|
||||
*
|
||||
* @example
|
||||
* <CustomGauge
|
||||
@@ -27,7 +31,15 @@ const MAXIMUM_VALUE = 100;
|
||||
*
|
||||
* @returns {React.ReactElement} Rendered CustomGauge component
|
||||
*/
|
||||
const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
|
||||
const CustomGauge = ({
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
threshold = 50,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
// Calculate the length of the stroke for the circle
|
||||
const { circumference, totalSize, strokeLength } = useMemo(
|
||||
@@ -54,8 +66,22 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
|
||||
const fillColor =
|
||||
progressWithinRange > threshold
|
||||
? theme.palette.error.lowContrast // CAIO_REVIEW
|
||||
: theme.palette.accent.main; // CAIO_REVIEW
|
||||
? theme.palette.error.lowContrast
|
||||
: theme.palette.accent.main;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack
|
||||
className="radial-chart"
|
||||
width={radius}
|
||||
height={radius}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<CircularProgress color="accent" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -71,7 +97,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
>
|
||||
<circle
|
||||
className="radial-chart-base"
|
||||
stroke={theme.palette.secondary.light} // CAIO_REVIEW
|
||||
stroke={theme.palette.secondary.light}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
cx={totalSize / 2} // Center the circle
|
||||
@@ -103,7 +129,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
fill: theme.typography.h2.color,
|
||||
}}
|
||||
>
|
||||
{`${progressWithinRange.toFixed(1)}%`}
|
||||
{`${progressWithinRange.toFixed(precision)}${unit}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@@ -112,8 +138,11 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
export default CustomGauge;
|
||||
|
||||
CustomGauge.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
progress: PropTypes.number,
|
||||
radius: PropTypes.number,
|
||||
strokeWidth: PropTypes.number,
|
||||
threshold: PropTypes.number,
|
||||
precision: PropTypes.number,
|
||||
unit: PropTypes.string,
|
||||
};
|
||||
@@ -4,15 +4,14 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing(20),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.spacing(2),
|
||||
"& > *": {
|
||||
paddingTop: theme.spacing(12),
|
||||
paddingBottom: theme.spacing(18),
|
||||
paddingTop: theme.spacing(15),
|
||||
paddingBottom: theme.spacing(15),
|
||||
},
|
||||
"& > div:first-of-type": {
|
||||
flex: 0.7,
|
||||
@@ -25,7 +24,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
"& > div:last-of-type": {
|
||||
flex: 1,
|
||||
paddingRight: theme.spacing(20),
|
||||
paddingLeft: theme.spacing(18),
|
||||
paddingLeft: theme.spacing(20),
|
||||
},
|
||||
"& h1, & h2": {
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const DEFAULT_GAP = 6;
|
||||
const FieldWrapper = ({
|
||||
label,
|
||||
children,
|
||||
gap,
|
||||
labelMb,
|
||||
labelFontWeight = 500,
|
||||
labelVariant = "h3",
|
||||
labelSx = {},
|
||||
sx = {},
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
gap={gap ?? theme.spacing(DEFAULT_GAP)}
|
||||
sx={sx}
|
||||
>
|
||||
{label && (
|
||||
<Typography
|
||||
component={labelVariant}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={labelFontWeight}
|
||||
sx={{
|
||||
...(labelMb !== undefined && { mb: theme.spacing(labelMb) }),
|
||||
...labelSx,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
FieldWrapper.propTypes = {
|
||||
label: PropTypes.node,
|
||||
children: PropTypes.node.isRequired,
|
||||
gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
|
||||
labelMb: PropTypes.number,
|
||||
labelFontWeight: PropTypes.number,
|
||||
labelVariant: PropTypes.string,
|
||||
labelSx: PropTypes.object,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default FieldWrapper;
|
||||
@@ -24,7 +24,17 @@ import "./index.css";
|
||||
* @returns {JSX.Element} - The rendered Radio component.
|
||||
*/
|
||||
|
||||
const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
|
||||
const Radio = ({
|
||||
name,
|
||||
checked,
|
||||
value,
|
||||
id,
|
||||
size,
|
||||
title,
|
||||
desc,
|
||||
onChange,
|
||||
labelSpacing,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@@ -53,7 +63,14 @@ const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
|
||||
onChange={onChange}
|
||||
label={
|
||||
<>
|
||||
<Typography component="p">{title}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
mb={
|
||||
labelSpacing !== undefined ? theme.spacing(labelSpacing) : theme.spacing(2)
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="h6"
|
||||
mt={theme.spacing(1)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTheme } from "@emotion/react";
|
||||
import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FieldWrapper from "../FieldWrapper";
|
||||
|
||||
/**
|
||||
* Search component using Material UI's Autocomplete.
|
||||
@@ -24,6 +25,7 @@ import { useTranslation } from "react-i18next";
|
||||
* @param {Function} props.handleChange - Function to call when the input changes
|
||||
* @param {Function} Prop.onBlur - Function to call when the input is blured
|
||||
* @param {Object} props.sx - Additional styles to apply to the component
|
||||
* @param {string} props.unit - Label to identify type of options
|
||||
* @returns {JSX.Element} The rendered Search component
|
||||
*/
|
||||
|
||||
@@ -49,7 +51,7 @@ const SearchAdornment = () => {
|
||||
);
|
||||
};
|
||||
|
||||
//TODO keep search state inside of component
|
||||
//TODO keep search state inside of component.
|
||||
const Search = ({
|
||||
label,
|
||||
id,
|
||||
@@ -68,6 +70,13 @@ const Search = ({
|
||||
startAdornment,
|
||||
endAdornment,
|
||||
onBlur,
|
||||
//FieldWrapper's props
|
||||
gap,
|
||||
labelMb,
|
||||
labelFontWeight,
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
unit = "option",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -139,15 +148,17 @@ const Search = ({
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
||||
renderInput={(params) => (
|
||||
<Stack>
|
||||
<Typography
|
||||
component="h3"
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={500}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<FieldWrapper
|
||||
label={label}
|
||||
labelMb={labelMb}
|
||||
labelVariant={labelVariant}
|
||||
labelFontWeight={labelFontWeight}
|
||||
labelSx={labelSx}
|
||||
gap={gap}
|
||||
sx={{
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(error)}
|
||||
@@ -175,7 +186,7 @@ const Search = ({
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</FieldWrapper>
|
||||
)}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
if (inputValue.trim() === "" && multiple && isAdorned) {
|
||||
@@ -186,7 +197,12 @@ const Search = ({
|
||||
);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return [{ [filteredBy]: "No monitors found", noOptions: true }];
|
||||
return [
|
||||
{
|
||||
[filteredBy]: t("general.noOptionsFound", { unit: unit }),
|
||||
noOptions: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
return filtered;
|
||||
}}
|
||||
@@ -281,7 +297,7 @@ Search.propTypes = {
|
||||
options: PropTypes.array.isRequired,
|
||||
filteredBy: PropTypes.string.isRequired,
|
||||
secondaryLabel: PropTypes.string,
|
||||
value: PropTypes.array,
|
||||
value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
inputValue: PropTypes.string.isRequired,
|
||||
handleInputChange: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func,
|
||||
@@ -292,6 +308,7 @@ Search.propTypes = {
|
||||
startAdornment: PropTypes.object,
|
||||
endAdornment: PropTypes.object,
|
||||
onBlur: PropTypes.func,
|
||||
unit: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@@ -2,6 +2,7 @@ import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import FieldWrapper from "../FieldWrapper";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@@ -50,8 +51,14 @@ const Select = ({
|
||||
onBlur,
|
||||
sx,
|
||||
name = "",
|
||||
labelControlSpacing = 2,
|
||||
labelControlSpacing = 6,
|
||||
maxWidth,
|
||||
//FieldWrapper's props
|
||||
labelMb,
|
||||
labelFontWeight,
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
fieldWrapperSx = {},
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const itemStyles = {
|
||||
@@ -69,20 +76,17 @@ const Select = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(labelControlSpacing)}
|
||||
className="select-wrapper"
|
||||
<FieldWrapper
|
||||
label={label}
|
||||
labelMb={labelMb}
|
||||
labelVariant={labelVariant}
|
||||
labelFontWeight={labelFontWeight}
|
||||
labelSx={labelSx}
|
||||
gap={labelControlSpacing}
|
||||
sx={{
|
||||
...fieldWrapperSx,
|
||||
}}
|
||||
>
|
||||
{label && (
|
||||
<Typography
|
||||
component="h3"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
<MuiSelect
|
||||
className="select-component"
|
||||
value={value}
|
||||
@@ -107,6 +111,13 @@ const Select = ({
|
||||
"& svg path": {
|
||||
fill: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
padding: "0 13px",
|
||||
minHeight: "34px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
lineHeight: 1,
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
renderValue={(selected) => {
|
||||
@@ -151,7 +162,7 @@ const Select = ({
|
||||
</MenuItem>
|
||||
))}
|
||||
</MuiSelect>
|
||||
</Stack>
|
||||
</FieldWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Stack, TextField, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { forwardRef, useState, cloneElement } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import FieldWrapper from "../FieldWrapper";
|
||||
|
||||
const getSx = (theme, type, maxWidth) => {
|
||||
const sx = {
|
||||
@@ -85,31 +86,43 @@ const TextInput = forwardRef(
|
||||
marginLeft,
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
//FieldWrapper's props
|
||||
gap,
|
||||
labelMb,
|
||||
labelFontWeight,
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
sx = {},
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [fieldType, setFieldType] = useState(type);
|
||||
const theme = useTheme();
|
||||
const labelContent = label && (
|
||||
<>
|
||||
{label}
|
||||
{isRequired && <Required />}
|
||||
{isOptional && <Optional optionalLabel={optionalLabel} />}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
flex={flex}
|
||||
display={hidden ? "none" : ""}
|
||||
marginTop={marginTop}
|
||||
marginRight={marginRight}
|
||||
marginBottom={marginBottom}
|
||||
marginLeft={marginLeft}
|
||||
<FieldWrapper
|
||||
label={labelContent}
|
||||
labelMb={labelMb}
|
||||
labelVariant={labelVariant}
|
||||
labelFontWeight={labelFontWeight}
|
||||
labelSx={labelSx}
|
||||
gap={gap}
|
||||
sx={{
|
||||
flex,
|
||||
display: hidden ? "none" : "",
|
||||
mt: marginTop,
|
||||
mr: marginRight,
|
||||
mb: marginBottom,
|
||||
ml: marginLeft,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="h3"
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={500}
|
||||
mb={theme.spacing(2)}
|
||||
>
|
||||
{label}
|
||||
{isRequired && <Required />}
|
||||
{isOptional && <Optional optionalLabel={optionalLabel} />}
|
||||
</Typography>
|
||||
<TextField
|
||||
id={id}
|
||||
name={name}
|
||||
@@ -132,7 +145,7 @@ const TextInput = forwardRef(
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</FieldWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -10,7 +10,18 @@ import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => {
|
||||
const NotificationConfig = ({
|
||||
notifications,
|
||||
setMonitor,
|
||||
setNotifications,
|
||||
//FieldWrapper's props
|
||||
gap,
|
||||
labelMb,
|
||||
labelFontWeight,
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
sx = {},
|
||||
}) => {
|
||||
// Local state
|
||||
const [notificationsSearch, setNotificationsSearch] = useState("");
|
||||
const [selectedNotifications, setSelectedNotifications] = useState([]);
|
||||
@@ -66,6 +77,14 @@ const NotificationConfig = ({ notifications, setMonitor, setNotifications }) =>
|
||||
handleChange={(value) => {
|
||||
handleSearch(value);
|
||||
}}
|
||||
labelMb={labelMb}
|
||||
labelVariant={labelVariant}
|
||||
labelFontWeight={labelFontWeight}
|
||||
labelSx={labelSx}
|
||||
gap={gap}
|
||||
sx={{
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
flex={1}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ToastBody = ({ body }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (Array.isArray(body)) {
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
{body.map((item, idx) => (
|
||||
<Typography
|
||||
key={`item-${idx}`}
|
||||
color={theme.palette.secondary.contrastText}
|
||||
>
|
||||
{item}
|
||||
</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
} else if (typeof body === "string") {
|
||||
return <Typography color={theme.palette.secondary.contrastText}>{body}</Typography>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ToastBody.propTypes = {
|
||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
};
|
||||
|
||||
export default ToastBody;
|
||||
@@ -0,0 +1,96 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import ToastBody from "./body";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const icons = {
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
};
|
||||
|
||||
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||
const theme = useTheme();
|
||||
const icon = icons[variant];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
paddingTop={theme.spacing(4)}
|
||||
paddingRight={theme.spacing(8)}
|
||||
paddingBottom={theme.spacing(4)}
|
||||
paddingLeft={theme.spacing(8)}
|
||||
backgroundColor={theme.palette.alert.main}
|
||||
border={`solid 1px ${theme.palette.alert.contrastText}`}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
{hasIcon && icon}
|
||||
{title && (
|
||||
<Typography
|
||||
fontWeight="700"
|
||||
color={theme.palette.secondary.contrastText}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{title && (
|
||||
<IconButton onClick={onClick}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
>
|
||||
<ToastBody body={body} />
|
||||
{!title && (
|
||||
<IconButton onClick={onClick}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
{hasDismiss && (
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
fontWeight: "600",
|
||||
width: "fit-content",
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
||||
|
||||
Toast.propTypes = {
|
||||
variant: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
hasDismiss: PropTypes.bool,
|
||||
hasIcon: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
@@ -32,4 +32,83 @@ const useFetchLogs = () => {
|
||||
return [logs, isLoading, error];
|
||||
};
|
||||
|
||||
export { useFetchLogs };
|
||||
const useFetchQueueData = (trigger) => {
|
||||
const [jobs, setJobs] = useState(undefined);
|
||||
const [metrics, setMetrics] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchJobs = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getQueueData();
|
||||
if (response.status === 200) {
|
||||
setJobs(response.data.data.jobs);
|
||||
setMetrics(response.data.data.metrics);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchJobs();
|
||||
}, [trigger]);
|
||||
|
||||
return [jobs, metrics, isLoading, error];
|
||||
};
|
||||
|
||||
const useFlushQueue = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
|
||||
const flushQueue = async (trigger, setTrigger) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.flushQueue();
|
||||
createToast({
|
||||
body: "Queue flushed",
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setTrigger(!trigger);
|
||||
}
|
||||
};
|
||||
return [flushQueue, isLoading, error];
|
||||
};
|
||||
|
||||
const useFetchDiagnostics = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
const [diagnostics, setDiagnostics] = useState(undefined);
|
||||
|
||||
const fetchDiagnostics = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getDiagnostics();
|
||||
setDiagnostics(response.data.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDiagnostics();
|
||||
}, []);
|
||||
|
||||
return [diagnostics, fetchDiagnostics, isLoading, error];
|
||||
};
|
||||
|
||||
export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics };
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
|
||||
const useFetchQueueData = (trigger) => {
|
||||
const [jobs, setJobs] = useState(undefined);
|
||||
const [metrics, setMetrics] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchJobs = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getQueueData();
|
||||
if (response.status === 200) {
|
||||
setJobs(response.data.data.jobs);
|
||||
setMetrics(response.data.data.metrics);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchJobs();
|
||||
}, [trigger]);
|
||||
|
||||
return [jobs, metrics, isLoading, error];
|
||||
};
|
||||
|
||||
const useFlushQueue = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
|
||||
const flushQueue = async (trigger, setTrigger) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.flushQueue();
|
||||
createToast({
|
||||
body: "Queue flushed",
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setTrigger(!trigger);
|
||||
}
|
||||
};
|
||||
return [flushQueue, isLoading, error];
|
||||
};
|
||||
|
||||
export { useFetchQueueData, useFlushQueue };
|
||||
@@ -62,14 +62,15 @@ export const CustomThreshold = ({
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
width: "50%",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
direction={{ sm: "column", md: "row" }}
|
||||
spacing={theme.spacing(2)}
|
||||
>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: { md: "45%", lg: "25%", xl: "20%" },
|
||||
}}
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
name={checkboxName}
|
||||
@@ -81,8 +82,10 @@ export const CustomThreshold = ({
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
justifyContent: "flex-end",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
alignItems="center"
|
||||
spacing={theme.spacing(4)}
|
||||
>
|
||||
<TextInput
|
||||
maxWidth="var(--env-var-width-4)"
|
||||
|
||||
@@ -25,6 +25,7 @@ import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { CustomThreshold } from "./Components/CustomThreshold";
|
||||
import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
|
||||
|
||||
const SELECT_VALUES = [
|
||||
{ _id: 0.25, name: "15 seconds" },
|
||||
@@ -283,7 +284,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
/>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<TextInput
|
||||
type="url"
|
||||
id="url"
|
||||
@@ -299,8 +300,10 @@ const CreateInfrastructureMonitor = () => {
|
||||
disabled={!isCreate}
|
||||
/>
|
||||
{isCreate && (
|
||||
<Box>
|
||||
<Typography component="p">{t("infrastructureProtocol")}</Typography>
|
||||
<FieldWrapper
|
||||
label={t("infrastructureProtocol")}
|
||||
labelVariant="p"
|
||||
>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="group"
|
||||
@@ -317,7 +320,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
{t("http")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
</FieldWrapper>
|
||||
)}
|
||||
<TextInput
|
||||
type="text"
|
||||
|
||||
@@ -11,7 +11,7 @@ import Filter from "./Components/Filters";
|
||||
import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
|
||||
@@ -58,6 +58,12 @@ const InfrastructureMonitors = () => {
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearching) {
|
||||
setPage(0);
|
||||
}
|
||||
}, [isSearching]);
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedStatus(undefined);
|
||||
setToFilterStatus(undefined);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Gauge from "../../../../../Components/Charts/CustomGauge";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import { getPercentage } from "../../utils/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const GaugeBox = ({ title, subtitle, children }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
p={theme.spacing(2)}
|
||||
maxWidth={150}
|
||||
width={150}
|
||||
>
|
||||
{children}
|
||||
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
<Typography variant="body2">{subtitle}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
GaugeBox.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const Gauges = ({ diagnostics, isLoading }) => {
|
||||
const heapTotalSize = getPercentage(
|
||||
diagnostics?.v8HeapStats?.totalHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.heapSizeLimitBytes
|
||||
);
|
||||
|
||||
const heapUsedSize = getPercentage(
|
||||
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.heapSizeLimitBytes
|
||||
);
|
||||
|
||||
const actualHeapUsed = getPercentage(
|
||||
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.totalHeapSizeBytes
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(4)}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapAllocationTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapAllocationSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={heapTotalSize}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapUsageTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapUsageSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={heapUsedSize}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapUtilizationTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapUtilizationSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={actualHeapUsed}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.instantCpuUsageTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.instantCpuUsageSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={diagnostics?.cpuUsage?.usagePercentage}
|
||||
precision={2}
|
||||
/>
|
||||
</GaugeBox>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Gauges.propTypes = {
|
||||
diagnostics: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Gauges;
|
||||
@@ -0,0 +1,97 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Card from "@mui/material/Card";
|
||||
import CardContent from "@mui/material/CardContent";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
|
||||
import { formatBytes } from "../../utils/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StatsCard = ({ title, value, unit = "", isLoading }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Card sx={{ width: 150, maxWidth: 150, height: 80, maxHeight: 80 }}>
|
||||
{isLoading ? (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
height={80}
|
||||
maxHeight={80}
|
||||
>
|
||||
<CircularProgress color="accent" />
|
||||
</Stack>
|
||||
) : (
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{value} {unit}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
StatsCard.propTypes = {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
unit: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
const Stats = ({ diagnostics, isLoading }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.eventLoopDelayTitle")}
|
||||
value={getHumanReadableDuration(diagnostics?.eventLoopDelayMs)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.uptimeTitle")}
|
||||
value={getHumanReadableDuration(diagnostics?.uptimeMs)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.usedHeapSizeTitle")}
|
||||
value={formatBytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.totalHeapSizeTitle")}
|
||||
value={formatBytes(diagnostics?.v8HeapStats?.totalHeapSizeBytes)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.osMemoryLimitTitle")}
|
||||
value={formatBytes(diagnostics?.osStats?.totalMemoryBytes)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Stats.propTypes = {
|
||||
diagnostics: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Stats;
|
||||
@@ -0,0 +1,54 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Gauges from "./components/gauges";
|
||||
import Stats from "./components/stats";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchDiagnostics } from "../../../Hooks/logHooks";
|
||||
|
||||
const Diagnostics = () => {
|
||||
// Local state
|
||||
|
||||
// Hooks
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics();
|
||||
// Setup
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Box>
|
||||
<Typography variant="h2">{t("diagnosticsPage.diagnosticDescription")}</Typography>
|
||||
</Box>
|
||||
<Divider color={theme.palette.accent.main} />
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Gauges
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Stats
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={fetchDiagnostics}
|
||||
loading={isLoading}
|
||||
>
|
||||
Fetch Diagnostics
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Diagnostics;
|
||||
@@ -0,0 +1,16 @@
|
||||
export const getPercentage = (value, total) => {
|
||||
if (!value || !total) return 0;
|
||||
return (value / total) * 100;
|
||||
};
|
||||
|
||||
export const formatBytes = (bytes) => {
|
||||
if (!bytes) return "N/A";
|
||||
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
import { useFetchLogs } from "../../../Hooks/logHooks";
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -51,12 +52,15 @@ const Logs = () => {
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Box>
|
||||
<Typography variant="body">{t("logsPage.description")}</Typography>
|
||||
<Typography variant="h2">{t("logsPage.description")}</Typography>
|
||||
</Box>
|
||||
<Divider color={theme.palette.accent.main} />
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
|
||||
<Select
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import StatBox from "../../../../../Components/StatBox";
|
||||
import StatusBoxes from "../../../../../Components/StatusBoxes";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -24,20 +22,17 @@ const Metrics = ({ metrics = {} }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
|
||||
<StatusBoxes flexWrap="wrap">
|
||||
{data.map((metric) => {
|
||||
return (
|
||||
<StatBox
|
||||
key={metric.key}
|
||||
heading={metric.title}
|
||||
subHeading={metric.value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StatusBoxes>
|
||||
</Stack>
|
||||
<StatusBoxes flexWrap="wrap">
|
||||
{data.map((metric) => {
|
||||
return (
|
||||
<StatBox
|
||||
key={metric.key}
|
||||
heading={metric.title}
|
||||
subHeading={metric.value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
export default Metrics;
|
||||
@@ -5,10 +5,12 @@ import Metrics from "./components/Metrics";
|
||||
import FailedJobTable from "./components/FailedJobTable";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useFetchQueueData, useFlushQueue } from "../../../Hooks/queueHooks";
|
||||
import { useFetchQueueData, useFlushQueue } from "../../../Hooks/logHooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -26,39 +28,46 @@ const QueueDetails = () => {
|
||||
if (error || flushError) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Metrics metrics={metrics} />
|
||||
<JobTable jobs={jobs} />
|
||||
<FailedJobTable metrics={metrics} />
|
||||
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{
|
||||
position: "sticky",
|
||||
bottom: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
p: theme.spacing(4),
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderRadius: theme.spacing(2),
|
||||
}}
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
|
||||
<Divider color={theme.palette.accent.main} />
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrigger(!trigger);
|
||||
<Metrics metrics={metrics} />
|
||||
<JobTable jobs={jobs} />
|
||||
<FailedJobTable metrics={metrics} />
|
||||
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{
|
||||
position: "sticky",
|
||||
bottom: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
p: theme.spacing(4),
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderRadius: theme.spacing(2),
|
||||
}}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("queuePage.refreshButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => flushQueue(trigger, setTrigger)}
|
||||
loading={isFlushing}
|
||||
>
|
||||
{t("queuePage.flushButton")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrigger(!trigger);
|
||||
}}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("queuePage.refreshButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => flushQueue(trigger, setTrigger)}
|
||||
loading={isFlushing}
|
||||
>
|
||||
{t("queuePage.flushButton")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Queue from "./Queue";
|
||||
import LogsComponent from "./Logs";
|
||||
import Diagnostics from "./Diagnostics";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,7 +15,7 @@ const Logs = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
// Local state
|
||||
const [value, setValue] = useState(0);
|
||||
const [value, setValue] = useState(2);
|
||||
|
||||
// Handlers
|
||||
const handleChange = (event, newValue) => {
|
||||
@@ -31,9 +32,11 @@ const Logs = () => {
|
||||
>
|
||||
<Tab label={t("logsPage.tabs.logs")} />
|
||||
<Tab label={t("logsPage.tabs.queue")} />
|
||||
<Tab label={t("logsPage.tabs.diagnostics")} />
|
||||
</Tabs>
|
||||
{value === 0 && <LogsComponent />}
|
||||
{value === 1 && <Queue />}
|
||||
{value === 2 && <Diagnostics />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorListItem = ({ monitor, onDelete }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
width="100%"
|
||||
>
|
||||
<Typography flexGrow={1}>{monitor.name}</Typography>
|
||||
<DeleteIcon
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => onDelete(monitor)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorListItem.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
|
||||
const onDelete = (monitorToDelete) => {
|
||||
const newMonitors = selectedMonitors.filter(
|
||||
(monitor) => monitor._id !== monitorToDelete._id
|
||||
);
|
||||
setSelectedMonitors(newMonitors);
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(6)}
|
||||
width="100%"
|
||||
>
|
||||
{selectedMonitors?.map((monitor) => (
|
||||
<MonitorListItem
|
||||
key={monitor._id}
|
||||
monitor={monitor}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorList.propTypes = {
|
||||
selectedMonitors: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
setSelectedMonitors: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MonitorList;
|
||||
@@ -9,6 +9,8 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
|
||||
import { maintenanceWindowValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import MonitorList from "./Components/MonitorList";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
@@ -212,6 +214,17 @@ const CreateMaintenance = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleMonitorsChange = (selected) => {
|
||||
setForm((prev) => ({ ...prev, monitors: selected }));
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ monitors: selected },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, "monitors", error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return;
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
@@ -525,19 +538,25 @@ const CreateMaintenance = () => {
|
||||
id={"monitors"}
|
||||
label={t("addMonitors")}
|
||||
multiple={true}
|
||||
isAdorned={false}
|
||||
options={monitors ? monitors : []}
|
||||
isAdorned={true}
|
||||
options={monitors}
|
||||
filteredBy="name"
|
||||
secondaryLabel={"type"}
|
||||
inputValue={search}
|
||||
value={form.monitors}
|
||||
handleInputChange={handleSearch}
|
||||
handleChange={handleSelectMonitors}
|
||||
handleInputChange={setSearch}
|
||||
handleChange={handleMonitorsChange}
|
||||
error={errors["monitors"]}
|
||||
disabled={maintenanceWindowId !== undefined}
|
||||
/>
|
||||
<MonitorList
|
||||
selectedMonitors={form.monitors}
|
||||
setSelectedMonitors={(monitors) =>
|
||||
setForm((prev) => ({ ...prev, monitors }))
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
|
||||
<Box
|
||||
ml="auto"
|
||||
display="inline-block"
|
||||
|
||||
@@ -65,12 +65,19 @@ const CreateNotifications = () => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const getNotificationTypeValue = (typeId) => {
|
||||
return NOTIFICATION_TYPES.find((type) => type._id === typeId)?.value || "email";
|
||||
};
|
||||
|
||||
const extractError = (error, field) =>
|
||||
error?.details.find((d) => d.path.includes(field))?.message;
|
||||
|
||||
// handlers
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const form = {
|
||||
...notification,
|
||||
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
|
||||
type: getNotificationTypeValue(notification.type),
|
||||
};
|
||||
|
||||
let error = null;
|
||||
@@ -84,7 +91,7 @@ const CreateNotifications = () => {
|
||||
});
|
||||
console.log(JSON.stringify(newErrors));
|
||||
console.log(JSON.stringify(form, null, 2));
|
||||
createToast({ body: "Please check the form for errors." });
|
||||
createToast({ body: Object.values(newErrors)[0] });
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
@@ -98,22 +105,32 @@ const CreateNotifications = () => {
|
||||
|
||||
const onChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
let rawNotification = { ...notification, [name]: value };
|
||||
let newNotification = {
|
||||
...rawNotification,
|
||||
type: getNotificationTypeValue(rawNotification.type),
|
||||
};
|
||||
|
||||
const newNotification = { ...notification, [name]: value };
|
||||
const { error } = notificationValidation.validate(newNotification, {
|
||||
abortEarly: false,
|
||||
});
|
||||
let validationError = { ...errors };
|
||||
|
||||
const { error } = notificationValidation.extract(name).validate(value);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: error?.message,
|
||||
}));
|
||||
if (name === "type") {
|
||||
validationError["type"] = extractError(error, "type");
|
||||
validationError["address"] = extractError(error, "address");
|
||||
} else {
|
||||
validationError[name] = extractError(error, name);
|
||||
}
|
||||
|
||||
setNotification(newNotification);
|
||||
setNotification(rawNotification);
|
||||
setErrors(validationError);
|
||||
};
|
||||
|
||||
const onTestNotification = () => {
|
||||
const form = {
|
||||
...notification,
|
||||
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
|
||||
type: getNotificationTypeValue(notification.type),
|
||||
};
|
||||
|
||||
let error = null;
|
||||
@@ -125,7 +142,7 @@ const CreateNotifications = () => {
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
createToast({ body: "Please check the form for errors." });
|
||||
createToast({ body: Object.values(newErrors)[0] });
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +156,7 @@ const CreateNotifications = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const type = NOTIFICATION_TYPES.find((type) => type._id === notification.type).value;
|
||||
const type = getNotificationTypeValue(notification.type);
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
|
||||
@@ -117,8 +117,8 @@ const SettingsEmail = ({
|
||||
<Box>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.emailSettings.labelHost")}</Typography>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelHost")}
|
||||
name="systemEmailHost"
|
||||
placeholder="smtp.gmail.com"
|
||||
value={systemEmailHost}
|
||||
@@ -126,8 +126,8 @@ const SettingsEmail = ({
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.emailSettings.labelPort")}</Typography>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelPort")}
|
||||
name="systemEmailPort"
|
||||
placeholder="425"
|
||||
type="number"
|
||||
@@ -136,8 +136,8 @@ const SettingsEmail = ({
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.emailSettings.labelUser")}</Typography>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelUser")}
|
||||
name="systemEmailUser"
|
||||
placeholder={t("settingsPage.emailSettings.placeholderUser")}
|
||||
value={systemEmailUser}
|
||||
@@ -145,8 +145,8 @@ const SettingsEmail = ({
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.emailSettings.labelAddress")}</Typography>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelAddress")}
|
||||
name="systemEmailAddress"
|
||||
placeholder="uptime@bluewavelabs.ca"
|
||||
value={systemEmailAddress}
|
||||
@@ -155,8 +155,8 @@ const SettingsEmail = ({
|
||||
</Box>
|
||||
{(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.emailSettings.labelPassword")}</Typography>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelPassword")}
|
||||
name="systemEmailPassword"
|
||||
type="password"
|
||||
placeholder="123 456 789 101112"
|
||||
@@ -188,8 +188,8 @@ const SettingsEmail = ({
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.emailSettings.labelTLSServername")}</Typography>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelTLSServername")}
|
||||
name="systemEmailTLSServername"
|
||||
placeholder="bluewavelabs.ca"
|
||||
value={systemEmailTLSServername}
|
||||
@@ -197,8 +197,8 @@ const SettingsEmail = ({
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsPage.emailSettings.labelConnectionHost")}</Typography>
|
||||
<TextInput
|
||||
label={t("settingsPage.emailSettings.labelConnectionHost")}
|
||||
name="systemEmailConnectionHost"
|
||||
placeholder="bluewavelabs.ca"
|
||||
value={systemEmailConnectionHost}
|
||||
|
||||
@@ -2,16 +2,37 @@ import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import Search from "../../Components/Inputs/Search";
|
||||
import timezones from "../../Utils/timezones.json";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [rawInput, setRawInput] = useState("");
|
||||
|
||||
const selectedTimezone = useMemo(
|
||||
() => timezones.find((tz) => tz._id === timezone) ?? null,
|
||||
[timezone]
|
||||
);
|
||||
|
||||
const handleTimezoneChange = useCallback(
|
||||
(newValue) => {
|
||||
setRawInput("");
|
||||
handleChange({
|
||||
target: {
|
||||
name: "timezone",
|
||||
value: newValue?._id ?? "",
|
||||
},
|
||||
});
|
||||
},
|
||||
[handleChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
@@ -28,12 +49,17 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
label={t("settingsPage.timezoneSettings.label")}
|
||||
name="timezone"
|
||||
value={timezone}
|
||||
onChange={handleChange}
|
||||
items={timezones}
|
||||
<Search
|
||||
id="timezone"
|
||||
label={t("settingsDisplayTimezone")}
|
||||
options={timezones}
|
||||
filteredBy="name"
|
||||
value={selectedTimezone}
|
||||
inputValue={rawInput}
|
||||
handleInputChange={(val) => setRawInput(val)}
|
||||
handleChange={handleTimezoneChange}
|
||||
isAdorned={false}
|
||||
unit="timezone"
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TabPanel } from "@mui/lab";
|
||||
import ConfigBox from "../../../../../Components/ConfigBox";
|
||||
import Checkbox from "../../../../../Components/Inputs/Checkbox";
|
||||
import TextInput from "../../../../../Components/Inputs/TextInput";
|
||||
import Select from "../../../../../Components/Inputs/Select";
|
||||
import Search from "../../../../../Components/Inputs/Search";
|
||||
import ImageUpload from "../../../../../Components/Inputs/ImageUpload";
|
||||
import ColorPicker from "../../../../../Components/Inputs/ColorPicker";
|
||||
import Progress from "../Progress";
|
||||
@@ -14,6 +14,7 @@ import { useTheme } from "@emotion/react";
|
||||
import timezones from "../../../../../Utils/timezones.json";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo, useState, useCallback } from "react";
|
||||
|
||||
const TabSettings = ({
|
||||
isCreate,
|
||||
@@ -28,6 +29,25 @@ const TabSettings = ({
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [rawInput, setRawInput] = useState("");
|
||||
|
||||
const selectedTimezone = useMemo(
|
||||
() => timezones.find((tz) => tz._id === form.timezone) ?? null,
|
||||
[form.timezone]
|
||||
);
|
||||
|
||||
const handleTimezoneChange = useCallback(
|
||||
(newValue) => {
|
||||
setRawInput("");
|
||||
handleFormChange({
|
||||
target: {
|
||||
name: "timezone",
|
||||
value: newValue?._id ?? "",
|
||||
},
|
||||
});
|
||||
},
|
||||
[handleFormChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<TabPanel value={tabValue}>
|
||||
@@ -101,13 +121,17 @@ const TabSettings = ({
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Select
|
||||
<Search
|
||||
id="timezone"
|
||||
name="timezone"
|
||||
label={t("settingsDisplayTimezone")}
|
||||
items={timezones}
|
||||
value={form.timezone}
|
||||
onChange={handleFormChange}
|
||||
options={timezones}
|
||||
filteredBy="name"
|
||||
value={selectedTimezone}
|
||||
inputValue={rawInput}
|
||||
handleInputChange={(newVal) => setRawInput(newVal)}
|
||||
handleChange={handleTimezoneChange}
|
||||
isAdorned={false}
|
||||
unit="timezone"
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
|
||||
@@ -1091,6 +1091,14 @@ class NetworkService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getDiagnostics() {
|
||||
return this.axiosInstance.get(`/diagnostic/system`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
|
||||
@@ -553,6 +553,11 @@ const baseTheme = (palette) => ({
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
padding: "0 13px",
|
||||
minHeight: "34px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
lineHeight: 1,
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -85,6 +85,7 @@ export const getHumanReadableDuration = (ms) => {
|
||||
hours: durationObj.hours(),
|
||||
minutes: durationObj.minutes(),
|
||||
seconds: durationObj.seconds(),
|
||||
milliseconds: durationObj.milliseconds(),
|
||||
};
|
||||
|
||||
const result = [];
|
||||
@@ -101,6 +102,9 @@ export const getHumanReadableDuration = (ms) => {
|
||||
if (result.length < 2 && parts.seconds > 0) {
|
||||
result.push(`${parts.seconds}s`);
|
||||
}
|
||||
if (result.length < 2 && parts.milliseconds > 0 && parts.seconds < 1) {
|
||||
result.push(`${parts.milliseconds.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// fallback for durations < 1s
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { toast, Slide } from "react-toastify";
|
||||
import Alert from "../Components/Alert";
|
||||
|
||||
import Toast from "../Components/Toast";
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {'info' | 'error' | 'warning'} - The variant of the alert (e.g., "info", "error").
|
||||
@@ -15,6 +14,7 @@ export const createToast = ({
|
||||
variant = "info",
|
||||
title,
|
||||
body,
|
||||
hasDismiss = false,
|
||||
hasIcon = false,
|
||||
config = {},
|
||||
}) => {
|
||||
@@ -29,13 +29,14 @@ export const createToast = ({
|
||||
|
||||
toast(
|
||||
({ closeToast }) => (
|
||||
<Alert
|
||||
<Toast
|
||||
variant={variant}
|
||||
title={title}
|
||||
body={body}
|
||||
isToast={true}
|
||||
hasIcon={hasIcon}
|
||||
onClick={closeToast}
|
||||
hasDismiss={hasDismiss}
|
||||
hasIcon={hasIcon}
|
||||
/>
|
||||
),
|
||||
toastConfig
|
||||
@@ -47,5 +48,6 @@ createToast.propTypes = {
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.string.isRequired,
|
||||
hasIcon: PropTypes.bool,
|
||||
hasDismiss: PropTypes.bool,
|
||||
config: PropTypes.object,
|
||||
};
|
||||
@@ -410,14 +410,33 @@ const notificationValidation = joi.object({
|
||||
"string.empty": "Notification name is required",
|
||||
"any.required": "Notification name is required",
|
||||
}),
|
||||
address: joi.string().required().messages({
|
||||
"string.empty": "This field cannot be empty",
|
||||
"string.base": "This field must be a string",
|
||||
"any.required": "This field is required",
|
||||
}),
|
||||
type: joi.string().required().messages({
|
||||
"string.empty": "This field is required",
|
||||
"any.required": "This field is required",
|
||||
|
||||
type: joi
|
||||
.string()
|
||||
.valid("email", "webhook", "slack", "discord", "pager_duty")
|
||||
.required()
|
||||
.messages({
|
||||
"string.empty": "Notification type is required",
|
||||
"any.required": "Notification type is required",
|
||||
"any.only": "Notification type must be email, webhook, or pager_duty",
|
||||
}),
|
||||
|
||||
address: joi.when("type", {
|
||||
is: "email",
|
||||
then: joi
|
||||
.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required()
|
||||
.messages({
|
||||
"string.empty": "E-mail address cannot be empty",
|
||||
"any.required": "E-mail address is required",
|
||||
"string.email": "Please enter a valid e-mail address",
|
||||
}),
|
||||
otherwise: joi.string().uri().required().messages({
|
||||
"string.empty": "Webhook URL cannot be empty",
|
||||
"any.required": "Webhook URL is required",
|
||||
"string.uri": "Please enter a valid Webhook URL",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -229,6 +229,9 @@
|
||||
"unknownError": "Unknown error"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"noOptionsFound": "No {{unit}} found"
|
||||
},
|
||||
"commonSave": "Save",
|
||||
"commonSaving": "Saving...",
|
||||
"companyName": "Company name",
|
||||
@@ -443,7 +446,7 @@
|
||||
"inviteNoTokenFound": "No invite token found",
|
||||
"loginHere": "Login here",
|
||||
"logsPage": {
|
||||
"description": "This page shows the latest 1000 lines of logs from the Checkmate server",
|
||||
"description": "System logs - last 1000 lines",
|
||||
"logLevelSelect": {
|
||||
"title": "Log level",
|
||||
"values": {
|
||||
@@ -456,13 +459,35 @@
|
||||
},
|
||||
"tabs": {
|
||||
"logs": "Server logs",
|
||||
"queue": "Job queue"
|
||||
"queue": "Job queue",
|
||||
"diagnostics": "Diagnostics"
|
||||
},
|
||||
"title": "Logs",
|
||||
"toast": {
|
||||
"fetchLogsSuccess": "Logs fetched successfully"
|
||||
}
|
||||
},
|
||||
"diagnosticsPage": {
|
||||
"diagnosticDescription": "System diagnostics",
|
||||
"statsDescription": "System statistics",
|
||||
"gauges": {
|
||||
"heapAllocationTitle": "Heap allocation",
|
||||
"heapAllocationSubtitle": "% of available memory",
|
||||
"heapUsageTitle": "Heap usage",
|
||||
"heapUsageSubtitle": "% of available memory",
|
||||
"heapUtilizationTitle": "Heap utilization",
|
||||
"heapUtilizationSubtitle": "% of allocated",
|
||||
"instantCpuUsageTitle": "Instant CPU usage",
|
||||
"instantCpuUsageSubtitle": "% of 1s used by CPU"
|
||||
},
|
||||
"stats": {
|
||||
"eventLoopDelayTitle": "Event loop delay",
|
||||
"uptimeTitle": "Uptime",
|
||||
"usedHeapSizeTitle": "Used heap size",
|
||||
"totalHeapSizeTitle": "Total heap size",
|
||||
"osMemoryLimitTitle": "OS Memory Limit"
|
||||
}
|
||||
},
|
||||
"low": "low",
|
||||
"maintenance": "maintenance",
|
||||
"maintenanceRepeat": "Maintenance Repeat",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAnnouncementValidation } from "../validation/joi.js";
|
||||
import { handleError } from "./controllerUtils.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "announcementController";
|
||||
|
||||
@@ -28,16 +28,10 @@ class AnnouncementController {
|
||||
*
|
||||
* @returns {Promise<void>} A promise that resolves once the response is sent.
|
||||
*/
|
||||
createAnnouncement = async (req, res, next) => {
|
||||
try {
|
||||
createAnnouncement = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await createAnnouncementValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
return next(handleError(error, SERVICE_NAME)); // Handle Joi validation errors
|
||||
}
|
||||
|
||||
const { title, message } = req.body;
|
||||
|
||||
try {
|
||||
const { title, message } = req.body;
|
||||
const announcementData = {
|
||||
title: title.trim(),
|
||||
message: message.trim(),
|
||||
@@ -49,10 +43,10 @@ class AnnouncementController {
|
||||
msg: this.stringService.createAnnouncement,
|
||||
data: newAnnouncement,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createAnnouncement"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"createAnnouncement"
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles retrieving announcements with pagination.
|
||||
@@ -63,17 +57,17 @@ class AnnouncementController {
|
||||
* - `msg`: A message about the success of the request.
|
||||
* @param {Function} next - The next middleware function in the stack for error handling.
|
||||
*/
|
||||
getAnnouncement = async (req, res, next) => {
|
||||
try {
|
||||
getAnnouncement = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const allAnnouncements = await this.db.getAnnouncements();
|
||||
return res.success({
|
||||
msg: this.stringService.getAnnouncement,
|
||||
data: allAnnouncements,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAnnouncement"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getAnnouncement"
|
||||
);
|
||||
}
|
||||
|
||||
export default AnnouncementController;
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
import jwt from "jsonwebtoken";
|
||||
import { getTokenFromHeaders } from "../utils/utils.js";
|
||||
import crypto from "crypto";
|
||||
import { handleValidationError, handleError } from "./controllerUtils.js";
|
||||
import { asyncHandler, createAuthError, createError } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "authController";
|
||||
|
||||
class AuthController {
|
||||
@@ -32,15 +33,10 @@ class AuthController {
|
||||
* @throws {Error}
|
||||
*/
|
||||
issueToken = (payload, appSettings) => {
|
||||
try {
|
||||
const tokenTTL = appSettings?.jwtTTL ?? "2h";
|
||||
const tokenSecret = appSettings?.jwtSecret;
|
||||
const payloadData = payload;
|
||||
|
||||
return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
|
||||
} catch (error) {
|
||||
throw handleError(error, SERVICE_NAME, "issueToken");
|
||||
}
|
||||
const tokenTTL = appSettings?.jwtTTL ?? "2h";
|
||||
const tokenSecret = appSettings?.jwtSecret;
|
||||
const payloadData = payload;
|
||||
return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,19 +51,14 @@ class AuthController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the creation of the user, the created user data, and a JWT token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
registerUser = async (req, res, next) => {
|
||||
try {
|
||||
registerUser = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
if (req.body?.email) {
|
||||
req.body.email = req.body.email?.toLowerCase();
|
||||
}
|
||||
await registrationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
// Create a new user
|
||||
try {
|
||||
|
||||
// Create a new user
|
||||
const user = req.body;
|
||||
// If superAdmin exists, a token should be attached to all further register requests
|
||||
const superAdminExists = await this.db.checkSuperadmin(req, res);
|
||||
@@ -123,10 +114,10 @@ class AuthController {
|
||||
msg: this.stringService.authCreateUser,
|
||||
data: { user: newUser, token: token },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "registerController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"registerUser"
|
||||
);
|
||||
|
||||
/**
|
||||
* Logs in a user by validating the user's credentials and issuing a JWT token.
|
||||
@@ -140,18 +131,13 @@ class AuthController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the login of the user, the user data (without password and avatar image), and a JWT token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect.
|
||||
*/
|
||||
loginUser = async (req, res, next) => {
|
||||
try {
|
||||
loginUser = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
if (req.body?.email) {
|
||||
req.body.email = req.body.email?.toLowerCase();
|
||||
}
|
||||
await loginValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Check if user exists
|
||||
@@ -160,10 +146,7 @@ class AuthController {
|
||||
// Compare password
|
||||
const match = await user.comparePassword(password);
|
||||
if (match !== true) {
|
||||
const error = new Error(this.stringService.authIncorrectPassword);
|
||||
error.status = 401;
|
||||
next(error);
|
||||
return;
|
||||
throw createAuthError(this.stringService.authIncorrectPassword);
|
||||
}
|
||||
|
||||
// Remove password from user object. Should this be abstracted to DB layer?
|
||||
@@ -184,11 +167,10 @@ class AuthController {
|
||||
token: token,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
error.status = 401;
|
||||
next(handleError(error, SERVICE_NAME, "loginUser"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"loginUser"
|
||||
);
|
||||
|
||||
/**
|
||||
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
|
||||
@@ -204,26 +186,16 @@ class AuthController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the update of the user, and the updated user data.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403).
|
||||
*/
|
||||
editUser = async (req, res, next) => {
|
||||
try {
|
||||
editUser = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await editUserParamValidation.validateAsync(req.params);
|
||||
await editUserBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO is this neccessary any longer? Verify ownership middleware should handle this
|
||||
if (req.params.userId !== req.user._id.toString()) {
|
||||
const error = new Error(this.stringService.unauthorized);
|
||||
error.status = 401;
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
// TODO is this neccessary any longer? Verify ownership middleware should handle this
|
||||
if (req.params.userId !== req.user._id.toString()) {
|
||||
throw createAuthError(this.stringService.unauthorized);
|
||||
}
|
||||
|
||||
try {
|
||||
// Change Password check
|
||||
if (req.body.password && req.body.newPassword) {
|
||||
// Get token from headers
|
||||
@@ -240,10 +212,7 @@ class AuthController {
|
||||
// If not a match, throw a 403
|
||||
// 403 instead of 401 to avoid triggering axios interceptor
|
||||
if (!match) {
|
||||
const error = new Error(this.stringService.authIncorrectPassword);
|
||||
error.status = 403;
|
||||
next(error);
|
||||
return;
|
||||
throw createError(this.stringService.authIncorrectPassword, 403);
|
||||
}
|
||||
// If a match, update the password
|
||||
req.body.password = req.body.newPassword;
|
||||
@@ -254,10 +223,10 @@ class AuthController {
|
||||
msg: this.stringService.authUpdateUser,
|
||||
data: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "userEditController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"editUser"
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if a superadmin account exists in the database.
|
||||
@@ -268,18 +237,17 @@ class AuthController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the existence of a superadmin, and a boolean indicating the existence of a superadmin.
|
||||
* @throws {Error} If there is an error during the process.
|
||||
*/
|
||||
checkSuperadminExists = async (req, res, next) => {
|
||||
try {
|
||||
checkSuperadminExists = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const superAdminExists = await this.db.checkSuperadmin(req, res);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.authAdminExists,
|
||||
data: superAdminExists,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "checkSuperadminController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"checkSuperadminExists"
|
||||
);
|
||||
/**
|
||||
* Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email.
|
||||
* @async
|
||||
@@ -291,16 +259,9 @@ class AuthController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the creation of the recovery token, and the message ID of the sent email.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
requestRecovery = async (req, res, next) => {
|
||||
try {
|
||||
requestRecovery = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await recoveryValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { email } = req.body;
|
||||
const user = await this.db.getUserByEmail(email);
|
||||
const recoveryToken = await this.db.requestRecoveryToken(req, res);
|
||||
@@ -323,10 +284,10 @@ class AuthController {
|
||||
msg: this.stringService.authCreateRecoveryToken,
|
||||
data: msgId,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "recoveryRequestController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"requestRecovery"
|
||||
);
|
||||
/**
|
||||
* Validates a recovery token. The recovery token is validated and if valid, a success message is returned.
|
||||
* @async
|
||||
@@ -338,25 +299,17 @@ class AuthController {
|
||||
* @returns {Object} The response object with a success status and a message indicating the validation of the recovery token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
validateRecovery = async (req, res, next) => {
|
||||
try {
|
||||
validateRecovery = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await recoveryTokenValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.db.validateRecoveryToken(req, res);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.authVerifyRecoveryToken,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"validateRecovery"
|
||||
);
|
||||
|
||||
/**
|
||||
* Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued.
|
||||
@@ -370,27 +323,20 @@ class AuthController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
resetPassword = async (req, res, next) => {
|
||||
try {
|
||||
resetPassword = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await newPasswordValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await this.db.resetPassword(req, res);
|
||||
const appSettings = await this.settingsService.getSettings();
|
||||
const token = this.issueToken(user._doc, appSettings);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.authResetPassword,
|
||||
data: { user, token },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "resetPasswordController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"resetPassword"
|
||||
);
|
||||
|
||||
/**
|
||||
* Deletes a user and all associated monitors, checks, and alerts.
|
||||
@@ -401,8 +347,8 @@ class AuthController {
|
||||
* @returns {Object} The response object with success status and message.
|
||||
* @throws {Error} If user validation fails or user is not found in the database.
|
||||
*/
|
||||
deleteUser = async (req, res, next) => {
|
||||
try {
|
||||
deleteUser = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const decodedToken = jwt.decode(token);
|
||||
const { email } = decodedToken;
|
||||
@@ -430,23 +376,22 @@ class AuthController {
|
||||
return res.success({
|
||||
msg: this.stringService.authDeleteUser,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteUserController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteUser"
|
||||
);
|
||||
|
||||
getAllUsers = async (req, res, next) => {
|
||||
try {
|
||||
getAllUsers = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const allUsers = await this.db.getAllUsers(req, res);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.authGetAllUsers,
|
||||
data: allUsers,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAllUsersController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getAllUsers"
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthController;
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
ackAllChecksParamValidation,
|
||||
ackAllChecksBodyValidation,
|
||||
} from "../validation/joi.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { getTokenFromHeaders } from "../utils/utils.js";
|
||||
import { handleValidationError, handleError } from "./controllerUtils.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "checkController";
|
||||
|
||||
@@ -25,16 +23,11 @@ class CheckController {
|
||||
this.stringService = stringService;
|
||||
}
|
||||
|
||||
createCheck = async (req, res, next) => {
|
||||
try {
|
||||
createCheck = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await createCheckParamValidation.validateAsync(req.params);
|
||||
await createCheckBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const checkData = { ...req.body };
|
||||
const check = await this.db.createCheck(checkData);
|
||||
|
||||
@@ -42,21 +35,16 @@ class CheckController {
|
||||
msg: this.stringService.checkCreate,
|
||||
data: check,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createCheck"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"createCheck"
|
||||
);
|
||||
|
||||
getChecksByMonitor = async (req, res, next) => {
|
||||
try {
|
||||
getChecksByMonitor = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getChecksParamValidation.validateAsync(req.params);
|
||||
await getChecksQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } =
|
||||
req.query;
|
||||
@@ -76,20 +64,16 @@ class CheckController {
|
||||
msg: this.stringService.checkGet,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getChecks"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getChecksByMonitor"
|
||||
);
|
||||
|
||||
getChecksByTeam = async (req, res, next) => {
|
||||
try {
|
||||
getChecksByTeam = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getTeamChecksParamValidation.validateAsync(req.params);
|
||||
await getTeamChecksQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = req.query;
|
||||
const { teamId } = req.user;
|
||||
|
||||
@@ -106,33 +90,28 @@ class CheckController {
|
||||
msg: this.stringService.checkGet,
|
||||
data: checkData,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getTeamChecks"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getChecksByTeam"
|
||||
);
|
||||
|
||||
getChecksSummaryByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
getChecksSummaryByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { teamId } = req.user;
|
||||
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
|
||||
return res.success({
|
||||
msg: this.stringService.checkGetSummary,
|
||||
data: summary,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getChecksSummaryByTeamId"
|
||||
);
|
||||
|
||||
ackCheck = async (req, res, next) => {
|
||||
try {
|
||||
ackCheck = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await ackCheckBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { checkId } = req.params;
|
||||
const { ack } = req.body;
|
||||
const { teamId } = req.user;
|
||||
@@ -143,21 +122,16 @@ class CheckController {
|
||||
msg: this.stringService.checkUpdateStatus,
|
||||
data: updatedCheck,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "ackCheck"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"ackCheck"
|
||||
);
|
||||
|
||||
ackAllChecks = async (req, res, next) => {
|
||||
try {
|
||||
ackAllChecks = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await ackAllChecksParamValidation.validateAsync(req.params);
|
||||
await ackAllChecksBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId, path } = req.params;
|
||||
const { ack } = req.body;
|
||||
const { teamId } = req.user;
|
||||
@@ -168,40 +142,30 @@ class CheckController {
|
||||
msg: this.stringService.checkUpdateStatus,
|
||||
data: updatedChecks,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "ackAllChecks"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"ackAllChecks"
|
||||
);
|
||||
|
||||
deleteChecks = async (req, res, next) => {
|
||||
try {
|
||||
deleteChecks = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await deleteChecksParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const deletedCount = await this.db.deleteChecks(req.params.monitorId);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.checkDelete,
|
||||
data: { deletedCount },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteChecks"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteChecks"
|
||||
);
|
||||
|
||||
deleteChecksByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
deleteChecksByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { teamId } = req.user;
|
||||
const deletedCount = await this.db.deleteChecksByTeamId(teamId);
|
||||
|
||||
@@ -209,23 +173,17 @@ class CheckController {
|
||||
msg: this.stringService.checkDelete,
|
||||
data: { deletedCount },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteChecksByTeamId"
|
||||
);
|
||||
|
||||
updateChecksTTL = async (req, res, next) => {
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
updateChecksTTL = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
|
||||
try {
|
||||
await updateChecksTTLBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get user's teamId
|
||||
const { teamId } = req.user;
|
||||
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
|
||||
await this.db.updateChecksTTL(teamId, ttl);
|
||||
@@ -233,9 +191,9 @@ class CheckController {
|
||||
return res.success({
|
||||
msg: this.stringService.checkUpdateTTL,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "updateTTL"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"updateChecksTtl"
|
||||
);
|
||||
}
|
||||
export default CheckController;
|
||||
@@ -1,16 +1,4 @@
|
||||
const handleValidationError = (error, serviceName) => {
|
||||
error.status = 422;
|
||||
error.service = serviceName;
|
||||
error.message = error.details?.[0]?.message || error.message || "Validation Error";
|
||||
return error;
|
||||
};
|
||||
|
||||
const handleError = (error, serviceName, method, status = 500) => {
|
||||
error.status === undefined ? (error.status = status) : null;
|
||||
error.service === undefined ? (error.service = serviceName) : null;
|
||||
error.method === undefined ? (error.method = method) : null;
|
||||
return error;
|
||||
};
|
||||
import { createServerError } from "../utils/errorUtils.js";
|
||||
|
||||
const fetchMonitorCertificate = async (sslChecker, monitor) => {
|
||||
const monitorUrl = new URL(monitor.url);
|
||||
@@ -18,9 +6,9 @@ const fetchMonitorCertificate = async (sslChecker, monitor) => {
|
||||
const cert = await sslChecker(hostname);
|
||||
// Throw an error if no cert or if cert.validTo is not present
|
||||
if (cert?.validTo === null || cert?.validTo === undefined) {
|
||||
throw new Error("Certificate not found");
|
||||
throw createServerError("Certificate not found");
|
||||
}
|
||||
return cert;
|
||||
};
|
||||
|
||||
export { handleValidationError, handleError, fetchMonitorCertificate };
|
||||
export { fetchMonitorCertificate };
|
||||
@@ -1,6 +1,14 @@
|
||||
import { handleError } from "./controllerUtils.js";
|
||||
import v8 from "v8";
|
||||
import os from "os";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "diagnosticController";
|
||||
|
||||
const obs = new PerformanceObserver((items) => {
|
||||
const entry = items.getEntries()[0];
|
||||
performance.clearMarks();
|
||||
});
|
||||
obs.observe({ entryTypes: ["measure"] });
|
||||
class DiagnosticController {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
@@ -9,20 +17,20 @@ class DiagnosticController {
|
||||
this.getDbStats = this.getDbStats.bind(this);
|
||||
}
|
||||
|
||||
async getMonitorsByTeamIdExecutionStats(req, res, next) {
|
||||
try {
|
||||
getMonitorsByTeamIdExecutionStats = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const data = await this.db.getMonitorsByTeamIdExecutionStats(req);
|
||||
return res.success({
|
||||
msg: "OK",
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamIdExecutionStats"));
|
||||
}
|
||||
}
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMonitorsByTeamIdExecutionStats"
|
||||
);
|
||||
|
||||
async getDbStats(req, res, next) {
|
||||
try {
|
||||
getDbStats = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { methodName, args = [] } = req.body;
|
||||
if (!methodName || !this.db[methodName]) {
|
||||
return res.error({
|
||||
@@ -40,9 +48,87 @@ class DiagnosticController {
|
||||
msg: "OK",
|
||||
data: stats,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getDbStats"));
|
||||
}
|
||||
}
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getDbStats"
|
||||
);
|
||||
|
||||
getCPUUsage = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const startUsage = process.cpuUsage();
|
||||
const timingPeriod = 1000; // measured in ms
|
||||
await new Promise((resolve) => setTimeout(resolve, timingPeriod));
|
||||
const endUsage = process.cpuUsage(startUsage);
|
||||
const cpuUsage = {
|
||||
userUsageMs: endUsage.user / 1000,
|
||||
systemUsageMs: endUsage.system / 1000,
|
||||
usagePercentage: ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100,
|
||||
};
|
||||
return cpuUsage;
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getCPUUsage"
|
||||
);
|
||||
|
||||
getSystemStats = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
// Memory Usage
|
||||
const totalMemory = os.totalmem();
|
||||
const freeMemory = os.freemem();
|
||||
|
||||
const osStats = {
|
||||
freeMemoryBytes: freeMemory, // bytes
|
||||
totalMemoryBytes: totalMemory, // bytes
|
||||
};
|
||||
|
||||
const used = process.memoryUsage();
|
||||
const memoryUsage = {};
|
||||
for (let key in used) {
|
||||
memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB
|
||||
}
|
||||
|
||||
// CPU Usage
|
||||
const cpuMetrics = await this.getCPUUsage();
|
||||
|
||||
// V8 Heap Statistics
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
const v8Metrics = {
|
||||
totalHeapSizeBytes: heapStats.total_heap_size, // bytes
|
||||
usedHeapSizeBytes: heapStats.used_heap_size, // bytes
|
||||
heapSizeLimitBytes: heapStats.heap_size_limit, // bytes
|
||||
};
|
||||
|
||||
// Event Loop Delay
|
||||
let eventLoopDelay = 0;
|
||||
performance.mark("start");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
performance.mark("end");
|
||||
performance.measure("eventLoopDelay", "start", "end");
|
||||
const entries = performance.getEntriesByName("eventLoopDelay");
|
||||
if (entries.length > 0) {
|
||||
eventLoopDelay = entries[0].duration;
|
||||
}
|
||||
|
||||
// Uptime
|
||||
const uptimeMs = process.uptime() * 1000; // ms
|
||||
|
||||
// Combine Metrics
|
||||
const diagnostics = {
|
||||
osStats,
|
||||
memoryUsage,
|
||||
cpuUsage: cpuMetrics,
|
||||
v8HeapStats: v8Metrics,
|
||||
eventLoopDelayMs: eventLoopDelay,
|
||||
uptimeMs,
|
||||
};
|
||||
|
||||
return res.success({
|
||||
msg: "OK",
|
||||
data: diagnostics,
|
||||
});
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getSystemStats"
|
||||
);
|
||||
}
|
||||
export default DiagnosticController;
|
||||
@@ -3,10 +3,9 @@ import {
|
||||
inviteBodyValidation,
|
||||
inviteVerificationBodyValidation,
|
||||
} from "../validation/joi.js";
|
||||
import logger from "../utils/logger.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { handleError, handleValidationError } from "./controllerUtils.js";
|
||||
import { getTokenFromHeaders } from "../utils/utils.js";
|
||||
import { asyncHandler, createServerError } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "inviteController";
|
||||
|
||||
@@ -31,99 +30,73 @@ class InviteController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
getInviteToken = async (req, res, next) => {
|
||||
try {
|
||||
getInviteToken = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
// Only admins can invite
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { role, teamId } = jwt.decode(token);
|
||||
req.body.teamId = teamId;
|
||||
try {
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
|
||||
const inviteToken = await this.db.requestInviteToken({ ...req.body });
|
||||
return res.success({
|
||||
msg: this.stringService.inviteIssued,
|
||||
data: inviteToken,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "inviteController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getInviteToken"
|
||||
);
|
||||
|
||||
sendInviteEmail = async (req, res, next) => {
|
||||
try {
|
||||
sendInviteEmail = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
// Only admins can invite
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { role, firstname, teamId } = jwt.decode(token);
|
||||
req.body.teamId = teamId;
|
||||
try {
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
|
||||
const inviteToken = await this.db.requestInviteToken({ ...req.body });
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
|
||||
try {
|
||||
const html = await this.emailService.buildEmail("employeeActivationTemplate", {
|
||||
name: firstname,
|
||||
link: `${clientHost}/register/${inviteToken.token}`,
|
||||
});
|
||||
const result = await this.emailService.sendEmail(
|
||||
req.body.email,
|
||||
"Welcome to Uptime Monitor",
|
||||
html
|
||||
const html = await this.emailService.buildEmail("employeeActivationTemplate", {
|
||||
name: firstname,
|
||||
link: `${clientHost}/register/${inviteToken.token}`,
|
||||
});
|
||||
const result = await this.emailService.sendEmail(
|
||||
req.body.email,
|
||||
"Welcome to Uptime Monitor",
|
||||
html
|
||||
);
|
||||
if (!result) {
|
||||
throw createServerError(
|
||||
"Failed to send invite e-mail... Please verify your settings."
|
||||
);
|
||||
if (!result) {
|
||||
return res.error({
|
||||
msg: "Failed to send invite e-mail... Please verify your settings.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "sendInviteEmail",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.inviteIssued,
|
||||
data: inviteToken,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "inviteController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"sendInviteEmail"
|
||||
);
|
||||
|
||||
inviteVerifyController = async (req, res, next) => {
|
||||
try {
|
||||
inviteVerifyController = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await inviteVerificationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const invite = await this.db.getInviteToken(req.body.token);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.inviteVerified,
|
||||
data: invite,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "inviteVerifyController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"inviteVerifyController"
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteController;
|
||||
@@ -1,23 +1,22 @@
|
||||
import { handleError } from "./controllerUtils.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "JobQueueController";
|
||||
const SERVICE_NAME = "LogController";
|
||||
|
||||
class LogController {
|
||||
constructor(logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
getLogs = async (req, res, next) => {
|
||||
try {
|
||||
getLogs = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const logs = await this.logger.getLogs();
|
||||
res.success({
|
||||
msg: "Logs fetched successfully",
|
||||
data: logs,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getLogs"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getLogs"
|
||||
);
|
||||
}
|
||||
export default LogController;
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getMaintenanceWindowsByTeamIdQueryValidation,
|
||||
deleteMaintenanceWindowByIdParamValidation,
|
||||
} from "../validation/joi.js";
|
||||
import { handleValidationError, handleError } from "./controllerUtils.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "maintenanceWindowController";
|
||||
|
||||
@@ -18,14 +18,10 @@ class MaintenanceWindowController {
|
||||
this.stringService = stringService;
|
||||
}
|
||||
|
||||
createMaintenanceWindows = async (req, res, next) => {
|
||||
try {
|
||||
createMaintenanceWindows = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
const { teamId } = req.user;
|
||||
const monitorIds = req.body.monitors;
|
||||
const dbTransactions = monitorIds.map((monitorId) => {
|
||||
@@ -44,39 +40,28 @@ class MaintenanceWindowController {
|
||||
return res.success({
|
||||
msg: this.stringService.maintenanceWindowCreate,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createMaintenanceWindow"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"createMaintenanceWindows"
|
||||
);
|
||||
|
||||
getMaintenanceWindowById = async (req, res, next) => {
|
||||
try {
|
||||
getMaintenanceWindowById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const maintenanceWindow = await this.db.getMaintenanceWindowById(req.params.id);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.maintenanceWindowGetById,
|
||||
data: maintenanceWindow,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMaintenanceWindowById"
|
||||
);
|
||||
|
||||
getMaintenanceWindowsByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
getMaintenanceWindowsByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { teamId } = req.user;
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId(
|
||||
teamId,
|
||||
@@ -87,20 +72,15 @@ class MaintenanceWindowController {
|
||||
msg: this.stringService.maintenanceWindowGetByTeam,
|
||||
data: maintenanceWindows,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMaintenanceWindowsByTeamId"
|
||||
);
|
||||
|
||||
getMaintenanceWindowsByMonitorId = async (req, res, next) => {
|
||||
try {
|
||||
getMaintenanceWindowsByMonitorId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(
|
||||
req.params.monitorId
|
||||
);
|
||||
@@ -109,37 +89,27 @@ class MaintenanceWindowController {
|
||||
msg: this.stringService.maintenanceWindowGetByUser,
|
||||
data: maintenanceWindows,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMaintenanceWindowsByMonitorId"
|
||||
);
|
||||
|
||||
deleteMaintenanceWindow = async (req, res, next) => {
|
||||
try {
|
||||
deleteMaintenanceWindow = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.db.deleteMaintenanceWindowById(req.params.id);
|
||||
return res.success({
|
||||
msg: this.stringService.maintenanceWindowDelete,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteMaintenanceWindow"
|
||||
);
|
||||
|
||||
editMaintenanceWindow = async (req, res, next) => {
|
||||
try {
|
||||
editMaintenanceWindow = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const editedMaintenanceWindow = await this.db.editMaintenanceWindowById(
|
||||
req.params.id,
|
||||
req.body
|
||||
@@ -148,10 +118,10 @@ class MaintenanceWindowController {
|
||||
msg: this.stringService.maintenanceWindowEdit,
|
||||
data: editedMaintenanceWindow,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "editMaintenanceWindow"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"editMaintenanceWindow"
|
||||
);
|
||||
}
|
||||
|
||||
export default MaintenanceWindowController;
|
||||
@@ -16,12 +16,12 @@ import {
|
||||
} from "../validation/joi.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
import logger from "../utils/logger.js";
|
||||
import { handleError, handleValidationError } from "./controllerUtils.js";
|
||||
import axios from "axios";
|
||||
import seedDb from "../db/mongo/utils/seedDb.js";
|
||||
const SERVICE_NAME = "monitorController";
|
||||
import pkg from "papaparse";
|
||||
import { asyncHandler, createServerError } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
class MonitorController {
|
||||
constructor(db, settingsService, jobQueue, stringService, emailService) {
|
||||
this.db = db;
|
||||
@@ -40,17 +40,17 @@ class MonitorController {
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
getAllMonitors = async (req, res, next) => {
|
||||
try {
|
||||
getAllMonitors = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const monitors = await this.db.getAllMonitors();
|
||||
return res.success({
|
||||
msg: this.stringService.monitorGetAll,
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAllMonitors"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getAllMonitors"
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns all monitors with uptime stats for 1,7,30, and 90 days
|
||||
@@ -61,20 +61,20 @@ class MonitorController {
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
getAllMonitorsWithUptimeStats = async (req, res, next) => {
|
||||
try {
|
||||
getAllMonitorsWithUptimeStats = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const monitors = await this.db.getAllMonitorsWithUptimeStats();
|
||||
return res.success({
|
||||
msg: this.stringService.monitorGetAll,
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getAllMonitorsWithUptimeStats"
|
||||
);
|
||||
|
||||
getUptimeDetailsById = async (req, res, next) => {
|
||||
try {
|
||||
getUptimeDetailsById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { monitorId } = req.params;
|
||||
const { dateRange, normalize } = req.query;
|
||||
|
||||
@@ -87,10 +87,10 @@ class MonitorController {
|
||||
msg: this.stringService.monitorGetByIdSuccess,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorDetailsById"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getUptimeDetailsById"
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns monitor stats for monitor with matching ID
|
||||
@@ -101,16 +101,11 @@ class MonitorController {
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
getMonitorStatsById = async (req, res, next) => {
|
||||
try {
|
||||
getMonitorStatsById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
const { monitorId } = req.params;
|
||||
|
||||
@@ -126,10 +121,10 @@ class MonitorController {
|
||||
msg: this.stringService.monitorStatsById,
|
||||
data: monitorStats,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorStatsById"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMonitorStatsById"
|
||||
);
|
||||
|
||||
/**
|
||||
* Get hardware details for a specific monitor by ID
|
||||
@@ -140,15 +135,11 @@ class MonitorController {
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error} - Throws error if monitor not found or other database errors
|
||||
*/
|
||||
getHardwareDetailsById = async (req, res, next) => {
|
||||
try {
|
||||
getHardwareDetailsById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
const { monitorId } = req.params;
|
||||
const { dateRange } = req.query;
|
||||
const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
|
||||
@@ -156,19 +147,15 @@ class MonitorController {
|
||||
msg: this.stringService.monitorGetByIdSuccess,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getHardwareDetailsById"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getHardwareDetailsById"
|
||||
);
|
||||
|
||||
getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => {
|
||||
try {
|
||||
getMonitorCertificate = asyncHandler(
|
||||
async (req, res, next, fetchMonitorCertificate) => {
|
||||
await getCertificateParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
|
||||
@@ -179,10 +166,10 @@ class MonitorController {
|
||||
certificateDate: new Date(certificate.validTo),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorCertificate"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMonitorCertificate"
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves a monitor by its ID.
|
||||
@@ -195,25 +182,20 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status, a message, and the retrieved monitor data.
|
||||
* @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422).
|
||||
*/
|
||||
getMonitorById = async (req, res, next) => {
|
||||
try {
|
||||
getMonitorById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const monitor = await this.db.getMonitorById(req.params.monitorId);
|
||||
return res.success({
|
||||
msg: this.stringService.monitorGetByIdSuccess,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorById"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMonitorById"
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates a new monitor and adds it to the job queue.
|
||||
@@ -226,15 +208,10 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
createMonitor = async (req, res, next) => {
|
||||
try {
|
||||
createMonitor = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await createMonitorBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { _id, teamId } = req.user;
|
||||
const monitor = await this.db.createMonitor({
|
||||
body: req.body,
|
||||
@@ -248,10 +225,10 @@ class MonitorController {
|
||||
msg: this.stringService.monitorCreate,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createMonitor"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"createMonitor"
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates bulk monitors and adds them to the job queue after parsing CSV.
|
||||
@@ -263,8 +240,8 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status and message.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
createBulkMonitors = async (req, res, next) => {
|
||||
try {
|
||||
createBulkMonitors = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { parse } = pkg;
|
||||
|
||||
// validate the file
|
||||
@@ -310,51 +287,43 @@ class MonitorController {
|
||||
return value;
|
||||
},
|
||||
complete: async ({ data, errors }) => {
|
||||
try {
|
||||
if (errors.length > 0) {
|
||||
throw new Error("Error parsing CSV");
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor) => ({
|
||||
userId: _id,
|
||||
teamId,
|
||||
...monitor,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
try {
|
||||
const monitors = await this.db.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor, index) => {
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.bulkMonitorsCreate,
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
}
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
if (errors.length > 0) {
|
||||
throw createServerError("Error parsing CSV");
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw createServerError("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor) => ({
|
||||
userId: _id,
|
||||
teamId,
|
||||
...monitor,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
const monitors = await this.db.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor, index) => {
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.bulkMonitorsCreate,
|
||||
data: monitors,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"createBulkMonitors"
|
||||
);
|
||||
/**
|
||||
* Checks if the endpoint can be resolved
|
||||
* @async
|
||||
@@ -365,15 +334,9 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status, a message, and the resolution result.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
checkEndpointResolution = async (req, res, next) => {
|
||||
try {
|
||||
checkEndpointResolution = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMonitorURLByQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorURL } = req.query;
|
||||
const parsedUrl = new URL(monitorURL);
|
||||
const response = await axios.get(parsedUrl, {
|
||||
@@ -384,10 +347,10 @@ class MonitorController {
|
||||
status: response.status,
|
||||
msg: response.statusText,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "checkEndpointResolution"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"checkEndpointResolution"
|
||||
);
|
||||
|
||||
/**
|
||||
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
|
||||
@@ -400,24 +363,18 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status and a message indicating the deletion of the monitor.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records.
|
||||
*/
|
||||
deleteMonitor = async (req, res, next) => {
|
||||
try {
|
||||
deleteMonitor = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const monitorId = req.params.monitorId;
|
||||
const monitor = await this.db.deleteMonitor({ monitorId });
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.deleteStatusPagesByMonitorId(monitor._id);
|
||||
return res.success({ msg: this.stringService.monitorDelete });
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteMonitor"
|
||||
);
|
||||
|
||||
/**
|
||||
* Deletes all monitors associated with a team.
|
||||
@@ -430,8 +387,8 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status and a message indicating the number of deleted monitors.
|
||||
* @throws {Error} If there is an error during the deletion process.
|
||||
*/
|
||||
deleteAllMonitors = async (req, res, next) => {
|
||||
try {
|
||||
deleteAllMonitors = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { teamId } = req.user;
|
||||
const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId);
|
||||
await Promise.all(
|
||||
@@ -442,7 +399,7 @@ class MonitorController {
|
||||
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await this.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
logger.warn({
|
||||
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteAllMonitors",
|
||||
@@ -452,10 +409,10 @@ class MonitorController {
|
||||
})
|
||||
);
|
||||
return res.success({ msg: `Deleted ${deletedCount} monitors` });
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteAllMonitors"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteAllMonitors"
|
||||
);
|
||||
|
||||
/**
|
||||
* Edits a monitor by its ID, updates its notifications, and updates its job in the job queue.
|
||||
@@ -470,16 +427,10 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
editMonitor = async (req, res, next) => {
|
||||
try {
|
||||
editMonitor = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await editMonitorBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
|
||||
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
|
||||
@@ -490,10 +441,10 @@ class MonitorController {
|
||||
msg: this.stringService.monitorEdit,
|
||||
data: editedMonitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "editMonitor"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"editMonitor"
|
||||
);
|
||||
|
||||
/**
|
||||
* Pauses or resumes a monitor based on its current state.
|
||||
@@ -506,14 +457,10 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data.
|
||||
* @throws {Error} If there is an error during the process.
|
||||
*/
|
||||
pauseMonitor = async (req, res, next) => {
|
||||
try {
|
||||
pauseMonitor = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await pauseMonitorParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
}
|
||||
|
||||
try {
|
||||
const monitorId = req.params.monitorId;
|
||||
const monitor = await this.db.pauseMonitor({ monitorId });
|
||||
monitor.isActive === true
|
||||
@@ -526,10 +473,10 @@ class MonitorController {
|
||||
: this.stringService.monitorPause,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "pauseMonitor"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"pauseMonitor"
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds demo monitors for a team.
|
||||
@@ -542,8 +489,8 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added.
|
||||
* @throws {Error} If there is an error during the process.
|
||||
*/
|
||||
addDemoMonitors = async (req, res, next) => {
|
||||
try {
|
||||
addDemoMonitors = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { _id, teamId } = req.user;
|
||||
const demoMonitors = await this.db.addDemoMonitors(_id, teamId);
|
||||
await Promise.all(
|
||||
@@ -554,10 +501,10 @@ class MonitorController {
|
||||
msg: this.stringService.monitorDemoAdded,
|
||||
data: demoMonitors.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "addDemoMonitors"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"addDemoMonitors"
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a test email to verify email delivery functionality.
|
||||
@@ -570,8 +517,8 @@ class MonitorController {
|
||||
* @returns {Object} The response object with a success status and the email delivery message ID.
|
||||
* @throws {Error} If there is an error while sending the test email.
|
||||
*/
|
||||
sendTestEmail = async (req, res, next) => {
|
||||
try {
|
||||
sendTestEmail = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { to } = req.body;
|
||||
if (!to || typeof to !== "string") {
|
||||
throw new Error(this.stringService.errorForValidEmailAddress);
|
||||
@@ -584,29 +531,23 @@ class MonitorController {
|
||||
const messageId = await this.emailService.sendEmail(to, subject, html);
|
||||
|
||||
if (!messageId) {
|
||||
return res.error({
|
||||
msg: "Failed to send test email.",
|
||||
});
|
||||
throw createServerError("Failed to send test email.");
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.sendTestEmail,
|
||||
data: { messageId },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "sendTestEmail"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"sendTestEmail"
|
||||
);
|
||||
|
||||
getMonitorsByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
getMonitorsByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
}
|
||||
|
||||
try {
|
||||
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
|
||||
const teamId = req.user.teamId;
|
||||
|
||||
@@ -624,20 +565,16 @@ class MonitorController {
|
||||
msg: this.stringService.monitorGetByTeamId,
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMonitorsByTeamId"
|
||||
);
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
getMonitorsAndSummaryByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
return next(handleValidationError(error, SERVICE_NAME));
|
||||
}
|
||||
|
||||
try {
|
||||
const { explain } = req;
|
||||
const { type } = req.query;
|
||||
const { teamId } = req.user;
|
||||
@@ -651,20 +588,16 @@ class MonitorController {
|
||||
msg: "OK", // TODO
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMonitorsAndSummaryByTeamId"
|
||||
);
|
||||
|
||||
getMonitorsWithChecksByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
getMonitorsWithChecksByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
return next(handleValidationError(error, SERVICE_NAME));
|
||||
}
|
||||
|
||||
try {
|
||||
const { explain } = req;
|
||||
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
|
||||
const { teamId } = req.user;
|
||||
@@ -684,23 +617,23 @@ class MonitorController {
|
||||
msg: "OK",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return next(handleError(error, SERVICE_NAME, "getMonitorsWithChecksByTeamId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMonitorsWithChecksByTeamId"
|
||||
);
|
||||
|
||||
seedDb = async (req, res, next) => {
|
||||
try {
|
||||
seedDb = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { _id, teamId } = req.user;
|
||||
await seedDb(_id, teamId);
|
||||
res.success({ msg: "Database seeded" });
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "seedDb"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"seedDb"
|
||||
);
|
||||
|
||||
exportMonitorsToCSV = async (req, res, next) => {
|
||||
try {
|
||||
exportMonitorsToCSV = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { teamId } = req.user;
|
||||
|
||||
const monitors = await this.db.getMonitorsByTeamId({ teamId });
|
||||
@@ -730,10 +663,10 @@ class MonitorController {
|
||||
"Content-Disposition": "attachment; filename=monitors.csv",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "exportMonitorsToCSV"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"exportMonitorsToCSV"
|
||||
);
|
||||
}
|
||||
|
||||
export default MonitorController;
|
||||
@@ -1,22 +1,8 @@
|
||||
import {
|
||||
triggerNotificationBodyValidation,
|
||||
createNotificationBodyValidation,
|
||||
} from "../validation/joi.js";
|
||||
import { handleError, handleValidationError } from "./controllerUtils.js";
|
||||
import { createNotificationBodyValidation } from "../validation/joi.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "NotificationController";
|
||||
|
||||
const NOTIFICATION_TYPES = {
|
||||
WEBHOOK: "webhook",
|
||||
TELEGRAM: "telegram",
|
||||
};
|
||||
|
||||
const PLATFORMS = {
|
||||
SLACK: "slack",
|
||||
DISCORD: "discord",
|
||||
TELEGRAM: "telegram",
|
||||
};
|
||||
|
||||
class NotificationController {
|
||||
constructor({ notificationService, stringService, statusService, db }) {
|
||||
this.notificationService = notificationService;
|
||||
@@ -25,8 +11,8 @@ class NotificationController {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
testNotification = async (req, res, next) => {
|
||||
try {
|
||||
testNotification = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const notification = req.body;
|
||||
|
||||
const success = await this.notificationService.sendTestNotification(notification);
|
||||
@@ -41,22 +27,17 @@ class NotificationController {
|
||||
return res.success({
|
||||
msg: "Notification sent successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "testWebhook"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"testNotification"
|
||||
);
|
||||
|
||||
createNotification = async (req, res, next) => {
|
||||
try {
|
||||
createNotification = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await createNotificationBodyValidation.validateAsync(req.body, {
|
||||
abortEarly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = req.body;
|
||||
const { _id, teamId } = req.user;
|
||||
body.userId = _id;
|
||||
@@ -66,69 +47,64 @@ class NotificationController {
|
||||
msg: "Notification created successfully",
|
||||
data: notification,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createNotification"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"createNotification"
|
||||
);
|
||||
|
||||
getNotificationsByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
getNotificationsByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const notifications = await this.db.getNotificationsByTeamId(req.user.teamId);
|
||||
return res.success({
|
||||
msg: "Notifications fetched successfully",
|
||||
data: notifications,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getNotificationsByTeamId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getNotificationsByTeamId"
|
||||
);
|
||||
|
||||
deleteNotification = async (req, res, next) => {
|
||||
try {
|
||||
deleteNotification = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await this.db.deleteNotificationById(req.params.id);
|
||||
return res.success({
|
||||
msg: "Notification deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteNotification"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteNotification"
|
||||
);
|
||||
|
||||
getNotificationById = async (req, res, next) => {
|
||||
try {
|
||||
getNotificationById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const notification = await this.db.getNotificationById(req.params.id);
|
||||
return res.success({
|
||||
msg: "Notification fetched successfully",
|
||||
data: notification,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getNotificationById"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getNotificationById"
|
||||
);
|
||||
|
||||
editNotification = async (req, res, next) => {
|
||||
try {
|
||||
editNotification = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await createNotificationBodyValidation.validateAsync(req.body, {
|
||||
abortEarly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const notification = await this.db.editNotification(req.params.id, req.body);
|
||||
return res.success({
|
||||
msg: "Notification updated successfully",
|
||||
data: notification,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "editNotification"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"editNotification"
|
||||
);
|
||||
|
||||
testAllNotifications = async (req, res, next) => {
|
||||
try {
|
||||
testAllNotifications = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const { monitorId } = req.body;
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
const notifications = monitor.notifications;
|
||||
@@ -138,10 +114,10 @@ class NotificationController {
|
||||
return res.success({
|
||||
msg: "All notifications sent successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "testAllNotifications"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"testAllNotifications"
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationController;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError } from "./controllerUtils.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "JobQueueController";
|
||||
|
||||
@@ -8,82 +8,76 @@ class JobQueueController {
|
||||
this.stringService = stringService;
|
||||
}
|
||||
|
||||
getMetrics = async (req, res, next) => {
|
||||
try {
|
||||
getMetrics = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const metrics = await this.jobQueue.getMetrics();
|
||||
res.success({
|
||||
msg: this.stringService.queueGetMetrics,
|
||||
data: metrics,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMetrics"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getMetrics"
|
||||
);
|
||||
|
||||
getJobs = async (req, res, next) => {
|
||||
try {
|
||||
getJobs = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const jobs = await this.jobQueue.getJobs();
|
||||
return res.success({
|
||||
msg: this.stringService.queueGetJobs,
|
||||
data: jobs,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getJobs"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getJobs"
|
||||
);
|
||||
|
||||
getAllMetrics = async (req, res, next) => {
|
||||
try {
|
||||
getAllMetrics = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const jobs = await this.jobQueue.getJobs();
|
||||
const metrics = await this.jobQueue.getMetrics();
|
||||
return res.success({
|
||||
msg: this.stringService.queueGetAllMetrics,
|
||||
data: { jobs, metrics },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAllMetrics"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getAllMetrics"
|
||||
);
|
||||
|
||||
addJob = async (req, res, next) => {
|
||||
try {
|
||||
addJob = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
|
||||
return res.success({
|
||||
msg: this.stringService.queueAddJob,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "addJob"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"addJob"
|
||||
);
|
||||
|
||||
flushQueue = async (req, res, next) => {
|
||||
try {
|
||||
flushQueue = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const result = await this.jobQueue.flushQueues();
|
||||
return res.success({
|
||||
msg: this.stringService.jobQueueFlush,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "flushQueue"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"flushQueue"
|
||||
);
|
||||
|
||||
checkQueueHealth = async (req, res, next) => {
|
||||
try {
|
||||
checkQueueHealth = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const stuckQueues = await this.jobQueue.checkQueueHealth();
|
||||
return res.success({
|
||||
msg: this.stringService.queueHealthCheck,
|
||||
data: stuckQueues,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "checkQueueHealth"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"checkQueueHealth"
|
||||
);
|
||||
}
|
||||
export default JobQueueController;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { updateAppSettingsBodyValidation } from "../validation/joi.js";
|
||||
import { handleValidationError, handleError } from "./controllerUtils.js";
|
||||
import { sendTestEmailBodyValidation } from "../validation/joi.js";
|
||||
import { asyncHandler, createServerError } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "SettingsController";
|
||||
|
||||
class SettingsController {
|
||||
@@ -32,45 +33,44 @@ class SettingsController {
|
||||
return returnSettings;
|
||||
};
|
||||
|
||||
getAppSettings = async (req, res, next) => {
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
getAppSettings = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
|
||||
const returnSettings = this.buildAppSettings(dbSettings);
|
||||
return res.success({
|
||||
msg: this.stringService.getAppSettings,
|
||||
data: returnSettings,
|
||||
});
|
||||
};
|
||||
const returnSettings = this.buildAppSettings(dbSettings);
|
||||
return res.success({
|
||||
msg: this.stringService.getAppSettings,
|
||||
data: returnSettings,
|
||||
});
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getAppSettings"
|
||||
);
|
||||
|
||||
updateAppSettings = async (req, res, next) => {
|
||||
try {
|
||||
updateAppSettings = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await updateAppSettingsBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedSettings = await this.db.updateAppSettings(req.body);
|
||||
const returnSettings = this.buildAppSettings(updatedSettings);
|
||||
return res.success({
|
||||
msg: this.stringService.updateAppSettings,
|
||||
data: returnSettings,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"updateAppSettings"
|
||||
);
|
||||
|
||||
sendTestEmail = async (req, res, next) => {
|
||||
try {
|
||||
await sendTestEmailBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
sendTestEmail = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
await sendTestEmailBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
to,
|
||||
systemEmailHost,
|
||||
@@ -107,20 +107,17 @@ class SettingsController {
|
||||
});
|
||||
|
||||
if (!messageId) {
|
||||
return res.error({
|
||||
msg: "Failed to send test email.",
|
||||
});
|
||||
throw createServerError("Failed to send test email.");
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.sendTestEmail,
|
||||
data: { messageId },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"sendTestEmail"
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsController;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { handleError, handleValidationError } from "./controllerUtils.js";
|
||||
import {
|
||||
createStatusPageBodyValidation,
|
||||
getStatusPageParamValidation,
|
||||
getStatusPageQueryValidation,
|
||||
imageValidation,
|
||||
} from "../validation/joi.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageController";
|
||||
|
||||
@@ -14,16 +14,11 @@ class StatusPageController {
|
||||
this.stringService = stringService;
|
||||
}
|
||||
|
||||
createStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
createStatusPage = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||
await imageValidation.validateAsync(req.file);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { _id, teamId } = req.user;
|
||||
const statusPage = await this.db.createStatusPage({
|
||||
statusPageData: req.body,
|
||||
@@ -35,21 +30,16 @@ class StatusPageController {
|
||||
msg: this.stringService.statusPageCreate,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createStatusPage"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"createStatusPage"
|
||||
);
|
||||
|
||||
updateStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
updateStatusPage = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||
await imageValidation.validateAsync(req.file);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const statusPage = await this.db.updateStatusPage(req.body, req.file);
|
||||
if (statusPage === null) {
|
||||
const error = new Error(this.stringService.statusPageNotFound);
|
||||
@@ -60,45 +50,40 @@ class StatusPageController {
|
||||
msg: this.stringService.statusPageUpdate,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "updateStatusPage"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"updateStatusPage"
|
||||
);
|
||||
|
||||
getStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
getStatusPage = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const statusPage = await this.db.getStatusPage();
|
||||
return res.success({
|
||||
msg: this.stringService.statusPageByUrl,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getStatusPage"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getStatusPage"
|
||||
);
|
||||
|
||||
getStatusPageByUrl = async (req, res, next) => {
|
||||
try {
|
||||
getStatusPageByUrl = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getStatusPageParamValidation.validateAsync(req.params);
|
||||
await getStatusPageQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const statusPage = await this.db.getStatusPageByUrl(req.params.url, req.query.type);
|
||||
return res.success({
|
||||
msg: this.stringService.statusPageByUrl,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getStatusPageByUrl"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getStatusPageByUrl"
|
||||
);
|
||||
|
||||
getStatusPagesByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
getStatusPagesByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const teamId = req.user.teamId;
|
||||
const statusPages = await this.db.getStatusPagesByTeamId(teamId);
|
||||
|
||||
@@ -106,21 +91,21 @@ class StatusPageController {
|
||||
msg: this.stringService.statusPageByTeamId,
|
||||
data: statusPages,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getStatusPageByTeamId"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getStatusPagesByTeamId"
|
||||
);
|
||||
|
||||
deleteStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
deleteStatusPage = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await this.db.deleteStatusPage(req.params.url);
|
||||
return res.success({
|
||||
msg: this.stringService.statusPageDelete,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteStatusPage"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteStatusPage"
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusPageController;
|
||||
@@ -3,6 +3,7 @@ import ServiceRegistry from "../service/serviceRegistry.js";
|
||||
import StringService from "../service/stringService.js";
|
||||
|
||||
const handleErrors = (error, req, res, next) => {
|
||||
console.log("ERROR", error);
|
||||
const status = error.status || 500;
|
||||
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
|
||||
const message = error.message || stringService.authIncorrectPassword;
|
||||
|
||||
@@ -13,6 +13,7 @@ class DiagnosticRoutes {
|
||||
);
|
||||
|
||||
this.router.post("/db/stats", this.diagnosticController.getDbStats);
|
||||
this.router.get("/system", this.diagnosticController.getSystemStats);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
class AppError extends Error {
|
||||
constructor(message, status = 500, service = null, method = null, details = null) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.service = service;
|
||||
this.method = method;
|
||||
this.details = details;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
export const createError = (
|
||||
message,
|
||||
status = 500,
|
||||
service = null,
|
||||
method = null,
|
||||
details = null
|
||||
) => {
|
||||
return new AppError(message, status, service, method, details);
|
||||
};
|
||||
|
||||
export const createValidationError = (
|
||||
message,
|
||||
details = null,
|
||||
service = null,
|
||||
method = null
|
||||
) => {
|
||||
return createError(message, 422, service, method, details);
|
||||
};
|
||||
|
||||
export const createAuthError = (
|
||||
message,
|
||||
details = null,
|
||||
service = null,
|
||||
method = null
|
||||
) => {
|
||||
return createError(message, 401, service, method, details);
|
||||
};
|
||||
|
||||
export const createForbiddenError = (
|
||||
message,
|
||||
details = null,
|
||||
service = null,
|
||||
method = null
|
||||
) => {
|
||||
return createError(message, 403, service, method, details);
|
||||
};
|
||||
|
||||
export const createNotFoundError = (
|
||||
message,
|
||||
details = null,
|
||||
service = null,
|
||||
method = null
|
||||
) => {
|
||||
return createError(message, 404, service, method, details);
|
||||
};
|
||||
|
||||
export const createConflictError = (
|
||||
message,
|
||||
details = null,
|
||||
service = null,
|
||||
method = null
|
||||
) => {
|
||||
return createError(message, 409, service, method, details);
|
||||
};
|
||||
|
||||
export const createServerError = (
|
||||
message,
|
||||
details = null,
|
||||
service = null,
|
||||
method = null
|
||||
) => {
|
||||
return createError(message, 500, service, method, details);
|
||||
};
|
||||
|
||||
export const asyncHandler = (fn, serviceName, methodName) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
await fn(req, res, next);
|
||||
} catch (error) {
|
||||
// Handle validation errors
|
||||
if (error.isJoi || error.name === "ValidationError") {
|
||||
const validationError = createValidationError(
|
||||
error.message,
|
||||
error.details,
|
||||
serviceName,
|
||||
methodName
|
||||
);
|
||||
return next(validationError);
|
||||
}
|
||||
|
||||
if (error instanceof AppError) {
|
||||
error.service = error.service || serviceName;
|
||||
error.method = error.method || methodName;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
if (error.code === "23505") {
|
||||
const appError = createConflictError("Resource already exists", {
|
||||
originalError: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
appError.service = serviceName;
|
||||
appError.method = methodName;
|
||||
return next(appError);
|
||||
}
|
||||
|
||||
// For unknown errors, create a server error
|
||||
const appError = createServerError(
|
||||
error.message || "An unexpected error occurred",
|
||||
{ originalError: error.message, stack: error.stack }
|
||||
);
|
||||
appError.service = serviceName;
|
||||
appError.method = methodName;
|
||||
appError.stack = error.stack; // Preserve original stack
|
||||
|
||||
return next(appError);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -577,14 +577,7 @@ const createNotificationBodyValidation = joi.object({
|
||||
"string.empty": "Notification name is required",
|
||||
"any.required": "Notification name is required",
|
||||
}),
|
||||
address: joi.when("type", {
|
||||
is: "email",
|
||||
then: joi.string().email().required().messages({
|
||||
"string.empty": "E-mail address cannot be empty",
|
||||
"any.required": "E-mail address is required",
|
||||
"string.email": "Please enter a valid e-mail address",
|
||||
}),
|
||||
}),
|
||||
|
||||
type: joi
|
||||
.string()
|
||||
.valid("email", "webhook", "slack", "discord", "pager_duty")
|
||||
@@ -594,6 +587,20 @@ const createNotificationBodyValidation = joi.object({
|
||||
"any.required": "Notification type is required",
|
||||
"any.only": "Notification type must be email, webhook, or pager_duty",
|
||||
}),
|
||||
|
||||
address: joi.when("type", {
|
||||
is: "email",
|
||||
then: joi.string().email().required().messages({
|
||||
"string.empty": "E-mail address cannot be empty",
|
||||
"any.required": "E-mail address is required",
|
||||
"string.email": "Please enter a valid e-mail address",
|
||||
}),
|
||||
otherwise: joi.string().uri().required().messages({
|
||||
"string.empty": "Webhook URL cannot be empty",
|
||||
"any.required": "Webhook URL is required",
|
||||
"string.uri": "Please enter a valid Webhook URL",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
|
||||
Reference in new issue
Block a user