mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-18 23:48:43 -05:00
Merge branch 'develop' into feat/fe/refactor/input
This commit is contained in:
Generated
+39
-39
@@ -15,8 +15,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "6.1.8",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.22.2",
|
||||
"@mui/x-date-pickers": "7.22.2",
|
||||
"@mui/x-data-grid": "7.22.3",
|
||||
"@mui/x-date-pickers": "7.22.3",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
@@ -362,16 +362,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz",
|
||||
"integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==",
|
||||
"version": "11.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.16.7",
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/hash": "^0.9.2",
|
||||
"@emotion/memoize": "^0.9.0",
|
||||
"@emotion/serialize": "^1.2.0",
|
||||
"@emotion/serialize": "^1.3.3",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"convert-source-map": "^1.5.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
@@ -381,14 +381,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/cache": {
|
||||
"version": "11.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
|
||||
"integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==",
|
||||
"version": "11.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz",
|
||||
"integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.9.0",
|
||||
"@emotion/sheet": "^1.4.0",
|
||||
"@emotion/utils": "^1.4.0",
|
||||
"@emotion/utils": "^1.4.2",
|
||||
"@emotion/weak-memoize": "^0.4.0",
|
||||
"stylis": "4.2.0"
|
||||
}
|
||||
@@ -415,17 +415,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/react": {
|
||||
"version": "11.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz",
|
||||
"integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==",
|
||||
"version": "11.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz",
|
||||
"integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.12.0",
|
||||
"@emotion/cache": "^11.13.0",
|
||||
"@emotion/serialize": "^1.3.1",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/cache": "^11.13.5",
|
||||
"@emotion/serialize": "^1.3.3",
|
||||
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
|
||||
"@emotion/utils": "^1.4.0",
|
||||
"@emotion/utils": "^1.4.2",
|
||||
"@emotion/weak-memoize": "^0.4.0",
|
||||
"hoist-non-react-statics": "^3.3.1"
|
||||
},
|
||||
@@ -439,15 +439,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/serialize": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz",
|
||||
"integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
|
||||
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/hash": "^0.9.2",
|
||||
"@emotion/memoize": "^0.9.0",
|
||||
"@emotion/unitless": "^0.10.0",
|
||||
"@emotion/utils": "^1.4.1",
|
||||
"@emotion/utils": "^1.4.2",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
@@ -458,17 +458,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/styled": {
|
||||
"version": "11.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz",
|
||||
"integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==",
|
||||
"version": "11.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz",
|
||||
"integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.12.0",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@emotion/serialize": "^1.3.0",
|
||||
"@emotion/serialize": "^1.3.3",
|
||||
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
|
||||
"@emotion/utils": "^1.4.0"
|
||||
"@emotion/utils": "^1.4.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.0.0-rc.0",
|
||||
@@ -496,9 +496,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/utils": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz",
|
||||
"integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
|
||||
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/weak-memoize": {
|
||||
@@ -1535,9 +1535,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-charts": {
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.2.tgz",
|
||||
"integrity": "sha512-0Y2du4Ed7gOT53l8vVJ4vKT+Jz4Dh/iHnLy8TtL3+XhbPH9Ndu9Q30WwyyzOn84yt37hSUru/njQ1BWaSvVPHw==",
|
||||
"version": "7.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.3.tgz",
|
||||
"integrity": "sha512-w23+AwIK86bpNWkuHewyQwOKi1wYbLDzrvUEqvZ9KVYzZvnqpJmbTKideX1pLVgSNt0On8NDXytzCntV48Nobw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1593,9 +1593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.2.tgz",
|
||||
"integrity": "sha512-yfy2s5A6tbajQZiEdsba49T4FYb9F0WPrzbbG30dl1+sIiX4ZRX7ma44UIDGPZrsZv8xkkE+p8qeJxZ7OaMteA==",
|
||||
"version": "7.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.3.tgz",
|
||||
"integrity": "sha512-O6kBf6yt/GkOcWjHca5xWN10qBQ/MkITvJmBuIOtX+LH7YtOAriMgD2zkhNbXxHChi7QdEud3bNC3jw5RLRVCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1630,9 +1630,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers": {
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.2.tgz",
|
||||
"integrity": "sha512-1KHSlIlnSoY3oHm820By8X344pIdGYqPvCCvfVHrEeeIQ/pHdxDD8tjZFWkFl4Jgm9oVFK90fMcqNZAzc+WaCw==",
|
||||
"version": "7.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.3.tgz",
|
||||
"integrity": "sha512-shNp92IrST5BiVy2f4jbrmRaD32QhyUthjh1Oexvpcn0v6INyuWgxfodoTi5ZCnE5Ue5UVFSs4R9Xre0UbJ5DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "6.1.8",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.22.2",
|
||||
"@mui/x-date-pickers": "7.22.2",
|
||||
"@mui/x-data-grid": "7.22.3",
|
||||
"@mui/x-date-pickers": "7.22.3",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
min-width: var(--env-var-width-3);
|
||||
}
|
||||
|
||||
.field-infrastructure-alert {
|
||||
.field-infrastructure-alert{
|
||||
max-width: var(--env-var-width-4);
|
||||
}
|
||||
|
||||
.field-infrastructure-alert .MuiInputBase-root:has(input) {
|
||||
/* height: var(--env-var-height-2); */
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.field h3.MuiTypography-root,
|
||||
|
||||
@@ -7,23 +7,31 @@ import Visibility from "@mui/icons-material/Visibility";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Field component for rendering various types of input fields with customizable properties
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string} [props.type] - Type of input field (e.g., 'text', 'password').
|
||||
* @param {string} props.id - ID of the input field.
|
||||
* @param {string} props.name - Name of the input field.
|
||||
* @param {string} [props.label] - Label for the input field.
|
||||
* @param {boolean} [props.https] - Indicates if it should display http or https.
|
||||
* @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk.
|
||||
* @param {boolean} [props.isOptional] - Indicates if the field is optional, will display optional text.
|
||||
* @param {string} [props.optionalLabel] - Optional label for the input field.
|
||||
* @param {string} [props.autoComplete] - Autocomplete value for the input field.
|
||||
* @param {string} [props.type='text'] - Type of input field (text, password, url, email, description, number).
|
||||
* @param {string} props.id - Unique identifier for the input field.
|
||||
* @param {string} props.name - Name attribute for the input field.
|
||||
* @param {string} [props.label] - Label text displayed above the input field.
|
||||
* @param {boolean} [props.https=true] - For URL type, determines whether to show https:// or http://.
|
||||
* @param {boolean} [props.isRequired=false] - Displays a red asterisk if the field is required.
|
||||
* @param {boolean} [props.isOptional=false] - Displays an optional label next to the field.
|
||||
* @param {string} [props.optionalLabel='(optional)'] - Custom text for optional label.
|
||||
* @param {string} [props.autoComplete] - Autocomplete attribute for the input.
|
||||
* @param {string} [props.placeholder] - Placeholder text for the input field.
|
||||
* @param {string} props.value - Value of the input field.
|
||||
* @param {function} props.onChange - Function called on input change.
|
||||
* @param {string} [props.error] - Error message to display for the input field.
|
||||
* @param {boolean} [props.disabled] - Indicates if the input field is disabled.
|
||||
* @param {boolean} [props.hidden] - Indicates if the input field is hidden.
|
||||
* @param {React.Ref} [ref] - Ref forwarded to the underlying `TextField` component. Allows for direct interactions such as focusing.
|
||||
* @param {string} props.value - Current value of the input field.
|
||||
* @param {function} props.onChange - Callback function triggered on input value change.
|
||||
* @param {function} [props.onBlur] - Callback function triggered when input loses focus.
|
||||
* @param {function} [props.onInput] - Callback function triggered on input event.
|
||||
* @param {string} [props.error] - Error message to display below the input field.
|
||||
* @param {boolean} [props.disabled=false] - Disables the input field if true.
|
||||
* @param {boolean} [props.hidden=false] - Hides the entire input field if true.
|
||||
* @param {string} [props.className] - Additional CSS class names for the input container.
|
||||
* @param {boolean} [props.hideErrorText=false] - Hides the error message if true.
|
||||
* @param {React.Ref} [ref] - Ref forwarded to the underlying TextField component.
|
||||
*
|
||||
* @returns {React.ReactElement} Rendered input field component
|
||||
*/
|
||||
|
||||
const Field = forwardRef(
|
||||
@@ -47,6 +55,7 @@ const Field = forwardRef(
|
||||
disabled,
|
||||
hidden,
|
||||
className,
|
||||
hideErrorText = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -57,7 +66,7 @@ const Field = forwardRef(
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
className={`${className ?? `field field-${type}`}`}
|
||||
className={`field field-${type} ${className}`}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
@@ -190,7 +199,7 @@ const Field = forwardRef(
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
hidden={className ? true : false}
|
||||
hidden={hideErrorText}
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
@@ -226,6 +235,7 @@ Field.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
hideErrorText: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Field;
|
||||
|
||||
@@ -548,23 +548,6 @@ const Login = () => {
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">Don't have an account? —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Sign Up
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
page: 0,
|
||||
rowsPerPage: 14,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
|
||||
@@ -4,9 +4,8 @@ import Checkbox from "../../../../Components/Inputs/Checkbox";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
|
||||
/**
|
||||
* `CustomThreshold` is a functional React component that displays a
|
||||
* `CustomThreshold` is a functional React component that displays a
|
||||
* group of CheckBox with a label and its correspondant threshold input field.
|
||||
*
|
||||
* @param {{ checkboxId: any; checkboxLabel: any; onCheckboxChange: any; fieldId: any; onFieldChange: any; onFieldBlur: any; alertUnit: any; infrastructureMonitor: any; errors: any; }} param0
|
||||
@@ -20,64 +19,65 @@ import PropTypes from "prop-types";
|
||||
* @param {object} param0.infrastructureMonitor the form object of the create infrastrcuture monitor page
|
||||
* @param {object} param0.errors the object that holds all the errors of the form page
|
||||
* @returns A compound React component that renders the custom threshold alert section
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export const CustomThreshold = ({
|
||||
checkboxId,
|
||||
checkboxLabel,
|
||||
onCheckboxChange,
|
||||
fieldId,
|
||||
onFieldChange,
|
||||
onFieldBlur,
|
||||
alertUnit,
|
||||
infrastructureMonitor,
|
||||
errors
|
||||
}) =>
|
||||
{
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
width: "50%",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
label={checkboxLabel}
|
||||
isChecked={infrastructureMonitor[checkboxId]}
|
||||
onChange={onCheckboxChange}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
type="number"
|
||||
className="field-infrastructure-alert"
|
||||
id={fieldId}
|
||||
value={infrastructureMonitor[fieldId]}
|
||||
onBlur={onFieldBlur}
|
||||
onChange={onFieldChange}
|
||||
error={errors[fieldId]}
|
||||
disabled={!infrastructureMonitor[checkboxId]}
|
||||
></Field>
|
||||
<Typography
|
||||
component="p"
|
||||
m={theme.spacing(3)}
|
||||
>
|
||||
{alertUnit}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
checkboxId,
|
||||
checkboxLabel,
|
||||
onCheckboxChange,
|
||||
fieldId,
|
||||
onFieldChange,
|
||||
onFieldBlur,
|
||||
alertUnit,
|
||||
infrastructureMonitor,
|
||||
errors,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
width: "50%",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
label={checkboxLabel}
|
||||
isChecked={infrastructureMonitor[checkboxId]}
|
||||
onChange={onCheckboxChange}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
type="number"
|
||||
className="field-infrastructure-alert"
|
||||
id={fieldId}
|
||||
value={infrastructureMonitor[fieldId]}
|
||||
onBlur={onFieldBlur}
|
||||
onChange={onFieldChange}
|
||||
error={errors[fieldId]}
|
||||
disabled={!infrastructureMonitor[checkboxId]}
|
||||
hideErrorText={true}
|
||||
></Field>
|
||||
<Typography
|
||||
component="p"
|
||||
m={theme.spacing(3)}
|
||||
>
|
||||
{alertUnit}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
CustomThreshold.propTypes = {
|
||||
checkboxId: PropTypes.string.isRequired,
|
||||
@@ -89,4 +89,4 @@ CustomThreshold.propTypes = {
|
||||
alertUnit: PropTypes.string.isRequired,
|
||||
infrastructureMonitor: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -132,7 +132,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
Object.keys(form)
|
||||
.filter((k) => k.startsWith(THRESHOLD_FIELD_PREFIX))
|
||||
.map((k) => {
|
||||
if (form[k]) thresholds[k] = form[k];
|
||||
if (form[k]) thresholds[k] = form[k] / 100;
|
||||
delete form[k];
|
||||
delete form[k.substring(THRESHOLD_FIELD_PREFIX.length)];
|
||||
});
|
||||
@@ -158,6 +158,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
: infrastructureMonitor.name,
|
||||
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
|
||||
delete form.notifications;
|
||||
if (hasValidationErrors(form, infrastructureMonitorValidation, setErrors)) {
|
||||
return;
|
||||
@@ -194,25 +195,6 @@ const CreateInfrastructureMonitor = () => {
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
];
|
||||
|
||||
const NOTIFY_MULTIPLE_EMAIL_LABEL = (
|
||||
<Box>
|
||||
<Typography mb={theme.spacing(4)}>
|
||||
Also notify via email to multiple addresses (coming soon)
|
||||
</Typography>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
<Typography mt={theme.spacing(4)}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="create-infrastructure-monitor">
|
||||
<Breadcrumbs
|
||||
@@ -253,7 +235,8 @@ const CreateInfrastructureMonitor = () => {
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
Here you can select the URL of the host, together with the type of monitor.
|
||||
Here you can select the URL of the host, together with the friendly name and
|
||||
authorization secret to connect to the server agent.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
@@ -307,15 +290,6 @@ const CreateInfrastructureMonitor = () => {
|
||||
onChange={(e) => handleChange(e)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label={NOTIFY_MULTIPLE_EMAIL_LABEL}
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
onBlur={handleBlur}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
|
||||
@@ -323,8 +297,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
<Box>
|
||||
<Typography component="h2">Customize alerts</Typography>
|
||||
<Typography component="p">
|
||||
Send a notification to user(s) When the thresholds exceed a certain number
|
||||
or percentage.
|
||||
Send a notification to user(s) when thresholds exceed a specified percentage.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
|
||||
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
|
||||
import { Box, Typography, Stack } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import { useSelector } from "react-redux";
|
||||
const Empty = ({ styles }) => {
|
||||
const theme = useTheme();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
return (
|
||||
<Box sx={{ ...styles, marginTop: theme.spacing(24) }}>
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(8)}
|
||||
alignItems="center"
|
||||
>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
|
||||
<Typography variant="h2">Your infrastructure dashboard will show here</Typography>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Hang tight! When we receive data, we'll show it here. Please check back in a few
|
||||
minutes.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Empty.propTypes = {
|
||||
styles: PropTypes.object,
|
||||
mode: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Empty;
|
||||
@@ -5,10 +5,14 @@ import { Stack, Box, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CustomGauge from "../../../Components/Charts/CustomGauge";
|
||||
import AreaChart from "../../../Components/Charts/AreaChart";
|
||||
import { useSelector } from "react-redux";
|
||||
import { networkService } from "../../../main";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import useUtils from "../../Monitors/utils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Empty from "./empty";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
|
||||
import axios from "axios";
|
||||
import {
|
||||
TzTick,
|
||||
PercentTick,
|
||||
@@ -25,6 +29,7 @@ const TYPOGRAPHY_PADDING = 8;
|
||||
* @returns {number} Converted value in gigabytes
|
||||
*/
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === undefined || bytes === null) return "0 GB";
|
||||
if (typeof bytes !== "number") return "0 GB";
|
||||
if (bytes === 0) return "0 GB";
|
||||
|
||||
@@ -38,6 +43,22 @@ const formatBytes = (bytes) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a decimal value to a percentage
|
||||
*
|
||||
* @function decimalToPercentage
|
||||
* @param {number} value - Decimal value to convert
|
||||
* @returns {number} Percentage representation
|
||||
*
|
||||
* @example
|
||||
* decimalToPercentage(0.75) // Returns 75
|
||||
* decimalToPercentage(null) // Returns 0
|
||||
*/
|
||||
const decimalToPercentage = (value) => {
|
||||
if (value === null || value === undefined) return 0;
|
||||
return value * 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a base box with consistent styling
|
||||
* @param {Object} props - Component properties
|
||||
@@ -105,6 +126,7 @@ StatBox.propTypes = {
|
||||
*/
|
||||
const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BaseBox>
|
||||
<Stack
|
||||
@@ -160,6 +182,7 @@ GaugeBox.propTypes = {
|
||||
* @returns {React.ReactElement} Infrastructure details page component
|
||||
*/
|
||||
const InfrastructureDetails = () => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { monitorId } = useParams();
|
||||
const navList = [
|
||||
@@ -167,6 +190,8 @@ const InfrastructureDetails = () => {
|
||||
{ name: "details", path: `/infrastructure/${monitorId}` },
|
||||
];
|
||||
const [monitor, setMonitor] = useState(null);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [dateRange, setDateRange] = useState("all");
|
||||
const { statusColor, determineState } = useUtils();
|
||||
// These calculations are needed because ResponsiveContainer
|
||||
// doesn't take padding of parent/siblings into account
|
||||
@@ -183,25 +208,31 @@ const InfrastructureDetails = () => {
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await axios.get("http://localhost:5000/api/v1/dummy-data", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
const response = await networkService.getStatsByMonitorId({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
sortOrder: "asc",
|
||||
limit: null,
|
||||
dateRange: dateRange,
|
||||
numToDisplay: 50,
|
||||
normalize: false,
|
||||
});
|
||||
|
||||
setMonitor(response.data.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
navigate("/not-found", { replace: true });
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [authToken, monitorId, dateRange]);
|
||||
|
||||
|
||||
const statBoxConfigs = [
|
||||
{
|
||||
id: 0,
|
||||
heading: "CPU",
|
||||
subHeading: `${monitor?.checks[0]?.cpu?.physical_core} cores`,
|
||||
subHeading: `${monitor?.checks[0]?.cpu?.physical_core ?? 0} cores`,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
@@ -224,7 +255,7 @@ const InfrastructureDetails = () => {
|
||||
const gaugeBoxConfigs = [
|
||||
{
|
||||
type: "memory",
|
||||
value: monitor?.checks[0]?.memory?.usage_percent * 100,
|
||||
value: decimalToPercentage(monitor?.checks[0]?.memory?.usage_percent),
|
||||
heading: "Memory Usage",
|
||||
metricOne: "Used",
|
||||
valueOne: formatBytes(monitor?.checks[0]?.memory?.used_bytes),
|
||||
@@ -233,17 +264,17 @@ const InfrastructureDetails = () => {
|
||||
},
|
||||
{
|
||||
type: "cpu",
|
||||
value: monitor?.checks[0]?.cpu?.usage_percent * 100,
|
||||
value: decimalToPercentage(monitor?.checks[0]?.cpu?.usage_percent),
|
||||
heading: "CPU Usage",
|
||||
metricOne: "Cores",
|
||||
valueOne: monitor?.checks[0]?.cpu?.physical_core,
|
||||
valueOne: monitor?.checks[0]?.cpu?.physical_core ?? 0,
|
||||
metricTwo: "Frequency",
|
||||
valueTwo: `${(monitor?.checks[0]?.cpu?.frequency / 1000).toFixed(2)} Ghz`,
|
||||
valueTwo: `${(monitor?.checks[0]?.cpu?.frequency ?? 0 / 1000).toFixed(2)} Ghz`,
|
||||
},
|
||||
...(monitor?.checks[0]?.disk ?? []).map((disk, idx) => ({
|
||||
...(monitor?.checks?.[0]?.disk ?? []).map((disk, idx) => ({
|
||||
type: "disk",
|
||||
diskIndex: idx,
|
||||
value: disk.usage_percent * 100,
|
||||
value: decimalToPercentage(disk.usage_percent),
|
||||
heading: `Disk${idx} usage`,
|
||||
metricOne: "Used",
|
||||
valueOne: formatBytes(disk.total_bytes - disk.free_bytes),
|
||||
@@ -278,9 +309,9 @@ const InfrastructureDetails = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
monitor && (
|
||||
<Box>
|
||||
<Breadcrumbs list={navList} />
|
||||
<Box>
|
||||
<Breadcrumbs list={navList} />
|
||||
{monitor?.checks?.length > 0 ? (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(10)}
|
||||
@@ -386,8 +417,18 @@ const InfrastructureDetails = () => {
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<Empty
|
||||
styles={{
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
p: theme.spacing(30),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,6 +44,14 @@ class Logger {
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
if (logLevel === "debug") {
|
||||
this.error = console.error.bind(console);
|
||||
this.warn = console.warn.bind(console);
|
||||
this.info = console.info.bind(console);
|
||||
this.log = console.log.bind(console);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -16,6 +16,28 @@ const NotificationSchema = mongoose.Schema(
|
||||
phone: {
|
||||
type: String,
|
||||
},
|
||||
alertThreshold: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
cpuAlertThreshold: {
|
||||
type: Number,
|
||||
default: function () {
|
||||
return this.alertThreshold;
|
||||
},
|
||||
},
|
||||
memoryAlertThreshold: {
|
||||
type: Number,
|
||||
default: function () {
|
||||
return this.alertThreshold;
|
||||
},
|
||||
},
|
||||
diskAlertThreshold: {
|
||||
type: Number,
|
||||
default: function () {
|
||||
return this.alertThreshold;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -198,7 +198,7 @@ const getIncidents = (checks) => {
|
||||
|
||||
/**
|
||||
* Get date range parameters
|
||||
* @param {string} dateRange - 'day' | 'week' | 'month'
|
||||
* @param {string} dateRange - 'day' | 'week' | 'month' | 'all'
|
||||
* @returns {Object} Start and end dates
|
||||
*/
|
||||
const getDateRange = (dateRange) => {
|
||||
@@ -206,6 +206,7 @@ const getDateRange = (dateRange) => {
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
all: new Date(0),
|
||||
};
|
||||
return {
|
||||
start: startDates[dateRange],
|
||||
|
||||
Generated
+4
-4
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.28.0",
|
||||
"bullmq": "5.29.1",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -1217,9 +1217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.28.0",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.28.0.tgz",
|
||||
"integrity": "sha512-HyYr6bfbBuzfyUvRkP6Ux1Zrv2eZcM4zXMl90Q7oneORp3jKnXYyqjCewJGAoo4PqKyE4TWCcdUnFkdxAzysng==",
|
||||
"version": "5.29.1",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.29.1.tgz",
|
||||
"integrity": "sha512-TZWiwRlPnpaN+Qwh4D8IQf2cYLpkiDX1LbaaWEabc6y37ojIttWOSynxDewpVHyW233LssSIC4+aLMSvAjtpmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.6.0",
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.28.0",
|
||||
"bullmq": "5.29.1",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
@@ -65,7 +65,7 @@ class EmailService {
|
||||
serverIsDownTemplate: this.loadTemplate("serverIsDown"),
|
||||
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
|
||||
passwordResetTemplate: this.loadTemplate("passwordReset"),
|
||||
thresholdViolatedTemplate: this.loadTemplate("thresholdViolated"),
|
||||
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -165,17 +165,12 @@ class JobQueue {
|
||||
// Handle status change
|
||||
const { monitor, statusChanged, prevStatus } =
|
||||
await this.statusService.updateStatus(networkResponse);
|
||||
|
||||
//If status hasn't changed, we're done
|
||||
if (statusChanged === false) return;
|
||||
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (prevStatus === undefined) return;
|
||||
|
||||
// Handle notifications
|
||||
this.notificationService.handleNotifications({
|
||||
...networkResponse,
|
||||
monitor,
|
||||
prevStatus,
|
||||
statusChanged,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
|
||||
@@ -14,15 +14,34 @@ class NotificationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email notification based on the network response.
|
||||
* Sends an email notification for hardware infrastructure alerts
|
||||
*
|
||||
* @param {Object} networkResponse - The response from the network monitor.
|
||||
* @param {Object} networkResponse.monitor - The monitor object containing details about the monitored service.
|
||||
* @param {string} networkResponse.monitor.name - The name of the monitor.
|
||||
* @param {string} networkResponse.monitor.url - The URL of the monitor.
|
||||
* @param {boolean} networkResponse.status - The current status of the monitor (true for up, false for down).
|
||||
* @param {boolean} networkResponse.prevStatus - The previous status of the monitor (true for up, false for down).
|
||||
* @param {string} address - The email address to send the notification to.
|
||||
* @async
|
||||
* @function sendHardwareEmail
|
||||
* @param {Object} networkResponse - Response object containing monitor information
|
||||
* @param {string} address - Email address to send the notification to
|
||||
* @param {Array} [alerts=[]] - List of hardware alerts to include in the email
|
||||
* @returns {Promise<boolean>} - Indicates whether email was sent successfully
|
||||
* @throws {Error}
|
||||
*/
|
||||
async sendHardwareEmail(networkResponse, address, alerts = []) {
|
||||
if (alerts.length === 0) return false;
|
||||
const { monitor, status, prevStatus } = networkResponse;
|
||||
const template = "hardwareIncidentTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url, alerts };
|
||||
const subject = `Monitor ${monitor.name} infrastructure alerts`;
|
||||
this.emailService.buildAndSendEmail(template, context, address, subject);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email notification about monitor status change
|
||||
*
|
||||
* @async
|
||||
* @function sendEmail
|
||||
* @param {Object} networkResponse - Response object containing monitor status information
|
||||
* @param {string} address - Email address to send the notification to
|
||||
* @returns {Promise<boolean>} - Indicates email was sent successfully
|
||||
*/
|
||||
async sendEmail(networkResponse, address) {
|
||||
const { monitor, status, prevStatus } = networkResponse;
|
||||
@@ -30,25 +49,133 @@ class NotificationService {
|
||||
const context = { monitor: monitor.name, url: monitor.url };
|
||||
const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`;
|
||||
this.emailService.buildAndSendEmail(template, context, address, subject);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles notifications based on the network response.
|
||||
*
|
||||
* @param {Object} networkResponse - The response from the network monitor.
|
||||
* @param {string} networkResponse.monitorId - The ID of the monitor.
|
||||
*/
|
||||
async handleNotifications(networkResponse) {
|
||||
async handleStatusNotifications(networkResponse) {
|
||||
try {
|
||||
//If status hasn't changed, we're done
|
||||
if (networkResponse.statusChanged === false) return false;
|
||||
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (networkResponse.prevStatus === undefined) return false;
|
||||
const notifications = await this.db.getNotificationsByMonitorId(
|
||||
networkResponse.monitorId
|
||||
);
|
||||
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
this.sendEmail(networkResponse, notification.address);
|
||||
}
|
||||
// Handle other types of notifications here
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleNotifications",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handles status change notifications for a monitor
|
||||
*
|
||||
* @async
|
||||
* @function handleStatusNotifications
|
||||
* @param {Object} networkResponse - Response object containing monitor status information
|
||||
* @returns {Promise<boolean>} - Indicates whether notifications were processed
|
||||
* @throws {Error}
|
||||
*/
|
||||
async handleHardwareNotifications(networkResponse) {
|
||||
const thresholds = networkResponse?.monitor?.thresholds;
|
||||
if (thresholds === undefined) return false; // No thresholds set, we're done
|
||||
|
||||
// Get thresholds from monitor
|
||||
const {
|
||||
usage_cpu: cpuThreshold = -1,
|
||||
usage_memory: memoryThreshold = -1,
|
||||
usage_disk: diskThreshold = -1,
|
||||
} = thresholds;
|
||||
|
||||
// Get metrics from response
|
||||
const metrics = networkResponse?.payload?.data ?? null;
|
||||
if (metrics === null) return false;
|
||||
|
||||
const {
|
||||
cpu: { usage_percent: cpuUsage = -1 } = {},
|
||||
memory: { usage_percent: memoryUsage = -1 } = {},
|
||||
disk = [],
|
||||
} = metrics;
|
||||
|
||||
const alerts = {
|
||||
cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
|
||||
memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
|
||||
disk: disk.some((d) => diskThreshold !== -1 && d.usage_percent > diskThreshold)
|
||||
? true
|
||||
: false,
|
||||
};
|
||||
|
||||
const notifications = await this.db.getNotificationsByMonitorId(
|
||||
networkResponse.monitorId
|
||||
);
|
||||
for (const notification of notifications) {
|
||||
const alertsToSend = [];
|
||||
const alertTypes = ["cpu", "memory", "disk"];
|
||||
|
||||
for (const type of alertTypes) {
|
||||
// Iterate over each alert type to see if any need to be decremented
|
||||
if (alerts[type] === true) {
|
||||
notification[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered
|
||||
|
||||
if (notification[`${type}AlertThreshold`] <= 0) {
|
||||
// If threshold drops below 0, reset and send notification
|
||||
notification[`${type}AlertThreshold`] = notification.alertThreshold;
|
||||
|
||||
const formatAlert = {
|
||||
cpu: () =>
|
||||
`Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
|
||||
memory: () =>
|
||||
`Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
|
||||
disk: () =>
|
||||
`Your current disk usage: ${disk
|
||||
.map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`)
|
||||
.join(
|
||||
", "
|
||||
)} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
|
||||
};
|
||||
alertsToSend.push(formatAlert[type]());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await notification.save();
|
||||
|
||||
if (alertsToSend.length === 0) continue; // No alerts to send, we're done
|
||||
|
||||
if (notification.type === "email") {
|
||||
this.sendHardwareEmail(networkResponse, notification.address, alertsToSend);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles notifications for different monitor types
|
||||
*
|
||||
* @async
|
||||
* @function handleNotifications
|
||||
* @param {Object} networkResponse - Response object containing monitor information
|
||||
* @returns {Promise<boolean>} - Indicates whether notifications were processed successfully
|
||||
*/
|
||||
async handleNotifications(networkResponse) {
|
||||
try {
|
||||
if (networkResponse.monitor.type === "hardware") {
|
||||
this.handleHardwareNotifications(networkResponse);
|
||||
}
|
||||
this.handleStatusNotifications(networkResponse);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
|
||||
@@ -11,6 +11,11 @@ class StatusService {
|
||||
this.SERVICE_NAME = "StatusService";
|
||||
}
|
||||
|
||||
getStatusString = (status) => {
|
||||
if (status === true) return "up";
|
||||
if (status === false) return "down";
|
||||
return "unknown";
|
||||
};
|
||||
/**
|
||||
* Updates the status of a monitor based on the network response.
|
||||
*
|
||||
@@ -29,12 +34,13 @@ class StatusService {
|
||||
const { monitorId, status } = networkResponse;
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
// No change in monitor status, return early
|
||||
if (monitor.status === status) return { statusChanged: false };
|
||||
if (monitor.status === status)
|
||||
return { monitor, statusChanged: false, prevStatus: monitor.status };
|
||||
// Monitor status changed, save prev status and update monitor
|
||||
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
message: `${monitor.name} went from ${monitor.status === true ? "up" : "down"} to ${status === true ? "up" : "down"}`,
|
||||
message: `${monitor.name} went from ${this.getStatusString(monitor.status)} to ${this.getStatusString(status)}`,
|
||||
prevStatus: monitor.status,
|
||||
newStatus: status,
|
||||
});
|
||||
@@ -103,7 +109,7 @@ class StatusService {
|
||||
|
||||
if (type === "hardware") {
|
||||
const { cpu, memory, disk, host } = payload?.data ?? {};
|
||||
const { errors } = payload;
|
||||
const { errors } = payload?.errors ?? [];
|
||||
check.cpu = cpu ?? {};
|
||||
check.memory = memory ?? {};
|
||||
check.disk = disk ?? {};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text align="left" font-size="10px">
|
||||
Message from BlueWave Infrastructure Monitoring
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="45%" padding-top="20px">
|
||||
<mj-text align="center" font-weight="500" padding="0px" font-size="18px" color="red">
|
||||
Infrastructure Alerts
|
||||
</mj-text>
|
||||
<mj-divider border-width="2px" border-color="#616161"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column width="100%">
|
||||
<mj-text>
|
||||
<p>Hello {{name}}!</p>
|
||||
<p>{{monitor}} at {{url}} has the following infrastructure alerts:</p>
|
||||
{{#each alerts}}
|
||||
<p>• {{this}}</p>
|
||||
{{/each}}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="100%">
|
||||
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
|
||||
<mj-button background-color="#1570EF"> View Infrastructure Details </mj-button>
|
||||
<mj-text font-size="12px">
|
||||
<p>This email was sent by BlueWave Infrastructure Monitoring.</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -1,40 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text align="left" font-size="10px">
|
||||
Message from BlueWave Uptime Service
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="45%" padding-top="20px">
|
||||
<mj-text font-weight="500" padding="0px" font-size="18px">
|
||||
{{message}}
|
||||
</mj-text>
|
||||
<mj-text font-weight="500" padding="0px" font-size="18px">
|
||||
{{#if cpu}}
|
||||
{{cpu}}
|
||||
{{/if}}
|
||||
</mj-text>
|
||||
<mj-text font-weight="500" padding="0px" font-size="18px">
|
||||
{{#if disk}}
|
||||
{{disk}}
|
||||
{{/if}}
|
||||
</mj-text>
|
||||
<mj-text font-weight="500" padding="0px" font-size="18px">
|
||||
{{#if memory}}
|
||||
{{memory}}
|
||||
{{/if}}
|
||||
</mj-text>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -78,11 +78,23 @@ describe("NotificationService", () => {
|
||||
describe("handleNotifications", async () => {
|
||||
it("should handle notifications based on the network response", async () => {
|
||||
notificationService.sendEmail = sinon.stub();
|
||||
notificationService.db.getNotificationsByMonitorId.resolves([
|
||||
{ type: "email", address: "www.google.com" },
|
||||
]);
|
||||
await notificationService.handleNotifications({ monitorId: "123" });
|
||||
expect(notificationService.sendEmail.calledOnce).to.be.true;
|
||||
const res = await notificationService.handleNotifications({
|
||||
monitor: {
|
||||
type: "email",
|
||||
address: "www.google.com",
|
||||
},
|
||||
});
|
||||
expect(res).to.be.true;
|
||||
});
|
||||
it("should handle hardware notifications", async () => {
|
||||
notificationService.sendEmail = sinon.stub();
|
||||
const res = await notificationService.handleNotifications({
|
||||
monitor: {
|
||||
type: "hardware",
|
||||
address: "www.google.com",
|
||||
},
|
||||
});
|
||||
expect(res).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle an error when getting notifications", async () => {
|
||||
@@ -92,4 +104,184 @@ describe("NotificationService", () => {
|
||||
expect(notificationService.logger.warn.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendHardwareEmail", async () => {
|
||||
let networkResponse, address, alerts;
|
||||
beforeEach(() => {
|
||||
networkResponse = {
|
||||
monitor: {
|
||||
name: "Test Monitor",
|
||||
url: "http://test.com",
|
||||
},
|
||||
status: true,
|
||||
prevStatus: false,
|
||||
};
|
||||
address = "test@test.com";
|
||||
alerts = ["test"];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
it("should send an email notification with Hardware Template", async () => {
|
||||
emailService.buildAndSendEmail.resolves(true);
|
||||
const res = await notificationService.sendHardwareEmail(
|
||||
networkResponse,
|
||||
address,
|
||||
alerts
|
||||
);
|
||||
expect(res).to.be.true;
|
||||
});
|
||||
it("should return false if no alerts are provided", async () => {
|
||||
alerts = [];
|
||||
emailService.buildAndSendEmail.resolves(true);
|
||||
const res = await notificationService.sendHardwareEmail(
|
||||
networkResponse,
|
||||
address,
|
||||
alerts
|
||||
);
|
||||
expect(res).to.be.false;
|
||||
});
|
||||
});
|
||||
describe("handleStatusNotifications", async () => {
|
||||
let networkResponse;
|
||||
beforeEach(() => {
|
||||
networkResponse = {
|
||||
monitor: {
|
||||
name: "Test Monitor",
|
||||
url: "http://test.com",
|
||||
},
|
||||
statusChanged: true,
|
||||
status: true,
|
||||
prevStatus: false,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should handle status notifications", async () => {
|
||||
db.getNotificationsByMonitorId.resolves([
|
||||
{ type: "email", address: "test@test.com" },
|
||||
]);
|
||||
const res = await notificationService.handleStatusNotifications(networkResponse);
|
||||
expect(res).to.be.true;
|
||||
});
|
||||
it("should return false if status hasn't changed", async () => {
|
||||
networkResponse.statusChanged = false;
|
||||
const res = await notificationService.handleStatusNotifications(networkResponse);
|
||||
expect(res).to.be.false;
|
||||
});
|
||||
it("should return false if prevStatus is undefined", async () => {
|
||||
networkResponse.prevStatus = undefined;
|
||||
const res = await notificationService.handleStatusNotifications(networkResponse);
|
||||
expect(res).to.be.false;
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const testError = new Error("Test Error");
|
||||
db.getNotificationsByMonitorId.rejects(testError);
|
||||
try {
|
||||
await notificationService.handleStatusNotifications(networkResponse);
|
||||
} catch (error) {
|
||||
expect(error).to.be.an.instanceOf(Error);
|
||||
expect(error.message).to.equal("Test Error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleHardwareNotifications", async () => {
|
||||
let networkResponse;
|
||||
beforeEach(() => {
|
||||
networkResponse = {
|
||||
monitor: {
|
||||
name: "Test Monitor",
|
||||
url: "http://test.com",
|
||||
thresholds: {
|
||||
usage_cpu: 1,
|
||||
usage_memory: 1,
|
||||
usage_disk: 1,
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
data: {
|
||||
cpu: {
|
||||
usage_percent: 0.655,
|
||||
},
|
||||
memory: {
|
||||
usage_percent: 0.783,
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
name: "/dev/sda1",
|
||||
usage_percent: 0.452,
|
||||
},
|
||||
{
|
||||
name: "/dev/sdb1",
|
||||
usage_percent: 0.627,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("it should return false if no thresholds are set", () => {
|
||||
it("should return false if no thresholds are set", async () => {
|
||||
networkResponse.monitor.thresholds = undefined;
|
||||
const res =
|
||||
await notificationService.handleHardwareNotifications(networkResponse);
|
||||
expect(res).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false if metrics are null", async () => {
|
||||
networkResponse.payload.data = null;
|
||||
const res =
|
||||
await notificationService.handleHardwareNotifications(networkResponse);
|
||||
expect(res).to.be.false;
|
||||
});
|
||||
|
||||
it("should return true if request is well formed and thresholds > 0", async () => {
|
||||
db.getNotificationsByMonitorId.resolves([
|
||||
{
|
||||
type: "email",
|
||||
address: "test@test.com",
|
||||
alertThreshold: 1,
|
||||
cpuAlertThreshold: 1,
|
||||
memoryAlertThreshold: 1,
|
||||
diskAlertThreshold: 1,
|
||||
save: sinon.stub().resolves(),
|
||||
},
|
||||
]);
|
||||
const res =
|
||||
await notificationService.handleHardwareNotifications(networkResponse);
|
||||
expect(res).to.be.true;
|
||||
});
|
||||
|
||||
it("should return true if thresholds are exceeded", async () => {
|
||||
db.getNotificationsByMonitorId.resolves([
|
||||
{
|
||||
type: "email",
|
||||
address: "test@test.com",
|
||||
alertThreshold: 1,
|
||||
cpuAlertThreshold: 1,
|
||||
memoryAlertThreshold: 1,
|
||||
diskAlertThreshold: 1,
|
||||
save: sinon.stub().resolves(),
|
||||
},
|
||||
]);
|
||||
networkResponse.monitor.thresholds = {
|
||||
usage_cpu: 0.01,
|
||||
usage_memory: 0.01,
|
||||
usage_disk: 0.01,
|
||||
};
|
||||
const res =
|
||||
await notificationService.handleHardwareNotifications(networkResponse);
|
||||
expect(res).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,18 @@ describe("StatusService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatusString", () => {
|
||||
it("should return 'up' if status is true", () => {
|
||||
expect(statusService.getStatusString(true)).to.equal("up");
|
||||
});
|
||||
it("should return 'down' if status is false", () => {
|
||||
expect(statusService.getStatusString(false)).to.equal("down");
|
||||
});
|
||||
it("should return 'unknown' if status is undefined or null", () => {
|
||||
expect(statusService.getStatusString(undefined)).to.equal("unknown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateStatus", async () => {
|
||||
beforeEach(() => {
|
||||
// statusService.insertCheck = sinon.stub().resolves;
|
||||
|
||||
@@ -178,7 +178,7 @@ const getMonitorStatsByIdQueryValidation = joi.object({
|
||||
status: joi.string(),
|
||||
limit: joi.number(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
dateRange: joi.string().valid("day", "week", "month"),
|
||||
dateRange: joi.string().valid("day", "week", "month", "all"),
|
||||
numToDisplay: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user