Merge latest changes

This commit is contained in:
Daniel Cojocea committed 2024-08-28 00:20:14 -04:00
commit b34a17004b
57 files changed
+1242 -844

No files matched your search

+9 -5
View File
@@ -7,7 +7,7 @@ import Login from "./Pages/Auth/Login";
import Register from "./Pages/Auth/Register/Register";
import HomeLayout from "./Layouts/HomeLayout";
import Account from "./Pages/Account";
import Monitors from "./Pages/Monitors";
import Monitors from "./Pages/Monitors/Home";
import CreateMonitor from "./Pages/Monitors/CreateMonitor";
import Incidents from "./Pages/Incidents";
import Status from "./Pages/Status";
@@ -21,6 +21,7 @@ import ProtectedRoute from "./Components/ProtectedRoute";
import Details from "./Pages/Monitors/Details";
import Maintenance from "./Pages/Maintenance";
import withAdminCheck from "./HOC/withAdminCheck";
import withAdminProp from "./HOC/withAdminProp";
import Configure from "./Pages/Monitors/Configure";
import PageSpeed from "./Pages/PageSpeed";
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
@@ -30,6 +31,9 @@ import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
function App() {
const AdminCheckedRegister = withAdminCheck(Register);
const MonitorsWithAdminProp = withAdminProp(Monitors);
const DetailsWithAdminProp = withAdminProp(Details);
const PageSpeedWithAdminProp = withAdminProp(PageSpeed);
return (
<>
<Routes>
@@ -37,11 +41,11 @@ function App() {
<Route
exact
path="/"
element={<ProtectedRoute Component={Monitors} />}
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
/>
<Route
path="/monitors"
element={<ProtectedRoute Component={Monitors} />}
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
/>
<Route
path="/monitors/create"
@@ -49,7 +53,7 @@ function App() {
/>
<Route
path="/monitors/:monitorId/"
element={<ProtectedRoute Component={Details} />}
element={<ProtectedRoute Component={DetailsWithAdminProp} />}
/>
<Route
path="/monitors/configure/:monitorId/"
@@ -94,7 +98,7 @@ function App() {
/>
<Route
path="pagespeed"
element={<ProtectedRoute Component={PageSpeed} />}
element={<ProtectedRoute Component={PageSpeedWithAdminProp} />}
/>
<Route
path="pagespeed/create"
+11 -8
View File
@@ -19,7 +19,7 @@ import "./index.css";
* @returns {JSX.Element} The rendered fallback UI.
*/
const Fallback = ({ title, checks, link = "/" }) => {
const Fallback = ({ title, checks, link = "/", isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
@@ -40,7 +40,7 @@ const Fallback = ({ title, checks, link = "/" }) => {
marginY={theme.spacing(6)}
color={theme.palette.text.secondary}
>
A {title} is used to:
A {title} monitor is used to:
</Typography>
{checks.map((check, index) => (
<Check
@@ -50,12 +50,14 @@ const Fallback = ({ title, checks, link = "/" }) => {
/>
))}
</Stack>
<Button
level="primary"
label={`Let's create your ${title}`}
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
/>
{isAdmin && (
<Button
level="primary"
label={`Let's create your ${title}`}
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
/>
)}
</Stack>
);
};
@@ -64,6 +66,7 @@ Fallback.propTypes = {
title: PropTypes.string.isRequired,
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
link: PropTypes.string,
isAdmin: PropTypes.bool,
};
export default Fallback;
+8 -1
View File
@@ -199,7 +199,14 @@ const Field = forwardRef(
Field.displayName = "Field";
Field.propTypes = {
type: PropTypes.oneOf(["text", "password", "url", "email", "description"]),
type: PropTypes.oneOf([
"text",
"password",
"url",
"email",
"description",
"number",
]),
id: PropTypes.string.isRequired,
label: PropTypes.string,
https: PropTypes.bool,
+2 -2
View File
@@ -339,7 +339,7 @@ function Sidebar() {
if (
child.name === "Team" &&
authState.user?.role &&
!authState.user.role.includes("admin")
!authState.user.role.includes("superadmin")
) {
return null;
}
@@ -399,7 +399,7 @@ function Sidebar() {
if (
child.name === "Team" &&
authState.user?.role &&
!authState.user.role.includes("admin")
!authState.user.role.includes("superadmin")
) {
return null;
}
@@ -32,6 +32,12 @@ import Select from "../../Inputs/Select";
*/
const TeamPanel = () => {
const roleMap = {
superadmin: "Super admin",
admin: "Admin",
user: "Team member",
};
const theme = useTheme();
const { authToken, user } = useSelector((state) => state.auth);
@@ -78,6 +84,7 @@ const TeamPanel = () => {
// { id: 4, name: "ACTION" },
],
rows: team?.map((member, idx) => {
const roles = member.role.map((role) => roleMap[role]).join(",");
return {
id: member._id,
data: [
@@ -100,7 +107,7 @@ const TeamPanel = () => {
{
// TODO - Add select dropdown
id: idx + 2,
data: member.role.includes("admin") ? "Administrator" : "Member",
data: roles,
},
// FEATURE STILL TO BE IMPLEMENTED
// {
@@ -28,12 +28,12 @@ export const createPageSpeed = createAsyncThunk(
}
);
export const getPageSpeedByUserId = createAsyncThunk(
"pageSpeedMonitors/getPageSpeedByUserId",
export const getPageSpeedByTeamId = createAsyncThunk(
"pageSpeedMonitors/getPageSpeedByTeamId",
async (token, thunkApi) => {
const user = jwtDecode(token);
try {
const res = await networkService.getMonitorsByUserId(
const res = await networkService.getMonitorsByTeamId(
token,
user._id,
25,
@@ -127,15 +127,15 @@ const pageSpeedMonitorSlice = createSlice({
// Monitors by userId
// *****************************************************
.addCase(getPageSpeedByUserId.pending, (state) => {
.addCase(getPageSpeedByTeamId.pending, (state) => {
state.isLoading = true;
})
.addCase(getPageSpeedByUserId.fulfilled, (state, action) => {
.addCase(getPageSpeedByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.msg;
state.monitors = action.payload.data;
})
.addCase(getPageSpeedByUserId.rejected, (state, action) => {
.addCase(getPageSpeedByTeamId.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
@@ -28,14 +28,14 @@ export const createUptimeMonitor = createAsyncThunk(
}
);
export const getUptimeMonitorsByUserId = createAsyncThunk(
"montiors/getMonitorsByUserId",
export const getUptimeMonitorsByTeamId = createAsyncThunk(
"montiors/getMonitorsByTeamId",
async (token, thunkApi) => {
const user = jwtDecode(token);
try {
const res = await networkService.getMonitorsByUserId(
const res = await networkService.getMonitorsByTeamId(
token,
user._id,
user.teamId,
25,
["http", "ping"],
null,
@@ -126,15 +126,15 @@ const uptimeMonitorsSlice = createSlice({
// Monitors by userId
// *****************************************************
.addCase(getUptimeMonitorsByUserId.pending, (state) => {
.addCase(getUptimeMonitorsByTeamId.pending, (state) => {
state.isLoading = true;
})
.addCase(getUptimeMonitorsByUserId.fulfilled, (state, action) => {
.addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.msg;
state.monitors = action.payload.data;
})
.addCase(getUptimeMonitorsByUserId.rejected, (state, action) => {
.addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
+2 -2
View File
@@ -10,7 +10,7 @@ const withAdminCheck = (WrappedComponent) => {
useEffect(() => {
networkService
.doesAdminExist()
.doesSuperAdminExist()
.then((response) => {
if (response.data.data === true) {
navigate("/login");
@@ -20,7 +20,7 @@ const withAdminCheck = (WrappedComponent) => {
logger.error(error);
});
}, [navigate]);
return <WrappedComponent {...props} isAdmin={true} />;
return <WrappedComponent {...props} isSuperAdmin={true} />;
};
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
+20
View File
@@ -0,0 +1,20 @@
import { useSelector } from "react-redux";
const withAdminProp = (WrappedComponent) => {
const WithAdminProp = (props) => {
const { user } = useSelector((state) => state.auth);
const isAdmin =
(user?.role?.includes("admin") ?? false) ||
(user?.role?.includes("superadmin") ?? false);
return <WrappedComponent {...props} isAdmin={isAdmin} />;
};
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
WithAdminProp.displayName = `WithAdminProp(${wrappedComponentName})`;
return WithAdminProp;
};
export default withAdminProp;
+6 -2
View File
@@ -25,7 +25,11 @@ const Account = ({ open = "profile" }) => {
let tabList = ["Profile", "Password", "Team"];
const { user } = useSelector((state) => state.auth);
if (!user.role.includes("admin")) tabList = ["Profile", "Password"];
const requiredRoles = ["superadmin", "admin"];
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
if (hideTeams) {
tabList = ["Profile", "Password"];
}
return (
<Box
@@ -71,7 +75,7 @@ const Account = ({ open = "profile" }) => {
</Box>
<ProfilePanel />
<PasswordPanel />
{user.role.includes("admin") && <TeamPanel />}
{!hideTeams && <TeamPanel />}
</TabContext>
</Box>
);
+1 -1
View File
@@ -321,7 +321,7 @@ const Login = () => {
return;
}
networkService
.doesAdminExist()
.doesSuperAdminExist()
.then((response) => {
if (response.data.data === false) {
navigate("/register");
+21 -13
View File
@@ -23,11 +23,11 @@ import { logger } from "../../../Utils/Logger";
* Displays the initial landing page.
*
* @param {Object} props
* @param {boolean} props.isAdmin - Whether the user is creating and admin account
* @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
* @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
* @returns {JSX.Element}
*/
const LandingPage = ({ isAdmin, onSignup }) => {
const LandingPage = ({ isSuperAdmin, onSignup }) => {
const theme = useTheme();
return (
@@ -40,7 +40,8 @@ const LandingPage = ({ isAdmin, onSignup }) => {
<Box>
<Typography component="h1">Sign Up</Typography>
<Typography>
Create your {isAdmin ? "admin " : ""}account to get started.
Create your {isSuperAdmin ? "Super admin " : ""}account to get
started.
</Typography>
</Box>
<Box width="100%">
@@ -103,7 +104,7 @@ const LandingPage = ({ isAdmin, onSignup }) => {
};
LandingPage.propTypes = {
isAdmin: PropTypes.bool,
isSuperAdmin: PropTypes.bool,
onSignup: PropTypes.func,
};
@@ -476,7 +477,7 @@ StepThree.propTypes = {
onBack: PropTypes.func,
};
const Register = ({ isAdmin }) => {
const Register = ({ isSuperAdmin }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { token } = useParams();
@@ -497,6 +498,7 @@ const Register = ({ isAdmin }) => {
password: "",
confirm: "",
role: [],
teamId: "",
});
const [errors, setErrors] = useState({});
const [step, setStep] = useState(0);
@@ -506,15 +508,16 @@ const Register = ({ isAdmin }) => {
if (token !== undefined) {
try {
const res = await networkService.verifyInvitationToken(token);
const { role, email } = res.data.data;
setForm({ ...form, email, role });
const invite = res.data.data;
const { role, email, teamId } = invite;
setForm({ ...form, email, role, teamId });
} catch (error) {
logger.error(error);
navigate("/register", { replace: true });
}
}
};
fetchInvite();
}, [token, form]);
}, []);
/**
* Validates the form data against the validation schema.
@@ -547,7 +550,6 @@ const Register = ({ isAdmin }) => {
const handleStepOne = async (e) => {
e.preventDefault();
let error = validateForm({
firstName: form.firstName,
lastName: form.lastName,
@@ -579,7 +581,10 @@ const Register = ({ isAdmin }) => {
const handleStepThree = async (e) => {
e.preventDefault();
let registerForm = { ...form, role: isAdmin ? ["admin"] : form.role };
let registerForm = {
...form,
role: isSuperAdmin ? ["superadmin"] : form.role,
};
let error = validateForm(registerForm, {
context: { password: form.password },
});
@@ -684,7 +689,10 @@ const Register = ({ isAdmin }) => {
}}
>
{step === 0 ? (
<LandingPage isAdmin={isAdmin} onSignup={() => setStep(1)} />
<LandingPage
isSuperAdmin={isSuperAdmin}
onSignup={() => setStep(1)}
/>
) : step === 1 ? (
<StepOne
form={form}
@@ -732,6 +740,6 @@ const Register = ({ isAdmin }) => {
);
};
Register.propTypes = {
isAdmin: PropTypes.bool,
isSuperAdmin: PropTypes.bool,
};
export default Register;
@@ -46,9 +46,9 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
try {
let res;
if (selectedMonitor === "0") {
res = await networkService.getChecksByUser(
res = await networkService.getChecksByTeam(
authToken,
user._id,
user.teamId,
"desc",
null,
null,
+2 -3
View File
@@ -48,16 +48,15 @@ const Incidents = () => {
useEffect(() => {
const fetchMonitors = async () => {
setLoading(true);
const res = await networkService.getMonitorsByUserId(
const res = await networkService.getMonitorsByTeamId(
authState.authToken,
authState.user._id,
authState.user.teamId,
1,
null,
null,
null,
null
);
// Reduce to a lookup object for 0(1) lookup
if (res.data && res.data.data.length > 0) {
const monitorLookup = res.data.data.reduce((acc, monitor) => {
@@ -23,7 +23,7 @@
}
.duration-config .field-text .MuiTextField-root,
.duration-config .field {
.maintenance-options .duration-config .field {
width: 70px;
min-width: 70px;
max-width: 70px;
@@ -10,6 +10,9 @@ import dayjs from "dayjs";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import Field from "../../../Components/Inputs/Field";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { logger } from "../../../Utils/Logger";
import { createToast } from "../../../Utils/toastUtils";
const directory = {
title: "Create a maintenance window",
@@ -59,9 +62,10 @@ const CreateNewMaintenanceWindow = () => {
startTime: dayjs(),
duration: "60",
unit: "minutes",
friendlyName: "",
displayName: "",
AddMonitors: "",
});
const [errors, setErrors] = useState({});
const handleChange = (event, name) => {
const { value } = event.target;
@@ -71,6 +75,41 @@ const CreateNewMaintenanceWindow = () => {
}));
};
const handleSubmit = async () => {
const data = {
repeat: values.repeat,
date: values.date.format("YYYY-MM-DD"),
startTime: values.startTime.format("HH:mm"),
duration: values.duration,
unit: values.unit,
displayName: values.displayName,
addMonitors: values.AddMonitors,
};
const { error } = maintenanceWindowValidation.validate(data, {
abortEarly: false,
});
logger.log("error: ", error);
if (!error || error.details.length === 0) {
setErrors({});
} else {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data",
});
logger.error("Validation errors:", error.details);
}
logger.log("Submitting data: ", data);
};
const configOptions = [
{
title: "Repeat",
@@ -126,6 +165,8 @@ const CreateNewMaintenanceWindow = () => {
placeholder="60"
onChange={(e) => handleChange(e, "duration")}
value={values.duration}
error={errors.duration}
type="number"
/>
<Select
onChange={(e) => handleChange(e, "unit")}
@@ -137,13 +178,14 @@ const CreateNewMaintenanceWindow = () => {
),
},
{
title: "Friendly name",
title: "Display name",
component: (
<Field
id="friendly-name"
id="display-name"
placeholder="Maintanence at __ : __ for ___ minutes"
value={values.friendlyName}
onChange={(e) => handleChange(e, "friendlyName")}
value={values.displayName}
onChange={(e) => handleChange(e, "displayName")}
error={errors.displayName}
/>
),
},
@@ -160,6 +202,7 @@ const CreateNewMaintenanceWindow = () => {
placeholder="Start typing to search for current monitors"
value={values.AddMonitors}
onChange={(e) => handleChange(e, "AddMonitors")}
error={errors.addMonitors}
/>
<Typography
sx={{
@@ -246,6 +289,7 @@ const CreateNewMaintenanceWindow = () => {
}}
level="primary"
label="Create"
onClick={handleSubmit}
/>
</Stack>
</Stack>
@@ -8,7 +8,7 @@ import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import {
updateUptimeMonitor,
getUptimeMonitorsByUserId,
getUptimeMonitorsByTeamId,
deleteUptimeMonitor,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Button from "../../../Components/Button";
@@ -177,7 +177,7 @@ const Configure = () => {
);
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor updated successfully!" });
dispatch(getUptimeMonitorsByUserId(authToken));
dispatch(getUptimeMonitorsByTeamId(authToken));
} else {
createToast({ body: "Failed to update monitor." });
}
@@ -314,7 +314,7 @@ const Configure = () => {
<Field
type="text"
id="monitor-name"
label="Friendly name"
label="Display name"
isOptional={true}
placeholder="Google"
value={monitor?.name || ""}
@@ -118,6 +118,7 @@ const CreateMonitor = () => {
form = {
...form,
description: form.name,
teamId: user.teamId,
userId: user._id,
notifications: monitor.notifications,
};
@@ -193,7 +194,7 @@ const CreateMonitor = () => {
<Field
type="text"
id="monitor-name"
label="Friendly name"
label="Display name"
isOptional={true}
placeholder="Google"
value={monitor.name}
+29 -24
View File
@@ -110,7 +110,7 @@ const SkeletonLayout = () => {
* Details page component displaying monitor details and related information.
* @component
*/
const DetailsPage = () => {
const DetailsPage = ({ isAdmin }) => {
const [monitor, setMonitor] = useState({});
const { monitorId } = useParams();
const { authToken } = useSelector((state) => state.auth);
@@ -196,29 +196,31 @@ const DetailsPage = () => {
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Box>
<Button
level="tertiary"
label="Configure"
animate="rotate90"
img={
<SettingsIcon
style={{
minWidth: theme.gap.mlplus,
minHeight: theme.gap.mlplus,
}}
/>
}
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
backgroundColor: "#f4f4f4",
px: theme.gap.medium,
"& svg": {
mr: "6px",
},
}}
/>
{isAdmin && (
<Button
level="tertiary"
label="Configure"
animate="rotate90"
img={
<SettingsIcon
style={{
minWidth: theme.gap.mlplus,
minHeight: theme.gap.mlplus,
}}
/>
}
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
backgroundColor: "#f4f4f4",
px: theme.gap.medium,
"& svg": {
mr: "6px",
},
}}
/>
)}
</Stack>
<Stack
direction="row"
@@ -339,4 +341,7 @@ const DetailsPage = () => {
);
};
DetailsPage.propTypes = {
isAdmin: PropTypes.bool,
};
export default DetailsPage;
@@ -0,0 +1,105 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Arrow from "../../../assets/icons/top-right-arrow.svg?react";
import background from "../../../assets/Images/background-grid.svg";
import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react";
const StatusBox = ({ title, value }) => {
const theme = useTheme();
let sharedStyles = { position: "absolute", right: 8, opacity: 0.5 };
let color;
let icon;
if (title === "up") {
color = theme.pie.green.stroke;
icon = (
<Box sx={{ ...sharedStyles, top: 8 }}>
<Arrow />
</Box>
);
} else if (title === "down") {
color = theme.pie.red.stroke;
icon = (
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
<Arrow />
</Box>
);
} else if (title === "paused") {
color = theme.pie.yellow.stroke;
icon = (
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
<ClockSnooze />
</Box>
);
}
return (
<Box
position="relative"
flex={1}
border={1}
borderColor={theme.palette.otherColors.graishWhite}
borderRadius={`${theme.shape.borderRadius}px`}
backgroundColor={theme.palette.otherColors.white}
px={theme.gap.large}
py={theme.gap.ml}
overflow="hidden"
sx={{
"&:hover": {
backgroundColor: "#f9fafb",
},
"&:after": {
position: "absolute",
content: `""`,
backgroundImage: `url(${background})`,
width: "400px",
height: "200px",
top: "-10%",
left: "5%",
zIndex: 10000,
pointerEvents: "none",
},
}}
>
<Box
textTransform="uppercase"
fontSize={15}
letterSpacing={0.5}
color={theme.palette.otherColors.bluishGray}
mb={theme.gap.ml}
sx={{ opacity: 0.6 }}
>
{title}
</Box>
{icon}
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontWeight={600}
color={color}
gap="2px"
>
{value}
<Typography
component="span"
fontSize={20}
fontWeight={300}
color={theme.palette.otherColors.bluishGray}
sx={{ opacity: 0.3 }}
>
#
</Typography>
</Stack>
</Box>
);
};
StatusBox.propTypes = {
title: PropTypes.oneOf(["up", "down", "paused"]).isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
};
export default StatusBox;
@@ -0,0 +1,199 @@
import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { createToast } from "../../../Utils/toastUtils";
import {
IconButton,
Menu,
MenuItem,
Modal,
Stack,
Typography,
} from "@mui/material";
import {
deleteUptimeMonitor,
getUptimeMonitorsByTeamId,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Settings from "../../../assets/icons/settings-bold.svg?react";
import { useTheme } from "@emotion/react";
import Button from "../../../Components/Button";
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
const ActionsMenu = ({ monitor, isAdmin }) => {
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
const dispatch = useDispatch();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
let monitor = { _id: actions.id };
const action = await dispatch(
deleteUptimeMonitor({ authToken: authState.authToken, monitor })
);
if (action.meta.requestStatus === "fulfilled") {
setIsOpen(false); // close modal
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
createToast({ body: "Monitor deleted successfully." });
} else {
createToast({ body: "Failed to delete monitor." });
}
};
const openMenu = (event, id, url) => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(event.currentTarget);
setActions({ id: id, url: url });
};
const openRemove = (e) => {
closeMenu(e);
setIsOpen(true);
};
const closeMenu = (e) => {
e.stopPropagation();
setAnchorEl(null);
};
const navigate = useNavigate();
return (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(
event,
monitor._id,
monitor.type === "ping" ? null : monitor.url
);
}}
sx={{
"&:focus": {
outline: "none",
},
}}
>
<Settings />
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => closeMenu(e)}
>
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/${actions.id}`);
}}
>
Details
</MenuItem>
{/* TODO - pass monitor id to Incidents page */}
<MenuItem disabled>Incidents</MenuItem>
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
>
Remove
</MenuItem>
)}
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
aria-describedby="delete-monitor-confirmation"
open={isOpen}
onClose={() => setIsOpen(false)}
disablePortal
>
<Stack
gap={theme.gap.xs}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "white",
border: "solid 1px #f2f2f2",
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: 24,
p: "30px",
"&:focus": {
outline: "none",
},
}}
>
<Typography id="modal-delete-monitor" component="h2">
Do you really want to delete this monitor?
</Typography>
<Typography id="delete-monitor-confirmation">
Once deleted, this monitor cannot be retrieved.
</Typography>
<Stack
direction="row"
gap={theme.gap.small}
mt={theme.gap.large}
justifyContent="flex-end"
>
<Button
level="tertiary"
label="Cancel"
onClick={() => setIsOpen(false)}
/>
<Button
level="error"
label="Delete"
onClick={(e) => handleRemove(e)}
/>
</Stack>
</Stack>
</Modal>
</>
);
};
ActionsMenu.propTypes = {
monitor: PropTypes.shape({
_id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
}).isRequired,
isAdmin: PropTypes.bool,
};
export default ActionsMenu;
@@ -0,0 +1,43 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import Button from "../../../Components/Button";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
const Fallback = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<Stack
alignItems="center"
backgroundColor={theme.palette.otherColors.white}
p={theme.gap.xxl}
gap={theme.gap.xs}
border={1}
borderRadius={`${theme.shape.borderRadius}px`}
borderColor={theme.palette.otherColors.graishWhite}
>
<Typography component="h2">No monitors found</Typography>
<Typography>
It looks like you don’t have any monitors set up yet.
</Typography>
{isAdmin && (
<Button
level="primary"
label="Create your first monitor"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ mt: theme.gap.large }}
/>
)}
</Stack>
);
};
Fallback.propTypes = {
isAdmin: PropTypes.bool,
};
export default Fallback;
+33
View File
@@ -0,0 +1,33 @@
import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
*
* @component
* @param {Object} params - An object containing the following properties:
* @param {string} params.title - The name of the host.
* @param {string} params.percentageColor - The color of the percentage text.
* @param {number} params.percentage - The percentage to display.
* @returns {React.ElementType} Returns a div element with the host details.
*/
const Host = ({ params }) => {
return (
<Stack direction="row" alignItems="baseline" className="host">
{params.title}
<Typography component="span" sx={{ color: params.percentageColor }}>
{params.percentage}%
</Typography>
</Stack>
);
};
Host.propTypes = {
params: PropTypes.shape({
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.number,
}).isRequired,
};
export default Host;
File renamed without changes.
+110
View File
@@ -0,0 +1,110 @@
import "./index.css";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useNavigate } from "react-router-dom";
import Button from "../../../Components/Button";
import { useTheme } from "@emotion/react";
import BasicTable from "../../../Components/BasicTable";
import { Box, Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import SkeletonLayout from "./skeleton";
import Fallback from "./fallback";
import StatusBox from "./StatusBox";
import { buildData } from "./monitorData";
const Monitors = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const monitorState = useSelector((state) => state.uptimeMonitors);
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
useEffect(() => {
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
}, [authState.authToken, dispatch]);
const up = monitorState.monitors.reduce((acc, cur) => {
return cur.status === true ? acc + 1 : acc;
}, 0);
const down = monitorState.monitors.length - up;
const data = buildData(monitorState.monitors, isAdmin, navigate);
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
return (
<Stack className="monitors" gap={theme.gap.large}>
{loading ? (
<SkeletonLayout />
) : (
<>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography
component="h1"
sx={{ lineHeight: 1, alignSelf: "flex-end" }}
>
Hello, {authState.user.firstName}
</Typography>
{monitorState.monitors?.length !== 0 && (
<Button
level="primary"
label="Create monitor"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
/>
)}
</Stack>
{monitorState.monitors?.length === 0 && (
<Fallback isAdmin={isAdmin} />
)}
{monitorState.monitors?.length !== 0 && (
<>
<Stack
gap={theme.gap.large}
direction="row"
justifyContent="space-between"
>
<StatusBox title="up" value={up} />
<StatusBox title="down" value={down} />
<StatusBox title="paused" value={0} />
</Stack>
<Box
flex={1}
p={theme.gap.lgplus}
border={1}
borderColor={theme.palette.otherColors.graishWhite}
backgroundColor={theme.palette.otherColors.white}
sx={{
borderRadius: `${theme.shape.borderRadius}px`,
}}
>
<Stack direction="row" alignItems="center" mb={theme.gap.large}>
<Typography component="h2">Current monitors</Typography>
<Box className="current-monitors-counter">
{monitorState.monitors.length}
</Box>
{/* TODO - add search bar */}
</Stack>
<BasicTable data={data} paginated={true} table={"monitors"} />
</Box>
</>
)}
</>
)}
</Stack>
);
};
Monitors.propTypes = {
isAdmin: PropTypes.bool,
};
export default Monitors;
@@ -0,0 +1,77 @@
import Host from "./host";
import ActionsMenu from "./actionsMenu";
import BarChart from "../../../Components/Charts/BarChart";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import { StatusLabel } from "../../../Components/Label";
import { Box } from "@mui/material";
const data = {
cols: [
{ id: 1, name: "Host" },
{
id: 2,
name: (
<Box width="max-content">
Status
<span>
<ArrowDownwardRoundedIcon />
</span>
</Box>
),
},
{ id: 3, name: "Response Time" },
{ id: 4, name: "Type" },
{ id: 5, name: "Actions" },
],
rows: [],
};
export const buildData = (monitors, isAdmin, navigate) => {
data.rows = monitors.map((monitor, idx) => {
const params = {
title: monitor.name,
percentage: 100,
percentageColor:
monitor.status === true
? "var(--env-var-color-17)"
: "var(--env-var-color-19)",
status: monitor.status === true ? "up" : "down",
};
// Reverse checks so latest check is on the right
const reversedChecks = monitor.checks.slice().reverse();
return {
id: monitor._id,
// disabled for now
handleClick: () => {
navigate(`/monitors/${monitor._id}`);
},
data: [
{ id: idx, data: <Host params={params} /> },
{
id: idx + 1,
data: (
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
),
},
{ id: idx + 2, data: <BarChart checks={reversedChecks} /> },
{
id: idx + 3,
data: (
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
),
},
{
id: idx + 4,
data: <ActionsMenu monitor={monitor} isAdmin={isAdmin} />,
},
],
};
});
return data;
};
@@ -0,0 +1,36 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
/**
* Renders a skeleton layout.
*
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
const theme = useTheme();
return (
<>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Skeleton variant="rounded" width="50%" height={36} />
<Skeleton variant="rounded" width="15%" height={36} />
</Stack>
<Stack
gap={theme.gap.large}
direction="row"
justifyContent="space-between"
>
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
</Stack>
<Stack gap={theme.gap.large} p={theme.gap.xl} backgroundColor="#f9fafb">
<Skeleton variant="rounded" width="50%" height={25} />
<Skeleton variant="rounded" width="100%" height={300} />
<Skeleton variant="rounded" width="100%" height={100} />
</Stack>
</>
);
};
export default SkeletonLayout;
-623
View File
@@ -1,623 +0,0 @@
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
deleteUptimeMonitor,
getUptimeMonitorsByUserId,
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { StatusLabel } from "../../Components/Label";
import { createToast } from "../../Utils/toastUtils";
import {
Box,
IconButton,
Menu,
MenuItem,
Modal,
Skeleton,
Stack,
Typography,
} from "@mui/material";
import BasicTable from "../../Components/BasicTable";
import Button from "../../Components/Button";
import Settings from "../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import BarChart from "../../Components/Charts/BarChart";
import Breadcrumbs from "../../Components/Breadcrumbs";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import background from "../../assets/Images/background-grid.svg";
import Arrow from "../../assets/icons/top-right-arrow.svg?react";
import ClockSnooze from "../../assets/icons/clock-snooze.svg?react";
import "./index.css";
const ActionsMenu = ({ monitor }) => {
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
const dispatch = useDispatch();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
let monitor = { _id: actions.id };
const action = await dispatch(
deleteUptimeMonitor({ authToken: authState.authToken, monitor })
);
if (action.meta.requestStatus === "fulfilled") {
setIsOpen(false); // close modal
dispatch(getUptimeMonitorsByUserId(authState.authToken));
createToast({ body: "Monitor deleted successfully." });
} else {
createToast({ body: "Failed to delete monitor." });
}
};
const openMenu = (event, id, url) => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(event.currentTarget);
setActions({ id: id, url: url });
};
const openRemove = (e) => {
closeMenu(e);
setIsOpen(true);
};
const closeMenu = (e) => {
e.stopPropagation();
setAnchorEl(null);
};
const navigate = useNavigate();
return (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(
event,
monitor._id,
monitor.type === "ping" ? null : monitor.url
);
}}
sx={{
"&:focus": {
outline: "none",
},
}}
>
<Settings />
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => closeMenu(e)}
>
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/${actions.id}`);
}}
>
Details
</MenuItem>
{/* TODO - pass monitor id to Incidents page */}
<MenuItem disabled>Incidents</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
>
Remove
</MenuItem>
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
aria-describedby="delete-monitor-confirmation"
open={isOpen}
onClose={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
disablePortal
>
<Stack
gap={theme.gap.xs}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "white",
border: "solid 1px #f2f2f2",
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: 24,
p: "30px",
"&:focus": {
outline: "none",
},
}}
>
<Typography id="modal-delete-monitor" component="h2">
Do you really want to delete this monitor?
</Typography>
<Typography id="delete-monitor-confirmation">
Once deleted, this monitor cannot be retrieved.
</Typography>
<Stack
direction="row"
gap={theme.gap.small}
mt={theme.gap.large}
justifyContent="flex-end"
>
<Button
level="tertiary"
label="Cancel"
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
/>
<Button
level="error"
label="Delete"
onClick={(e) => handleRemove(e)}
/>
</Stack>
</Stack>
</Modal>
</>
);
};
ActionsMenu.propTypes = {
monitor: PropTypes.shape({
_id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
}).isRequired,
};
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
*
* @component
* @param {Object} params - An object containing the following properties:
* @param {string} params.title - The name of the host.
* @param {string} params.percentageColor - The color of the percentage text.
* @param {number} params.percentage - The percentage to display.
* @returns {React.ElementType} Returns a div element with the host details.
*/
const Host = ({ params }) => {
return (
<Box className="host">
<Box
display="inline-block"
position="relative"
sx={{
fontWeight: 500,
"&:before": {
position: "absolute",
content: `""`,
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "gray",
opacity: 0.8,
right: "-10px",
top: "42%",
},
}}
>
{params.title}
</Box>
<Typography
component="span"
sx={{
color: params.percentageColor,
fontWeight: 500,
ml: "15px",
}}
>
{params.percentage}%
</Typography>
<Box sx={{ opacity: 0.6 }}>{params.url}</Box>
</Box>
);
};
Host.propTypes = {
params: PropTypes.shape({
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.number,
}).isRequired,
};
/**
* StatusBox component displays a status box with a title and value.
* The icon and color change based on the status title.
*
* @param {Object} props
* @param {string} props.title - The status title, which determines the icon and color.
* @param {number|string} props.value - The value to be displayed inside the box.
* @returns {JSX.Element} The rendered StatusBox component.
*/
const StatusBox = ({ title, value }) => {
const theme = useTheme();
let sharedStyles = { position: "absolute", right: 8, opacity: 0.5 };
let color;
let icon;
if (title === "up") {
color = theme.pie.green.stroke;
icon = (
<Box sx={{ ...sharedStyles, top: 8 }}>
<Arrow />
</Box>
);
} else if (title === "down") {
color = theme.pie.red.stroke;
icon = (
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
<Arrow />
</Box>
);
} else if (title === "paused") {
color = theme.pie.yellow.stroke;
icon = (
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
<ClockSnooze />
</Box>
);
}
return (
<Box
position="relative"
flex={1}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
px={theme.spacing(12)}
py={theme.spacing(8)}
overflow="hidden"
sx={{
"&:hover": {
backgroundColor: "#f9fafb",
},
"&:after": {
position: "absolute",
content: `""`,
backgroundImage: `url(${background})`,
width: "400px",
height: "200px",
top: "-10%",
left: "5%",
zIndex: 10000,
pointerEvents: "none",
},
}}
>
<Box
textTransform="uppercase"
fontSize={15}
letterSpacing={0.5}
color={theme.palette.otherColors.bluishGray}
mb={theme.gap.ml}
sx={{ opacity: 0.6 }}
>
{title}
</Box>
{icon}
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontWeight={600}
color={color}
gap="2px"
>
{value}
<Typography
component="span"
fontSize={20}
fontWeight={300}
color={theme.palette.otherColors.bluishGray}
sx={{ opacity: 0.3 }}
>
#
</Typography>
</Stack>
</Box>
);
};
StatusBox.propTypes = {
title: PropTypes.oneOf(["up", "down", "paused"]).isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
};
/**
* Renders a skeleton layout.
*
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
const theme = useTheme();
return (
<>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Skeleton variant="rounded" width="50%" height={36} />
<Skeleton variant="rounded" width="15%" height={36} />
</Stack>
<Stack
gap={theme.gap.large}
direction="row"
justifyContent="space-between"
>
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
</Stack>
<Stack gap={theme.gap.large} p={theme.gap.xl} backgroundColor="#f9fafb">
<Skeleton variant="rounded" width="50%" height={25} />
<Skeleton variant="rounded" width="100%" height={300} />
<Skeleton variant="rounded" width="100%" height={100} />
</Stack>
</>
);
};
const Monitors = () => {
const theme = useTheme();
const navigate = useNavigate();
const monitorState = useSelector((state) => state.uptimeMonitors);
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
useEffect(() => {
dispatch(getUptimeMonitorsByUserId(authState.authToken));
}, [authState.authToken, dispatch]);
const up = monitorState.monitors.reduce((acc, cur) => {
return cur.status === true ? acc + 1 : acc;
}, 0);
const down = monitorState.monitors.length - up;
const data = {
cols: [
{ id: 1, name: "Host" },
{
id: 2,
name: (
<Box width="max-content">
Status
<span>
<ArrowDownwardRoundedIcon />
</span>
</Box>
),
},
{ id: 3, name: "Response Time" },
{ id: 4, name: "Type" },
{ id: 5, name: "Actions" },
],
rows: [],
};
data.rows = monitorState.monitors.map((monitor, idx) => {
const params = {
url: monitor.url,
title: monitor.name,
percentage: 100,
percentageColor:
monitor.status === true ? "var(--success-color)" : "var(--error-color)",
status: monitor.status === true ? "up" : "down",
};
// Reverse checks so latest check is on the right
const reversedChecks = monitor.checks.slice().reverse();
return {
id: monitor._id,
// disabled for now
handleClick: () => {
navigate(`/monitors/${monitor._id}`);
},
data: [
{ id: idx, data: <Host params={params} /> },
{
id: idx + 1,
data: (
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
),
},
{ id: idx + 2, data: <BarChart checks={reversedChecks} /> },
{
id: idx + 3,
data: (
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
),
},
{
id: idx + 4,
data: <ActionsMenu monitor={monitor} />,
},
],
};
});
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
const now = new Date();
const hour = now.getHours();
let greeting = "";
let emoji = "";
if (hour < 12) {
greeting = "morning";
emoji = "🌅";
} else if (hour < 18) {
greeting = "afternoon";
emoji = "🌞";
} else {
greeting = "evening";
emoji = "🌙";
}
return (
<Stack className="monitors" gap={theme.gap.large}>
{loading ? (
<SkeletonLayout />
) : (
<>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.gap.medium}
>
<Box>
<Typography component="h1" lineHeight={1}>
<Typography
component="span"
fontSize="inherit"
color={theme.palette.otherColors.bluishGray}
>
Good {greeting},{" "}
</Typography>
<Typography
component="span"
fontSize="inherit"
fontWeight="inherit"
>
{authState.user.firstName} {emoji}
</Typography>
</Typography>
<Typography
sx={{ opacity: 0.8 }}
lineHeight={1}
fontWeight={300}
>
Here’s an overview of your uptime monitors.
</Typography>
</Box>
{monitorState.monitors?.length !== 0 && (
<Button
level="primary"
label="Create monitor"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500, alignSelf: "flex-end" }}
/>
)}
</Stack>
</Box>
{monitorState.monitors?.length === 0 ? (
<Stack
alignItems="center"
backgroundColor={theme.palette.otherColors.white}
p={theme.gap.xxl}
gap={theme.gap.xs}
border={1}
borderRadius={`${theme.shape.borderRadius}px`}
borderColor={theme.palette.otherColors.graishWhite}
>
<Typography component="h2">No monitors found</Typography>
<Typography>
It looks like you don’t have any monitors set up yet.
</Typography>
<Button
level="primary"
label="Create your first monitor"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ mt: theme.gap.large }}
/>
</Stack>
) : (
<>
<Stack
gap={theme.gap.large}
direction="row"
justifyContent="space-between"
>
<StatusBox title="up" value={up} />
<StatusBox title="down" value={down} />
<StatusBox title="paused" value={0} />
</Stack>
<Box
flex={1}
py={theme.gap.large}
px={theme.gap.lgplus}
border={1}
borderColor={theme.palette.otherColors.graishWhite}
backgroundColor={theme.palette.otherColors.white}
sx={{
borderRadius: `${theme.shape.borderRadius}px`,
}}
>
<Stack direction="row" alignItems="center" mb={theme.gap.ml}>
<Typography component="h2" letterSpacing={-0.5}>
Actively monitoring
</Typography>
<Box className="current-monitors-counter">
{monitorState.monitors.length}
</Box>
{/* TODO - add search bar */}
</Stack>
<BasicTable data={data} paginated={true} table={"monitors"} />
</Box>
</>
)}
</>
)}
</Stack>
);
};
export default Monitors;
@@ -5,7 +5,7 @@ import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router";
import {
deletePageSpeed,
getPageSpeedByUserId,
getPageSpeedByTeamId,
updatePageSpeed,
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { monitorValidation } from "../../../Validation/validation";
@@ -127,7 +127,7 @@ const PageSpeedConfigure = () => {
);
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor updated successfully!" });
dispatch(getPageSpeedByUserId(authToken));
dispatch(getPageSpeedByTeamId(authToken));
} else {
createToast({ body: "Failed to update monitor." });
}
@@ -223,7 +223,7 @@ const PageSpeedConfigure = () => {
</Stack>
<Stack gap={theme.gap.xl}>
<Stack direction="row">
<Typography component="h3">Monitor friendly name</Typography>
<Typography component="h3">Monitor display name</Typography>
<Field
type="text"
id="monitor-name"
@@ -116,7 +116,7 @@ const CreatePageSpeed = () => {
<Typography component="h1">Create pagespeed monitor</Typography>
<Stack gap={theme.gap.xl}>
<Stack direction="row">
<Typography component="h3">Monitor friendly name</Typography>
<Typography component="h3">Monitor display name</Typography>
<Field
type="text"
id="monitor-name"
+7 -3
View File
@@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react";
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
import { StatusLabel } from "../../Components/Label";
import { useDispatch, useSelector } from "react-redux";
import { getPageSpeedByUserId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { getPageSpeedByTeamId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
import Fallback from "../../Components/Fallback";
import "./index.css";
@@ -127,7 +127,7 @@ const SkeletonLayout = () => {
);
};
const PageSpeed = () => {
const PageSpeed = ({ isAdmin }) => {
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -137,7 +137,7 @@ const PageSpeed = () => {
(state) => state.pageSpeedMonitors
);
useEffect(() => {
dispatch(getPageSpeedByUserId(authToken));
dispatch(getPageSpeedByTeamId(authToken));
}, [authToken, dispatch]);
// will show skeletons only on initial load
@@ -196,10 +196,14 @@ const PageSpeed = () => {
"Give suggestions on how the page can be improved",
]}
link="/pagespeed/create"
isAdmin={isAdmin}
/>
)}
</Box>
);
};
PageSpeed.propTypes = {
isAdmin: PropTypes.bool,
};
export default PageSpeed;
+10 -10
View File
@@ -52,9 +52,9 @@ class NetworkService {
* @param {boolean} [normalize] - Whether to normalize the retrieved monitors.
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*/
async getMonitorsByUserId(
async getMonitorsByTeamId(
authToken,
userId,
teamId,
limit,
types,
status,
@@ -74,7 +74,7 @@ class NetworkService {
if (normalize) params.append("normalize", normalize);
return this.axiosInstance.get(
`/monitors/user/${userId}?${params.toString()}`,
`/monitors/team/${teamId}?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -293,8 +293,8 @@ class NetworkService {
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*
*/
async doesAdminExist() {
return this.axiosInstance.get("/auth/users/admin");
async doesSuperAdminExist() {
return this.axiosInstance.get("/auth/users/superadmin");
}
/**
@@ -327,7 +327,7 @@ class NetworkService {
*/
async requestInvitationToken(authToken, email, role) {
return this.axiosInstance.post(
`/auth/invite`,
`/invite`,
{ email, role },
{
headers: { Authorization: `Bearer ${authToken}` },
@@ -346,7 +346,7 @@ class NetworkService {
*
*/
async verifyInvitationToken(token) {
return this.axiosInstance.post(`/auth/invite/verify`, {
return this.axiosInstance.post(`/invite/verify`, {
token,
});
}
@@ -409,9 +409,9 @@ class NetworkService {
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*
*/
async getChecksByUser(
async getChecksByTeam(
authToken,
userId,
teamId,
sortOrder,
limit,
dateRange,
@@ -427,7 +427,7 @@ class NetworkService {
if (page) params.append("page", page);
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
return this.axiosInstance.get(
`/checks/user/${userId}?${params.toString()}`,
`/checks/team/${teamId}?${params.toString()}`,
{
headers: { Authorization: `Bearer ${authToken}` },
}
+41 -1
View File
@@ -76,6 +76,7 @@ const credentials = joi.object({
return value;
}),
role: joi.array(),
teamId: joi.string().allow("").optional(),
});
const monitorValidation = joi.object({
@@ -111,4 +112,43 @@ const imageValidation = joi.object({
}),
});
export { credentials, imageValidation, monitorValidation };
const maintenanceWindowValidation = joi.object({
repeat: joi.number().valid(1, 2, 3).required().messages({
"number.base": "Repeat must be a number.",
"any.only": "Repeat must be one of [1, 2, 3].",
"any.required": "Repeat is required.",
}),
date: joi.date().required().messages({
"date.base": "Date must be a valid date.",
"any.required": "Date is required.",
}),
startTime: joi.string().required().messages({
"string.base": "Start time must be a valid time.",
"any.required": "Start time is required.",
}),
duration: joi.number().required().messages({
"number.empty": "duration is required.",
"number.base": "Duration must be a number.",
"any.required": "Duration is required.",
}),
unit: joi.string().valid("minutes", "hours", "days").required().messages({
"string.base": "Unit must be a string.",
"any.only": "Unit must be one of ['minutes', 'hours', 'days'].",
"any.required": "Unit is required.",
}),
displayName: joi.string().max(50).required().messages({
"string.empty": "Display name is required.",
"string.max": "Display name must be less than 50 characters long",
}),
addMonitors: joi.string().max(50).required().messages({
"string.empty": "Add monitors is required.",
"string.max": "Add monitors must be less than 50 characters long",
}),
});
export {
credentials,
imageValidation,
monitorValidation,
maintenanceWindowValidation,
};