diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx
index 2f89d86df..054f43b92 100644
--- a/Client/src/Components/Inputs/Checkbox/index.jsx
+++ b/Client/src/Components/Inputs/Checkbox/index.jsx
@@ -3,33 +3,50 @@ import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
import { useTheme } from "@emotion/react";
import CheckboxOutline from "../../../assets/icons/checkbox-outline.svg?react";
import CheckboxFilled from "../../../assets/icons/checkbox-filled.svg?react";
-
import "./index.css";
/**
- * @param {Object} props
- * @param {string} props.id - The id attribute for the checkbox input.
- * @param {string} props.label - The label to display next to the checkbox.
- * @param {('small' | 'medium' | 'large')} - The size of the checkbox.
- * @param {boolean} props.isChecked - Whether the checkbox is checked or not.
- * @param {string} [props.value] - The value of the checkbox input.
- * @param {function} [props.onChange] - The function to call when the checkbox value changes.
- * @param {boolean} [props.isDisabled] - Whether the checkbox is disabled or not.
+ * Checkbox Component
*
- * @returns {JSX.Element}
+ * A customized checkbox component using Material-UI that supports custom sizing,
+ * disabled states, and custom icons.
+ *
+ * @component
+ * @param {Object} props - Component properties
+ * @param {string} props.id - Unique identifier for the checkbox input
+ * @param {string} [props.name] - Optional name attribute for the checkbox
+ * @param {(string|React.ReactNode)} props.label - Label text or node for the checkbox
+ * @param {('small'|'medium'|'large')} [props.size='medium'] - Size of the checkbox icon
+ * @param {boolean} props.isChecked - Current checked state of the checkbox
+ * @param {string} [props.value] - Optional value associated with the checkbox
+ * @param {Function} [props.onChange] - Callback function triggered when checkbox state changes
+ * @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled
+ *
+ * @returns {React.ReactElement} Rendered Checkbox component
*
* @example
+ * // Basic usage
*
+ *
+ * @example
+ * // With custom size and disabled state
+ *
*/
-
const Checkbox = ({
id,
+ name,
label,
size = "medium",
isChecked,
@@ -46,6 +63,7 @@ const Checkbox = ({
control={
}
@@ -54,7 +72,7 @@ const Checkbox = ({
"aria-label": "controlled checkbox",
id: id,
}}
- sx={{
+ sx={{
"&:hover": { backgroundColor: "transparent" },
"& svg": { width: sizes[size], height: sizes[size] },
alignSelf: "flex-start",
@@ -89,6 +107,7 @@ const Checkbox = ({
Checkbox.propTypes = {
id: PropTypes.string.isRequired,
+ name: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
size: PropTypes.oneOf(["small", "medium", "large"]),
isChecked: PropTypes.bool.isRequired,
diff --git a/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx b/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx
index c7b9f0c76..3220582a8 100644
--- a/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx
+++ b/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx
@@ -5,32 +5,58 @@ 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.
+ * CustomThreshold Component
*
- * @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
+ * A reusable component that renders a checkbox with an associated numeric input field
+ * and an optional unit label. The input field can be enabled/disabled based on checkbox state.
*
+ * @component
+ * @param {Object} props - Component properties
+ * @param {string} [props.checkboxId] - Optional unique identifier for the checkbox
+ * @param {string} [props.checkboxName] - Optional name attribute for the checkbox
+ * @param {string} props.checkboxLabel - Label text for the checkbox
+ * @param {boolean} props.isChecked - Current checked state of the checkbox
+ * @param {Function} props.onCheckboxChange - Callback function when checkbox is toggled
+ * @param {string} props.fieldId - Unique identifier for the input field
+ * @param {string} [props.fieldName] - Optional name attribute for the input field
+ * @param {string} props.fieldValue - Current value of the input field
+ * @param {Function} props.onFieldChange - Callback function when input field value changes
+ * @param {Function} props.onFieldBlur - Callback function when input field loses focus
+ * @param {string} props.alertUnit - Unit label displayed next to the input field
+ * @param {Object} props.errors - Object containing validation errors for the field
+ * @param {Object} props.infrastructureMonitor - Infrastructure monitor configuration object
+ *
+ * @returns {React.ReactElement} Rendered CustomThreshold component
+ *
+ * @example
+ *
*/
-
export const CustomThreshold = ({
checkboxId,
+ checkboxName,
checkboxLabel,
onCheckboxChange,
+ isChecked,
fieldId,
+ fieldName,
+ fieldValue,
onFieldChange,
onFieldBlur,
alertUnit,
- infrastructureMonitor,
errors,
}) => {
const theme = useTheme();
@@ -46,8 +72,9 @@ export const CustomThreshold = ({
@@ -61,11 +88,12 @@ export const CustomThreshold = ({
maxWidth="var(--env-var-width-4)"
type="number"
id={fieldId}
- value={infrastructureMonitor[fieldId]}
+ name={fieldName}
+ value={fieldValue}
onBlur={onFieldBlur}
onChange={onFieldChange}
error={errors[fieldId] ? true : false}
- disabled={!infrastructureMonitor[checkboxId]}
+ disabled={!isChecked}
/>
{
+ return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
+};
+
+const getAlertError = (errors) => {
+ return Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))
+ ? errors[Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))]
+ : null;
+};
const CreateInfrastructureMonitor = () => {
+ const theme = useTheme();
+ const { user, authToken } = useSelector((state) => state.auth);
+ const monitorState = useSelector((state) => state.infrastructureMonitor);
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ // State
+ const [errors, setErrors] = useState({});
+ const [https, setHttps] = useState(false);
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
@@ -38,118 +73,73 @@ const CreateInfrastructureMonitor = () => {
secret: "",
});
- const MS_PER_MINUTE = 60000;
- const THRESHOLD_FIELD_PREFIX = "usage_";
- const HARDWARE_MONITOR_TYPES = ["cpu", "memory", "disk", "temperature"];
- 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, appendID) => {
+ // Handlers
+ const handleCreateInfrastructureMonitor = async (event) => {
event.preventDefault();
- const { value, id } = event.target;
- let name = idMap[id] ?? id;
- if (name === "url" && infrastructureMonitor.name === "") {
- setInfrastructureMonitor((prev) => ({
- ...prev,
- name: parseDomainName(value),
- }));
+ // Build the form
+ let form = {
+ url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
+ name:
+ infrastructureMonitor.name === ""
+ ? infrastructureMonitor.url
+ : infrastructureMonitor.name,
+ interval: infrastructureMonitor.interval * MS_PER_MINUTE,
+ cpu: infrastructureMonitor.cpu,
+ ...(infrastructureMonitor.cpu
+ ? { usage_cpu: infrastructureMonitor.usage_cpu }
+ : {}),
+ memory: infrastructureMonitor.memory,
+ ...(infrastructureMonitor.memory
+ ? { usage_memory: infrastructureMonitor.usage_memory }
+ : {}),
+ disk: infrastructureMonitor.disk,
+ ...(infrastructureMonitor.disk
+ ? { usage_disk: infrastructureMonitor.usage_disk }
+ : {}),
+ temperature: infrastructureMonitor.temperature,
+ ...(infrastructureMonitor.temperature
+ ? { usage_temperature: infrastructureMonitor.usage_temperature }
+ : {}),
+ secret: infrastructureMonitor.secret,
+ };
+
+ const { error } = infrastructureMonitorValidation.validate(form, {
+ abortEarly: false,
+ });
+
+ if (error) {
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ setErrors(newErrors);
+ createToast({ body: "Please check the form for errors." });
+ return;
}
- if (id?.startsWith("notify-email-")) return;
- const { error } = infrastructureMonitorValidation.validate(
- { [id ?? appendID]: value },
- {
- abortEarly: false,
- }
- );
- setErrors((prev) => {
- return buildErrors(prev, id ?? appendID, error);
- });
- };
+ // Build the thresholds for the form
+ const {
+ cpu,
+ usage_cpu,
+ memory,
+ usage_memory,
+ disk,
+ usage_disk,
+ temperature,
+ usage_temperature,
+ ...rest
+ } = form;
- 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)];
- });
+ const thresholds = {
+ ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
+ ...(memory ? { usage_memory: usage_memory / 100 } : {}),
+ ...(disk ? { usage_disk: usage_disk / 100 } : {}),
+ ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
+ };
form = {
- ...form,
+ ...rest,
description: form.name,
teamId: user.teamId,
userId: user._id,
@@ -157,54 +147,69 @@ const CreateInfrastructureMonitor = () => {
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;
+ const action = await dispatch(
+ createInfrastructureMonitor({ authToken, monitor: form })
+ );
+ if (action.meta.requestStatus === "fulfilled") {
+ createToast({ body: "Infrastructure monitor created successfully!" });
+ navigate("/infrastructure");
} 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." });
- }
+ 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 handleChange = (event) => {
+ const { value, name } = event.target;
+ setInfrastructureMonitor({
+ ...infrastructureMonitor,
+ [name]: value,
+ });
+
+ const { error } = infrastructureMonitorValidation.validate(
+ { [name]: value },
+ { abortEarly: false }
+ );
+ setErrors((prev) => ({
+ ...prev,
+ ...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
+ }));
+ };
+
+ const handleCheckboxChange = (event) => {
+ const { name } = event.target;
+ const { checked } = event.target;
+ setInfrastructureMonitor({
+ ...infrastructureMonitor,
+ [name]: checked,
+ });
+ };
+
+ const handleNotifications = (event, type) => {
+ const { value } = event.target;
+ let notifications = [...infrastructureMonitor.notifications];
+ const notificationExists = notifications.some((notification) => {
+ if (notification.type === type && notification.address === value) {
+ return true;
+ }
+ return false;
+ });
+ if (notificationExists) {
+ notifications = notifications.filter((notification) => {
+ if (notification.type === type && notification.address === value) {
+ return false;
+ }
+ return true;
+ });
+ } else {
+ notifications.push({ type, address: value });
+ }
+
+ setInfrastructureMonitor((prev) => ({
+ ...prev,
+ notifications,
+ }));
+ };
return (
@@ -239,56 +244,75 @@ const CreateInfrastructureMonitor = () => {
fontSize="inherit"
fontWeight="inherit"
>
- infrastructure monitor
+ monitor
-
-
- General settings
-
- Here you can select the URL of the host, together with the friendly name
- and authorization secret to connect to the server agent.
-
-
- The server you are monitoring must be running the{" "}
-
-
-
-
+
+ General settings
+
+ Here you can select the URL of the host, together with the friendly name and
+ authorization secret to connect to the server agent.
+
+
+ The server you are monitoring must be running the{" "}
+
+
+
}
+ placeholder={"localhost:59232/api/v1/metrics"}
label="Server URL"
- placeholder="https://"
+ https={https}
value={infrastructureMonitor.url}
- onBlur={handleBlur}
onChange={handleChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
+
+ Protocol
+
+
+
+
+
{
(notification) => notification.type === "email"
)}
value={user?.email}
- onChange={(e) => handleChange(e)}
- onBlur={handleBlur}
+ onChange={(event) => handleNotifications(event, "email")}
/>
-
Customize alerts
@@ -325,25 +347,31 @@ const CreateInfrastructureMonitor = () => {
- {HARDWARE_MONITOR_TYPES.map((type, idx) => (
-
- ))}
- {alertErrKeyLen > 0 && (
+ {METRICS.map((metric) => {
+ return (
+
+ );
+ })}
+ {/* Error text */}
+ {hasAlertError(errors) && (
{
opacity: 0.8,
}}
>
- {
- errors[
- THRESHOLD_FIELD_PREFIX +
- HARDWARE_MONITOR_TYPES.filter(
- (type) => errors[THRESHOLD_FIELD_PREFIX + type]
- )[0]
- ]
- }
+ {getAlertError(errors)}
)}
@@ -372,11 +393,12 @@ const CreateInfrastructureMonitor = () => {
diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js
index 52ed3aa10..a7d6b3eb6 100644
--- a/Client/src/Validation/validation.js
+++ b/Client/src/Validation/validation.js
@@ -91,10 +91,24 @@ const credentials = joi.object({
});
const monitorValidation = 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.",
- }),
+ url: joi
+ .string()
+ .trim()
+ .custom((value, helpers) => {
+ const urlRegex =
+ /^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;
+
+ if (!urlRegex.test(value)) {
+ return helpers.error("string.invalidUrl");
+ }
+
+ return value;
+ })
+ .messages({
+ "string.empty": "This field is required.",
+ "string.uri": "The URL you provided is not valid.",
+ "string.invalidUrl": "Please enter a valid URL with optional port",
+ }),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
@@ -176,10 +190,24 @@ const advancedSettingsValidation = joi.object({
});
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.",
- }),
+ url: joi
+ .string()
+ .trim()
+ .custom((value, helpers) => {
+ const urlRegex =
+ /^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;
+
+ if (!urlRegex.test(value)) {
+ return helpers.error("string.invalidUrl");
+ }
+
+ return value;
+ })
+ .messages({
+ "string.empty": "This field is required.",
+ "string.uri": "The URL you provided is not valid.",
+ "string.invalidUrl": "Please enter a valid URL with optional port",
+ }),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),