Merge branch 'develop' into fix/colorful-revamp-extravaganza

This commit is contained in:
Alex Holliday
2024-08-29 16:12:40 -07:00
16 changed files with 319 additions and 89 deletions

View File

@@ -73,7 +73,14 @@ const TeamPanel = () => {
useEffect(() => {
let team = members;
if (filter !== "all")
team = members.filter((member) => member.role.includes(filter));
team = members.filter((member) => {
if (filter === "admin") {
return (
member.role.includes("admin") || member.role.includes("superadmin")
);
}
return member.role.includes(filter);
});
const data = {
cols: [

View File

@@ -1,6 +1,7 @@
import { networkService } from "../../main";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { jwtDecode } from "jwt-decode";
import axios from "axios";
const initialState = {
isLoading: false,
@@ -57,9 +58,8 @@ export const update = createAsyncThunk(
form.password && fd.append("password", form.password);
form.newPassword && fd.append("newPassword", form.newPassword);
if (form.file && form.file !== "") {
const imageResult = await networkService.get(form.file, {
const imageResult = await axios.get(form.file, {
responseType: "blob",
baseURL: "",
});
fd.append("profileImage", imageResult.data);
}

View File

@@ -28,6 +28,26 @@ export const createUptimeMonitor = createAsyncThunk(
}
);
export const getUptimeMonitorById = createAsyncThunk(
"monitors/getMonitorById",
async (data, thunkApi) => {
try {
const { authToken, monitorId } = data;
const res = await networkService.getMonitorByid(authToken, 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 getUptimeMonitorsByTeamId = createAsyncThunk(
"montiors/getMonitorsByTeamId",
async (token, thunkApi) => {
@@ -109,6 +129,26 @@ export const deleteUptimeMonitor = createAsyncThunk(
}
);
export const pauseUptimeMonitor = createAsyncThunk(
"monitors/pauseMonitor",
async (data, thunkApi) => {
try {
const { authToken, monitorId } = data;
const res = await networkService.pauseMonitorById(authToken, 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);
}
}
);
const uptimeMonitorsSlice = createSlice({
name: "uptimeMonitors",
initialState,
@@ -160,7 +200,24 @@ const uptimeMonitorsSlice = createSlice({
? action.payload.msg
: "Failed to create uptime monitor";
})
// *****************************************************
// Get Monitor By Id
// *****************************************************
.addCase(getUptimeMonitorById.pending, (state) => {
state.isLoading = true;
})
.addCase(getUptimeMonitorById.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(getUptimeMonitorById.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to pause uptime monitor";
})
// *****************************************************
// update Monitor
// *****************************************************
@@ -197,6 +254,24 @@ const uptimeMonitorsSlice = createSlice({
state.msg = action.payload
? action.payload.msg
: "Failed to delete uptime monitor";
})
// *****************************************************
// Pause Monitor
// *****************************************************
.addCase(pauseUptimeMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(pauseUptimeMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(pauseUptimeMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to pause uptime monitor";
});
},
});

View File

@@ -146,6 +146,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
<TableCell>Monitor Name</TableCell>
<TableCell>Status</TableCell>
<TableCell>Date & Time</TableCell>
<TableCell>Status Code</TableCell>
<TableCell>Message</TableCell>
</TableRow>
</TableHead>
@@ -166,7 +167,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
<TableCell>
{new Date(check.createdAt).toLocaleString()}
</TableCell>
<TableCell>{check.statusCode}</TableCell>
<TableCell>
{check.statusCode ? check.statusCode : "N/A"}
</TableCell>
<TableCell>{check.message}</TableCell>
</TableRow>
);
})}

View File

@@ -2,13 +2,15 @@ import { useNavigate, useParams } from "react-router";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
import { useEffect, useState } from "react";
import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
import { Box, Modal, Stack, Typography } from "@mui/material";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../styled";
import {
updateUptimeMonitor,
pauseUptimeMonitor,
getUptimeMonitorById,
getUptimeMonitorsByTeamId,
deleteUptimeMonitor,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
@@ -20,7 +22,8 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import "./index.css";
import SkeletonLayout from "./skeleton";
import ButtonSpinner from "../../../Components/ButtonSpinner";
/**
* Parses a URL string and returns a URL object.
*
@@ -35,54 +38,6 @@ const parseUrl = (url) => {
}
};
/**
* Renders a skeleton layout.
*
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
const theme = useTheme();
return (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.spacing(4) }}
/>
<Skeleton variant="rounded" width="50%" height={18} />
</Box>
<Stack
direction="row"
gap={theme.spacing(6)}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<Skeleton variant="rounded" width={150} height={34} />
</Stack>
</Stack>
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Stack direction="row" justifyContent="flex-end">
<Skeleton variant="rounded" width="15%" height={34} />
</Stack>
</Stack>
</>
);
};
/**
* Configure page displays monitor configurations and allows for editing actions.
* @component
@@ -93,7 +48,7 @@ const Configure = () => {
const theme = useTheme();
const dispatch = useDispatch();
const { user, authToken } = useSelector((state) => state.auth);
const { monitors } = useSelector((state) => state.uptimeMonitors);
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const [monitor, setMonitor] = useState({});
const [errors, setErrors] = useState({});
const { monitorId } = useParams();
@@ -107,15 +62,25 @@ const Configure = () => {
};
useEffect(() => {
const data = monitors.find((monitor) => monitor._id === monitorId);
if (!data) {
logger.error("Error fetching monitor of id: " + monitorId);
navigate("/not-found", { replace: true });
}
setMonitor({
...data,
});
}, [monitorId, authToken, monitors, navigate]);
const fetchMonitor = async () => {
try {
const action = await dispatch(
getUptimeMonitorById({ authToken, monitorId })
);
if (getUptimeMonitorById.fulfilled.match(action)) {
const monitor = action.payload.data;
setMonitor(monitor);
} else if (getUptimeMonitorById.rejected.match(action)) {
throw new Error(action.error.message);
}
} catch (error) {
logger.error("Error fetching monitor of id: " + monitorId);
navigate("/not-found", { replace: true });
}
};
fetchMonitor();
}, [monitorId, authToken, navigate]);
const handleChange = (event, name) => {
let { value, id } = event.target;
@@ -171,6 +136,23 @@ const Configure = () => {
}
};
const handlePause = async () => {
try {
const action = await dispatch(
pauseUptimeMonitor({ authToken, monitorId })
);
if (pauseUptimeMonitor.fulfilled.match(action)) {
const monitor = action.payload.data;
setMonitor(monitor);
} else if (pauseUptimeMonitor.rejected.match(action)) {
throw new Error(action.error.message);
}
} catch (error) {
logger.error("Error pausing monitor: " + monitorId);
createToast({ body: "Failed to pause monitor" });
}
};
const handleSubmit = async (event) => {
event.preventDefault();
const action = await dispatch(
@@ -207,11 +189,9 @@ const Configure = () => {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
let loading = Object.keys(monitor).length === 0;
return (
<Stack className="configure-monitor" gap={theme.spacing(12)}>
{loading ? (
{Object.keys(monitor).length === 0 ? (
<SkeletonLayout />
) : (
<>
@@ -268,9 +248,10 @@ const Configure = () => {
ml: "auto",
}}
>
<Button
<ButtonSpinner
isLoading={isLoading}
level="tertiary"
label="Pause"
label={monitor?.isActive ? "Pause" : "Resume"}
animate="rotate180"
img={<PauseCircleOutlineIcon />}
sx={{
@@ -282,8 +263,10 @@ const Configure = () => {
mr: theme.spacing(2),
},
}}
onClick={handlePause}
/>
<Button
<ButtonSpinner
isLoading={isLoading}
level="error"
label="Remove"
sx={{
@@ -398,7 +381,8 @@ const Configure = () => {
</Stack>
</ConfigBox>
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
<ButtonSpinner
isLoading={isLoading}
level="primary"
label="Save"
sx={{ px: theme.spacing(12) }}

View File

@@ -0,0 +1,52 @@
import { Box, Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
/**
* Renders a skeleton layout.
*
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
const theme = useTheme();
return (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.gap.small }}
/>
<Skeleton variant="rounded" width="50%" height={18} />
</Box>
<Stack
direction="row"
gap={theme.gap.medium}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<Skeleton variant="rounded" width={150} height={34} />
</Stack>
</Stack>
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Stack direction="row" justifyContent="flex-end">
<Skeleton variant="rounded" width="15%" height={34} />
</Stack>
</Stack>
</>
);
};
export default SkeletonLayout;

View File

@@ -101,6 +101,7 @@ const PaginationTable = ({ monitorId, dateRange }) => {
<TableRow>
<TableCell>Status</TableCell>
<TableCell>Date & Time</TableCell>
<TableCell>Status Code</TableCell>
<TableCell>Message</TableCell>
</TableRow>
</TableHead>
@@ -120,7 +121,10 @@ const PaginationTable = ({ monitorId, dateRange }) => {
<TableCell>
{new Date(check.createdAt).toLocaleString()}
</TableCell>
<TableCell>{check.statusCode}</TableCell>
<TableCell>
{check.statusCode ? check.statusCode : "N/A"}
</TableCell>
<TableCell>{check.message}</TableCell>
</TableRow>
);
})}

View File

@@ -172,6 +172,9 @@ const DetailsPage = ({ isAdmin }) => {
useEffect(() => {
const fetchCertificate = async () => {
if (monitor?.type !== "http") {
return;
}
try {
const res = await networkService.getCertificateExpiry(
authToken,
@@ -183,7 +186,7 @@ const DetailsPage = ({ isAdmin }) => {
}
};
fetchCertificate();
}, [authToken, monitorId]);
}, [authToken, monitorId, monitor]);
let loading = Object.keys(monitor).length === 0;
return (

View File

@@ -25,11 +25,20 @@ const Monitors = ({ isAdmin }) => {
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
}, [authState.authToken, dispatch]);
const up = monitorState.monitors.reduce((acc, cur) => {
return cur.status === true ? acc + 1 : acc;
}, 0);
const monitorStats = monitorState.monitors.reduce(
(acc, monitor) => {
if (monitor.isActive === false) {
acc["paused"] += 1;
} else if (monitor.status === true) {
acc["up"] += 1;
} else {
acc["down"] += 1;
}
return acc;
},
{ paused: 0, up: 0, down: 0 }
);
const down = monitorState.monitors.length - up;
const data = buildData(monitorState.monitors, isAdmin, navigate);
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
@@ -117,9 +126,9 @@ const Monitors = ({ isAdmin }) => {
direction="row"
justifyContent="space-between"
>
<StatusBox title="up" value={up} />
<StatusBox title="down" value={down} />
<StatusBox title="paused" value={0} />
<StatusBox title="up" value={monitorStats.up} />
<StatusBox title="down" value={monitorStats.down} />
<StatusBox title="paused" value={monitorStats.paused} />
</Stack>
<Box
flex={1}

View File

@@ -17,6 +17,27 @@ class NetworkService {
);
}
/**
*
* ************************************
* Create a new monitor
* ************************************
*
* @async
* @param {string} authToken - The authorization token to be used in the request header.
* @param {string} monitorId - The monitor ID to be sent in the param.
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*/
async getMonitorByid(authToken, monitorId) {
return this.axiosInstance.get(`/monitors/${monitorId}`, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
}
/**
*
* ************************************
@@ -163,6 +184,28 @@ class NetworkService {
},
});
}
/**
* ************************************
* Pauses a single monitor by its ID
* ************************************
*
* @async
* @param {string} authToken - The authorization token to be used in the request header.
* @param {string} monitorId - The ID of the monitor to be paused.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async pauseMonitorById(authToken, monitorId) {
return this.axiosInstance.post(
`/monitors/pause/${monitorId}`,
{},
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
@@ -226,7 +269,6 @@ class NetworkService {
return this.axiosInstance.put(`/auth/user/${userId}`, form, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
}

View File

@@ -5,6 +5,7 @@ const {
createMonitorBodyValidation,
editMonitorBodyValidation,
getMonitorsByTeamIdQueryValidation,
pauseMonitorParamValidation,
} = require("../validation/joi");
const sslChecker = require("ssl-checker");
@@ -123,7 +124,7 @@ const getMonitorById = async (req, res, next) => {
}
try {
const monitor = await req.db.getMonitorById(req, res);
const monitor = await req.db.getMonitorById(req.params.monitorId);
if (!monitor) {
const error = new Error(errorMessages.MONITOR_GET_BY_ID);
error.status = 404;
@@ -309,7 +310,7 @@ const editMonitor = async (req, res, next) => {
// Get notifications from the request body
const notifications = req.body.notifications;
const editedMonitor = await req.db.editMonitor(req, res);
const editedMonitor = await req.db.editMonitor(monitorId, req.body);
await req.db.deleteNotificationsByMonitorId(editedMonitor._id);
@@ -337,6 +338,43 @@ const editMonitor = async (req, res, next) => {
}
};
const pauseMonitor = async (req, res, next) => {
try {
await pauseMonitorParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
}
try {
const monitor = await req.db.getMonitorById(req.params.monitorId);
if (monitor.isActive) {
await req.jobQueue.deleteJob(monitor);
} else {
await req.jobQueue.addJob(monitor._id, monitor);
}
monitor.isActive = !monitor.isActive;
const updatedMonitor = await req.db.editMonitor(
req.params.monitorId,
monitor
);
return res.status(200).json({
success: true,
msg: updatedMonitor.isActive
? successMessages.MONITOR_RESUME
: successMessages.MONITOR_PAUSE,
data: updatedMonitor,
});
} catch (error) {
error.service = SERVICE_NAME;
error.method = "pauseMonitor";
next(error);
}
};
module.exports = {
getAllMonitors,
getMonitorStatsById,
@@ -347,4 +385,5 @@ module.exports = {
deleteMonitor,
deleteAllMonitors,
editMonitor,
pauseMonitor,
};

View File

@@ -15,7 +15,7 @@ const { NormalizeData } = require("../../../utils/dataUtils");
*/
const getAllMonitors = async (req, res) => {
try {
const monitors = await Monitor.find({ isActive: true });
const monitors = await Monitor.find();
return monitors;
} catch (error) {
throw error;
@@ -431,9 +431,7 @@ const deleteMonitorsByUserId = async (userId) => {
* @returns {Promise<Monitor>}
* @throws {Error}
*/
const editMonitor = async (req, res) => {
const candidateId = req.params.monitorId;
const candidateMonitor = req.body;
const editMonitor = async (candidateId, candidateMonitor) => {
candidateMonitor.notifications = undefined;
try {

View File

@@ -89,7 +89,6 @@ const getUserByEmail = async (email) => {
const updateUser = async (req, res) => {
const candidateUserId = req.params.userId;
try {
const candidateUser = { ...req.body };
// ******************************************

View File

@@ -31,4 +31,11 @@ router.delete(
isAllowed(["superadmin"]),
monitorController.deleteAllMonitors
);
router.post(
"/pause/:monitorId",
isAllowed(["admin", "superadmin"]),
monitorController.pauseMonitor
);
module.exports = router;

View File

@@ -87,6 +87,8 @@ const successMessages = {
//Job Queue
JOB_QUEUE_DELETE_JOB: "Job removed successfully",
JOB_QUEUE_OBLITERATE: "Queue OBLITERATED!!!",
JOB_QUEUE_PAUSE_JOB: "Job paused successfully",
JOB_QUEUE_RESUME_JOB: "Job resumed successfully",
//Maintenance Window Controller
MAINTENANCE_WINDOW_CREATE: "Maintenance Window created successfully",

View File

@@ -197,6 +197,10 @@ const editMonitorBodyValidation = joi.object({
notifications: joi.array().items(joi.object()),
});
const pauseMonitorParamValidation = joi.object({
monitorId: joi.string().required(),
});
//****************************************
// Alerts
//****************************************
@@ -350,6 +354,7 @@ module.exports = {
getMonitorsByTeamIdValidation,
getMonitorsByTeamIdQueryValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
editUserParamValidation,
editUserBodyValidation,
createAlertParamValidation,