mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-20 16:49:46 -06:00
chore: merge changes
This commit is contained in:
78
Client/package-lock.json
generated
78
Client/package-lock.json
generated
@@ -15,8 +15,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.16.7",
|
||||
"@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": {
|
||||
@@ -1400,9 +1400,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",
|
||||
@@ -1458,9 +1458,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",
|
||||
@@ -1495,9 +1495,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",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.16.7",
|
||||
"@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",
|
||||
|
||||
@@ -10,6 +10,7 @@ import Register from "./Pages/Auth/Register/Register";
|
||||
import Account from "./Pages/Account";
|
||||
import Monitors from "./Pages/Monitors/Home";
|
||||
import CreateMonitor from "./Pages/Monitors/CreateMonitor";
|
||||
import CreateInfrastructureMonitor from "./Pages/Infrastructure/CreateMonitor";
|
||||
import Incidents from "./Pages/Incidents";
|
||||
import Status from "./Pages/Status";
|
||||
import Integrations from "./Pages/Integrations";
|
||||
@@ -39,6 +40,7 @@ import { getAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
import { Infrastructure } from "./Pages/Infrastructure";
|
||||
import InfrastructureDetails from "./Pages/Infrastructure/Details";
|
||||
function App() {
|
||||
const AdminCheckedRegister = withAdminCheck(Register);
|
||||
const MonitorsWithAdminProp = withAdminProp(Monitors);
|
||||
@@ -48,6 +50,7 @@ function App() {
|
||||
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
|
||||
const SettingsWithAdminProp = withAdminProp(Settings);
|
||||
const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
|
||||
const InfrastructureDetailsWithAdminProp = withAdminProp(InfrastructureDetails);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
@@ -127,6 +130,14 @@ function App() {
|
||||
path="infrastructure"
|
||||
element={<ProtectedRoute Component={Infrastructure} />}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/create"
|
||||
element={<ProtectedRoute Component={CreateInfrastructureMonitor} />}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/:monitorId"
|
||||
element={<ProtectedRoute Component={InfrastructureDetailsWithAdminProp} />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="incidents/:monitorId?"
|
||||
|
||||
157
Client/src/Components/Charts/AreaChart/index.jsx
Normal file
157
Client/src/Components/Charts/AreaChart/index.jsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { createGradient } from "../Utils/gradientUtils";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useId } from "react";
|
||||
/**
|
||||
* CustomAreaChart component for rendering an area chart with optional gradient and custom ticks.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {Array} props.data - The data array for the chart.
|
||||
* @param {string} props.xKey - The key for the x-axis data.
|
||||
* @param {string} props.yKey - The key for the y-axis data.
|
||||
* @param {Object} [props.xTick] - Custom tick component for the x-axis.
|
||||
* @param {Object} [props.yTick] - Custom tick component for the y-axis.
|
||||
* @param {string} [props.strokeColor] - The stroke color for the area.
|
||||
* @param {string} [props.fillColor] - The fill color for the area.
|
||||
* @param {boolean} [props.gradient=false] - Whether to apply a gradient fill.
|
||||
* @param {string} [props.gradientDirection="vertical"] - The direction of the gradient.
|
||||
* @param {string} [props.gradientStartColor] - The start color of the gradient.
|
||||
* @param {string} [props.gradientEndColor] - The end color of the gradient.
|
||||
* @param {Object} [props.customTooltip] - Custom tooltip component.
|
||||
* @returns {JSX.Element} The rendered area chart component.
|
||||
*
|
||||
* @example
|
||||
* // Example usage of CustomAreaChart
|
||||
* import React from 'react';
|
||||
* import CustomAreaChart from './CustomAreaChart';
|
||||
* import { TzTick, PercentTick, InfrastructureTooltip } from './chartUtils';
|
||||
*
|
||||
* const data = [
|
||||
* { createdAt: '2023-01-01T00:00:00Z', cpu: { usage_percent: 0.5 } },
|
||||
* { createdAt: '2023-01-01T01:00:00Z', cpu: { usage_percent: 0.6 } },
|
||||
* // more data points...
|
||||
* ];
|
||||
*
|
||||
* const MyChartComponent = () => {
|
||||
* return (
|
||||
* <CustomAreaChart
|
||||
* data={data}
|
||||
* xKey="createdAt"
|
||||
* yKey="cpu.usage_percent"
|
||||
* xTick={<TzTick />}
|
||||
* yTick={<PercentTick />}
|
||||
* strokeColor="#8884d8"
|
||||
* fillColor="#8884d8"
|
||||
* gradient={true}
|
||||
* gradientStartColor="#8884d8"
|
||||
* gradientEndColor="#82ca9d"
|
||||
* customTooltip={({ active, payload, label }) => (
|
||||
* <InfrastructureTooltip
|
||||
* label={label?.toString() ?? ""}
|
||||
* yKey="cpu.usage_percent"
|
||||
* yLabel="CPU Usage"
|
||||
* active={active}
|
||||
* payload={payload}
|
||||
* />
|
||||
* )}
|
||||
* />
|
||||
* );
|
||||
* };
|
||||
*
|
||||
* export default MyChartComponent;
|
||||
*/
|
||||
const CustomAreaChart = ({
|
||||
data,
|
||||
dataKey,
|
||||
xKey,
|
||||
yKey,
|
||||
xTick,
|
||||
yTick,
|
||||
strokeColor,
|
||||
fillColor,
|
||||
gradient = false,
|
||||
gradientDirection = "vertical",
|
||||
gradientStartColor,
|
||||
gradientEndColor,
|
||||
customTooltip,
|
||||
height = "100%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const uniqueId = useId();
|
||||
const gradientId = `gradient-${uniqueId}`;
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={height}
|
||||
// FE team HELP! Why does this overflow if set to 100%?
|
||||
>
|
||||
<AreaChart data={data}>
|
||||
<XAxis
|
||||
dataKey={xKey}
|
||||
{...(xTick && { tick: xTick })}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey={yKey}
|
||||
{...(yTick && { tick: yTick })}
|
||||
/>
|
||||
{gradient === true &&
|
||||
createGradient({
|
||||
id: gradientId,
|
||||
startColor: gradientStartColor,
|
||||
endColor: gradientEndColor,
|
||||
direction: gradientDirection,
|
||||
})}
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.border.light}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
stroke={strokeColor}
|
||||
fill={gradient === true ? `url(#${gradientId})` : fillColor}
|
||||
/>
|
||||
{customTooltip ? (
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={customTooltip}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip />
|
||||
)}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
CustomAreaChart.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
dataKey: PropTypes.string.isRequired,
|
||||
xTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
|
||||
yTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
|
||||
xKey: PropTypes.string.isRequired,
|
||||
yKey: PropTypes.string.isRequired,
|
||||
fillColor: PropTypes.string,
|
||||
strokeColor: PropTypes.string,
|
||||
gradient: PropTypes.bool,
|
||||
gradientDirection: PropTypes.string,
|
||||
gradientStartColor: PropTypes.string,
|
||||
gradientEndColor: PropTypes.string,
|
||||
customTooltip: PropTypes.func,
|
||||
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default CustomAreaChart;
|
||||
14
Client/src/Components/Charts/CustomGauge/index.css
Normal file
14
Client/src/Components/Charts/CustomGauge/index.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.radial-chart {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.radial-chart-base {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.radial-chart-progress {
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: center;
|
||||
transition: stroke-dashoffset 1.5s ease-in-out;
|
||||
}
|
||||
113
Client/src/Components/Charts/CustomGauge/index.jsx
Normal file
113
Client/src/Components/Charts/CustomGauge/index.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* A Performant SVG based circular gauge
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component properties
|
||||
* @param {number} [props.progress=0] - Progress percentage (0-100)
|
||||
* @param {number} [props.radius=60] - Radius of the gauge circle
|
||||
* @param {string} [props.color="#000000"] - Color of the progress stroke
|
||||
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
|
||||
*
|
||||
* @example
|
||||
* <CustomGauge
|
||||
* progress={75}
|
||||
* radius={50}
|
||||
* color="#00ff00"
|
||||
* strokeWidth={10}
|
||||
* />
|
||||
*
|
||||
* @returns {React.ReactElement} Rendered CustomGauge component
|
||||
*/
|
||||
const CustomGauge = ({
|
||||
progress = 0,
|
||||
radius = 60,
|
||||
color = "#000000",
|
||||
strokeWidth = 15,
|
||||
}) => {
|
||||
// Calculate the length of the stroke for the circle
|
||||
const { circumference, totalSize, strokeLength } = useMemo(
|
||||
() => ({
|
||||
circumference: 2 * Math.PI * radius,
|
||||
totalSize: radius * 2 + strokeWidth * 2,
|
||||
strokeLength: (progress / 100) * (2 * Math.PI * radius),
|
||||
}),
|
||||
[radius, strokeWidth, progress]
|
||||
);
|
||||
const [offset, setOffset] = useState(circumference);
|
||||
const theme = useTheme();
|
||||
|
||||
// Handle initial animation
|
||||
useEffect(() => {
|
||||
setOffset(circumference);
|
||||
const timer = setTimeout(() => {
|
||||
setOffset(circumference - strokeLength);
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [progress, circumference, strokeLength]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="radial-chart"
|
||||
width={radius}
|
||||
height={radius}
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${totalSize} ${totalSize}`}
|
||||
width={radius}
|
||||
height={radius}
|
||||
>
|
||||
<circle
|
||||
className="radial-chart-base"
|
||||
stroke={theme.palette.background.fill}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
cx={totalSize / 2} // Center the circle
|
||||
cy={totalSize / 2} // Center the circle
|
||||
r={radius}
|
||||
/>
|
||||
<circle
|
||||
className="radial-chart-progress"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
cx={totalSize / 2}
|
||||
cy={totalSize / 2}
|
||||
r={radius}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<Typography
|
||||
className="radial-chart-text"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
...theme.typography.body2,
|
||||
fill: theme.typography.body2.color,
|
||||
}}
|
||||
>
|
||||
{`${progress.toFixed(2)}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomGauge;
|
||||
|
||||
CustomGauge.propTypes = {
|
||||
progress: PropTypes.number,
|
||||
radius: PropTypes.number,
|
||||
color: PropTypes.string,
|
||||
strokeWidth: PropTypes.number,
|
||||
};
|
||||
190
Client/src/Components/Charts/Utils/chartUtils.jsx
Normal file
190
Client/src/Components/Charts/Utils/chartUtils.jsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { Text } from "recharts";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
|
||||
/**
|
||||
* Custom tick component for rendering time with timezone.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {number} props.x - The x-coordinate for the tick.
|
||||
* @param {number} props.y - The y-coordinate for the tick.
|
||||
* @param {Object} props.payload - The payload object containing tick data.
|
||||
* @param {number} props.index - The index of the tick.
|
||||
* @returns {JSX.Element} The rendered tick component.
|
||||
*/
|
||||
export const TzTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 10}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.tertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
TzTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tick component for rendering percentage values.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {number} props.x - The x-coordinate for the tick.
|
||||
* @param {number} props.y - The y-coordinate for the tick.
|
||||
* @param {Object} props.payload - The payload object containing tick data.
|
||||
* @param {number} props.index - The index of the tick.
|
||||
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
|
||||
*/
|
||||
export const PercentTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
if (index === 0) return null;
|
||||
return (
|
||||
<Text
|
||||
x={x - 20}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.tertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{`${payload?.value * 100}%`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
PercentTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a decimal value to a formatted percentage string.
|
||||
*
|
||||
* @param {number} value - The decimal value to convert (e.g., 0.75)
|
||||
* @returns {string} Formatted percentage string (e.g., "75.00%") or original input if not a number
|
||||
*
|
||||
* @example
|
||||
* getFormattedPercentage(0.7543) // Returns "75.43%"
|
||||
* getFormattedPercentage(1) // Returns "100.00%"
|
||||
* getFormattedPercentage("test") // Returns "test"
|
||||
*/
|
||||
const getFormattedPercentage = (value) => {
|
||||
if (typeof value !== "number") return value;
|
||||
return `${(value * 100).toFixed(2)}.%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tooltip component for displaying infrastructure data.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {boolean} props.active - Indicates if the tooltip is active.
|
||||
* @param {Array} props.payload - The payload array containing tooltip data.
|
||||
* @param {string} props.label - The label for the tooltip.
|
||||
* @param {string} props.yKey - The key for the y-axis data.
|
||||
* @param {string} props.yLabel - The label for the y-axis data.
|
||||
* @param {string} props.dotColor - The color of the dot in the tooltip.
|
||||
* @returns {JSX.Element|null} The rendered tooltip component or null if inactive.
|
||||
*/
|
||||
export const InfrastructureTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
yKey,
|
||||
yIdx = -1,
|
||||
yLabel,
|
||||
dotColor,
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
if (active && payload && payload.length) {
|
||||
const [hardwareType, metric] = yKey.split(".");
|
||||
return (
|
||||
<Box
|
||||
className="area-tooltip"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={dotColor}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(3)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{yIdx >= 0
|
||||
? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}`
|
||||
: `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][metric])}`}
|
||||
</Typography>
|
||||
<Typography component="span"></Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{/* Display original value */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
InfrastructureTooltip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.array,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Date),
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
yKey: PropTypes.string,
|
||||
yIdx: PropTypes.number,
|
||||
yLabel: PropTypes.string,
|
||||
dotColor: PropTypes.string,
|
||||
};
|
||||
47
Client/src/Components/Charts/Utils/gradientUtils.jsx
Normal file
47
Client/src/Components/Charts/Utils/gradientUtils.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Creates an SVG gradient definition for use in charts
|
||||
* @param {Object} params - The gradient parameters
|
||||
* @param {string} [params.id="colorUv"] - Unique identifier for the gradient
|
||||
* @param {string} params.startColor - Starting color of the gradient (hex, rgb, or color name)
|
||||
* @param {string} params.endColor - Ending color of the gradient (hex, rgb, or color name)
|
||||
* @param {number} [params.startOpacity=0.8] - Starting opacity (0-1)
|
||||
* @param {number} [params.endOpacity=0] - Ending opacity (0-1)
|
||||
* @param {('vertical'|'horizontal')} [params.direction="vertical"] - Direction of the gradient
|
||||
* @returns {JSX.Element} SVG gradient definition element
|
||||
* @example
|
||||
* createCustomGradient({
|
||||
* startColor: "#1976D2",
|
||||
* endColor: "#42A5F5",
|
||||
* direction: "horizontal"
|
||||
* })
|
||||
*/
|
||||
|
||||
export const createGradient = ({
|
||||
id,
|
||||
startColor,
|
||||
endColor,
|
||||
startOpacity = 0.8,
|
||||
endOpacity = 0,
|
||||
direction = "vertical", // or "horizontal"
|
||||
}) => (
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={id}
|
||||
x1={direction === "vertical" ? "0" : "0"}
|
||||
y1={direction === "vertical" ? "0" : "0"}
|
||||
x2={direction === "vertical" ? "0" : "1"}
|
||||
y2={direction === "vertical" ? "1" : "0"}
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={startColor}
|
||||
stopOpacity={startOpacity}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={endColor}
|
||||
stopOpacity={endOpacity}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
);
|
||||
@@ -37,9 +37,9 @@ const Checkbox = ({
|
||||
onChange,
|
||||
isDisabled,
|
||||
}) => {
|
||||
/* TODO move sizes to theme */
|
||||
const sizes = { small: "14px", medium: "16px", large: "18px" };
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="checkbox-wrapper"
|
||||
@@ -54,9 +54,10 @@ const Checkbox = ({
|
||||
"aria-label": "controlled checkbox",
|
||||
id: id,
|
||||
}}
|
||||
sx={{
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"& svg": { width: sizes[size], height: sizes[size] },
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -65,7 +66,6 @@ const Checkbox = ({
|
||||
sx={{
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
p: theme.spacing(2.5),
|
||||
m: theme.spacing(-2.5),
|
||||
"& .MuiButtonBase-root": {
|
||||
width: theme.spacing(10),
|
||||
p: 0,
|
||||
@@ -78,6 +78,10 @@ const Checkbox = ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
".MuiFormControlLabel-label.Mui-disabled": {
|
||||
color: theme.palette.text.tertiary,
|
||||
opacity: 0.25,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -85,7 +89,7 @@ const Checkbox = ({
|
||||
|
||||
Checkbox.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||
isChecked: PropTypes.bool.isRequired,
|
||||
value: PropTypes.string,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
.field {
|
||||
min-width: 250px;
|
||||
min-width: var(--env-var-width-3);
|
||||
}
|
||||
|
||||
.field-infrastructure-alert{
|
||||
max-width: var(--env-var-width-4);
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.field h3.MuiTypography-root,
|
||||
.field h5.MuiTypography-root,
|
||||
.field input,
|
||||
@@ -14,7 +20,7 @@
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
.field .MuiInputBase-root:has(input) {
|
||||
height: 34px;
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
.field .MuiInputBase-root:has(.MuiInputAdornment-root) {
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
|
||||
@@ -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(
|
||||
@@ -46,6 +54,8 @@ const Field = forwardRef(
|
||||
error,
|
||||
disabled,
|
||||
hidden,
|
||||
className,
|
||||
hideErrorText = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -56,7 +66,7 @@ const Field = forwardRef(
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
className={`field field-${type}`}
|
||||
className={`field field-${type} ${className}`}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
@@ -67,7 +77,7 @@ const Field = forwardRef(
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
"&:has(.input-error) .MuiOutlinedInput-root fieldset": {
|
||||
borderColor: theme.palette.error.contrastText,
|
||||
borderColor: theme.palette.error.main,
|
||||
},
|
||||
display: hidden ? "none" : "",
|
||||
}}
|
||||
@@ -83,7 +93,7 @@ const Field = forwardRef(
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.spacing(1)}
|
||||
color={theme.palette.error.contrastText}
|
||||
color={theme.palette.error.main}
|
||||
>
|
||||
*
|
||||
</Typography>
|
||||
@@ -189,7 +199,8 @@ const Field = forwardRef(
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.contrastText}
|
||||
hidden={hideErrorText}
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
@@ -223,6 +234,8 @@ Field.propTypes = {
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
hideErrorText: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Field;
|
||||
|
||||
@@ -90,6 +90,7 @@ const URL_MAP = {
|
||||
const PATH_MAP = {
|
||||
monitors: "Dashboard",
|
||||
pagespeed: "Dashboard",
|
||||
infrastructure: "Dashboard",
|
||||
account: "Account",
|
||||
settings: "Other",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/createMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const res = await networkService.createMonitor({
|
||||
authToken: authToken,
|
||||
monitor: monitor,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const checkInfrastructureEndpointResolution = createAsyncThunk(
|
||||
"infrastructureMonitors/CheckEndpoint",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorURL } = data;
|
||||
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
authToken: authToken,
|
||||
monitorURL: monitorURL,
|
||||
})
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const getInfrastructureMonitorById = createAsyncThunk(
|
||||
"infrastructureMonitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorId } = data;
|
||||
const res = await networkService.getMonitorById({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getInfrastructureMonitorsByTeamId = createAsyncThunk(
|
||||
"infrastructureMonitors/getMonitorsByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
authToken: token,
|
||||
teamId: user.teamId,
|
||||
types: ["hardware"],
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/updateMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
threshold: monitor.threshold
|
||||
};
|
||||
const res = await networkService.updateMonitor({
|
||||
authToken: authToken,
|
||||
monitorId: monitor._id,
|
||||
updatedFields: updatedFields,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const res = await networkService.deleteMonitorById({
|
||||
authToken: authToken,
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const pauseInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/pauseMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorId } = data;
|
||||
const res = await networkService.pauseMonitorById({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteInfrastructureMonitorChecksByTeamId = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteChecksByTeamId",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, teamId } = data;
|
||||
const res = await networkService.deleteChecksByTeamId({
|
||||
authToken: authToken,
|
||||
teamId: teamId,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAllInfrastructureMonitors = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteAllMonitors",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken } = data;
|
||||
const res = await networkService.deleteAllMonitors({
|
||||
authToken: authToken,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const infrastructureMonitorsSlice = createSlice({
|
||||
name: "infrastructureMonitors",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearInfrastructureMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getInfrastructureMonitorsByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorsByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorsByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting infrastructure monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkInfrastructureEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkInfrastructureEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkInfrastructureEndpointResolution.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to check endpoint resolution";
|
||||
})
|
||||
// *****************************************************
|
||||
// Get Monitor By Id
|
||||
// *****************************************************
|
||||
.addCase(getInfrastructureMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to get infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updateInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updateInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update infrastructure monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deleteInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete Monitor checks by Team ID
|
||||
// *****************************************************
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete monitor checks";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pauseInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pauseInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pauseInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete all Monitors
|
||||
// *****************************************************
|
||||
.addCase(deleteAllInfrastructureMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteAllInfrastructureMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteAllInfrastructureMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setInfrastructureMonitors, clearInfrastructureMonitorState } = infrastructureMonitorsSlice.actions;
|
||||
|
||||
export default infrastructureMonitorsSlice.reducer;
|
||||
@@ -40,7 +40,7 @@ export const checkEndpointResolution = createAsyncThunk(
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
authToken: authToken,
|
||||
monitorURL: monitorURL,
|
||||
})
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -53,7 +53,7 @@ export const checkEndpointResolution = createAsyncThunk(
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const getUptimeMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
@@ -86,7 +86,7 @@ export const getUptimeMonitorsByTeamId = createAsyncThunk(
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
authToken: token,
|
||||
teamId: user.teamId,
|
||||
types: ["http", "ping"],
|
||||
types: ["http", "ping", "docker"],
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Field from "../../../../Components/Inputs/Field";
|
||||
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
|
||||
* 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
|
||||
* @param {string} param0.checkboxId - The text is the id of the checkbox.
|
||||
* @param {string} param0.checkboxLabel - The text to be displayed as the label next to the check icon.
|
||||
* @param {func} param0.onCheckboxChange - The function to invoke when checkbox is checked or unchecked.
|
||||
* @param {string} param0.fieldId - The text is the id of the input field.
|
||||
* @param {func} param0.onFieldChange - The function to invoke when input field is changed.
|
||||
* @param {func} param0.onFieldBlur - The function to invoke when input field is losing focus.
|
||||
* @param {string} param0.alertUnit the threshold unit such as usage percentage '%' etc
|
||||
* @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]}
|
||||
hideErrorText={true}
|
||||
></Field>
|
||||
<Typography
|
||||
component="p"
|
||||
m={theme.spacing(3)}
|
||||
>
|
||||
{alertUnit}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
CustomThreshold.propTypes = {
|
||||
checkboxId: PropTypes.string.isRequired,
|
||||
checkboxLabel: PropTypes.string.isRequired,
|
||||
onCheckboxChange: PropTypes.func.isRequired,
|
||||
fieldId: PropTypes.string.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onFieldBlur: PropTypes.func.isRequired,
|
||||
alertUnit: PropTypes.string.isRequired,
|
||||
infrastructureMonitor: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
};
|
||||
386
Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
Normal file
386
Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
Normal file
@@ -0,0 +1,386 @@
|
||||
import { useState } from "react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { infrastructureMonitorValidation } from "../../../Validation/validation";
|
||||
import {
|
||||
createInfrastructureMonitor,
|
||||
checkInfrastructureEndpointResolution,
|
||||
} from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../../Monitors/styled";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
|
||||
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
|
||||
import { CustomThreshold } from "../CreateMonitor/CustomThreshold";
|
||||
|
||||
const CreateInfrastructureMonitor = () => {
|
||||
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
interval: 0.25,
|
||||
cpu: false,
|
||||
usage_cpu: "",
|
||||
memory: false,
|
||||
usage_memory: "",
|
||||
disk: false,
|
||||
usage_disk: "",
|
||||
secret: "",
|
||||
});
|
||||
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const THRESHOLD_FIELD_PREFIX = "usage_";
|
||||
const HARDWARE_MONITOR_TYPES = ["cpu", "memory", "disk"];
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const monitorState = useSelector((state) => state.infrastructureMonitor);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const alertErrKeyLen = Object.keys(errors).filter((k) =>
|
||||
k.startsWith(THRESHOLD_FIELD_PREFIX)
|
||||
).length;
|
||||
|
||||
const handleCustomAlertCheckChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setInfrastructureMonitor((prev) => {
|
||||
const newState = {
|
||||
[id]: prev[id] == undefined && value == "on" ? true : !prev[id],
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
...newState,
|
||||
[THRESHOLD_FIELD_PREFIX + id]: newState[id]
|
||||
? prev[THRESHOLD_FIELD_PREFIX + id]
|
||||
: "",
|
||||
};
|
||||
});
|
||||
// Remove the error if unchecked
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, [THRESHOLD_FIELD_PREFIX + id]);
|
||||
});
|
||||
};
|
||||
|
||||
const handleBlur = (event, appenedID) => {
|
||||
event.preventDefault();
|
||||
const { value, id } = event.target;
|
||||
if (id?.startsWith("notify-email-")) return;
|
||||
const { error } = infrastructureMonitorValidation.validate(
|
||||
{ [id ?? appenedID]: value },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, id ?? appenedID, error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (event, appendedId) => {
|
||||
event.preventDefault();
|
||||
const { value, id } = event.target;
|
||||
let name = appendedId ?? idMap[id] ?? id;
|
||||
|
||||
if (name.includes("notification-")) {
|
||||
name = name.replace("notification-", "");
|
||||
let hasNotif = infrastructureMonitor.notifications.some(
|
||||
(notification) => notification.type === name
|
||||
);
|
||||
setInfrastructureMonitor((prev) => {
|
||||
const notifs = [...prev.notifications];
|
||||
if (hasNotif) {
|
||||
return {
|
||||
...prev,
|
||||
notifications: notifs.filter((notif) => notif.type !== name),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...prev,
|
||||
notifications: [
|
||||
...notifs,
|
||||
name === "email"
|
||||
? { type: name, address: value }
|
||||
: // TODO - phone number
|
||||
{ type: name, phone: value },
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setInfrastructureMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const generatePayload = (form) => {
|
||||
let thresholds = {};
|
||||
Object.keys(form)
|
||||
.filter((k) => k.startsWith(THRESHOLD_FIELD_PREFIX))
|
||||
.map((k) => {
|
||||
if (form[k]) thresholds[k] = form[k] / 100;
|
||||
delete form[k];
|
||||
delete form[k.substring(THRESHOLD_FIELD_PREFIX.length)];
|
||||
});
|
||||
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
type: "hardware",
|
||||
notifications: infrastructureMonitor.notifications,
|
||||
thresholds,
|
||||
};
|
||||
return form;
|
||||
};
|
||||
const handleCreateInfrastructureMonitor = async (event) => {
|
||||
event.preventDefault();
|
||||
let form = {
|
||||
...infrastructureMonitor,
|
||||
name:
|
||||
infrastructureMonitor.name === ""
|
||||
? infrastructureMonitor.url
|
||||
: infrastructureMonitor.name,
|
||||
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
|
||||
delete form.notifications;
|
||||
if (hasValidationErrors(form, infrastructureMonitorValidation, setErrors)) {
|
||||
return;
|
||||
} else {
|
||||
const checkEndpointAction = await dispatch(
|
||||
checkInfrastructureEndpointResolution({ authToken, monitorURL: form.url })
|
||||
);
|
||||
if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
createToast({
|
||||
body: "The endpoint you entered doesn't resolve. Check the URL again.",
|
||||
});
|
||||
setErrors({ url: "The entered URL is not reachable." });
|
||||
return;
|
||||
}
|
||||
const action = await dispatch(
|
||||
createInfrastructureMonitor({ authToken, monitor: generatePayload(form) })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Infrastructure monitor created successfully!" });
|
||||
navigate("/infrastructure");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//select values
|
||||
const frequencies = [
|
||||
{ _id: 0.25, name: "15 seconds" },
|
||||
{ _id: 0.5, name: "30 seconds" },
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box className="create-infrastructure-monitor">
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "Infrastructure monitors", path: "infrastructure" },
|
||||
{ name: "create", path: `infrastructure/create` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-infrastructure-monitor-form"
|
||||
onSubmit={handleCreateInfrastructureMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
>
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h2"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
infrastructure monitor
|
||||
</Typography>
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
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)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="url"
|
||||
label="Server URL"
|
||||
placeholder="https://"
|
||||
value={infrastructureMonitor.url}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="name"
|
||||
label="Friendly name"
|
||||
isOptional={true}
|
||||
value={infrastructureMonitor.name}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="secret"
|
||||
label="Authorization secret"
|
||||
value={infrastructureMonitor.secret}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
error={errors["secret"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p">
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">When there is a new incident,</Typography>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={infrastructureMonitor.notifications.some(
|
||||
(notification) => notification.type === "email"
|
||||
)}
|
||||
value={user?.email}
|
||||
onChange={(e) => handleChange(e)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Customize alerts</Typography>
|
||||
<Typography component="p">
|
||||
Send a notification to user(s) when thresholds exceed a specified percentage.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
{HARDWARE_MONITOR_TYPES.map((type, idx) => (
|
||||
<CustomThreshold
|
||||
key={idx}
|
||||
checkboxId={type}
|
||||
checkboxLabel={
|
||||
type !== "cpu" ? capitalizeFirstLetter(type) : type.toUpperCase()
|
||||
}
|
||||
onCheckboxChange={handleCustomAlertCheckChange}
|
||||
fieldId={THRESHOLD_FIELD_PREFIX + type}
|
||||
fieldValue={infrastructureMonitor[THRESHOLD_FIELD_PREFIX + type] ?? ""}
|
||||
onFieldChange={handleChange}
|
||||
onFieldBlur={handleBlur}
|
||||
// TODO: need BE, maybe in another PR
|
||||
alertUnit={type == "temperature" ? "°C" : "%"}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
errors={errors}
|
||||
/>
|
||||
))}
|
||||
{alertErrKeyLen > 0 && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{
|
||||
errors[
|
||||
THRESHOLD_FIELD_PREFIX +
|
||||
HARDWARE_MONITOR_TYPES.filter(
|
||||
(type) => errors[THRESHOLD_FIELD_PREFIX + type]
|
||||
)[0]
|
||||
]
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
id="interval"
|
||||
label="Check frequency"
|
||||
value={infrastructureMonitor.interval || 15}
|
||||
onChange={(e) => handleChange(e, "interval")}
|
||||
onBlur={(e) => handleBlur(e, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
{/* <Field
|
||||
type={"number"}
|
||||
id="monitor-retries"
|
||||
label="Maximum retries before the service is marked as down"
|
||||
value={infrastructureMonitor.url}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors["url"]}
|
||||
/> */}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateInfrastructureMonitor}
|
||||
loading={monitorState?.isLoading}
|
||||
>
|
||||
Create infrastructure monitor
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateInfrastructureMonitor;
|
||||
37
Client/src/Pages/Infrastructure/Details/empty.jsx
Normal file
37
Client/src/Pages/Infrastructure/Details/empty.jsx
Normal file
@@ -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;
|
||||
435
Client/src/Pages/Infrastructure/Details/index.jsx
Normal file
435
Client/src/Pages/Infrastructure/Details/index.jsx
Normal file
@@ -0,0 +1,435 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
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 {
|
||||
TzTick,
|
||||
PercentTick,
|
||||
InfrastructureTooltip,
|
||||
} from "../../../Components/Charts/Utils/chartUtils";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const BASE_BOX_PADDING_VERTICAL = 4;
|
||||
const BASE_BOX_PADDING_HORIZONTAL = 8;
|
||||
const TYPOGRAPHY_PADDING = 8;
|
||||
/**
|
||||
* Converts bytes to gigabytes
|
||||
* @param {number} bytes - Number of bytes to convert
|
||||
* @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";
|
||||
|
||||
const GB = bytes / (1024 * 1024 * 1024);
|
||||
const MB = bytes / (1024 * 1024);
|
||||
|
||||
if (GB >= 1) {
|
||||
return `${Number(GB.toFixed(0))} GB`;
|
||||
} else {
|
||||
return `${Number(MB.toFixed(0))} MB`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {React.ReactNode} props.children - Child components to render inside the box
|
||||
* @param {Object} props.sx - Additional styling for the box
|
||||
* @returns {React.ReactElement} Styled box component
|
||||
*/
|
||||
const BaseBox = ({ children, sx = {} }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
padding: `${theme.spacing(BASE_BOX_PADDING_VERTICAL)} ${theme.spacing(BASE_BOX_PADDING_HORIZONTAL)}`,
|
||||
minWidth: 200,
|
||||
width: 225,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
BaseBox.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a statistic box with a heading and subheading
|
||||
* @param {Object} props - Component properties
|
||||
* @param {string} props.heading - Primary heading text
|
||||
* @param {string} props.subHeading - Secondary heading text
|
||||
* @returns {React.ReactElement} Stat box component
|
||||
*/
|
||||
const StatBox = ({ heading, subHeading }) => {
|
||||
return (
|
||||
<BaseBox>
|
||||
<Typography component="h2">{heading}</Typography>
|
||||
<Typography>{subHeading}</Typography>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
heading: PropTypes.string.isRequired,
|
||||
subHeading: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a gauge box with usage visualization
|
||||
* @param {Object} props - Component properties
|
||||
* @param {number} props.value - Percentage value for gauge
|
||||
* @param {string} props.heading - Box heading
|
||||
* @param {string} props.metricOne - First metric label
|
||||
* @param {string} props.valueOne - First metric value
|
||||
* @param {string} props.metricTwo - Second metric label
|
||||
* @param {string} props.valueTwo - Second metric value
|
||||
* @returns {React.ReactElement} Gauge box component
|
||||
*/
|
||||
const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BaseBox>
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
>
|
||||
<CustomGauge
|
||||
progress={value}
|
||||
radius={100}
|
||||
color={theme.palette.primary.main}
|
||||
/>
|
||||
<Typography component="h2">{heading}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
borderTop: `1px solid ${theme.palette.border.light}`,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{metricOne}</Typography>
|
||||
<Typography>{valueOne}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography>{metricTwo}</Typography>
|
||||
<Typography>{valueTwo}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
GaugeBox.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
heading: PropTypes.string.isRequired,
|
||||
metricOne: PropTypes.string.isRequired,
|
||||
valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
metricTwo: PropTypes.string.isRequired,
|
||||
valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the infrastructure details page
|
||||
* @returns {React.ReactElement} Infrastructure details page component
|
||||
*/
|
||||
const InfrastructureDetails = () => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { monitorId } = useParams();
|
||||
const navList = [
|
||||
{ name: "infrastructure monitors", path: "/infrastructure" },
|
||||
{ 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
|
||||
// when calculating height.
|
||||
const chartContainerHeight = 300;
|
||||
const totalChartContainerPadding =
|
||||
parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2;
|
||||
const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2;
|
||||
const areaChartHeight =
|
||||
(chartContainerHeight - totalChartContainerPadding - totalTypographyPadding) * 0.95;
|
||||
// end height calculations
|
||||
|
||||
// Fetch data
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
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) {
|
||||
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 ?? 0} cores`,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
heading: "Memory",
|
||||
subHeading: formatBytes(monitor?.checks[0]?.memory?.total_bytes),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
heading: "Disk",
|
||||
subHeading: formatBytes(monitor?.checks[0]?.disk[0]?.total_bytes),
|
||||
},
|
||||
{ id: 3, heading: "Uptime", subHeading: "100%" },
|
||||
{
|
||||
id: 4,
|
||||
heading: "Status",
|
||||
subHeading: monitor?.status === true ? "Active" : "Inactive",
|
||||
},
|
||||
];
|
||||
|
||||
const gaugeBoxConfigs = [
|
||||
{
|
||||
type: "memory",
|
||||
value: decimalToPercentage(monitor?.checks[0]?.memory?.usage_percent),
|
||||
heading: "Memory Usage",
|
||||
metricOne: "Used",
|
||||
valueOne: formatBytes(monitor?.checks[0]?.memory?.used_bytes),
|
||||
metricTwo: "Total",
|
||||
valueTwo: formatBytes(monitor?.checks[0]?.memory?.total_bytes),
|
||||
},
|
||||
{
|
||||
type: "cpu",
|
||||
value: decimalToPercentage(monitor?.checks[0]?.cpu?.usage_percent),
|
||||
heading: "CPU Usage",
|
||||
metricOne: "Cores",
|
||||
valueOne: monitor?.checks[0]?.cpu?.physical_core ?? 0,
|
||||
metricTwo: "Frequency",
|
||||
valueTwo: `${(monitor?.checks[0]?.cpu?.frequency ?? 0 / 1000).toFixed(2)} Ghz`,
|
||||
},
|
||||
...(monitor?.checks?.[0]?.disk ?? []).map((disk, idx) => ({
|
||||
type: "disk",
|
||||
diskIndex: idx,
|
||||
value: decimalToPercentage(disk.usage_percent),
|
||||
heading: `Disk${idx} usage`,
|
||||
metricOne: "Used",
|
||||
valueOne: formatBytes(disk.total_bytes - disk.free_bytes),
|
||||
metricTwo: "Total",
|
||||
valueTwo: formatBytes(disk.total_bytes),
|
||||
})),
|
||||
];
|
||||
|
||||
const areaChartConfigs = [
|
||||
{
|
||||
type: "memory",
|
||||
dataKey: "memory.usage_percent",
|
||||
heading: "Memory usage",
|
||||
strokeColor: theme.palette.primary.main,
|
||||
yLabel: "Memory Usage",
|
||||
},
|
||||
{
|
||||
type: "cpu",
|
||||
dataKey: "cpu.usage_percent",
|
||||
heading: "CPU usage",
|
||||
strokeColor: theme.palette.success.main,
|
||||
yLabel: "CPU Usage",
|
||||
},
|
||||
...(monitor?.checks?.[0]?.disk?.map((disk, idx) => ({
|
||||
type: "disk",
|
||||
diskIndex: idx,
|
||||
dataKey: `disk[${idx}].usage_percent`,
|
||||
heading: `Disk${idx} usage`,
|
||||
strokeColor: theme.palette.warning.main,
|
||||
yLabel: "Disk Usage",
|
||||
})) || []),
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Breadcrumbs list={navList} />
|
||||
{monitor?.checks?.length > 0 ? (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(10)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
<Typography
|
||||
alignSelf="end"
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Typography alignSelf="end">{monitor.url || "..."}</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Typography alignSelf="end">
|
||||
Checking every {formatDurationRounded(monitor?.interval)}
|
||||
</Typography>
|
||||
<Typography alignSelf="end">
|
||||
Last checked {formatDurationSplit(monitor?.lastChecked).time}{" "}
|
||||
{formatDurationSplit(monitor?.lastChecked).format} ago
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
{statBoxConfigs.map((statBox) => (
|
||||
<StatBox
|
||||
key={statBox.id}
|
||||
{...statBox}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
{gaugeBoxConfigs.map((config) => (
|
||||
<GaugeBox
|
||||
key={`${config.type}-${config.diskIndex ?? ""}`}
|
||||
value={config.value}
|
||||
heading={config.heading}
|
||||
metricOne={config.metricOne}
|
||||
valueOne={config.valueOne}
|
||||
metricTwo={config.metricTwo}
|
||||
valueTwo={config.valueTwo}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
height={chartContainerHeight} // FE team HELP!
|
||||
gap={theme.spacing(8)} // FE team HELP!
|
||||
flexWrap="wrap" // //FE team HELP! Better way to do this?
|
||||
sx={{
|
||||
"& > *": {
|
||||
flexBasis: `calc(50% - ${theme.spacing(8)})`,
|
||||
maxWidth: `calc(50% - ${theme.spacing(8)})`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{areaChartConfigs.map((config) => (
|
||||
<BaseBox key={`${config.type}-${config.diskIndex ?? ""}`}>
|
||||
<Typography
|
||||
component="h2"
|
||||
padding={theme.spacing(8)}
|
||||
>
|
||||
{config.heading}
|
||||
</Typography>
|
||||
<AreaChart
|
||||
height={areaChartHeight}
|
||||
data={monitor?.checks ?? []}
|
||||
dataKey={config.dataKey}
|
||||
xKey="createdAt"
|
||||
yKey={config.dataKey}
|
||||
customTooltip={({ active, payload, label }) => (
|
||||
<InfrastructureTooltip
|
||||
label={label}
|
||||
yKey={
|
||||
config.type === "disk" ? "disk.usage_percent" : config.dataKey
|
||||
}
|
||||
yLabel={config.yLabel}
|
||||
yIdx={config.diskIndex}
|
||||
active={active}
|
||||
payload={payload}
|
||||
/>
|
||||
)}
|
||||
xTick={<TzTick />}
|
||||
yTick={<PercentTick />}
|
||||
strokeColor={config.strokeColor}
|
||||
gradient={true}
|
||||
gradientStartColor={config.strokeColor}
|
||||
gradientEndColor="#ffffff"
|
||||
/>
|
||||
</BaseBox>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<Empty
|
||||
styles={{
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
p: theme.spacing(30),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureDetails;
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
// Typography,
|
||||
} from "@mui/material";
|
||||
import { Heading } from "../../Components/Heading";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Greeting from "../../Utils/greeting";
|
||||
import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
@@ -68,6 +69,8 @@ Apply to Monitor Table, and Account/Team.
|
||||
Analyze existing BasicTable
|
||||
*/
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
/**
|
||||
* This is the Infrastructure monitoring page. This is a work in progress
|
||||
*
|
||||
@@ -77,6 +80,7 @@ Analyze existing BasicTable
|
||||
|
||||
function Infrastructure() {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Stack
|
||||
component="main"
|
||||
@@ -100,7 +104,7 @@ function Infrastructure() {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
// navigate("/monitors/create");
|
||||
navigate("/infrastructure/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
|
||||
@@ -31,9 +31,28 @@ const CreateMonitor = () => {
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"monitor-checks-docker": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
const monitorTypeMaps = {
|
||||
http: {
|
||||
label: "URL to monitor",
|
||||
placeholder: "google.com",
|
||||
namePlaceholder: "Google",
|
||||
},
|
||||
ping: {
|
||||
label: "IP address to monitor",
|
||||
placeholder: "1.1.1.1",
|
||||
namePlaceholder: "Google",
|
||||
},
|
||||
docker: {
|
||||
label: "Container ID",
|
||||
placeholder: "abc123",
|
||||
namePlaceholder: "My Container",
|
||||
},
|
||||
};
|
||||
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({
|
||||
url: "",
|
||||
@@ -112,7 +131,6 @@ const CreateMonitor = () => {
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
console.log(error);
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) updatedErrors[name] = error.details[0].message;
|
||||
@@ -234,9 +252,9 @@ const CreateMonitor = () => {
|
||||
<Field
|
||||
type={monitor.type === "http" ? "url" : "text"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
label={monitorTypeMaps[monitor.type].label || "URL to monitor"}
|
||||
https={https}
|
||||
placeholder="google.com"
|
||||
placeholder={monitorTypeMaps[monitor.type].placeholder || ""}
|
||||
value={monitor.url}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
@@ -246,7 +264,7 @@ const CreateMonitor = () => {
|
||||
id="monitor-name"
|
||||
label="Display name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
placeholder={monitorTypeMaps[monitor.type].namePlaceholder || ""}
|
||||
value={monitor.name}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
@@ -301,6 +319,15 @@ const CreateMonitor = () => {
|
||||
checked={monitor.type === "ping"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Radio
|
||||
id="monitor-checks-docker"
|
||||
title="Docker container monitoring"
|
||||
desc="Check whether your container is running or not."
|
||||
size="small"
|
||||
value="docker"
|
||||
checked={monitor.type === "docker"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
|
||||
@@ -150,7 +150,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
|
||||
authToken,
|
||||
teamId: user.teamId,
|
||||
limit: 25,
|
||||
types: ["http", "ping"],
|
||||
types: ["http", "ping", "docker"],
|
||||
status: null,
|
||||
checkOrder: "desc",
|
||||
normalize: true,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
17
Client/src/Utils/stringUtils.js
Normal file
17
Client/src/Utils/stringUtils.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Helper function to get first letter capitalized string
|
||||
* @param {string} str String whose first letter is to be capitalized
|
||||
* @returns A string with first letter capitalized
|
||||
*/
|
||||
export const capitalizeFirstLetter = (str) => {
|
||||
if (str === null || str === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Input must be a string");
|
||||
}
|
||||
if (str.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message?? "Validation error";
|
||||
updatedErrors[id] = error.details[0].message ?? "Validation error";
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
@@ -22,15 +22,29 @@ const hasValidationErrors = (form, validation, setErrors) => {
|
||||
"dbConnectionString",
|
||||
"refreshTokenTTL",
|
||||
"jwtTTL",
|
||||
"notify-email-list",
|
||||
].includes(err.path[0])
|
||||
) {
|
||||
newErrors[err.path[0]] = err.message ?? "Validation error";
|
||||
}
|
||||
// Handle conditionally usage number required cases
|
||||
if (!form.cpu || form.usage_cpu) {
|
||||
newErrors["usage_cpu"] = null;
|
||||
}
|
||||
if (!form.memory || form.usage_memory) {
|
||||
newErrors["usage_memory"] = null;
|
||||
}
|
||||
if (!form.disk || form.usage_disk) {
|
||||
newErrors["usage_disk"] = null;
|
||||
}
|
||||
});
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
if (Object.values(newErrors).some(v=> v)) {
|
||||
setErrors(newErrors);
|
||||
return true;
|
||||
} else return false;
|
||||
} else {
|
||||
setErrors({});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import joi from "joi";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number.";
|
||||
|
||||
const nameSchema = joi
|
||||
.string()
|
||||
.max(50)
|
||||
@@ -173,6 +175,42 @@ const advancedSettingsValidation = joi.object({
|
||||
pagespeedApiKey: joi.string().allow(""),
|
||||
});
|
||||
|
||||
const infrastructureMonitorValidation = joi.object({
|
||||
url: joi.string().uri({ allowRelative: true }).trim().messages({
|
||||
"string.empty": "This field is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
}),
|
||||
name: joi.string().trim().max(50).allow("").messages({
|
||||
"string.max": "This field should not exceed the 50 characters limit.",
|
||||
}),
|
||||
secret: joi.string().trim().messages({ "string.empty": "This field is required." }),
|
||||
usage_cpu: joi.number().messages({
|
||||
"number.base": THRESHOLD_COMMON_BASE_MSG,
|
||||
}),
|
||||
cpu: joi.boolean(),
|
||||
memory: joi.boolean(),
|
||||
disk: joi.boolean(),
|
||||
usage_memory: joi.number().messages({
|
||||
"number.base": THRESHOLD_COMMON_BASE_MSG,
|
||||
}),
|
||||
usage_disk: joi.number().messages({
|
||||
"number.base": THRESHOLD_COMMON_BASE_MSG,
|
||||
}),
|
||||
// usage_temperature: joi.number().messages({
|
||||
// "number.base": "Temperature must be a number.",
|
||||
// }),
|
||||
// usage_system: joi.number().messages({
|
||||
// "number.base": "System load must be a number.",
|
||||
// }),
|
||||
// usage_swap: joi.number().messages({
|
||||
// "number.base": "Swap used must be a number.",
|
||||
// }),
|
||||
interval: joi.number().messages({
|
||||
"number.base": "Frequency must be a number.",
|
||||
"any.required": "Frequency is required.",
|
||||
}),
|
||||
});
|
||||
|
||||
export {
|
||||
credentials,
|
||||
imageValidation,
|
||||
@@ -180,4 +218,5 @@ export {
|
||||
settingsValidation,
|
||||
maintenanceWindowValidation,
|
||||
advancedSettingsValidation,
|
||||
infrastructureMonitorValidation,
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ html {
|
||||
|
||||
--env-var-width-1: 100vw;
|
||||
--env-var-width-2: 360px;
|
||||
--env-var-width-3: 250px;
|
||||
--env-var-width-4: 100px;
|
||||
|
||||
--env-var-height-1: 100vh;
|
||||
--env-var-height-2: 34px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { configureStore, combineReducers } from "@reduxjs/toolkit";
|
||||
|
||||
import uptimeMonitorsReducer from "./Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import infrastructureMonitorsReducer from "./Features/InfrastructureMonitors/infrastructureMonitorsSlice";
|
||||
import pageSpeedMonitorReducer from "./Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import authReducer from "./Features/Auth/authSlice";
|
||||
import uiReducer from "./Features/UI/uiSlice";
|
||||
@@ -28,6 +29,7 @@ const persistConfig = {
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
uptimeMonitors: uptimeMonitorsReducer,
|
||||
infrastructureMonitors: infrastructureMonitorsReducer,
|
||||
auth: authReducer,
|
||||
pageSpeedMonitors: pageSpeedMonitorReducer,
|
||||
ui: uiReducer,
|
||||
|
||||
2
Docker/dist/docker-compose.yaml
vendored
2
Docker/dist/docker-compose.yaml
vendored
@@ -18,6 +18,8 @@ services:
|
||||
environment:
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
|
||||
- REDIS_HOST=redis
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
redis:
|
||||
image: bluewaveuptime/uptime_redis:latest
|
||||
ports:
|
||||
|
||||
@@ -19,7 +19,7 @@ import jwt from "jsonwebtoken";
|
||||
import { getTokenFromHeaders } from "../utils/utils.js";
|
||||
import logger from "../utils/logger.js";
|
||||
import { handleError, handleValidationError } from "./controllerUtils.js";
|
||||
import dns from "dns";
|
||||
import axios from "axios";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
|
||||
@@ -288,18 +288,16 @@ const checkEndpointResolution = async (req, res, next) => {
|
||||
}
|
||||
|
||||
try {
|
||||
let { monitorURL } = req.query;
|
||||
monitorURL = new URL(monitorURL);
|
||||
await new Promise((resolve, reject) => {
|
||||
dns.resolve(monitorURL.hostname, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
const { monitorURL } = req.query;
|
||||
const parsedUrl = new URL(monitorURL);
|
||||
const response = await axios.get(parsedUrl, {
|
||||
timeout: 5000,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
code: response.status,
|
||||
statusText: response.statusText,
|
||||
msg: `URL resolved successfully`,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -28,7 +28,7 @@ const MonitorSchema = mongoose.Schema(
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ["http", "ping", "pagespeed", "hardware"],
|
||||
enum: ["http", "ping", "pagespeed", "hardware", "docker"],
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -118,7 +118,6 @@ import {
|
||||
|
||||
import {
|
||||
createPageSpeedCheck,
|
||||
getPageSpeedChecks,
|
||||
deletePageSpeedChecksByMonitorId,
|
||||
} from "./modules/pageSpeedCheckModule.js";
|
||||
|
||||
@@ -212,7 +211,6 @@ export default {
|
||||
updateChecksTTL,
|
||||
deleteMonitorsByUserId,
|
||||
createPageSpeedCheck,
|
||||
getPageSpeedChecks,
|
||||
deletePageSpeedChecksByMonitorId,
|
||||
createHardwareCheck,
|
||||
createMaintenanceWindow,
|
||||
|
||||
@@ -20,6 +20,7 @@ const SERVICE_NAME = "monitorModule";
|
||||
const CHECK_MODEL_LOOKUP = {
|
||||
http: Check,
|
||||
ping: Check,
|
||||
docker: Check,
|
||||
pagespeed: PageSpeedCheck,
|
||||
hardware: HardwareCheck,
|
||||
};
|
||||
@@ -197,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) => {
|
||||
@@ -205,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],
|
||||
@@ -349,7 +351,7 @@ const getMonitorStatsById = async (req) => {
|
||||
),
|
||||
};
|
||||
|
||||
if (monitor.type === "http" || monitor.type === "ping") {
|
||||
if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker") {
|
||||
// HTTP/PING Specific stats
|
||||
monitorStats.periodAvgResponseTime = getAverageResponseTime(checksForDateRange);
|
||||
monitorStats.periodUptime = getUptimePercentage(checksForDateRange);
|
||||
|
||||
@@ -25,25 +25,6 @@ const createPageSpeedCheck = async (pageSpeedCheckData) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all PageSpeed checks for a monitor
|
||||
* @async
|
||||
* @param {string} monitorId
|
||||
* @returns {Promise<Array<PageSpeedCheck>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
|
||||
const getPageSpeedChecks = async (monitorId) => {
|
||||
try {
|
||||
const pageSpeedChecks = await PageSpeedCheck.find({ monitorId });
|
||||
return pageSpeedChecks;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getPageSpeedChecks";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all PageSpeed checks for a monitor
|
||||
* @async
|
||||
@@ -63,4 +44,4 @@ const deletePageSpeedChecksByMonitorId = async (monitorId) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { createPageSpeedCheck, getPageSpeedChecks, deletePageSpeedChecksByMonitorId };
|
||||
export { createPageSpeedCheck, deletePageSpeedChecksByMonitorId };
|
||||
|
||||
@@ -15,7 +15,11 @@ const SERVICE_NAME = "userModule";
|
||||
* @returns {Promise<UserModel>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const insertUser = async (userData, imageFile) => {
|
||||
const insertUser = async (
|
||||
userData,
|
||||
imageFile,
|
||||
generateAvatarImage = GenerateAvatarImage
|
||||
) => {
|
||||
try {
|
||||
if (imageFile) {
|
||||
// 1. Save the full size image
|
||||
@@ -25,7 +29,7 @@ const insertUser = async (userData, imageFile) => {
|
||||
};
|
||||
|
||||
// 2. Get the avatar sized image
|
||||
const avatar = await GenerateAvatarImage(imageFile);
|
||||
const avatar = await generateAvatarImage(imageFile);
|
||||
userData.avatarImage = avatar;
|
||||
}
|
||||
|
||||
@@ -90,7 +94,12 @@ const getUserByEmail = async (email) => {
|
||||
* @throws {Error}
|
||||
*/
|
||||
|
||||
const updateUser = async (req, res) => {
|
||||
const updateUser = async (
|
||||
req,
|
||||
res,
|
||||
parseBoolean = ParseBoolean,
|
||||
generateAvatarImage = GenerateAvatarImage
|
||||
) => {
|
||||
const candidateUserId = req.params.userId;
|
||||
try {
|
||||
const candidateUser = { ...req.body };
|
||||
@@ -98,7 +107,7 @@ const updateUser = async (req, res) => {
|
||||
// Handle profile image
|
||||
// ******************************************
|
||||
|
||||
if (ParseBoolean(candidateUser.deleteProfileImage) === true) {
|
||||
if (parseBoolean(candidateUser.deleteProfileImage) === true) {
|
||||
candidateUser.profileImage = null;
|
||||
candidateUser.avatarImage = null;
|
||||
} else if (req.file) {
|
||||
@@ -108,8 +117,8 @@ const updateUser = async (req, res) => {
|
||||
contentType: req.file.mimetype,
|
||||
};
|
||||
|
||||
// 2. Get the avaatar sized image
|
||||
const avatar = await GenerateAvatarImage(req.file);
|
||||
// 2. Get the avatar sized image
|
||||
const avatar = await generateAvatarImage(req.file);
|
||||
candidateUser.avatarImage = avatar;
|
||||
}
|
||||
|
||||
@@ -164,6 +173,7 @@ const deleteUser = async (userId) => {
|
||||
const deleteTeam = async (teamId) => {
|
||||
try {
|
||||
await TeamModel.findByIdAndDelete(teamId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteTeam";
|
||||
@@ -174,6 +184,7 @@ const deleteTeam = async (teamId) => {
|
||||
const deleteAllOtherUsers = async () => {
|
||||
try {
|
||||
await UserModel.deleteMany({ role: { $ne: "superadmin" } });
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteAllOtherUsers";
|
||||
|
||||
@@ -28,6 +28,7 @@ import NetworkService from "./service/networkService.js";
|
||||
import axios from "axios";
|
||||
import ping from "ping";
|
||||
import http from "http";
|
||||
import Docker from "dockerode";
|
||||
|
||||
// Email service and dependencies
|
||||
import EmailService from "./service/emailService.js";
|
||||
@@ -45,7 +46,7 @@ import NotificationService from "./service/notificationService.js";
|
||||
|
||||
import db from "./db/mongo/MongoDB.js";
|
||||
const SERVICE_NAME = "Server";
|
||||
const SHUTDOWN_TIMEOUT = 0;
|
||||
const SHUTDOWN_TIMEOUT = 1000;
|
||||
|
||||
let isShuttingDown = false;
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -91,6 +92,23 @@ const startApp = async () => {
|
||||
app.use("/api/v1/queue", verifyJWT, queueRouter);
|
||||
app.use("/api/v1/status-page", statusPageRouter);
|
||||
|
||||
app.use("/api/v1/dummy-data", async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
"https://gist.githubusercontent.com/ajhollid/9afa39410c7bbf52cc905f285a2225bf/raw/429a231a3559ebc95f6f488ed2c766bd7d6f46e5/dummyData.json",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
}
|
||||
);
|
||||
return res.status(200).json(response.data);
|
||||
} catch (error) {
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
//health check
|
||||
app.use("/api/v1/healthy", (req, res) => {
|
||||
try {
|
||||
@@ -130,7 +148,7 @@ const startApp = async () => {
|
||||
nodemailer,
|
||||
logger
|
||||
);
|
||||
const networkService = new NetworkService(axios, ping, logger, http);
|
||||
const networkService = new NetworkService(axios, ping, logger, http, Docker);
|
||||
const statusService = new StatusService(db, logger);
|
||||
const notificationService = new NotificationService(emailService, db, logger);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
|
||||
334
Server/package-lock.json
generated
334
Server/package-lock.json
generated
@@ -9,11 +9,11 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sendgrid/mail": "^8.1.3",
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.25.6",
|
||||
"bullmq": "5.29.1",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"handlebars": "^4.7.8",
|
||||
@@ -89,6 +89,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@balena/dockerignore": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
|
||||
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
@@ -738,41 +744,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sendgrid/client": {
|
||||
"version": "8.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.3.tgz",
|
||||
"integrity": "sha512-mRwTticRZIdUTsnyzvlK6dMu3jni9ci9J+dW/6fMMFpGRAJdCJlivFVYQvqk8kRS3RnFzS7sf6BSmhLl1ldDhA==",
|
||||
"dependencies": {
|
||||
"@sendgrid/helpers": "^8.0.0",
|
||||
"axios": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/@sendgrid/helpers": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
|
||||
"integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sendgrid/mail": {
|
||||
"version": "8.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.3.tgz",
|
||||
"integrity": "sha512-Wg5iKSUOER83/cfY6rbPa+o3ChnYzWwv1OcsR8gCV8SKi+sUPIMroildimlnb72DBkQxcbylxng1W7f0RIX7MQ==",
|
||||
"dependencies": {
|
||||
"@sendgrid/client": "^8.1.3",
|
||||
"@sendgrid/helpers": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
@@ -996,6 +967,15 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
@@ -1032,6 +1012,26 @@
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
||||
@@ -1045,6 +1045,15 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -1056,6 +1065,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
@@ -1152,6 +1172,30 @@
|
||||
"node": ">=16.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
@@ -1163,10 +1207,19 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.25.6",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.6.tgz",
|
||||
"integrity": "sha512-jxpa/DB02V20CqBAgyqpQazT630CJm0r4fky8EchH3mcJAomRtKXLS6tRA0J8tb29BDGlr/LXhlUuZwdBJBSdA==",
|
||||
"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",
|
||||
@@ -1708,6 +1761,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buildcheck": "~0.0.6",
|
||||
"nan": "^2.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
@@ -1927,14 +1994,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
@@ -2014,6 +2073,58 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/docker-modem": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
|
||||
"integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"readable-stream": "^3.5.0",
|
||||
"split-ca": "^1.0.1",
|
||||
"ssh2": "^1.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docker-modem/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/docker-modem/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dockerode": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz",
|
||||
"integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"docker-modem": "^5.0.3",
|
||||
"tar-fs": "~2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -2145,6 +2256,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@@ -2439,6 +2559,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
@@ -2822,6 +2948,26 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
@@ -4129,6 +4275,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "10.8.2",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
|
||||
@@ -4370,9 +4522,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.1.tgz",
|
||||
"integrity": "sha512-l7DgeY1szT98+EKU8GYnga5WnyatAu+kOQ2VlVX1Mxif6A0Umt0YkSiksCiyGxzx8SPhGe9a53ND1GD4yVDrPA==",
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.2.tgz",
|
||||
"integrity": "sha512-jCTSqDANfRzk909v4YoZQi7jlGRB2MTvgG+spVBc/BA4tOs1oWJr//V6yYujqNq9UybpOtsSfBqxI0dSOEFJHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.7.0",
|
||||
@@ -4503,6 +4655,13 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
|
||||
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
@@ -5553,6 +5712,16 @@
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -6046,6 +6215,29 @@
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/split-ca": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
|
||||
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ssh2": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
||||
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"asn1": "^0.2.6",
|
||||
"bcrypt-pbkdf": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cpu-features": "~0.0.10",
|
||||
"nan": "^2.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ssl-checker": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/ssl-checker/-/ssl-checker-2.0.10.tgz",
|
||||
@@ -6248,6 +6440,40 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
|
||||
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs/node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
@@ -6311,6 +6537,12 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-detect": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sendgrid/mail": "^8.1.3",
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.25.6",
|
||||
"bullmq": "5.29.1",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"handlebars": "^4.7.8",
|
||||
|
||||
@@ -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,23 +165,18 @@ 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({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
service: error.service ?? SERVICE_NAME,
|
||||
method: error.method ?? "createJobHandler",
|
||||
details: `Error processing job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
|
||||
@@ -8,11 +8,12 @@ import { errorMessages, successMessages } from "../utils/messages.js";
|
||||
* @param {Object} http - The HTTP utility for network operations.
|
||||
*/
|
||||
class NetworkService {
|
||||
constructor(axios, ping, logger, http) {
|
||||
constructor(axios, ping, logger, http, Docker) {
|
||||
this.TYPE_PING = "ping";
|
||||
this.TYPE_HTTP = "http";
|
||||
this.TYPE_PAGESPEED = "pagespeed";
|
||||
this.TYPE_HARDWARE = "hardware";
|
||||
this.TYPE_DOCKER = "docker";
|
||||
this.SERVICE_NAME = "NetworkService";
|
||||
this.NETWORK_ERROR = 5000;
|
||||
this.PING_ERROR = 5001;
|
||||
@@ -20,6 +21,7 @@ class NetworkService {
|
||||
this.ping = ping;
|
||||
this.logger = logger;
|
||||
this.http = http;
|
||||
this.Docker = Docker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,28 +64,34 @@ class NetworkService {
|
||||
* @property {string} message - The message indicating the result of the ping request.
|
||||
*/
|
||||
async requestPing(job) {
|
||||
const url = job.data.url;
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
this.ping.promise.probe(url)
|
||||
);
|
||||
try {
|
||||
const url = job.data.url;
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
this.ping.promise.probe(url)
|
||||
);
|
||||
|
||||
const pingResponse = {
|
||||
monitorId: job.data._id,
|
||||
type: "ping",
|
||||
responseTime,
|
||||
payload: response,
|
||||
};
|
||||
if (error) {
|
||||
pingResponse.status = false;
|
||||
pingResponse.code = this.PING_ERROR;
|
||||
pingResponse.message = errorMessages.PING_CANNOT_RESOLVE;
|
||||
const pingResponse = {
|
||||
monitorId: job.data._id,
|
||||
type: "ping",
|
||||
responseTime,
|
||||
payload: response,
|
||||
};
|
||||
if (error) {
|
||||
pingResponse.status = false;
|
||||
pingResponse.code = this.PING_ERROR;
|
||||
pingResponse.message = errorMessages.PING_CANNOT_RESOLVE;
|
||||
return pingResponse;
|
||||
}
|
||||
|
||||
pingResponse.code = 200;
|
||||
pingResponse.status = response.alive;
|
||||
pingResponse.message = successMessages.PING_SUCCESS;
|
||||
return pingResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPing";
|
||||
throw error;
|
||||
}
|
||||
|
||||
pingResponse.code = 200;
|
||||
pingResponse.status = response.alive;
|
||||
pingResponse.message = successMessages.PING_SUCCESS;
|
||||
return pingResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,34 +112,40 @@ class NetworkService {
|
||||
* @property {string} message - The message indicating the result of the HTTP request.
|
||||
*/
|
||||
async requestHttp(job) {
|
||||
const url = job.data.url;
|
||||
const config = {};
|
||||
try {
|
||||
const url = job.data.url;
|
||||
const config = {};
|
||||
|
||||
job.data.secret !== undefined &&
|
||||
(config.headers = { Authorization: `Bearer ${job.data.secret}` });
|
||||
job.data.secret !== undefined &&
|
||||
(config.headers = { Authorization: `Bearer ${job.data.secret}` });
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
this.axios.get(url, config)
|
||||
);
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
this.axios.get(url, config)
|
||||
);
|
||||
|
||||
const httpResponse = {
|
||||
monitorId: job.data._id,
|
||||
type: job.data.type,
|
||||
responseTime,
|
||||
payload: response?.data,
|
||||
};
|
||||
const httpResponse = {
|
||||
monitorId: job.data._id,
|
||||
type: job.data.type,
|
||||
responseTime,
|
||||
payload: response?.data,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
const code = error.response?.status || this.NETWORK_ERROR;
|
||||
httpResponse.code = code;
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.http.STATUS_CODES[code] || "Network Error";
|
||||
if (error) {
|
||||
const code = error.response?.status || this.NETWORK_ERROR;
|
||||
httpResponse.code = code;
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.http.STATUS_CODES[code] || "Network Error";
|
||||
return httpResponse;
|
||||
}
|
||||
httpResponse.status = true;
|
||||
httpResponse.code = response.status;
|
||||
httpResponse.message = this.http.STATUS_CODES[response.status];
|
||||
return httpResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestHttp";
|
||||
throw error;
|
||||
}
|
||||
httpResponse.status = true;
|
||||
httpResponse.code = response.status;
|
||||
httpResponse.message = this.http.STATUS_CODES[response.status];
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,15 +165,114 @@ class NetworkService {
|
||||
* @property {string} message - The message indicating the result of the PageSpeed request.
|
||||
*/
|
||||
async requestPagespeed(job) {
|
||||
const url = job.data.url;
|
||||
const updatedJob = { ...job };
|
||||
const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||
updatedJob.data.url = pagespeedUrl;
|
||||
return this.requestHttp(updatedJob);
|
||||
try {
|
||||
const url = job.data.url;
|
||||
const updatedJob = { ...job };
|
||||
const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||
updatedJob.data.url = pagespeedUrl;
|
||||
return await this.requestHttp(updatedJob);
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPagespeed";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP request to check hardware status and returns the response.
|
||||
*
|
||||
* @param {Object} job - The job object containing the data for the hardware request.
|
||||
* @param {Object} job.data - The data object within the job.
|
||||
* @param {string} job.data.url - The URL to send the hardware status request to.
|
||||
* @param {string} job.data._id - The monitor ID for the hardware request.
|
||||
* @param {string} job.data.type - The type of request, which is "hardware".
|
||||
* @returns {Promise<Object>} An object containing the hardware status response details.
|
||||
* @property {string} monitorId - The monitor ID for the hardware request.
|
||||
* @property {string} type - The type of request ("hardware").
|
||||
* @property {number} responseTime - The time taken for the request to complete, in milliseconds.
|
||||
* @property {Object} payload - The response payload from the hardware status request.
|
||||
* @property {boolean} status - The status of the request (true if successful, false otherwise).
|
||||
* @property {number} code - The response code (200 if successful, error code otherwise).
|
||||
* @property {string} message - The message indicating the result of the hardware status request.
|
||||
*/
|
||||
async requestHardware(job) {
|
||||
return this.requestHttp(job);
|
||||
try {
|
||||
return await this.requestHttp(job);
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestHardware";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to inspect a Docker container and returns its status.
|
||||
*
|
||||
* @param {Object} job - The job object containing the data for the Docker request.
|
||||
* @param {Object} job.data - The data object within the job.
|
||||
* @param {string} job.data.url - The container ID or name to inspect.
|
||||
* @param {string} job.data._id - The monitor ID for the Docker request.
|
||||
* @param {string} job.data.type - The type of request, which is "docker".
|
||||
* @returns {Promise<Object>} An object containing the Docker container status details.
|
||||
* @property {string} monitorId - The monitor ID for the Docker request.
|
||||
* @property {string} type - The type of request ("docker").
|
||||
* @property {number} responseTime - The time taken for the Docker inspection to complete, in milliseconds.
|
||||
* @property {boolean} status - The status of the container (true if running, false otherwise).
|
||||
* @property {number} code - The response code (200 if successful, error code otherwise).
|
||||
* @property {string} message - The message indicating the result of the Docker inspection.
|
||||
*/
|
||||
async requestDocker(job) {
|
||||
try {
|
||||
const docker = new this.Docker({
|
||||
socketPath: "/var/run/docker.sock",
|
||||
handleError: true, // Enable error handling
|
||||
});
|
||||
|
||||
const containers = await docker.listContainers({ all: true });
|
||||
const containerExists = containers.some((c) => c.Id.startsWith(job.data.url));
|
||||
if (!containerExists) {
|
||||
throw new Error(errorMessages.DOCKER_NOT_FOUND);
|
||||
}
|
||||
const container = docker.getContainer(job.data.url);
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
container.inspect()
|
||||
);
|
||||
|
||||
const dockerResponse = {
|
||||
monitorId: job.data._id,
|
||||
type: job.data.type,
|
||||
responseTime,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
dockerResponse.status = false;
|
||||
dockerResponse.code = error.statusCode || this.NETWORK_ERROR;
|
||||
dockerResponse.message = error.reason || errorMessages.DOCKER_FAIL;
|
||||
return dockerResponse;
|
||||
}
|
||||
dockerResponse.status = response?.State?.Status === "running" ? true : false;
|
||||
dockerResponse.code = 200;
|
||||
dockerResponse.message = successMessages.DOCKER_SUCCESS;
|
||||
return dockerResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestDocker";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unsupported job types by throwing an error with details.
|
||||
*
|
||||
* @param {string} type - The unsupported job type that was provided
|
||||
* @throws {Error} An error with service name, method name and unsupported type message
|
||||
*/
|
||||
handleUnsupportedType(type) {
|
||||
const err = new Error(`Unsupported type: ${type}`);
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "getStatus";
|
||||
throw err;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +285,8 @@ class NetworkService {
|
||||
* @throws {Error} Throws an error if the job type is unsupported.
|
||||
*/
|
||||
async getStatus(job) {
|
||||
switch (job.data.type) {
|
||||
const type = job?.data?.type ?? "unknown";
|
||||
switch (type) {
|
||||
case this.TYPE_PING:
|
||||
return await this.requestPing(job);
|
||||
case this.TYPE_HTTP:
|
||||
@@ -181,13 +295,10 @@ class NetworkService {
|
||||
return await this.requestPagespeed(job);
|
||||
case this.TYPE_HARDWARE:
|
||||
return await this.requestHardware(job);
|
||||
case this.TYPE_DOCKER:
|
||||
return await this.requestDocker(job);
|
||||
default:
|
||||
this.logger.error({
|
||||
message: `Unsupported type: ${job.data.type}`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "getStatus",
|
||||
});
|
||||
return {};
|
||||
return this.handleUnsupportedType(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? {};
|
||||
@@ -133,6 +139,7 @@ class StatusService {
|
||||
ping: this.db.createCheck,
|
||||
pagespeed: this.db.createPageSpeedCheck,
|
||||
hardware: this.db.createHardwareCheck,
|
||||
docker: this.db.createCheck,
|
||||
};
|
||||
const operation = operationMap[networkResponse.type];
|
||||
|
||||
|
||||
43
Server/templates/hardwareIncident.mjml
Normal file
43
Server/templates/hardwareIncident.mjml
Normal file
@@ -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>
|
||||
@@ -18,7 +18,7 @@ import jwt from "jsonwebtoken";
|
||||
import sinon from "sinon";
|
||||
import { successMessages } from "../../utils/messages.js";
|
||||
import logger from "../../utils/logger.js";
|
||||
import dns from "dns";
|
||||
import axios from "axios";
|
||||
const SERVICE_NAME = "monitorController";
|
||||
|
||||
describe("Monitor Controller - getAllMonitors", () => {
|
||||
@@ -476,38 +476,40 @@ describe("Monitor Controller - createMonitor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Monitor Controllor - checkEndpointResolution", () => {
|
||||
let req, res, next, dnsResolveStub;
|
||||
describe("Monitor Controller - checkEndpointResolution", () => {
|
||||
let req, res, next, axiosGetStub;
|
||||
beforeEach(() => {
|
||||
req = { query: { monitorURL: "https://example.com" } };
|
||||
res = { status: sinon.stub().returnsThis(), json: sinon.stub() };
|
||||
next = sinon.stub();
|
||||
dnsResolveStub = sinon.stub(dns, "resolve");
|
||||
axiosGetStub = sinon.stub(axios, "get");
|
||||
});
|
||||
afterEach(() => {
|
||||
dnsResolveStub.restore();
|
||||
sinon.restore();
|
||||
});
|
||||
it("should resolve the URL successfully", async () => {
|
||||
dnsResolveStub.callsFake((hostname, callback) => callback(null));
|
||||
axiosGetStub.resolves({ status: 200, statusText: "OK" });
|
||||
await checkEndpointResolution(req, res, next);
|
||||
expect(res.status.calledWith(200)).to.be.true;
|
||||
expect(
|
||||
res.json.calledWith({
|
||||
success: true,
|
||||
code: 200,
|
||||
statusText: "OK",
|
||||
msg: "URL resolved successfully",
|
||||
})
|
||||
).to.be.true;
|
||||
expect(next.called).to.be.false;
|
||||
});
|
||||
it("should return an error if DNS resolution fails", async () => {
|
||||
const dnsError = new Error("DNS resolution failed");
|
||||
dnsError.code = "ENOTFOUND";
|
||||
dnsResolveStub.callsFake((hostname, callback) => callback(dnsError));
|
||||
it("should return an error if endpoint resolution fails", async () => {
|
||||
const axiosError = new Error("resolution failed");
|
||||
axiosError.code = "ENOTFOUND";
|
||||
axiosGetStub.rejects(axiosError);
|
||||
await checkEndpointResolution(req, res, next);
|
||||
expect(next.calledOnce).to.be.true;
|
||||
const errorPassedToNext = next.getCall(0).args[0];
|
||||
expect(errorPassedToNext).to.be.an.instanceOf(Error);
|
||||
expect(errorPassedToNext.message).to.include("DNS resolution failed");
|
||||
expect(errorPassedToNext.message).to.include("resolution failed");
|
||||
expect(errorPassedToNext.code).to.equal("ENOTFOUND");
|
||||
expect(errorPassedToNext.status).to.equal(500);
|
||||
});
|
||||
|
||||
73
Server/tests/db/hardwareCheckModule.test.js
Normal file
73
Server/tests/db/hardwareCheckModule.test.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import sinon from "sinon";
|
||||
import HardwareCheck from "../../db/models/HardwareCheck.js";
|
||||
import { createHardwareCheck } from "../../db/mongo/modules/hardwareCheckModule.js";
|
||||
|
||||
const mockHardwareCheck = {
|
||||
data: {
|
||||
cpu: {
|
||||
physical_core: 4,
|
||||
logical_core: 8,
|
||||
frequency: 4800,
|
||||
current_frequency: 1411,
|
||||
temperature: [45, 50, 46, 47, 45, 50, 46, 47],
|
||||
free_percent: 0.8552990910595134,
|
||||
usage_percent: 0.14470090894048657,
|
||||
},
|
||||
memory: {
|
||||
total_bytes: 16467628032,
|
||||
available_bytes: 7895044096,
|
||||
used_bytes: 6599561216,
|
||||
usage_percent: 0.4008,
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
read_speed_bytes: null,
|
||||
write_speed_bytes: null,
|
||||
total_bytes: 931258499072,
|
||||
free_bytes: 737097256960,
|
||||
usage_percent: 0.1661,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
os: "linux",
|
||||
platform: "ubuntu",
|
||||
kernel_version: "6.8.0-48-generic",
|
||||
},
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
metric: ["cpu.temperature"],
|
||||
err: "unable to read CPU temperature",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("HardwareCheckModule", () => {
|
||||
let hardwareCheckSaveStub;
|
||||
beforeEach(() => {
|
||||
hardwareCheckSaveStub = sinon.stub(HardwareCheck.prototype, "save");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("createHardwareCheck", () => {
|
||||
it("should return a hardware check", async () => {
|
||||
hardwareCheckSaveStub.resolves(mockHardwareCheck);
|
||||
const hardwareCheck = await createHardwareCheck({});
|
||||
expect(hardwareCheck).to.exist;
|
||||
expect(hardwareCheck).to.deep.equal(mockHardwareCheck);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
hardwareCheckSaveStub.rejects(err);
|
||||
try {
|
||||
await createHardwareCheck({});
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -891,6 +891,18 @@ describe("monitorModule", () => {
|
||||
url: "https://test.com",
|
||||
}),
|
||||
};
|
||||
const mockMonitorDocker = {
|
||||
_id: "monitor123",
|
||||
type: "docker",
|
||||
name: "Test Monitor",
|
||||
url: "https://test.com",
|
||||
toObject: () => ({
|
||||
_id: "monitor123",
|
||||
type: "http",
|
||||
name: "Test Monitor",
|
||||
url: "https://test.com",
|
||||
}),
|
||||
};
|
||||
|
||||
const checkDocs = [
|
||||
{
|
||||
@@ -1004,6 +1016,30 @@ describe("monitorModule", () => {
|
||||
expect(result.periodUptime).to.be.a("number");
|
||||
expect(result.aggregateData).to.be.an("array");
|
||||
});
|
||||
it("should return monitor stats with calculated values, docker type", async () => {
|
||||
monitorFindByIdStub.returns(mockMonitorDocker);
|
||||
req.query.sortOrder = "desc";
|
||||
const result = await getMonitorStatsById(req);
|
||||
expect(result).to.include.keys([
|
||||
"_id",
|
||||
"type",
|
||||
"name",
|
||||
"url",
|
||||
"uptimeDuration",
|
||||
"lastChecked",
|
||||
"latestResponseTime",
|
||||
"periodIncidents",
|
||||
"periodTotalChecks",
|
||||
"periodAvgResponseTime",
|
||||
"periodUptime",
|
||||
"aggregateData",
|
||||
]);
|
||||
expect(result.latestResponseTime).to.equal(100);
|
||||
expect(result.periodTotalChecks).to.equal(3);
|
||||
expect(result.periodIncidents).to.equal(1);
|
||||
expect(result.periodUptime).to.be.a("number");
|
||||
expect(result.aggregateData).to.be.an("array");
|
||||
});
|
||||
it("should return monitor stats with calculated values", async () => {
|
||||
req.query.sortOrder = "asc";
|
||||
const result = await getMonitorStatsById(req);
|
||||
|
||||
65
Server/tests/db/pageSpeedCheckModule.test.js
Normal file
65
Server/tests/db/pageSpeedCheckModule.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import sinon from "sinon";
|
||||
import PageSpeedCheck from "../../db/models/PageSpeedCheck.js";
|
||||
import {
|
||||
createPageSpeedCheck,
|
||||
deletePageSpeedChecksByMonitorId,
|
||||
} from "../../db/mongo/modules/pageSpeedCheckModule.js";
|
||||
|
||||
const mockPageSpeedCheck = {
|
||||
monitorId: "monitorId",
|
||||
bestPractices: 1,
|
||||
seo: 1,
|
||||
performance: 1,
|
||||
};
|
||||
|
||||
const mockDeletedResult = { deletedCount: 1 };
|
||||
|
||||
describe("pageSpeedCheckModule", () => {
|
||||
let pageSpeedCheckSaveStub, pageSpeedCheckDeleteManyStub;
|
||||
beforeEach(() => {
|
||||
pageSpeedCheckSaveStub = sinon.stub(PageSpeedCheck.prototype, "save");
|
||||
pageSpeedCheckDeleteManyStub = sinon.stub(PageSpeedCheck, "deleteMany");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("createPageSpeedCheck", () => {
|
||||
it("should return a page speed check", async () => {
|
||||
pageSpeedCheckSaveStub.resolves(mockPageSpeedCheck);
|
||||
const pageSpeedCheck = await createPageSpeedCheck(mockPageSpeedCheck);
|
||||
expect(pageSpeedCheck).to.deep.equal(mockPageSpeedCheck);
|
||||
});
|
||||
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
pageSpeedCheckSaveStub.rejects(err);
|
||||
try {
|
||||
await expect(createPageSpeedCheck(mockPageSpeedCheck));
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deletePageSpeedChecksByMonitorId", () => {
|
||||
it("should return the number of deleted checks", async () => {
|
||||
pageSpeedCheckDeleteManyStub.resolves(mockDeletedResult);
|
||||
const result = await deletePageSpeedChecksByMonitorId("monitorId");
|
||||
expect(result).to.deep.equal(mockDeletedResult.deletedCount);
|
||||
});
|
||||
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
pageSpeedCheckDeleteManyStub.rejects(err);
|
||||
try {
|
||||
await expect(deletePageSpeedChecksByMonitorId("monitorId"));
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
171
Server/tests/db/recoveryModule.test.js
Normal file
171
Server/tests/db/recoveryModule.test.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import sinon from "sinon";
|
||||
import RecoveryToken from "../../db/models/RecoveryToken.js";
|
||||
import User from "../../db/models/User.js";
|
||||
import {
|
||||
requestRecoveryToken,
|
||||
validateRecoveryToken,
|
||||
resetPassword,
|
||||
} from "../../db/mongo/modules/recoveryModule.js";
|
||||
import { errorMessages } from "../../utils/messages.js";
|
||||
|
||||
const mockRecoveryToken = {
|
||||
email: "test@test.com",
|
||||
token: "1234567890",
|
||||
};
|
||||
|
||||
const mockUser = {
|
||||
email: "test@test.com",
|
||||
password: "oldPassword",
|
||||
};
|
||||
|
||||
const mockUserWithoutPassword = {
|
||||
email: "test@test.com",
|
||||
};
|
||||
|
||||
// Create a query builder that logs
|
||||
const createQueryChain = (finalResult, comparePasswordResult = false) => ({
|
||||
select: () => ({
|
||||
select: async () => {
|
||||
if (finalResult === mockUser) {
|
||||
// Return a new object with all required methods
|
||||
return {
|
||||
email: "test@test.com",
|
||||
password: "oldPassword",
|
||||
comparePassword: sinon.stub().resolves(comparePasswordResult),
|
||||
save: sinon.stub().resolves(),
|
||||
};
|
||||
}
|
||||
return finalResult;
|
||||
},
|
||||
}),
|
||||
// Add methods to the top level too
|
||||
comparePassword: sinon.stub().resolves(comparePasswordResult),
|
||||
save: sinon.stub().resolves(),
|
||||
});
|
||||
|
||||
describe("recoveryModule", () => {
|
||||
let deleteManyStub,
|
||||
saveStub,
|
||||
findOneStub,
|
||||
userCompareStub,
|
||||
userSaveStub,
|
||||
userFindOneStub;
|
||||
let req, res;
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
body: { email: "test@test.com" },
|
||||
};
|
||||
deleteManyStub = sinon.stub(RecoveryToken, "deleteMany");
|
||||
saveStub = sinon.stub(RecoveryToken.prototype, "save");
|
||||
findOneStub = sinon.stub(RecoveryToken, "findOne");
|
||||
userCompareStub = sinon.stub(User.prototype, "comparePassword");
|
||||
userSaveStub = sinon.stub(User.prototype, "save");
|
||||
userFindOneStub = sinon.stub().resolves();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("requestRecoveryToken", () => {
|
||||
it("should return a recovery token", async () => {
|
||||
deleteManyStub.resolves();
|
||||
saveStub.resolves(mockRecoveryToken);
|
||||
const result = await requestRecoveryToken(req, res);
|
||||
expect(result.email).to.equal(mockRecoveryToken.email);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("Test error");
|
||||
deleteManyStub.rejects(err);
|
||||
try {
|
||||
await requestRecoveryToken(req, res);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("validateRecoveryToken", () => {
|
||||
it("should return a recovery token if found", async () => {
|
||||
findOneStub.resolves(mockRecoveryToken);
|
||||
const result = await validateRecoveryToken(req, res);
|
||||
expect(result).to.deep.equal(mockRecoveryToken);
|
||||
});
|
||||
it("should thrown an error if a token is not found", async () => {
|
||||
findOneStub.resolves(null);
|
||||
try {
|
||||
await validateRecoveryToken(req, res);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.message).to.equal(errorMessages.DB_TOKEN_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
it("should handle DB errors", async () => {
|
||||
const err = new Error("Test error");
|
||||
findOneStub.rejects(err);
|
||||
try {
|
||||
await validateRecoveryToken(req, res);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetPassword", () => {
|
||||
beforeEach(() => {
|
||||
req.body = {
|
||||
password: "test",
|
||||
newPassword: "test1",
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should thrown an error if a recovery token is not found", async () => {
|
||||
findOneStub.resolves(null);
|
||||
try {
|
||||
await resetPassword(req, res);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.message).to.equal(errorMessages.DB_TOKEN_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
it("should throw an error if a user is not found", async () => {
|
||||
findOneStub.resolves(mockRecoveryToken);
|
||||
userFindOneStub = sinon.stub(User, "findOne").resolves(null);
|
||||
try {
|
||||
await resetPassword(req, res);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
it("should throw an error if the passwords match", async () => {
|
||||
findOneStub.resolves(mockRecoveryToken);
|
||||
saveStub.resolves();
|
||||
userFindOneStub = sinon
|
||||
.stub(User, "findOne")
|
||||
.returns(createQueryChain(mockUser, true));
|
||||
try {
|
||||
await resetPassword(req, res);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.message).to.equal(errorMessages.DB_RESET_PASSWORD_BAD_MATCH);
|
||||
}
|
||||
});
|
||||
it("should return a user without password if successful", async () => {
|
||||
findOneStub.resolves(mockRecoveryToken);
|
||||
saveStub.resolves();
|
||||
userFindOneStub = sinon
|
||||
.stub(User, "findOne")
|
||||
.returns(createQueryChain(mockUser)) // First call will resolve to mockUser
|
||||
.onSecondCall()
|
||||
.returns(createQueryChain(mockUserWithoutPassword));
|
||||
const result = await resetPassword(req, res);
|
||||
expect(result).to.deep.equal(mockUserWithoutPassword);
|
||||
});
|
||||
});
|
||||
});
|
||||
54
Server/tests/db/settingsModule.test.js
Normal file
54
Server/tests/db/settingsModule.test.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
getAppSettings,
|
||||
updateAppSettings,
|
||||
} from "../../db/mongo/modules/settingsModule.js";
|
||||
import AppSettings from "../../db/models/AppSettings.js";
|
||||
|
||||
const mockAppSettings = {
|
||||
appName: "Test App",
|
||||
};
|
||||
|
||||
describe("SettingsModule", () => {
|
||||
let appSettingsFindOneStub, appSettingsFindOneAndUpdateStub;
|
||||
beforeEach(() => {
|
||||
appSettingsFindOneStub = sinon.stub(AppSettings, "findOne");
|
||||
appSettingsFindOneAndUpdateStub = sinon.stub(AppSettings, "findOneAndUpdate");
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("getAppSettings", () => {
|
||||
it("should return app settings", async () => {
|
||||
appSettingsFindOneStub.resolves(mockAppSettings);
|
||||
const result = await getAppSettings();
|
||||
expect(result).to.deep.equal(mockAppSettings);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("Test error");
|
||||
appSettingsFindOneStub.throws(err);
|
||||
try {
|
||||
await getAppSettings();
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("updateAppSettings", () => {
|
||||
it("should update app settings", async () => {
|
||||
appSettingsFindOneAndUpdateStub.resolves(mockAppSettings);
|
||||
const result = await updateAppSettings(mockAppSettings);
|
||||
expect(result).to.deep.equal(mockAppSettings);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("Test error");
|
||||
appSettingsFindOneAndUpdateStub.throws(err);
|
||||
try {
|
||||
await updateAppSettings(mockAppSettings);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
286
Server/tests/db/userModule.test.js
Normal file
286
Server/tests/db/userModule.test.js
Normal file
@@ -0,0 +1,286 @@
|
||||
import sinon from "sinon";
|
||||
import UserModel from "../../db/models/User.js";
|
||||
import TeamModel from "../../db/models/Team.js";
|
||||
import {
|
||||
insertUser,
|
||||
getUserByEmail,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
deleteTeam,
|
||||
deleteAllOtherUsers,
|
||||
getAllUsers,
|
||||
logoutUser,
|
||||
} from "../../db/mongo/modules/userModule.js";
|
||||
import { errorMessages } from "../../utils/messages.js";
|
||||
|
||||
const mockUser = {
|
||||
email: "test@test.com",
|
||||
password: "password",
|
||||
role: ["user"],
|
||||
};
|
||||
const mockSuperUser = {
|
||||
email: "test@test.com",
|
||||
password: "password",
|
||||
role: ["superadmin"],
|
||||
};
|
||||
const imageFile = {
|
||||
image: 1,
|
||||
};
|
||||
|
||||
describe("userModule", () => {
|
||||
let teamSaveStub,
|
||||
teamFindByIdAndDeleteStub,
|
||||
userSaveStub,
|
||||
userFindStub,
|
||||
userFindOneStub,
|
||||
userFindByIdAndUpdateStub,
|
||||
userFindByIdAndDeleteStub,
|
||||
userDeleteManyStub,
|
||||
userUpdateOneStub,
|
||||
generateAvatarImageStub,
|
||||
parseBooleanStub;
|
||||
beforeEach(() => {
|
||||
teamSaveStub = sinon.stub(TeamModel.prototype, "save");
|
||||
teamFindByIdAndDeleteStub = sinon.stub(TeamModel, "findByIdAndDelete");
|
||||
userSaveStub = sinon.stub(UserModel.prototype, "save");
|
||||
userFindStub = sinon.stub(UserModel, "find");
|
||||
userFindOneStub = sinon.stub(UserModel, "findOne");
|
||||
userFindByIdAndUpdateStub = sinon.stub(UserModel, "findByIdAndUpdate");
|
||||
userFindByIdAndDeleteStub = sinon.stub(UserModel, "findByIdAndDelete");
|
||||
userDeleteManyStub = sinon.stub(UserModel, "deleteMany");
|
||||
userUpdateOneStub = sinon.stub(UserModel, "updateOne");
|
||||
generateAvatarImageStub = sinon.stub().resolves({ image: 2 });
|
||||
parseBooleanStub = sinon.stub().returns(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("insertUser", () => {
|
||||
it("should insert a regular user", async () => {
|
||||
userSaveStub.resolves(mockUser);
|
||||
userFindOneStub.returns({
|
||||
select: sinon.stub().returns({
|
||||
select: sinon.stub().resolves(mockUser),
|
||||
}),
|
||||
});
|
||||
const result = await insertUser(mockUser, imageFile, generateAvatarImageStub);
|
||||
expect(result).to.deep.equal(mockUser);
|
||||
});
|
||||
it("should insert a superadmin user", async () => {
|
||||
userSaveStub.resolves(mockSuperUser);
|
||||
userFindOneStub.returns({
|
||||
select: sinon.stub().returns({
|
||||
select: sinon.stub().resolves(mockSuperUser),
|
||||
}),
|
||||
});
|
||||
const result = await insertUser(mockSuperUser, imageFile, generateAvatarImageStub);
|
||||
expect(result).to.deep.equal(mockSuperUser);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
userSaveStub.rejects(err);
|
||||
try {
|
||||
await insertUser(mockUser, imageFile, generateAvatarImageStub);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
it("should handle a duplicate key error", async () => {
|
||||
const err = new Error("test error");
|
||||
err.code = 11000;
|
||||
userSaveStub.rejects(err);
|
||||
try {
|
||||
await insertUser(mockUser, imageFile, generateAvatarImageStub);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("getUserByEmail", () => {
|
||||
it("should return a user", async () => {
|
||||
userFindOneStub.returns({
|
||||
select: sinon.stub().resolves(mockUser),
|
||||
});
|
||||
const result = await getUserByEmail(mockUser.email);
|
||||
expect(result).to.deep.equal(mockUser);
|
||||
});
|
||||
});
|
||||
describe("getUserByEmail", () => {
|
||||
it("throw an error if a user is not found", async () => {
|
||||
userFindOneStub.returns({
|
||||
select: sinon.stub().resolves(null),
|
||||
});
|
||||
try {
|
||||
await getUserByEmail(mockUser.email);
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("updateUser", () => {
|
||||
let req, res;
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
params: {
|
||||
userId: "testId",
|
||||
},
|
||||
body: {
|
||||
deleteProfileImage: "false",
|
||||
email: "test@test.com",
|
||||
},
|
||||
file: {
|
||||
buffer: "test",
|
||||
mimetype: "test",
|
||||
},
|
||||
};
|
||||
res = {};
|
||||
});
|
||||
|
||||
afterEach(() => {});
|
||||
it("should update a user", async () => {
|
||||
parseBooleanStub.returns(false);
|
||||
userFindByIdAndUpdateStub.returns({
|
||||
select: sinon.stub().returns({
|
||||
select: sinon.stub().resolves(mockUser),
|
||||
}),
|
||||
});
|
||||
const result = await updateUser(
|
||||
req,
|
||||
res,
|
||||
parseBooleanStub,
|
||||
generateAvatarImageStub
|
||||
);
|
||||
expect(result).to.deep.equal(mockUser);
|
||||
});
|
||||
it("should delete a user profile image", async () => {
|
||||
req.body.deleteProfileImage = "true";
|
||||
userFindByIdAndUpdateStub.returns({
|
||||
select: sinon.stub().returns({
|
||||
select: sinon.stub().resolves(mockUser),
|
||||
}),
|
||||
});
|
||||
const result = await updateUser(
|
||||
req,
|
||||
res,
|
||||
parseBooleanStub,
|
||||
generateAvatarImageStub
|
||||
);
|
||||
expect(result).to.deep.equal(mockUser);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
userFindByIdAndUpdateStub.throws(err);
|
||||
try {
|
||||
await updateUser(req, res, parseBooleanStub, generateAvatarImageStub);
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("deleteUser", async () => {
|
||||
it("should return a deleted user", async () => {
|
||||
userFindByIdAndDeleteStub.resolves(mockUser);
|
||||
const result = await deleteUser("testId");
|
||||
expect(result).to.deep.equal(mockUser);
|
||||
});
|
||||
it("should throw an error if a user is not found", async () => {
|
||||
try {
|
||||
await deleteUser("testId");
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
userFindByIdAndDeleteStub.throws(err);
|
||||
try {
|
||||
await deleteUser("testId");
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteTeam", () => {
|
||||
it("should return true if team deleted", async () => {
|
||||
teamFindByIdAndDeleteStub.resolves();
|
||||
const result = await deleteTeam("testId");
|
||||
expect(result).to.equal(true);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
teamFindByIdAndDeleteStub.throws(err);
|
||||
try {
|
||||
await deleteTeam("testId");
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteAllOtherUsers", () => {
|
||||
it("should return true if all other users deleted", async () => {
|
||||
userDeleteManyStub.resolves(true);
|
||||
const result = await deleteAllOtherUsers();
|
||||
expect(result).to.equal(true);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
userDeleteManyStub.throws(err);
|
||||
try {
|
||||
await deleteAllOtherUsers();
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllUsers", () => {
|
||||
it("should return all users", async () => {
|
||||
userFindStub.returns({
|
||||
select: sinon.stub().returns({
|
||||
select: sinon.stub().resolves([mockUser]),
|
||||
}),
|
||||
});
|
||||
const result = await getAllUsers();
|
||||
expect(result).to.deep.equal([mockUser]);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
userFindStub.throws(err);
|
||||
try {
|
||||
await getAllUsers();
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("logoutUser", async () => {
|
||||
it("should return true if user logged out", async () => {
|
||||
userUpdateOneStub.resolves(true);
|
||||
const result = await logoutUser("testId");
|
||||
expect(result).to.equal(true);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
const err = new Error("test error");
|
||||
userUpdateOneStub.throws(err);
|
||||
try {
|
||||
await logoutUser("testId");
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,14 @@ class QueueStub {
|
||||
async getJobs() {
|
||||
return this.jobs;
|
||||
}
|
||||
|
||||
async pause() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async obliterate() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class WorkerStub {
|
||||
|
||||
@@ -2,8 +2,9 @@ import sinon from "sinon";
|
||||
import NetworkService from "../../service/networkService.js";
|
||||
import { expect } from "chai";
|
||||
import http from "http";
|
||||
import { errorMessages } from "../../utils/messages.js";
|
||||
describe("Network Service", () => {
|
||||
let axios, ping, logger, networkService;
|
||||
let axios, ping, Docker, logger, networkService;
|
||||
|
||||
beforeEach(() => {
|
||||
axios = {
|
||||
@@ -12,6 +13,17 @@ describe("Network Service", () => {
|
||||
status: 200,
|
||||
}),
|
||||
};
|
||||
Docker = class {
|
||||
listContainers = sinon.stub().resolves([
|
||||
{
|
||||
Names: ["http://test.com"],
|
||||
Id: "http://test.com",
|
||||
},
|
||||
]);
|
||||
getContainer = sinon.stub().returns({
|
||||
inspect: sinon.stub().resolves({ State: { Status: "running" } }),
|
||||
});
|
||||
};
|
||||
ping = {
|
||||
promise: {
|
||||
probe: sinon
|
||||
@@ -20,7 +32,7 @@ describe("Network Service", () => {
|
||||
},
|
||||
};
|
||||
logger = { error: sinon.stub() };
|
||||
networkService = new NetworkService(axios, ping, logger, http);
|
||||
networkService = new NetworkService(axios, ping, logger, http, Docker);
|
||||
});
|
||||
describe("constructor", () => {
|
||||
it("should create a new NetworkService instance", () => {
|
||||
@@ -70,6 +82,18 @@ describe("Network Service", () => {
|
||||
expect(pingResult.status).to.be.false;
|
||||
expect(pingResult.code).to.equal(networkService.PING_ERROR);
|
||||
});
|
||||
it("should throw an error if ping cannot resolve", async () => {
|
||||
const error = new Error("test error");
|
||||
networkService.timeRequest = sinon.stub().throws(error);
|
||||
try {
|
||||
await networkService.requestPing({
|
||||
data: { url: "http://test.com", _id: "123" },
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.method).to.equal("requestPing");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("requestHttp", () => {
|
||||
it("should return a response object if http successful", async () => {
|
||||
@@ -108,6 +132,18 @@ describe("Network Service", () => {
|
||||
expect(httpResult.status).to.be.false;
|
||||
expect(httpResult.code).to.equal(networkService.NETWORK_ERROR);
|
||||
});
|
||||
it("should throw an error if an error occurs", async () => {
|
||||
const error = new Error("test error");
|
||||
networkService.timeRequest = sinon.stub().throws(error);
|
||||
try {
|
||||
await networkService.requestHttp({
|
||||
data: { url: "http://test.com", _id: "123" },
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.method).to.equal("requestHttp");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestPagespeed", () => {
|
||||
@@ -147,6 +183,18 @@ describe("Network Service", () => {
|
||||
expect(pagespeedResult.status).to.be.false;
|
||||
expect(pagespeedResult.code).to.equal(networkService.NETWORK_ERROR);
|
||||
});
|
||||
it("should throw an error if pagespeed cannot resolve", async () => {
|
||||
const error = new Error("test error");
|
||||
networkService.timeRequest = sinon.stub().throws(error);
|
||||
try {
|
||||
await networkService.requestPagespeed({
|
||||
data: { url: "http://test.com", _id: "123" },
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.method).to.equal("requestPagespeed");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestHardware", () => {
|
||||
@@ -201,6 +249,99 @@ describe("Network Service", () => {
|
||||
expect(httpResult.status).to.be.false;
|
||||
expect(httpResult.code).to.equal(networkService.NETWORK_ERROR);
|
||||
});
|
||||
it("should throw an error if hardware cannot resolve", async () => {
|
||||
const error = new Error("test error");
|
||||
networkService.timeRequest = sinon.stub().throws(error);
|
||||
try {
|
||||
await networkService.requestHardware({
|
||||
data: { url: "http://test.com", _id: "123" },
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error).to.exist;
|
||||
expect(error.method).to.equal("requestHardware");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestDocker", () => {
|
||||
it("should return a response object if docker successful", async () => {
|
||||
const job = { data: { url: "http://test.com", _id: "123", type: "docker" } };
|
||||
const dockerResult = await networkService.requestDocker(job);
|
||||
expect(dockerResult.monitorId).to.equal("123");
|
||||
expect(dockerResult.type).to.equal("docker");
|
||||
expect(dockerResult.responseTime).to.be.a("number");
|
||||
expect(dockerResult.status).to.be.true;
|
||||
});
|
||||
|
||||
it("should return a response object with status false if container not running", async () => {
|
||||
Docker = class {
|
||||
listContainers = sinon.stub().resolves([
|
||||
{
|
||||
Names: ["/my_container"],
|
||||
Id: "abc123",
|
||||
},
|
||||
]);
|
||||
getContainer = sinon.stub().returns({
|
||||
inspect: sinon.stub().resolves({ State: { Status: "stopped" } }),
|
||||
});
|
||||
};
|
||||
networkService = new NetworkService(axios, ping, logger, http, Docker);
|
||||
const job = { data: { url: "abc123", _id: "123", type: "docker" } };
|
||||
const dockerResult = await networkService.requestDocker(job);
|
||||
expect(dockerResult.status).to.be.false;
|
||||
expect(dockerResult.code).to.equal(200);
|
||||
});
|
||||
|
||||
it("should handle an error when fetching the container", async () => {
|
||||
Docker = class {
|
||||
listContainers = sinon.stub().resolves([
|
||||
{
|
||||
Names: ["/my_container"],
|
||||
Id: "abc123",
|
||||
},
|
||||
]);
|
||||
getContainer = sinon.stub().returns({
|
||||
inspect: sinon.stub().throws(new Error("test error")),
|
||||
});
|
||||
};
|
||||
networkService = new NetworkService(axios, ping, logger, http, Docker);
|
||||
const job = { data: { url: "abc123", _id: "123", type: "docker" } };
|
||||
const dockerResult = await networkService.requestDocker(job);
|
||||
expect(dockerResult.status).to.be.false;
|
||||
expect(dockerResult.code).to.equal(networkService.NETWORK_ERROR);
|
||||
});
|
||||
|
||||
it("should throw an error if operations fail", async () => {
|
||||
Docker = class {
|
||||
listContainers = sinon.stub().resolves([
|
||||
{
|
||||
Names: ["/my_container"],
|
||||
Id: "abc123",
|
||||
},
|
||||
]);
|
||||
getContainer = sinon.stub().throws(new Error("test error"));
|
||||
};
|
||||
networkService = new NetworkService(axios, ping, logger, http, Docker);
|
||||
const job = { data: { url: "abc123", _id: "123", type: "docker" } };
|
||||
try {
|
||||
await networkService.requestDocker(job);
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("test error");
|
||||
}
|
||||
});
|
||||
it("should throw an error if no matching images found", async () => {
|
||||
Docker = class {
|
||||
listContainers = sinon.stub().resolves([]);
|
||||
getContainer = sinon.stub().throws(new Error("test error"));
|
||||
};
|
||||
networkService = new NetworkService(axios, ping, logger, http, Docker);
|
||||
const job = { data: { url: "abc123", _id: "123", type: "docker" } };
|
||||
try {
|
||||
await networkService.requestDocker(job);
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal(errorMessages.DOCKER_NOT_FOUND);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatus", () => {
|
||||
@@ -209,39 +350,81 @@ describe("Network Service", () => {
|
||||
networkService.requestHttp = sinon.stub();
|
||||
networkService.requestPagespeed = sinon.stub();
|
||||
networkService.requestHardware = sinon.stub();
|
||||
networkService.requestDocker = sinon.stub();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
it("should call requestPing if type is ping", () => {
|
||||
networkService.getStatus({ data: { type: "ping" } });
|
||||
it("should call requestPing if type is ping", async () => {
|
||||
await networkService.getStatus({ data: { type: "ping" } });
|
||||
expect(networkService.requestPing.calledOnce).to.be.true;
|
||||
expect(networkService.requestDocker.notCalled).to.be.true;
|
||||
expect(networkService.requestHttp.notCalled).to.be.true;
|
||||
expect(networkService.requestPagespeed.notCalled).to.be.true;
|
||||
});
|
||||
it("should call requestHttp if type is http", () => {
|
||||
networkService.getStatus({ data: { type: "http" } });
|
||||
it("should call requestHttp if type is http", async () => {
|
||||
await networkService.getStatus({ data: { type: "http" } });
|
||||
expect(networkService.requestPing.notCalled).to.be.true;
|
||||
expect(networkService.requestDocker.notCalled).to.be.true;
|
||||
expect(networkService.requestHttp.calledOnce).to.be.true;
|
||||
expect(networkService.requestPagespeed.notCalled).to.be.true;
|
||||
});
|
||||
it("should call requestPagespeed if type is pagespeed", () => {
|
||||
networkService.getStatus({ data: { type: "pagespeed" } });
|
||||
it("should call requestPagespeed if type is pagespeed", async () => {
|
||||
await networkService.getStatus({ data: { type: "pagespeed" } });
|
||||
expect(networkService.requestPing.notCalled).to.be.true;
|
||||
expect(networkService.requestDocker.notCalled).to.be.true;
|
||||
expect(networkService.requestHttp.notCalled).to.be.true;
|
||||
expect(networkService.requestPagespeed.calledOnce).to.be.true;
|
||||
});
|
||||
it("should call requestHardware if type is hardware", () => {
|
||||
networkService.getStatus({ data: { type: "hardware" } });
|
||||
it("should call requestHardware if type is hardware", async () => {
|
||||
await networkService.getStatus({ data: { type: "hardware" } });
|
||||
expect(networkService.requestHardware.calledOnce).to.be.true;
|
||||
expect(networkService.requestDocker.notCalled).to.be.true;
|
||||
expect(networkService.requestPing.notCalled).to.be.true;
|
||||
expect(networkService.requestPagespeed.notCalled).to.be.true;
|
||||
});
|
||||
it("should log an error if an unknown type is provided", () => {
|
||||
networkService.getStatus({ data: { type: "unknown" } });
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.args[0][0].message).to.equal("Unsupported type: unknown");
|
||||
it("should call requestDocker if type is Docker", async () => {
|
||||
await networkService.getStatus({ data: { type: "docker" } });
|
||||
expect(networkService.requestDocker.calledOnce).to.be.true;
|
||||
expect(networkService.requestHardware.notCalled).to.be.true;
|
||||
expect(networkService.requestPing.notCalled).to.be.true;
|
||||
expect(networkService.requestPagespeed.notCalled).to.be.true;
|
||||
});
|
||||
it("should throw an error if an unknown type is provided", async () => {
|
||||
try {
|
||||
await networkService.getStatus({ data: { type: "unknown" } });
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("NetworkService");
|
||||
expect(error.method).to.equal("getStatus");
|
||||
expect(error.message).to.equal("Unsupported type: unknown");
|
||||
}
|
||||
});
|
||||
it("should throw an error if job type is undefined", async () => {
|
||||
try {
|
||||
await networkService.getStatus({ data: { type: undefined } });
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("NetworkService");
|
||||
expect(error.method).to.equal("getStatus");
|
||||
expect(error.message).to.equal("Unsupported type: unknown");
|
||||
}
|
||||
});
|
||||
it("should throw an error if job is empty", async () => {
|
||||
try {
|
||||
await networkService.getStatus({});
|
||||
} catch (error) {
|
||||
expect(error.method).to.equal("getStatus");
|
||||
expect(error.message).to.equal("Unsupported type: unknown");
|
||||
}
|
||||
});
|
||||
it("should throw an error if job is null", async () => {
|
||||
try {
|
||||
await networkService.getStatus(null);
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("NetworkService");
|
||||
expect(error.method).to.equal("getStatus");
|
||||
expect(error.message).to.equal("Unsupported type: unknown");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -182,7 +194,7 @@ describe("StatusService", () => {
|
||||
responseTime: 100,
|
||||
code: 200,
|
||||
message: "Test message",
|
||||
payload: { cpu: "cpu", memory: "memory", disk: "disk", host: "host" },
|
||||
payload: { data: { cpu: "cpu", memory: "memory", disk: "disk", host: "host" } },
|
||||
});
|
||||
expect(check.monitorId).to.equal("test");
|
||||
expect(check.status).to.be.true;
|
||||
|
||||
@@ -46,6 +46,26 @@ describe("Logger", () => {
|
||||
const logger = new Logger();
|
||||
logger.logger.info(logMessage);
|
||||
});
|
||||
it("should convert details to JSON string if it is an object", function () {
|
||||
const logDetails = { key: "value" };
|
||||
const expectedDetails = JSON.stringify(logDetails, null, 2); // Removed .s
|
||||
|
||||
createLoggerStub.callsFake((config) => {
|
||||
const consoleTransport = config.transports[0];
|
||||
const logEntry = {
|
||||
level: "info",
|
||||
message: "", // Add empty message since it's required
|
||||
details: logDetails,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const formattedMessage = consoleTransport.format.transform(logEntry);
|
||||
expect(formattedMessage).to.include(expectedDetails);
|
||||
return { info: sinon.spy() }; // Changed to return info method
|
||||
});
|
||||
|
||||
const logger = new Logger();
|
||||
logger.logger.info("", { details: logDetails }); // Updated to pass details properly
|
||||
});
|
||||
});
|
||||
|
||||
describe("info", () => {
|
||||
|
||||
@@ -55,6 +55,10 @@ const errorMessages = {
|
||||
// Status Page Errors
|
||||
STATUS_PAGE_NOT_FOUND: "Status page not found",
|
||||
STATUS_PAGE_URL_NOT_UNIQUE: "Status page url must be unique",
|
||||
|
||||
// Docker
|
||||
DOCKER_FAIL: "Failed to fetch Docker container information",
|
||||
DOCKER_NOT_FOUND: "Docker container not found",
|
||||
};
|
||||
|
||||
const successMessages = {
|
||||
@@ -123,6 +127,9 @@ const successMessages = {
|
||||
// Status Page
|
||||
STATUS_PAGE_BY_URL: "Got status page by url successfully",
|
||||
STATUS_PAGE_CREATE: "Status page created successfully",
|
||||
|
||||
// Docker
|
||||
DOCKER_SUCCESS: "Docker container status fetched successfully",
|
||||
};
|
||||
|
||||
export { errorMessages, successMessages };
|
||||
|
||||
@@ -144,8 +144,8 @@ const getMonitorsAndSummaryByTeamIdQueryValidation = joi.object({
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.string().valid("http", "ping", "pagespeed"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed"))
|
||||
joi.string().valid("http", "ping", "pagespeed", "docker"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker"))
|
||||
),
|
||||
});
|
||||
|
||||
@@ -161,8 +161,8 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.string().valid("http", "ping", "pagespeed"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed"))
|
||||
joi.string().valid("http", "ping", "pagespeed", "docker"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker"))
|
||||
),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
---
|
||||
icon: sign-posts-wrench
|
||||
---
|
||||
|
||||
# Installing Uptime Manager
|
||||
|
||||
## Quickstart for users (quick method) <a href="#user-quickstart" id="user-quickstart"></a>
|
||||
|
||||
1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/blob/develop/Docker/dist/docker-compose.yaml)
|
||||
2. Run `docker compose up` to start the application
|
||||
3. Now the application is running at `http://localhost`
|
||||
|
||||
---
|
||||
|
||||
## Quickstart for developers <a href="#dev-quickstart" id="dev-quickstart"></a>
|
||||
|
||||
{% hint style="info" %}
|
||||
Make sure you change the directory to the specified directories, as paths in commands are relative.
|
||||
{% endhint %}
|
||||
|
||||
### Cloning and initial setup
|
||||
|
||||
1. Clone this repository.
|
||||
2. Checkout the `develop` branch `git checkout develop`
|
||||
|
||||
### Setting up Docker images
|
||||
|
||||
3. Change directory to the `Docker/dev` directory
|
||||
4. Build the docker images by running the `build_images.sh` script
|
||||
5. Run `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
|
||||
6. Run `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
|
||||
|
||||
### Server setup
|
||||
|
||||
6. CD to `Server` directory, and run `npm install`
|
||||
7. While in `Server` directory, create a `.env` file with the [required environmental variables](#env-vars-server)
|
||||
8. While in the `Server` directory, run `npm run dev`
|
||||
|
||||
### Client setup
|
||||
|
||||
9. CD to `Client` directory `run npm install`
|
||||
10. While in the `Client` directory, create a `.env` file with the [required environmental variables](#env-vars-client)
|
||||
11. While in the `Client` directory run `npm run dev`
|
||||
|
||||
### Access the application
|
||||
|
||||
12. Client is now running at `localhost:5173`
|
||||
13. Server is now running at `localhost:5000`
|
||||
|
||||
---
|
||||
|
||||
## Manual installation <a href="#manual-install" id="manual-install"></a>
|
||||
|
||||
### Client installation <a href="#install-client" id="install-client"></a>
|
||||
|
||||
1. Change directory to the `Client` directory
|
||||
2. Install all dependencies by running `npm install`
|
||||
3. Add a `.env` file to the `Client` directory with the following options:
|
||||
|
||||
#### Environment variables <a href="#env-vars-client" id="env-vars-client"></a>
|
||||
|
||||
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
|
||||
| --------------------- | ----------------- | --------- | ------------------ | ---------------------------------- |
|
||||
| VITE_APP_API_BASE_URL | Required | `string` | Base URL of server | {host}/api/v1 |
|
||||
| VITE_APP_LOG_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| |
|
||||
| VITE_APP_DEMO | Optional | `boolean` | Demo server or not | `true`\|`false` \| |
|
||||
|
||||
#### Starting the Client development server <a href="#start-client" id="start-client"></a>
|
||||
|
||||
1. Run `npm run dev` to start the development server.
|
||||
|
||||
### Server Installation <a href="#install-server" id="install-server"></a>
|
||||
|
||||
1. Change the directory to the `Server` directory
|
||||
2. Install all dependencies by running `npm install`
|
||||
3. Add a `.env` file to the `Server` directory with the following options:
|
||||
|
||||
#### Environment variables <a href="#env-vars-server" id="env-vars-server"></a>
|
||||
|
||||
Configure the server with the following environmental variables:
|
||||
|
||||
<table><thead><tr><th width="239">ENV Variable Name</th><th width="149">Required/Optional</th><th width="116">Type</th><th>Description</th><th>Accepted Values</th></tr></thead><tbody><tr><td>CLIENT_HOST</td><td>Required</td><td><code>string</code></td><td>Frontend Host</td><td></td></tr><tr><td>JWT_SECRET</td><td>Required</td><td><code>string</code></td><td>JWT secret</td><td></td></tr><tr><td>DB_TYPE</td><td>Optional</td><td><code>string</code></td><td>Specify DB to use</td><td><code>MongoDB | FakeDB</code></td></tr><tr><td>DB_CONNECTION_STRING</td><td>Required</td><td><code>string</code></td><td>Specifies URL for MongoDB Database</td><td></td></tr><tr><td>PORT</td><td>Optional</td><td><code>integer</code></td><td>Specifies Port for Server</td><td></td></tr><tr><td>LOGIN_PAGE_URL</td><td>Required</td><td><code>string</code></td><td>Login url to be used in emailing service</td><td></td></tr><tr><td>REDIS_HOST</td><td>Required</td><td><code>string</code></td><td>Host address for Redis database</td><td></td></tr><tr><td>REDIS_PORT</td><td>Required</td><td><code>integer</code></td><td>Port for Redis database</td><td></td></tr><tr><td>TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for token to live</td><td>In vercel/ms format https://github.com/vercel/ms</td></tr><tr><td>PAGESPEED_API_KEY</td><td>Optional</td><td><code>string</code></td><td>API Key for PageSpeed requests</td><td></td></tr><tr><td>SYSTEM_EMAIL_HOST</td><td>Required</td><td><code>string</code></td><td>Host to send System Emails From</td><td></td></tr><tr><td>SYSTEM_EMAIL_PORT</td><td>Required</td><td><code>number</code></td><td>Port for System Email Host</td><td></td></tr><tr><td>SYSTEM_EMAIL_ADDRESS</td><td>Required</td><td><code>string</code></td><td>System Email Address</td><td></td></tr><tr><td>SYSTEM_EMAIL_PASSWORD</td><td>Required</td><td><code>string</code></td><td>System Email Password</td><td></td></tr></tbody></table>
|
||||
|
||||
---
|
||||
|
||||
#### Databases <a href="#databases" id="databases"></a>
|
||||
|
||||
This project requires two databases:
|
||||
|
||||
1. **Main application database:** The project uses MongoDB for its primary database, with a MongoDB Docker image provided for easy setup.
|
||||
2. **Redis for queue management:** A Redis database is used for the PingService’s queue system, and a Redis Docker image is included for deployment.
|
||||
|
||||
You may use the included Dockerfiles to spin up databases quickly if you wish.
|
||||
|
||||
**(Optional) Dockerised databases**
|
||||
|
||||
Dockerfiles for the server and databases are located in the `Docker` directory
|
||||
|
||||
<details>
|
||||
|
||||
<summary>MongoDB Image</summary>
|
||||
|
||||
Location: `Docker/mongoDB.Dockerfile`
|
||||
|
||||
The `Docker/mongo/data` directory should be mounted to the MongoDB container in order to persist data.
|
||||
|
||||
From the `Docker` directory run
|
||||
|
||||
1. Build the image: `docker build -f mongoDB.Dockerfile -t uptime_database_mongo .`
|
||||
2. Run the docker image: `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Redis Image</summary>
|
||||
|
||||
Location `Docker/redis.Dockerfile`
|
||||
|
||||
the `Docker/redis/data` directory should be mounted to the Redis container in order to persist data.
|
||||
|
||||
From the `Docker` directory run
|
||||
|
||||
1. Build the image: `docker build -f redis.Dockerfile -t uptime_redis .`
|
||||
2. Run the image: `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
#### Starting the development server <a href="#start-server" id="start-server"></a>
|
||||
|
||||
- run `npm run dev` to start the development server
|
||||
|
||||
or,
|
||||
|
||||
- run `node index.js` to start server
|
||||
|
||||
---
|
||||
|
||||
### API documentation <a href="#api-documentation" id="api-documentation"></a>
|
||||
|
||||
Our API is documented in accordance with the [OpenAPI spec](https://www.openapis.org/).
|
||||
|
||||
You can see the documentation on your local development server at http://localhost:{port}/api-docs
|
||||
|
||||
You can also view the documentation on our demo server at [https://uptime-demo.bluewavelabs.ca/api-docs](https://uptime-demo.bluewavelabs.ca/api-docs)
|
||||
|
||||
### Error handling
|
||||
|
||||
Errors are returned in a standard format:
|
||||
|
||||
`{"success": false, "msg": "No token provided"}`
|
||||
|
||||
Errors are handled by error handling middleware and should be thrown with the following parameters
|
||||
|
||||
| Name | Type | Default | Notes |
|
||||
| ------- | --------- | ---------------------- | ------------------------------------ |
|
||||
| status | `integer` | 500 | Standard HTTP codes |
|
||||
| message | `string` | "Something went wrong" | An error message |
|
||||
| service | `string` | "Unknown Service" | Name of service that threw the error |
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
const myRoute = async(req, res, next) => {
|
||||
try{
|
||||
const result = myRiskyOperationHere();
|
||||
}
|
||||
catch(error){
|
||||
error.status = 404
|
||||
error.message = "Resource not found"
|
||||
error.service = service name
|
||||
next(error)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Errors should not be handled at the controller level and should be left to the middleware to handle.
|
||||
@@ -1,181 +0,0 @@
|
||||
---
|
||||
icon: sign-posts-wrench
|
||||
---
|
||||
|
||||
# Installing Uptime Manager
|
||||
|
||||
## Quickstart for users (quick method) <a href="#user-quickstart" id="user-quickstart"></a>
|
||||
|
||||
1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/blob/develop/Docker/dist/docker-compose.yaml)
|
||||
2. Run `docker compose up` to start the application
|
||||
3. Now the application is running at `http://localhost`
|
||||
|
||||
---
|
||||
|
||||
## Quickstart for developers <a href="#dev-quickstart" id="dev-quickstart"></a>
|
||||
|
||||
{% hint style="info" %}
|
||||
Make sure you change the directory to the specified directories, as paths in commands are relative.
|
||||
{% endhint %}
|
||||
|
||||
### Cloning and initial setup
|
||||
|
||||
1. Clone this repository.
|
||||
2. Checkout the `develop` branch `git checkout develop`
|
||||
|
||||
### Setting up Docker images
|
||||
|
||||
3. Change directory to the `Docker/dev` directory
|
||||
4. Build the docker images by running the `build_images.sh` script
|
||||
5. Run `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
|
||||
6. Run `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
|
||||
|
||||
### Server setup
|
||||
|
||||
6. CD to `Server` directory, and run `npm install`
|
||||
7. While in `Server` directory, create a `.env` file with the [required environmental variables](#env-vars-server)
|
||||
8. While in the `Server` directory, run `npm run dev`
|
||||
|
||||
### Client setup
|
||||
|
||||
9. CD to `Client` directory `run npm install`
|
||||
10. While in the `Client` directory, create a `.env` file with the [required environmental variables](#env-vars-client)
|
||||
11. While in the `Client` directory run `npm run dev`
|
||||
|
||||
### Access the application
|
||||
|
||||
12. Client is now running at `localhost:5173`
|
||||
13. Server is now running at `localhost:5000`
|
||||
|
||||
---
|
||||
|
||||
## Manual installation <a href="#manual-install" id="manual-install"></a>
|
||||
|
||||
### Client installation <a href="#install-client" id="install-client"></a>
|
||||
|
||||
1. Change directory to the `Client` directory
|
||||
2. Install all dependencies by running `npm install`
|
||||
3. Add a `.env` file to the `Client` directory with the following options:
|
||||
|
||||
#### Environment variables <a href="#env-vars-client" id="env-vars-client"></a>
|
||||
|
||||
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
|
||||
| --------------------- | ----------------- | --------- | ------------------ | ---------------------------------- |
|
||||
| VITE_APP_API_BASE_URL | Required | `string` | Base URL of server | {host}/api/v1 |
|
||||
| VITE_APP_LOG_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| |
|
||||
| VITE_APP_DEMO | Optional | `boolean` | Demo server or not | `true`\|`false` \| |
|
||||
|
||||
#### Starting the Client development server <a href="#start-client" id="start-client"></a>
|
||||
|
||||
1. Run `npm run dev` to start the development server.
|
||||
|
||||
### Server Installation <a href="#install-server" id="install-server"></a>
|
||||
|
||||
1. Change the directory to the `Server` directory
|
||||
2. Install all dependencies by running `npm install`
|
||||
3. Add a `.env` file to the `Server` directory with the following options:
|
||||
|
||||
#### Environment variables <a href="#env-vars-server" id="env-vars-server"></a>
|
||||
|
||||
Configure the server with the following environmental variables:
|
||||
|
||||
<table><thead><tr><th width="239">ENV Variable Name</th><th width="149">Required/Optional</th><th width="116">Type</th><th>Description</th><th>Accepted Values</th></tr></thead><tbody><tr><td>CLIENT_HOST</td><td>Required</td><td><code>string</code></td><td>Frontend Host</td><td></td></tr><tr><td>JWT_SECRET</td><td>Required</td><td><code>string</code></td><td>JWT secret</td><td></td></tr><tr><td>DB_TYPE</td><td>Optional</td><td><code>string</code></td><td>Specify DB to use</td><td><code>MongoDB | FakeDB</code></td></tr><tr><td>DB_CONNECTION_STRING</td><td>Required</td><td><code>string</code></td><td>Specifies URL for MongoDB Database</td><td></td></tr><tr><td>PORT</td><td>Optional</td><td><code>integer</code></td><td>Specifies Port for Server</td><td></td></tr><tr><td>LOGIN_PAGE_URL</td><td>Required</td><td><code>string</code></td><td>Login url to be used in emailing service</td><td></td></tr><tr><td>REDIS_HOST</td><td>Required</td><td><code>string</code></td><td>Host address for Redis database</td><td></td></tr><tr><td>REDIS_PORT</td><td>Required</td><td><code>integer</code></td><td>Port for Redis database</td><td></td></tr><tr><td>TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for token to live</td><td>In vercel/ms format https://github.com/vercel/ms</td></tr><tr><td>PAGESPEED_API_KEY</td><td>Optional</td><td><code>string</code></td><td>API Key for PageSpeed requests</td><td></td></tr><tr><td>SYSTEM_EMAIL_HOST</td><td>Required</td><td><code>string</code></td><td>Host to send System Emails From</td><td></td></tr><tr><td>SYSTEM_EMAIL_PORT</td><td>Required</td><td><code>number</code></td><td>Port for System Email Host</td><td></td></tr><tr><td>SYSTEM_EMAIL_ADDRESS</td><td>Required</td><td><code>string</code></td><td>System Email Address</td><td></td></tr><tr><td>SYSTEM_EMAIL_PASSWORD</td><td>Required</td><td><code>string</code></td><td>System Email Password</td><td></td></tr></tbody></table>
|
||||
|
||||
---
|
||||
|
||||
#### Databases <a href="#databases" id="databases"></a>
|
||||
|
||||
This project requires two databases:
|
||||
|
||||
1. **Main application database:** The project uses MongoDB for its primary database, with a MongoDB Docker image provided for easy setup.
|
||||
2. **Redis for queue management:** A Redis database is used for the PingService’s queue system, and a Redis Docker image is included for deployment.
|
||||
|
||||
You may use the included Dockerfiles to spin up databases quickly if you wish.
|
||||
|
||||
**(Optional) Dockerised databases**
|
||||
|
||||
Dockerfiles for the server and databases are located in the `Docker` directory
|
||||
|
||||
<details>
|
||||
|
||||
<summary>MongoDB Image</summary>
|
||||
|
||||
Location: `Docker/mongoDB.Dockerfile`
|
||||
|
||||
The `Docker/mongo/data` directory should be mounted to the MongoDB container in order to persist data.
|
||||
|
||||
From the `Docker` directory run
|
||||
|
||||
1. Build the image: `docker build -f mongoDB.Dockerfile -t uptime_database_mongo .`
|
||||
2. Run the docker image: `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Redis Image</summary>
|
||||
|
||||
Location `Docker/redis.Dockerfile`
|
||||
|
||||
the `Docker/redis/data` directory should be mounted to the Redis container in order to persist data.
|
||||
|
||||
From the `Docker` directory run
|
||||
|
||||
1. Build the image: `docker build -f redis.Dockerfile -t uptime_redis .`
|
||||
2. Run the image: `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
#### Starting the development server <a href="#start-server" id="start-server"></a>
|
||||
|
||||
- run `npm run dev` to start the development server
|
||||
|
||||
or,
|
||||
|
||||
- run `node index.js` to start server
|
||||
|
||||
---
|
||||
|
||||
### API documentation <a href="#api-documentation" id="api-documentation"></a>
|
||||
|
||||
Our API is documented in accordance with the [OpenAPI spec](https://www.openapis.org/).
|
||||
|
||||
You can see the documentation on your local development server at http://localhost:{port}/api-docs
|
||||
|
||||
You can also view the documentation on our demo server at [https://uptime-demo.bluewavelabs.ca/api-docs](https://uptime-demo.bluewavelabs.ca/api-docs)
|
||||
|
||||
### Error handling
|
||||
|
||||
Errors are returned in a standard format:
|
||||
|
||||
`{"success": false, "msg": "No token provided"}`
|
||||
|
||||
Errors are handled by error handling middleware and should be thrown with the following parameters
|
||||
|
||||
| Name | Type | Default | Notes |
|
||||
| ------- | --------- | ---------------------- | ------------------------------------ |
|
||||
| status | `integer` | 500 | Standard HTTP codes |
|
||||
| message | `string` | "Something went wrong" | An error message |
|
||||
| service | `string` | "Unknown Service" | Name of service that threw the error |
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
const myRoute = async(req, res, next) => {
|
||||
try{
|
||||
const result = myRiskyOperationHere();
|
||||
}
|
||||
catch(error){
|
||||
error.status = 404
|
||||
error.message = "Resource not found"
|
||||
error.service = service name
|
||||
next(error)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Errors should not be handled at the controller level and should be left to the middleware to handle.
|
||||
@@ -8,24 +8,25 @@ Creating a new monitor involves a few steps, mentioned below. 
|
||||
|
||||
### General settings
|
||||
|
||||
* **URL to monitor:** Enter the full URL of the website or service you want to monitor.
|
||||
* **Display name:** Optionally, provide a custom name for your monitor. This helps identify it easily in your dashboard.
|
||||
- **URL to monitor:** Enter the full URL of the website or service you want to monitor.
|
||||
- **Display name:** Optionally, provide a custom name for your monitor. This helps identify it easily in your dashboard.
|
||||
|
||||
### Checks to perform
|
||||
|
||||
* **Website monitoring:** This option uses HTTP(s) to monitor your website or API endpoint. You can choose between HTTPS and HTTP protocols.
|
||||
* **Ping monitoring:** Checks whether your server is available. This option is currently unselected.
|
||||
- **Website monitoring:** This option uses HTTP(s) to monitor your website or API endpoint. You can choose between HTTPS and HTTP protocols.
|
||||
- **Ping monitoring:** Checks whether your server is available. This option is currently unselected.
|
||||
- **Docker monitoring:** Checks whether a Docker container is running
|
||||
|
||||
### Incident notifications 
|
||||
|
||||
When there's a new incident, you can choose how to be notified:
|
||||
|
||||
* Notify via SMS (coming soon)
|
||||
* Notify via email (to the email address you logged in with)
|
||||
* Notify via email to multiple addresses (coming soon)
|
||||
- Notify via SMS (coming soon)
|
||||
- Notify via email (to the email address you logged in with)
|
||||
- Notify via email to multiple addresses (coming soon)
|
||||
|
||||
### Advanced settings
|
||||
|
||||
* **Check frequency:** Set how often the system should check your monitor. The current setting is 1 minute.
|
||||
- **Check frequency:** Set how often the system should check your monitor. The current setting is 1 minute.
|
||||
|
||||
After configuring all settings, click the "Create monitor" button at the bottom right to set up your new monitor.
|
||||
|
||||
@@ -6,10 +6,21 @@ icon: sign-posts-wrench
|
||||
|
||||
## Quickstart for users (quick method) <a href="#user-quickstart" id="user-quickstart"></a>
|
||||
|
||||
1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/blob/develop/Docker/dist/docker-compose.yaml)
|
||||
1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/raw/refs/heads/master/Docker/dist/docker-compose.yaml)
|
||||
2. Run `docker compose up` to start the application
|
||||
3. Now the application is running at `http://localhost`
|
||||
|
||||
##### Optional Config:
|
||||
|
||||
- If you want to monitor Docker containers, uncomment this line in `docker-compose.yaml`:
|
||||
|
||||
```
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
```
|
||||
|
||||
This gives the app access to your docker daemon via unix socket, please be aware of what you are doing.
|
||||
|
||||
---
|
||||
|
||||
## Quickstart for users (remote server) <a href="#user-quickstart" id="user-quickstart"></a>
|
||||
@@ -19,6 +30,17 @@ icon: sign-posts-wrench
|
||||
3. Run `docker compose up` to start the application
|
||||
4. Now the application is running at `http://<remote_server_ip>`
|
||||
|
||||
##### Optional Config:
|
||||
|
||||
- If you want to monitor Docker containers, uncomment this line in `docker-compose.yaml`:
|
||||
|
||||
```
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
```
|
||||
|
||||
This gives the app access to your docker daemon via unix socket, please be aware of what you are doing.
|
||||
|
||||
---
|
||||
|
||||
## Quickstart for developers <a href="#dev-quickstart" id="dev-quickstart"></a>
|
||||
@@ -74,6 +96,13 @@ Make sure you change the directory to the specified directories, as paths in com
|
||||
| VITE_APP_LOG_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| |
|
||||
| VITE_APP_DEMO | Optional | `boolean` | Demo server or not | `true`\|`false` \| |
|
||||
|
||||
Sample ENV file:
|
||||
|
||||
```
|
||||
VITE_APP_API_BASE_URL="http://localhost:5000/api/v1"
|
||||
VITE_APP_LOG_LEVEL="debug"
|
||||
```
|
||||
|
||||
#### Starting the Client development server <a href="#start-client" id="start-client"></a>
|
||||
|
||||
1. Run `npm run dev` to start the development server.
|
||||
@@ -90,6 +119,25 @@ Configure the server with the following environmental variables:
|
||||
|
||||
<table><thead><tr><th width="239">ENV Variable Name</th><th width="149">Required/Optional</th><th width="116">Type</th><th>Description</th><th>Accepted Values</th></tr></thead><tbody><tr><td>CLIENT_HOST</td><td>Required</td><td><code>string</code></td><td>Frontend Host</td><td></td></tr><tr><td>JWT_SECRET</td><td>Required</td><td><code>string</code></td><td>JWT secret</td><td></td></tr><tr><td>REFRESH_TOKEN_SECRET</td><td>Required</td><td><code>string</code></td><td>Refresh JWT secret</td><td></td></tr><tr><td>DB_TYPE</td><td>Optional</td><td><code>string</code></td><td>Specify DB to use</td><td><code>MongoDB | FakeDB</code></td></tr><tr><td>DB_CONNECTION_STRING</td><td>Required</td><td><code>string</code></td><td>Specifies URL for MongoDB Database</td><td></td></tr><tr><td>PORT</td><td>Optional</td><td><code>integer</code></td><td>Specifies Port for Server</td><td></td></tr><tr><td>LOGIN_PAGE_URL</td><td>Required</td><td><code>string</code></td><td>Login url to be used in emailing service</td><td></td></tr><tr><td>REDIS_HOST</td><td>Required</td><td><code>string</code></td><td>Host address for Redis database</td><td></td></tr><tr><td>REDIS_PORT</td><td>Required</td><td><code>integer</code></td><td>Port for Redis database</td><td></td></tr><tr><td>TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for token to live</td><td>In vercel/ms format https://github.com/vercel/ms</td></tr><tr><td>REFRESH_TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for refresh token to live</td><td></td></tr><tr><td>PAGESPEED_API_KEY</td><td>Optional</td><td><code>string</code></td><td>API Key for PageSpeed requests</td><td></td></tr><tr><td>SYSTEM_EMAIL_HOST</td><td>Required</td><td><code>string</code></td><td>Host to send System Emails From</td><td></td></tr><tr><td>SYSTEM_EMAIL_PORT</td><td>Required</td><td><code>number</code></td><td>Port for System Email Host</td><td></td></tr><tr><td>SYSTEM_EMAIL_ADDRESS</td><td>Required</td><td><code>string</code></td><td>System Email Address</td><td></td></tr><tr><td>SYSTEM_EMAIL_PASSWORD</td><td>Required</td><td><code>string</code></td><td>System Email Password</td><td></td></tr></tbody></table>
|
||||
|
||||
Sample env file
|
||||
|
||||
```
|
||||
CLIENT_HOST="http://localhost:5173"
|
||||
JWT_SECRET="my_secret"
|
||||
DB_TYPE="MongoDB"
|
||||
DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT=6379
|
||||
TOKEN_TTL="99d"
|
||||
PAGESPEED_API_KEY=<api_key>
|
||||
SYSTEM_EMAIL_HOST="smtp.gmail.com"
|
||||
SYSTEM_EMAIL_PORT=465
|
||||
SYSTEM_EMAIL_ADDRESS=<email_address>
|
||||
SYSTEM_EMAIL_PASSWORD=<password>
|
||||
REFRESH_TOKEN_SECRET="my_refresh"
|
||||
REFRESH_TOKEN_TTL="99d"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Databases <a href="#databases" id="databases"></a>
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "bluewave-uptime",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user