diff --git a/Client/src/App.jsx b/Client/src/App.jsx
index 82c8203a8..245d8f153 100644
--- a/Client/src/App.jsx
+++ b/Client/src/App.jsx
@@ -41,6 +41,8 @@ import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Infrastructure } from "./Pages/Infrastructure";
import InfrastructureDetails from "./Pages/Infrastructure/Details";
+import ConfigureInfrastructureMonitor from "./Pages/Infrastructure/Configure";
+
function App() {
const AdminCheckedRegister = withAdminCheck(Register);
const MonitorsWithAdminProp = withAdminProp(Monitors);
@@ -138,6 +140,10 @@ function App() {
path="infrastructure/:monitorId"
element={}
/>
+ }
+ />
{
try {
- const { authToken, monitor } = data;
+ const { authToken, monitorId } = data;
const res = await networkService.deleteMonitorById({
authToken: authToken,
- monitorId: monitor._id,
+ monitorId: monitorId,
});
return res.data;
} catch (error) {
@@ -236,6 +246,9 @@ const infrastructureMonitorsSlice = createSlice({
state.success = null;
state.msg = null;
},
+ resetInfrastructureMonitorFormAction: (state) => {
+ state.formAction = initialState.formAction;
+ },
},
extraReducers: (builder) => {
builder
@@ -245,6 +258,7 @@ const infrastructureMonitorsSlice = createSlice({
.addCase(getInfrastructureMonitorsByTeamId.pending, (state) => {
state.isLoading = true;
+ state.success = false;
})
.addCase(getInfrastructureMonitorsByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
@@ -264,6 +278,7 @@ const infrastructureMonitorsSlice = createSlice({
// *****************************************************
.addCase(createInfrastructureMonitor.pending, (state) => {
state.isLoading = true;
+ state.success = false;
})
.addCase(createInfrastructureMonitor.fulfilled, (state, action) => {
state.isLoading = false;
@@ -282,6 +297,7 @@ const infrastructureMonitorsSlice = createSlice({
// *****************************************************
.addCase(checkInfrastructureEndpointResolution.pending, (state) => {
state.isLoading = true;
+ state.success = false;
})
.addCase(checkInfrastructureEndpointResolution.fulfilled, (state, action) => {
state.isLoading = false;
@@ -299,12 +315,15 @@ const infrastructureMonitorsSlice = createSlice({
// Get Monitor By Id
// *****************************************************
.addCase(getInfrastructureMonitorById.pending, (state) => {
+ state.formAction = FormAction.GET;
state.isLoading = true;
+ state.success = false;
})
.addCase(getInfrastructureMonitorById.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
+ state.selectedInfraMonitor = action.payload.data;
})
.addCase(getInfrastructureMonitorById.rejected, (state, action) => {
state.isLoading = false;
@@ -318,6 +337,7 @@ const infrastructureMonitorsSlice = createSlice({
// *****************************************************
.addCase(updateInfrastructureMonitor.pending, (state) => {
state.isLoading = true;
+ state.success = false;
})
.addCase(updateInfrastructureMonitor.fulfilled, (state, action) => {
state.isLoading = false;
@@ -336,7 +356,9 @@ const infrastructureMonitorsSlice = createSlice({
// Delete Monitor
// *****************************************************
.addCase(deleteInfrastructureMonitor.pending, (state) => {
+ state.formAction = FormAction.DELETE;
state.isLoading = true;
+ state.success = false;
})
.addCase(deleteInfrastructureMonitor.fulfilled, (state, action) => {
state.isLoading = false;
@@ -355,6 +377,7 @@ const infrastructureMonitorsSlice = createSlice({
// *****************************************************
.addCase(deleteInfrastructureMonitorChecksByTeamId.pending, (state) => {
state.isLoading = true;
+ state.success = false;
})
.addCase(deleteInfrastructureMonitorChecksByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
@@ -373,11 +396,13 @@ const infrastructureMonitorsSlice = createSlice({
// *****************************************************
.addCase(pauseInfrastructureMonitor.pending, (state) => {
state.isLoading = true;
+ state.success = false;
})
.addCase(pauseInfrastructureMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
+ state.selectedInfraMonitor = action.payload.data;
})
.addCase(pauseInfrastructureMonitor.rejected, (state, action) => {
state.isLoading = false;
@@ -391,6 +416,7 @@ const infrastructureMonitorsSlice = createSlice({
// *****************************************************
.addCase(deleteAllInfrastructureMonitors.pending, (state) => {
state.isLoading = true;
+ state.success = false;
})
.addCase(deleteAllInfrastructureMonitors.fulfilled, (state, action) => {
state.isLoading = false;
@@ -405,7 +431,7 @@ const infrastructureMonitorsSlice = createSlice({
},
});
-export const { setInfrastructureMonitors, clearInfrastructureMonitorState } =
+export const { clearInfrastructureMonitorState, resetInfrastructureMonitorFormAction } =
infrastructureMonitorsSlice.actions;
export default infrastructureMonitorsSlice.reducer;
diff --git a/Client/src/Pages/Infrastructure/Configure/index.jsx b/Client/src/Pages/Infrastructure/Configure/index.jsx
new file mode 100644
index 000000000..6dfa07988
--- /dev/null
+++ b/Client/src/Pages/Infrastructure/Configure/index.jsx
@@ -0,0 +1,490 @@
+import { useEffect, useState } from "react";
+import { Box, Stack, Tooltip, Typography } from "@mui/material";
+import LoadingButton from "@mui/lab/LoadingButton";
+import { useSelector, useDispatch } from "react-redux";
+import { infrastructureMonitorValidation } from "../../../Validation/validation";
+import { parseDomainName } from "../../../Utils/monitorUtils";
+import {
+ getInfrastructureMonitorById,
+ pauseInfrastructureMonitor,
+ deleteInfrastructureMonitor,
+ FormAction,
+} from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
+import { useNavigate, useParams } from "react-router-dom";
+import { useTheme } from "@emotion/react";
+import { createToast } from "../../../Utils/toastUtils";
+import Link from "../../../Components/Link";
+import { ConfigBox } from "../../Monitors/styled";
+import TextInput from "../../../Components/Inputs/TextInput";
+import Select from "../../../Components/Inputs/Select";
+import Checkbox from "../../../Components/Inputs/Checkbox";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import { buildErrors } from "../../../Validation/error";
+import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
+import { CustomThreshold } from "../CreateMonitor/CustomThreshold";
+import useUtils from "../../Monitors/utils";
+import PulseDot from "../../../Components/Animated/PulseDot";
+import PauseIcon from "../../../assets/icons/pause-icon.svg?react";
+import ResumeIcon from "../../../assets/icons/resume-icon.svg?react";
+import Dialog from "../../../Components/Dialog";
+import { MS_PER_MINUTE } from "../../../Utils/timeUtils";
+import { HARDWARE_MONITOR_TYPES, THRESHOLD_FIELD_PREFIX } from "../constants";
+
+const ConfigureInfrastructureMonitor = () => {
+ const { user, authToken } = useSelector((state) => state.auth);
+ const { monitorId } = useParams();
+ const { isLoading, selectedInfraMonitor, success, formAction } = useSelector(
+ (state) => state.infrastructureMonitors
+ );
+ const [infrastructureMonitor, setInfrastructureMonitor] = useState(null);
+ const [isOpen, setIsOpen] = useState(false);
+ const { statusColor, statusMsg, determineState } = useUtils();
+ const [errors, setErrors] = useState({});
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const theme = useTheme();
+ const idMap = {
+ "notify-email-default": "notification-email",
+ };
+
+ const alertErrKeyLen = Object.keys(errors).filter((k) =>
+ k.startsWith(THRESHOLD_FIELD_PREFIX)
+ ).length;
+
+ useEffect(() => {
+ if (!infrastructureMonitor) {
+ dispatch(getInfrastructureMonitorById({ authToken, monitorId }));
+ }
+ }, [monitorId, authToken, dispatch]);
+
+ useEffect(() => {
+ if (formAction === FormAction.GET && !isLoading && success === false)
+ navigate("/not-found", { replace: true });
+ }, [success, isLoading, navigate]);
+
+ useEffect(() => {
+ if (selectedInfraMonitor) {
+ setInfrastructureMonitor(selectedInfraMonitor);
+ }
+ }, [selectedInfraMonitor]);
+
+ useEffect(() => {
+ if (formAction === FormAction.DELETE && !isLoading) {
+ if (success) {
+ navigate("/infrastructure", { replace: true });
+ } else {
+ createToast({ body: "Failed to delete monitor." });
+ }
+ }
+ }, [formAction, isLoading, success, navigate]);
+
+ const handlePause = () => {
+ dispatch(pauseInfrastructureMonitor({ authToken, monitorId }));
+ };
+
+ const handleRemove = () => {
+ dispatch(deleteInfrastructureMonitor({ authToken, monitorId }));
+ };
+
+ 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) => {
+ event.preventDefault();
+ const { value, id } = event.target;
+
+ let name = idMap[id] ?? id;
+ if (name === "url" && infrastructureMonitor?.name === "") {
+ setInfrastructureMonitor((prev) => ({
+ ...prev,
+ name: parseDomainName(value),
+ }));
+ }
+
+ if (id?.startsWith("notify-email-")) return;
+ const { error } = infrastructureMonitorValidation.validate(
+ { [id ?? appendID]: value },
+ {
+ abortEarly: false,
+ }
+ );
+ setErrors((prev) => {
+ return buildErrors(prev, id ?? appendID, 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,
+ }));
+ }
+ };
+
+ // TODO: add update infrastructure monitor functionalityf
+ const handleUpdateInfrastructureMonitor = () => {
+ console.log("Update infrastructure monitor...");
+ };
+
+ //select values
+ const frequencies = [
+ { _id: 0.25, name: "15 seconds" },
+ { _id: 0.5, name: "30 seconds" },
+ { _id: 1, name: "1 minute" },
+ { _id: 2, name: "2 minutes" },
+ { _id: 5, name: "5 minutes" },
+ { _id: 10, name: "10 minutes" },
+ ];
+
+ return (
+ infrastructureMonitor && (
+
+
+
+
+
+
+ {infrastructureMonitor.name}
+
+
+
+
+
+
+
+
+ {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
+
+
+ Editting...
+
+
+
+
+
+ {infrastructureMonitor?.isActive ? (
+ <>
+
+ Pause
+ >
+ ) : (
+ <>
+
+ Resume
+ >
+ )}
+
+ setIsOpen(true)}
+ >
+ Remove
+
+
+
+
+
+
+ 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{" "}
+
+
+
+
+
+
+
+
+
+
+
+
+ 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}
+ />
+
+
+
+
+
+ Customize alerts
+
+ Send a notification to user(s) when thresholds exceed a specified
+ 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
+
+
+
+
+
+
+ Save
+
+
+
+
+
+ )
+ );
+};
+
+export default ConfigureInfrastructureMonitor;
diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx
index 06fb1296a..a0069a900 100644
--- a/Client/src/Pages/Infrastructure/Details/index.jsx
+++ b/Client/src/Pages/Infrastructure/Details/index.jsx
@@ -1,7 +1,7 @@
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import Breadcrumbs from "../../../Components/Breadcrumbs";
-import { Stack, Box, Typography } from "@mui/material";
+import { Stack, Box, Typography, Tooltip, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import CustomGauge from "../../../Components/Charts/CustomGauge";
import AreaChart from "../../../Components/Charts/AreaChart";
@@ -12,7 +12,7 @@ import useUtils from "../../Monitors/utils";
import { useNavigate } from "react-router-dom";
import Empty from "./empty";
import { logger } from "../../../Utils/Logger";
-import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
+import { formatDurationRounded } from "../../../Utils/timeUtils";
import {
TzTick,
PercentTick,
@@ -20,6 +20,7 @@ import {
TemperatureTooltip,
} from "../../../Components/Charts/Utils/chartUtils";
import PropTypes from "prop-types";
+import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
const BASE_BOX_PADDING_VERTICAL = 4;
const BASE_BOX_PADDING_HORIZONTAL = 8;
@@ -182,7 +183,7 @@ GaugeBox.propTypes = {
* Renders the infrastructure details page
* @returns {React.ReactElement} Infrastructure details page component
*/
-const InfrastructureDetails = () => {
+const InfrastructureDetails = ({ isAdmin }) => {
const navigate = useNavigate();
const theme = useTheme();
const { monitorId } = useParams();
@@ -193,7 +194,7 @@ const InfrastructureDetails = () => {
const [monitor, setMonitor] = useState(null);
const { authToken } = useSelector((state) => state.auth);
const [dateRange, setDateRange] = useState("all");
- const { statusColor, determineState } = useUtils();
+ const { statusColor, statusMsg, determineState } = useUtils();
// These calculations are needed because ResponsiveContainer
// doesn't take padding of parent/siblings into account
// when calculating height.
@@ -482,26 +483,90 @@ const InfrastructureDetails = () => {
-
+
+ {monitor.name}
+
+
+
+
+
+
+
+
+ {monitor.url?.replace(/^https?:\/\//, "") || "..."}
+
+
+ Checking every {formatDurationRounded(monitor?.interval)}.
+
+
-
- {monitor.name}
-
- {monitor.url || "..."}
-
-
- Checking every {formatDurationRounded(monitor?.interval)}
-
-
- Last checked {formatDurationSplit(monitor?.lastChecked).time}{" "}
- {formatDurationSplit(monitor?.lastChecked).format} ago
-
+ {isAdmin && (
+
+ )}
navigate("/infrastructure/create");
@@ -129,6 +131,10 @@ function Infrastructure() {
fetchMonitors();
}, [page, rowsPerPage]);
+ useEffect(() => {
+ dispatch(resetInfrastructureMonitorFormAction());
+ }, []);
+
const { determineState } = useUtils();
const { monitors, total: totalMonitors } = monitorState;
// do it here