Merge branch 'develop' into feat/fe/infrastructure-details-connect-to-BE

This commit is contained in:
Shemy Gan committed 2024-11-20 09:14:17 -05:00
commit a0694d4f4a
19 files changed
+1231 -229

No files matched your search

+6 -2
View File
@@ -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={<ProtectedRoute Component={Infrastructure} />}
/>
<Route
path="infrastructure/create"
element={<ProtectedRoute Component={CreateInfrastructureMonitor} />}
/>
<Route
path="infrastructure/:monitorId"
element={<ProtectedRoute Component={InfrastructureDetailsWithAdminProp} />}
@@ -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;
@@ -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 (
<ResponsiveContainer
width="100%"
height="80%" // FE team HELP! Why does this overflow if set to 100%?
height={height}
// FE team HELP! Why does this overflow if set to 100%?
>
<AreaChart data={data}>
<XAxis
@@ -120,7 +123,6 @@ const CustomAreaChart = ({
stroke={strokeColor}
fill={gradient === true ? `url(#${gradientId})` : fillColor}
/>
{customTooltip ? (
<Tooltip
cursor={{ stroke: theme.palette.border.light }}
@@ -149,6 +151,7 @@ CustomAreaChart.propTypes = {
gradientStartColor: PropTypes.string,
gradientEndColor: PropTypes.string,
customTooltip: PropTypes.func,
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default CustomAreaChart;
@@ -1,5 +1,5 @@
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";
@@ -31,9 +31,14 @@ const CustomGauge = ({
strokeWidth = 15,
}) => {
// 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();
@@ -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])}`}
</Typography>
<Typography component="span"></Typography>
</Stack>
@@ -40,7 +40,6 @@ const Checkbox = ({
/* TODO move sizes to theme */
const sizes = { small: "14px", medium: "16px", large: "18px" };
const theme = useTheme();
return (
<FormControlLabel
className="checkbox-wrapper"
@@ -55,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",
}}
/>
}
@@ -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,
+11 -2
View File
@@ -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);
+5 -2
View File
@@ -46,6 +46,7 @@ const Field = forwardRef(
error,
disabled,
hidden,
className
},
ref
) => {
@@ -56,7 +57,7 @@ const Field = forwardRef(
return (
<Stack
gap={theme.spacing(2)}
className={`field field-${type}`}
className={`${className ?? `field field-${type}`}`}
sx={{
"& fieldset": {
borderColor: theme.palette.border.dark,
@@ -185,10 +186,11 @@ const Field = forwardRef(
),
}}
/>
{error && (
{ error && (
<Typography
component="span"
className="input-error"
hidden={className? true: false}
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
@@ -223,6 +225,7 @@ Field.propTypes = {
error: PropTypes.string,
disabled: PropTypes.bool,
hidden: PropTypes.bool,
className: PropTypes.string
};
export default Field;
+1
View File
@@ -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;
@@ -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]}
></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,
};
@@ -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 = (
<Box>
<Typography mb={theme.spacing(4)}>
Also notify via email to multiple addresses (coming soon)
</Typography>
<Field
id="notify-email-list"
type="text"
placeholder="name@gmail.com"
value=""
onChange={() => logger.warn("disabled")}
onBlur={handleBlur}
/>
<Typography mt={theme.spacing(4)}>
You can separate multiple emails with a comma
</Typography>
</Box>
);
return (
<Box className="create-infrastructure-monitor">
<Breadcrumbs
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 type of monitor.
</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}
/>
<Checkbox
id="notify-email"
label={NOTIFY_MULTIPLE_EMAIL_LABEL}
isChecked={false}
value=""
onChange={() => logger.warn("disabled")}
onBlur={handleBlur}
isDisabled={true}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Customize alerts</Typography>
<Typography component="p">
Send a notification to user(s) When the thresholds exceed a certain number
or percentage.
</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;
@@ -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 (
<Box
sx={{
height: "100%",
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
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}
@@ -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 && (
<Box>
@@ -195,64 +296,56 @@ const InfrastructureDetails = () => {
direction="row"
gap={theme.spacing(8)}
>
<StatBox
heading={"CPU"}
subHeading={`${monitor.checks[0].cpu.physical_core} cores`}
/>
<StatBox
heading={"Memory"}
subHeading={formatBytes(monitor.checks[0].memory.total_bytes)}
/>
<StatBox
heading={"Disk"}
subHeading={formatBytes(monitor.checks[0].disk[0].total_bytes)}
/>
<StatBox
heading={"Uptime"}
subHeading={"100%"}
/>
<StatBox
heading={"Status"}
subHeading={monitor.status === true ? "Active" : "Inactive"}
/>
<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)}
>
<GaugeBox
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)}
/>
<GaugeBox
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) => {
return (
<GaugeBox
key={disk._id}
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)}
/>
);
})}
{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={300} // FE team HELP!
height={chartContainerHeight} // FE team HELP!
gap={theme.spacing(8)} // FE team HELP!
flexWrap="wrap" // //FE team HELP! Better way to do this?
sx={{
@@ -262,100 +355,41 @@ const InfrastructureDetails = () => {
},
}}
>
<BaseBox>
<Typography
component="h2"
padding={theme.spacing(8)}
>
Memory usage
</Typography>
<AreaChart
data={monitor?.checks ?? []}
dataKey="memory.usage_percent"
xKey="createdAt"
yKey="memory.usage_percent"
customTooltip={({ active, payload, label }) => (
<InfrastructureTooltip
label={label}
yKey="memory.usage_percent"
yLabel="Memory Usage"
active={active}
payload={payload}
/>
)}
xTick={<TzTick />}
yTick={<PercentTick />}
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!
/>
</BaseBox>
<BaseBox>
<Typography
component="h2"
padding={theme.spacing(8)}
>
CPU usage
</Typography>
<AreaChart
data={monitor?.checks ?? []}
dataKey="cpu.usage_percent"
xKey="createdAt"
yKey="cpu.usage_percent"
customTooltip={({ active, payload, label }) => (
<InfrastructureTooltip
label={label}
yKey="cpu.usage_percent"
yLabel="CPU Usage"
active={active}
payload={payload}
/>
)}
xTick={<TzTick />}
yTick={<PercentTick />}
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"
/>
</BaseBox>
{monitor?.checks?.[0]?.disk?.map((disk, idx) => {
// disk is an array of disks, so we need to map over it
return (
<BaseBox key={disk._id}>
<Typography
component="h2"
padding={theme.spacing(8)}
>
{`Disk${idx} usage`}
</Typography>
<AreaChart
data={monitor?.checks ?? []}
dataKey={`disk[${idx}].usage_percent`}
xKey="createdAt"
yKey={`disk[${idx}].usage_percent`} // We are looking for the usage_percent of the current disk in the array
customTooltip={({ active, payload, label }) => (
<InfrastructureTooltip
label={label} // label must be a string
yKey={`disk.usage_percent`}
yLabel="Disk Usage"
yIdx={idx}
active={active}
payload={payload}
/>
)}
xTick={<TzTick />}
yTick={<PercentTick />}
strokeColor={theme.palette.warning.main}
gradient={true}
gradientStartColor={theme.palette.warning.main}
gradientEndColor="#ffffff"
/>
</BaseBox>
);
})}
{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>
</Box>
+3 -1
View File
@@ -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 (
<Box component="main">
<Button
variant="contained"
color="primary"
onClick={() => {
// navigate("/monitors/create");
navigate("/infrastructure/create");
}}
sx={{ fontWeight: 500 }}
>
+17
View 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);
};
+17 -3
View File
@@ -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;
};
+39
View File
@@ -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,
};
+2
View File
@@ -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;
+2
View File
@@ -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,