diff --git a/Client/src/App.jsx b/Client/src/App.jsx
index 03c406a6c..82c8203a8 100644
--- a/Client/src/App.jsx
+++ b/Client/src/App.jsx
@@ -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,7 +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";
+import InfrastructureDetails from "./Pages/Infrastructure/Details";
function App() {
const AdminCheckedRegister = withAdminCheck(Register);
const MonitorsWithAdminProp = withAdminProp(Monitors);
@@ -129,7 +130,10 @@ function App() {
path="infrastructure"
element={}
/>
-
+ }
+ />
}
diff --git a/Client/src/Components/Charts/AreaChart/data.js b/Client/src/Components/Charts/AreaChart/data.js
deleted file mode 100644
index 43efd165d..000000000
--- a/Client/src/Components/Charts/AreaChart/data.js
+++ /dev/null
@@ -1,59 +0,0 @@
-const ObjectId = (string) => string;
-const ISODate = (string) => string;
-
-// Helper to generate random percentage between 0.1 and 1.0
-const randomPercent = () => Number((Math.random() * 0.9 + 0.1).toFixed(2));
-
-// Create base timestamp and increment by 5 minutes for each entry
-const baseTime = new Date("2024-11-15T07:00:00.000Z");
-const checks = Array.from({ length: 20 }, (_, index) => {
- const timestamp = new Date(baseTime.getTime() + index * 5 * 60 * 1000);
-
- return {
- _id: ObjectId(`6736f4f449e23954c8b89a${index.toString(16).padStart(2, "0")}`),
- monitorId: ObjectId("6736e6c2939f02e0ca519465"),
- status: true,
- responseTime: Math.floor(Math.random() * 50) + 80, // Random between 80-130ms
- statusCode: 200,
- message: "OK",
- cpu: {
- physical_core: 0,
- logical_core: 0,
- frequency: 0,
- temperature: 0,
- free_percent: 0,
- usage_percent: randomPercent(),
- _id: ObjectId(`6736f4f449e23954c8b89b${index.toString(16).padStart(2, "0")}`),
- },
- memory: {
- total_bytes: 0,
- available_bytes: 0,
- used_bytes: 0,
- usage_percent: randomPercent(),
- _id: ObjectId(`6736f4f449e23954c8b89c${index.toString(16).padStart(2, "0")}`),
- },
- disk: [
- {
- read_speed_bytes: 0,
- write_speed_bytes: 0,
- total_bytes: 0,
- free_bytes: 0,
- usage_percent: randomPercent(),
- _id: ObjectId(`6736f4f449e23954c8b89d${index.toString(16).padStart(2, "0")}`),
- },
- ],
- host: {
- os: "",
- platform: "",
- kernel_version: "",
- _id: ObjectId(`6736f4f449e23954c8b89e${index.toString(16).padStart(2, "0")}`),
- },
- errors: [],
- expiry: ISODate(new Date(timestamp.getTime() + 24 * 60 * 60 * 1000).toISOString()),
- createdAt: ISODate(timestamp.toISOString()),
- updatedAt: ISODate(timestamp.toISOString()),
- __v: 0,
- };
-});
-
-export default checks;
diff --git a/Client/src/Components/Charts/AreaChart/index.jsx b/Client/src/Components/Charts/AreaChart/index.jsx
index 4aa507940..d05dbb14a 100644
--- a/Client/src/Components/Charts/AreaChart/index.jsx
+++ b/Client/src/Components/Charts/AreaChart/index.jsx
@@ -10,7 +10,7 @@ import {
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.
*
@@ -83,13 +83,16 @@ const CustomAreaChart = ({
gradientStartColor,
gradientEndColor,
customTooltip,
+ height = "100%",
}) => {
const theme = useTheme();
- const gradientId = `gradient-${Math.random().toString(36).slice(2, 9)}`;
+ const uniqueId = useId();
+ const gradientId = `gradient-${uniqueId}`;
return (
-
{customTooltip ? (
{
// Calculate the length of the stroke for the circle
- const circumference = 2 * Math.PI * radius;
- const totalSize = radius * 2 + strokeWidth * 2; // This is the total size of the SVG, needed for the viewBox
- const strokeLength = (progress / 100) * circumference;
+ 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();
diff --git a/Client/src/Components/Charts/Utils/chartUtils.jsx b/Client/src/Components/Charts/Utils/chartUtils.jsx
index ad673e472..043203525 100644
--- a/Client/src/Components/Charts/Utils/chartUtils.jsx
+++ b/Client/src/Components/Charts/Utils/chartUtils.jsx
@@ -74,6 +74,22 @@ PercentTick.propTypes = {
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.
*
@@ -146,8 +162,8 @@ export const InfrastructureTooltip = ({
sx={{ opacity: 0.8 }}
>
{yIdx >= 0
- ? `${yLabel} ${payload[0].payload[hardwareType][yIdx][metric] * 100}%`
- : `${yLabel} ${payload[0].payload[hardwareType][metric] * 100}%`}
+ ? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}`
+ : `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][metric])}`}
diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx
index a0348dedf..2f89d86df 100644
--- a/Client/src/Components/Inputs/Checkbox/index.jsx
+++ b/Client/src/Components/Inputs/Checkbox/index.jsx
@@ -40,7 +40,6 @@ const Checkbox = ({
/* TODO move sizes to theme */
const sizes = { small: "14px", medium: "16px", large: "18px" };
const theme = useTheme();
-
return (
}
@@ -66,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,
@@ -90,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,
diff --git a/Client/src/Components/Inputs/Field/index.css b/Client/src/Components/Inputs/Field/index.css
index 2d251e4bf..33196b3b9 100644
--- a/Client/src/Components/Inputs/Field/index.css
+++ b/Client/src/Components/Inputs/Field/index.css
@@ -1,6 +1,15 @@
.field {
- min-width: 250px;
+ min-width: var(--env-var-width-3);
}
+
+.field-infrastructure-alert {
+ max-width: var(--env-var-width-4);
+}
+
+.field-infrastructure-alert .MuiInputBase-root:has(input) {
+ height: var(--env-var-height-2);
+}
+
.field h3.MuiTypography-root,
.field h5.MuiTypography-root,
.field input,
@@ -14,7 +23,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);
diff --git a/Client/src/Components/Inputs/Field/index.jsx b/Client/src/Components/Inputs/Field/index.jsx
index 0e558c197..d1eab532d 100644
--- a/Client/src/Components/Inputs/Field/index.jsx
+++ b/Client/src/Components/Inputs/Field/index.jsx
@@ -46,6 +46,7 @@ const Field = forwardRef(
error,
disabled,
hidden,
+ className
},
ref
) => {
@@ -56,7 +57,7 @@ const Field = forwardRef(
return (
- {error && (
+ { error && (
{
+ 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;
diff --git a/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx b/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx
new file mode 100644
index 000000000..3cdde1bdb
--- /dev/null
+++ b/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx
@@ -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 (
+
+
+
+
+
+
+
+ {alertUnit}
+
+
+
+)}
+
+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,
+};
\ No newline at end of file
diff --git a/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx b/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
new file mode 100644
index 000000000..6a7081d9c
--- /dev/null
+++ b/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
@@ -0,0 +1,413 @@
+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];
+ 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" },
+ ];
+
+ const NOTIFY_MULTIPLE_EMAIL_LABEL = (
+
+
+ Also notify via email to multiple addresses (coming soon)
+
+ logger.warn("disabled")}
+ onBlur={handleBlur}
+ />
+
+ You can separate multiple emails with a comma
+
+
+ );
+
+ return (
+
+
+
+
+
+ Create your{" "}
+
+
+ infrastructure monitor
+
+
+
+
+ General settings
+
+ Here you can select the URL of the host, together with the type of monitor.
+
+
+
+
+
+
+
+
+
+
+ Incident notifications
+
+ When there is an incident, notify users.
+
+
+
+ When there is a new incident,
+ notification.type === "email"
+ )}
+ value={user?.email}
+ onChange={(e) => handleChange(e)}
+ onBlur={handleBlur}
+ />
+ logger.warn("disabled")}
+ onBlur={handleBlur}
+ isDisabled={true}
+ />
+
+
+
+
+
+ Customize alerts
+
+ Send a notification to user(s) When the thresholds exceed a certain number
+ or percentage.
+
+
+
+ {HARDWARE_MONITOR_TYPES.map((type, idx) => (
+
+ ))}
+ {alertErrKeyLen > 0 && (
+
+ {
+ errors[
+ THRESHOLD_FIELD_PREFIX +
+ HARDWARE_MONITOR_TYPES.filter(
+ (type) => errors[THRESHOLD_FIELD_PREFIX + type]
+ )[0]
+ ]
+ }
+
+ )}
+
+
+
+
+ Advanced settings
+
+
+
+
+
+
+ Create infrastructure monitor
+
+
+
+
+ );
+};
+
+export default CreateInfrastructureMonitor;
diff --git a/Client/src/Pages/Infrastructure/details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx
similarity index 51%
rename from Client/src/Pages/Infrastructure/details/index.jsx
rename to Client/src/Pages/Infrastructure/Details/index.jsx
index 63daeddb6..5f3ec7f77 100644
--- a/Client/src/Pages/Infrastructure/details/index.jsx
+++ b/Client/src/Pages/Infrastructure/Details/index.jsx
@@ -7,6 +7,9 @@ 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 { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
import {
TzTick,
PercentTick,
@@ -14,6 +17,9 @@ import {
} 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
@@ -37,21 +43,23 @@ const formatBytes = (bytes) => {
* 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 }) => {
+const BaseBox = ({ children, sx = {} }) => {
const theme = useTheme();
return (
{children}
@@ -61,6 +69,7 @@ const BaseBox = ({ children }) => {
BaseBox.propTypes = {
children: PropTypes.node.isRequired,
+ sx: PropTypes.object,
};
/**
@@ -161,7 +170,19 @@ const InfrastructureDetails = () => {
const [monitor, setMonitor] = useState(null);
const { authToken } = useSelector((state) => state.auth);
const [dateRange, setDateRange] = useState("day");
+ 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 {
@@ -182,6 +203,86 @@ const InfrastructureDetails = () => {
fetchData();
}, []);
+ const statBoxConfigs = [
+ {
+ id: 0,
+ heading: "CPU",
+ subHeading: `${monitor?.checks[0]?.cpu?.physical_core} 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: monitor?.checks[0]?.memory?.usage_percent * 100,
+ 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: monitor?.checks[0]?.cpu?.usage_percent * 100,
+ heading: "CPU Usage",
+ metricOne: "Cores",
+ valueOne: monitor?.checks[0]?.cpu?.physical_core,
+ metricTwo: "Frequency",
+ valueTwo: `${(monitor?.checks[0]?.cpu?.frequency / 1000).toFixed(2)} Ghz`,
+ },
+ ...(monitor?.checks[0]?.disk ?? []).map((disk, idx) => ({
+ type: "disk",
+ diskIndex: idx,
+ value: disk.usage_percent * 100,
+ 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 (
monitor && (
@@ -195,64 +296,56 @@ const InfrastructureDetails = () => {
direction="row"
gap={theme.spacing(8)}
>
-
-
-
-
-
+
+
+
+
+ {monitor.name}
+
+ {monitor.url || "..."}
+
+
+ Checking every {formatDurationRounded(monitor?.interval)}
+
+
+ Last checked {formatDurationSplit(monitor?.lastChecked).time}{" "}
+ {formatDurationSplit(monitor?.lastChecked).format} ago
+
-
-
- {monitor.checks[0].disk.map((disk, idx) => {
- return (
-
- );
- })}
+ {statBoxConfigs.map((statBox) => (
+
+ ))}
+
+
+ {gaugeBoxConfigs.map((config) => (
+
+ ))}
{
},
}}
>
-
-
- Memory usage
-
- (
-
- )}
- xTick={}
- yTick={}
- strokeColor={theme.palette.primary.main}
- gradient={true}
- gradientStartColor={theme.palette.primary.main} //FE team HELP! Not sure what colors to use
- gradientEndColor="#ffffff" // FE team HELP!
- />
-
-
-
- CPU usage
-
- (
-
- )}
- xTick={}
- yTick={}
- strokeColor={theme.palette.success.main} // FE team HELP!
- gradient={true}
- fill={theme.palette.success.main} // FE team HELP!
- gradientStartColor={theme.palette.success.main}
- gradientEndColor="#ffffff"
- />
-
- {monitor?.checks?.[0]?.disk?.map((disk, idx) => {
- // disk is an array of disks, so we need to map over it
- return (
-
-
- {`Disk${idx} usage`}
-
- (
-
- )}
- xTick={}
- yTick={}
- strokeColor={theme.palette.warning.main}
- gradient={true}
- gradientStartColor={theme.palette.warning.main}
- gradientEndColor="#ffffff"
- />
-
- );
- })}
+ {areaChartConfigs.map((config) => (
+
+
+ {config.heading}
+
+ (
+
+ )}
+ xTick={}
+ yTick={}
+ strokeColor={config.strokeColor}
+ gradient={true}
+ gradientStartColor={config.strokeColor}
+ gradientEndColor="#ffffff"
+ />
+
+ ))}
diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx
index 324268af7..0cca80940 100644
--- a/Client/src/Pages/Infrastructure/index.jsx
+++ b/Client/src/Pages/Infrastructure/index.jsx
@@ -1,5 +1,6 @@
import { Box, Button } from "@mui/material";
import { Heading } from "../../Components/Heading";
+import { useNavigate } from "react-router-dom";
/**
* This is the Infrastructure monitoring page. This is a work in progress
@@ -9,13 +10,14 @@ import { Heading } from "../../Components/Heading";
*/
function Infrastructure() {
+ const navigate = useNavigate();
return (