Merge latest changes
This commit is contained in:
commit
b34a17004b
57 files changed
+1242
-844
No files matched your search
Generated
+1
-1
@@ -18,7 +18,7 @@
|
||||
"@mui/x-data-grid": "7.3.2",
|
||||
"@mui/x-date-pickers": "7.3.2",
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"axios": "1.7.4",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
"dayjs": "1.11.11",
|
||||
"joi": "17.13.1",
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
"@mui/x-data-grid": "7.3.2",
|
||||
"@mui/x-date-pickers": "7.3.2",
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"axios": "1.7.4",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
"dayjs": "1.11.11",
|
||||
"joi": "17.13.1",
|
||||
|
||||
+9
-5
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -321,7 +321,7 @@ const Login = () => {
|
||||
return;
|
||||
}
|
||||
networkService
|
||||
.doesAdminExist()
|
||||
.doesSuperAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === false) {
|
||||
navigate("/register");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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.
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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}` },
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require("express");
|
||||
const {
|
||||
registerValidation,
|
||||
registrationBodyValidation,
|
||||
loginValidation,
|
||||
editUserParamValidation,
|
||||
editUserBodyValidation,
|
||||
@@ -50,7 +50,7 @@ const getTokenFromHeaders = (headers) => {
|
||||
const registerController = async (req, res, next) => {
|
||||
// joi validation
|
||||
try {
|
||||
await registerValidation.validateAsync(req.body);
|
||||
await registrationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -78,7 +78,7 @@ const registerController = async (req, res, next) => {
|
||||
|
||||
// Create a new user
|
||||
try {
|
||||
const newUser = await req.db.insertUser(req, res);
|
||||
const newUser = await req.db.insertUser({ ...req.body }, req.file);
|
||||
logger.info(successMessages.AUTH_CREATE_USER, {
|
||||
service: SERVICE_NAME,
|
||||
userId: newUser._id,
|
||||
@@ -90,12 +90,20 @@ const registerController = async (req, res, next) => {
|
||||
|
||||
const token = issueToken(userForToken);
|
||||
|
||||
await req.emailService.buildAndSendEmail(
|
||||
"welcomeEmailTemplate",
|
||||
{ name: newUser.firstName },
|
||||
newUser.email,
|
||||
"Welcome to Uptime Monitor"
|
||||
);
|
||||
req.emailService
|
||||
.buildAndSendEmail(
|
||||
"welcomeEmailTemplate",
|
||||
{ name: newUser.firstName },
|
||||
newUser.email,
|
||||
"Welcome to Uptime Monitor"
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error("Error sending welcome email", {
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_CREATE_USER,
|
||||
@@ -226,7 +234,8 @@ const inviteController = async (req, res, next) => {
|
||||
try {
|
||||
// Only admins can invite
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { role, firstname } = jwt.decode(token);
|
||||
const { role, firstname, teamId } = jwt.decode(token);
|
||||
req.body.teamId = teamId;
|
||||
try {
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
@@ -240,15 +249,22 @@ const inviteController = async (req, res, next) => {
|
||||
}
|
||||
|
||||
const inviteToken = await req.db.requestInviteToken(req, res);
|
||||
await req.emailService.buildAndSendEmail(
|
||||
"employeeActivationTemplate",
|
||||
{
|
||||
name: firstname,
|
||||
link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`,
|
||||
},
|
||||
req.body.email,
|
||||
"Welcome to Uptime Monitor"
|
||||
);
|
||||
req.emailService
|
||||
.buildAndSendEmail(
|
||||
"employeeActivationTemplate",
|
||||
{
|
||||
name: firstname,
|
||||
link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`,
|
||||
},
|
||||
req.body.email,
|
||||
"Welcome to Uptime Monitor"
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error("Error sending invite email", {
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
@@ -274,7 +290,6 @@ const inviteVerifyController = async (req, res, next) => {
|
||||
|
||||
try {
|
||||
const invite = await req.db.getInviteToken(req, res);
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.json({ status: "success", msg: "Invite verified", data: invite });
|
||||
@@ -293,13 +308,13 @@ const inviteVerifyController = async (req, res, next) => {
|
||||
* @returns {Promise<Express.Response>}
|
||||
*/
|
||||
|
||||
const checkAdminController = async (req, res) => {
|
||||
const checkSuperadminController = async (req, res) => {
|
||||
try {
|
||||
const adminExists = await req.db.checkAdmin(req, res);
|
||||
const superAdminExists = await req.db.checkSuperadmin(req, res);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_ADMIN_EXISTS,
|
||||
data: adminExists,
|
||||
data: superAdminExists,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -508,7 +523,7 @@ module.exports = {
|
||||
userEditController,
|
||||
inviteController,
|
||||
inviteVerifyController,
|
||||
checkAdminController,
|
||||
checkSuperadminController,
|
||||
recoveryRequestController,
|
||||
validateRecoveryTokenController,
|
||||
resetPasswordController,
|
||||
|
||||
@@ -3,8 +3,8 @@ const {
|
||||
createCheckBodyValidation,
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
getUserChecksParamValidation,
|
||||
getUserChecksQueryValidation,
|
||||
getTeamChecksParamValidation,
|
||||
getTeamChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
} = require("../validation/joi");
|
||||
const { successMessages } = require("../utils/messages");
|
||||
@@ -62,10 +62,10 @@ const getChecks = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getUserChecks = async (req, res, next) => {
|
||||
const getTeamChecks = async (req, res, next) => {
|
||||
try {
|
||||
await getUserChecksParamValidation.validateAsync(req.params);
|
||||
await getUserChecksQueryValidation.validateAsync(req.query);
|
||||
await getTeamChecksParamValidation.validateAsync(req.params);
|
||||
await getTeamChecksQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -75,7 +75,7 @@ const getUserChecks = async (req, res, next) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const checkData = await req.db.getUserChecks(req);
|
||||
const checkData = await req.db.getTeamChecks(req);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.CHECK_GET,
|
||||
@@ -115,6 +115,6 @@ const deleteChecks = async (req, res, next) => {
|
||||
module.exports = {
|
||||
createCheck,
|
||||
getChecks,
|
||||
getUserChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
const {
|
||||
inviteRoleValidation,
|
||||
inviteBodyValidation,
|
||||
inviteVerifciationBodyValidation,
|
||||
} = require("../validation/joi");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config();
|
||||
const { errorMessages, successMessages } = require("../utils/messages");
|
||||
var jwt = require("jsonwebtoken");
|
||||
const SERVICE_NAME = "inviteController";
|
||||
|
||||
const getTokenFromHeaders = (headers) => {
|
||||
const authorizationHeader = headers.authorization;
|
||||
if (!authorizationHeader) throw new Error("No auth headers");
|
||||
|
||||
const parts = authorizationHeader.split(" ");
|
||||
if (parts.length !== 2 || parts[0] !== "Bearer")
|
||||
throw new Error("Invalid auth headers");
|
||||
|
||||
return parts[1];
|
||||
};
|
||||
|
||||
const inviteController = async (req, res, next) => {
|
||||
try {
|
||||
// Only admins can invite
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { role, firstname, teamId } = jwt.decode(token);
|
||||
req.body.teamId = teamId;
|
||||
try {
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const inviteToken = await req.db.requestInviteToken({ ...req.body });
|
||||
req.emailService
|
||||
.buildAndSendEmail(
|
||||
"employeeActivationTemplate",
|
||||
{
|
||||
name: firstname,
|
||||
link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`,
|
||||
},
|
||||
req.body.email,
|
||||
"Welcome to Uptime Monitor"
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error("Error sending invite email", {
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json({ success: true, msg: "Invite sent", data: inviteToken });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const inviteVerifyController = async (req, res, next) => {
|
||||
try {
|
||||
await inviteVerifciationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const invite = await req.db.getInviteToken(req.body.token);
|
||||
res
|
||||
.status(200)
|
||||
.json({ status: "success", msg: "Invite verified", data: invite });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inviteController,
|
||||
inviteVerifyController,
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
const {
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByUserIdValidation,
|
||||
getMonitorsByTeamIdValidation,
|
||||
createMonitorBodyValidation,
|
||||
editMonitorBodyValidation,
|
||||
getMonitorsByUserIdQueryValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
} = require("../validation/joi");
|
||||
|
||||
const sslChecker = require("ssl-checker");
|
||||
@@ -149,10 +149,10 @@ const getMonitorById = async (req, res, next) => {
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorsByUserId = async (req, res, next) => {
|
||||
const getMonitorsByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorsByUserIdValidation.validateAsync(req.params);
|
||||
await getMonitorsByUserIdQueryValidation.validateAsync(req.query);
|
||||
await getMonitorsByTeamIdValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -163,12 +163,12 @@ const getMonitorsByUserId = async (req, res, next) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = req.params.userId;
|
||||
const monitors = await req.db.getMonitorsByUserId(req, res);
|
||||
const teamId = req.params.teamId;
|
||||
const monitors = await req.db.getMonitorsByTeamId(req, res);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_GET_BY_USER_ID(userId),
|
||||
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -342,7 +342,7 @@ module.exports = {
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
|
||||
@@ -15,10 +15,10 @@ const connect = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkAdmin = async (req, res) => {
|
||||
const checkSuperadmin = async (req, res) => {
|
||||
try {
|
||||
const admin = await UserModel.findOne({ role: "admin" });
|
||||
if (admin !== null) {
|
||||
const superAdmin = await UserModel.findOne({ role: "superadmin" });
|
||||
if (superAdmin !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -66,7 +66,7 @@ const {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
@@ -92,7 +92,7 @@ const {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
getUserChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
} = require("./modules/checkModule");
|
||||
|
||||
@@ -141,11 +141,11 @@ module.exports = {
|
||||
requestRecoveryToken,
|
||||
validateRecoveryToken,
|
||||
resetPassword,
|
||||
checkAdmin,
|
||||
checkSuperadmin,
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
@@ -153,7 +153,7 @@ module.exports = {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
getUserChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
createAlert,
|
||||
getAlertsByUserId,
|
||||
|
||||
@@ -116,12 +116,12 @@ const getChecks = async (req) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getUserChecks = async (req) => {
|
||||
const { userId } = req.params;
|
||||
const getTeamChecks = async (req) => {
|
||||
const { teamId } = req.params;
|
||||
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
|
||||
|
||||
// Get monitorIDs
|
||||
const userMonitors = await Monitor.find({ userId: userId });
|
||||
const userMonitors = await Monitor.find({ teamId: teamId });
|
||||
const monitorIds = userMonitors.map((monitor) => monitor._id);
|
||||
|
||||
//Build check query
|
||||
@@ -189,6 +189,6 @@ module.exports = {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
getUserChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
};
|
||||
@@ -2,14 +2,25 @@ const InviteToken = require("../../../models/InviteToken");
|
||||
const crypto = require("crypto");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
|
||||
const requestInviteToken = async (req, res) => {
|
||||
/**
|
||||
* Request an invite token for a user.
|
||||
*
|
||||
* This function deletes any existing invite tokens for the user's email,
|
||||
* generates a new token, saves it, and then returns the new token.
|
||||
*
|
||||
* @param {Object} userData - The user data.
|
||||
* @param {string} userData.email - The user's email.
|
||||
* @param {mongoose.Schema.Types.ObjectId} userData.teamId - The ID of the team.
|
||||
* @param {Array} userData.role - The user's role(s).
|
||||
* @param {Date} [userData.expiry=Date.now] - The expiry date of the token. Defaults to the current date and time.
|
||||
* @returns {Promise<InviteToken>} The invite token.
|
||||
* @throws {Error} If there is an error.
|
||||
*/
|
||||
const requestInviteToken = async (userData) => {
|
||||
try {
|
||||
await InviteToken.deleteMany({ email: req.body.email });
|
||||
let inviteToken = new InviteToken({
|
||||
email: req.body.email,
|
||||
role: req.body.role,
|
||||
token: crypto.randomBytes(32).toString("hex"),
|
||||
});
|
||||
await InviteToken.deleteMany({ email: userData.email });
|
||||
userData.token = crypto.randomBytes(32).toString("hex");
|
||||
let inviteToken = new InviteToken(userData);
|
||||
await inviteToken.save();
|
||||
return inviteToken;
|
||||
} catch (error) {
|
||||
@@ -17,10 +28,20 @@ const requestInviteToken = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getInviteToken = async (req, res) => {
|
||||
/**
|
||||
* Retrieves and deletes an invite token.
|
||||
*
|
||||
* This function searches for an invite token in the database and deletes it.
|
||||
* If the invite token is not found, it throws an error.
|
||||
*
|
||||
* @param {string} token - The invite token to search for.
|
||||
* @returns {Promise<InviteToken>} The invite token data.
|
||||
* @throws {Error} If the invite token is not found or there is another error.
|
||||
*/
|
||||
const getInviteToken = async (token) => {
|
||||
try {
|
||||
const invite = await InviteToken.findOneAndDelete({
|
||||
token: req.body.token,
|
||||
token,
|
||||
});
|
||||
if (invite === null) {
|
||||
throw new Error(errorMessages.AUTH_INVITE_NOT_FOUND);
|
||||
|
||||
@@ -287,17 +287,17 @@ const getMonitorById = async (monitorId) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Get monitors by UserID
|
||||
* Get monitors by TeamID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Array<Monitor>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorsByUserId = async (req, res) => {
|
||||
const getMonitorsByTeamId = async (req, res) => {
|
||||
try {
|
||||
let { limit, type, status, sortOrder, normalize } = req.query || {};
|
||||
const monitorQuery = { userId: req.params.userId };
|
||||
const monitorQuery = { teamId: req.params.teamId };
|
||||
|
||||
if (type !== undefined) {
|
||||
const types = Array.isArray(type) ? type : [type];
|
||||
@@ -367,7 +367,6 @@ const createMonitor = async (req, res) => {
|
||||
const monitor = new Monitor({ ...req.body });
|
||||
// Remove notifications fom monitor as they aren't needed here
|
||||
monitor.notifications = undefined;
|
||||
monitor.userId = req.user._id;
|
||||
await monitor.save();
|
||||
return monitor;
|
||||
} catch (error) {
|
||||
@@ -453,7 +452,7 @@ module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const UserModel = require("../../../models/user");
|
||||
const TeamModel = require("../../../models/Team");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
const { GenerateAvatarImage } = require("../../../utils/imageProcessing");
|
||||
|
||||
@@ -13,20 +14,29 @@ const { ParseBoolean } = require("../../../utils/utils");
|
||||
* @returns {Promise<UserModel>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const insertUser = async (req, res) => {
|
||||
const insertUser = async (userData, imageFile) => {
|
||||
try {
|
||||
const userData = { ...req.body };
|
||||
if (req.file) {
|
||||
if (imageFile) {
|
||||
// 1. Save the full size image
|
||||
userData.profileImage = {
|
||||
data: req.file.buffer,
|
||||
contentType: req.file.mimetype,
|
||||
data: imageFile.buffer,
|
||||
contentType: imageFile.mimetype,
|
||||
};
|
||||
|
||||
// 2. Get the avatar sized image
|
||||
const avatar = await GenerateAvatarImage(req.file);
|
||||
const avatar = await GenerateAvatarImage(imageFile);
|
||||
userData.avatarImage = avatar;
|
||||
}
|
||||
|
||||
// Handle creating team if superadmin
|
||||
if (userData.role.includes("superadmin")) {
|
||||
const team = new TeamModel({
|
||||
email: userData.email,
|
||||
});
|
||||
userData.teamId = team._id;
|
||||
await team.save();
|
||||
}
|
||||
|
||||
const newUser = new UserModel(userData);
|
||||
await newUser.save();
|
||||
return await UserModel.findOne({ _id: newUser._id })
|
||||
|
||||
@@ -7,6 +7,7 @@ const { verifyJWT } = require("./middleware/verifyJWT");
|
||||
const { handleErrors } = require("./middleware/handleErrors");
|
||||
const { errorMessages } = require("./utils/messages");
|
||||
const authRouter = require("./routes/authRoute");
|
||||
const inviteRouter = require("./routes/inviteRoute");
|
||||
const monitorRouter = require("./routes/monitorRoute");
|
||||
const checkRouter = require("./routes/checkRoute");
|
||||
const alertRouter = require("./routes/alertRoute");
|
||||
@@ -71,6 +72,7 @@ const startApp = async () => {
|
||||
|
||||
//routes
|
||||
app.use("/api/v1/auth", authRouter);
|
||||
app.use("/api/v1/invite", inviteRouter);
|
||||
app.use("/api/v1/monitors", verifyJWT, monitorRouter);
|
||||
app.use("/api/v1/checks", verifyJWT, checkRouter);
|
||||
app.use("/api/v1/alerts", verifyJWT, alertRouter);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const TOKEN_PREFIX = "Bearer ";
|
||||
const SERVICE_NAME = "allowedRoles";
|
||||
const { errorMessages } = require("../utils/messages");
|
||||
|
||||
const isAllowed = (allowedRoles) => {
|
||||
return (req, res, next) => {
|
||||
const token = req.headers["authorization"];
|
||||
|
||||
// If no token is pressent, return an error
|
||||
if (!token) {
|
||||
const error = new Error(errorMessages.NO_AUTH_TOKEN);
|
||||
error.status = 401;
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the token is improperly formatted, return an error
|
||||
if (!token.startsWith(TOKEN_PREFIX)) {
|
||||
const error = new Error(errorMessages.INVALID_AUTH_TOKEN);
|
||||
error.status = 400;
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
// Parse the token
|
||||
try {
|
||||
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
|
||||
var decoded = jwt.verify(parsedToken, process.env.JWT_SECRET);
|
||||
const userRoles = decoded.role;
|
||||
|
||||
// Check if the user has the required role
|
||||
if (userRoles.some((role) => allowedRoles.includes(role))) {
|
||||
next();
|
||||
return;
|
||||
} else {
|
||||
const error = new Error(errorMessages.INSUFFICIENT_PERMISSIONS);
|
||||
error.status = 401;
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
error.status = 401;
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { isAllowed };
|
||||
@@ -11,7 +11,7 @@ const { errorMessages } = require("../utils/messages");
|
||||
* @param {express.NextFunction} next
|
||||
* @returns {express.Response}
|
||||
*/
|
||||
const verifyAdmin = (req, res, next) => {
|
||||
const verifySuperAdmin = (req, res, next) => {
|
||||
const token = req.headers["authorization"];
|
||||
// Make sure a token is provided
|
||||
if (!token) {
|
||||
@@ -42,7 +42,7 @@ const verifyAdmin = (req, res, next) => {
|
||||
.json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN });
|
||||
}
|
||||
|
||||
if (decoded.role.includes("admin") === false) {
|
||||
if (decoded.role.includes("superadmin") === false) {
|
||||
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
@@ -54,4 +54,4 @@ const verifyAdmin = (req, res, next) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { verifyAdmin };
|
||||
module.exports = { verifySuperAdmin };
|
||||
@@ -7,6 +7,12 @@ const InviteTokenSchema = mongoose.Schema(
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
required: true,
|
||||
},
|
||||
role: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
||||
@@ -6,6 +6,13 @@ const MonitorSchema = mongoose.Schema(
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
immutable: true,
|
||||
required: true,
|
||||
},
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const TeamSchema = mongoose.Schema(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Team", TeamSchema);
|
||||
+12
-1
@@ -38,7 +38,12 @@ const UserSchema = mongoose.Schema(
|
||||
role: {
|
||||
type: [String],
|
||||
default: "user",
|
||||
enum: ["user", "admin"],
|
||||
enum: ["user", "admin", "superadmin"],
|
||||
},
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -70,4 +75,10 @@ UserSchema.methods.comparePassword = async function (submittedPassword) {
|
||||
return res;
|
||||
};
|
||||
|
||||
const User = mongoose.model("User", UserSchema);
|
||||
|
||||
User.init().then(() => {
|
||||
console.log("User model initialized");
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("User", UserSchema);
|
||||
@@ -1,7 +1,7 @@
|
||||
const router = require("express").Router();
|
||||
const { verifyJWT } = require("../middleware/verifyJWT");
|
||||
const { verifyAdmin } = require("../middleware/verifyAdmin");
|
||||
const { verifyOwnership } = require("../middleware/verifyOwnership");
|
||||
const { isAllowed } = require("../middleware/isAllowed");
|
||||
const multer = require("multer");
|
||||
const upload = multer();
|
||||
const User = require("../models/user");
|
||||
@@ -14,11 +14,9 @@ const {
|
||||
recoveryRequestController,
|
||||
validateRecoveryTokenController,
|
||||
resetPasswordController,
|
||||
checkAdminController,
|
||||
checkSuperadminController,
|
||||
getAllUsersController,
|
||||
deleteUserController,
|
||||
inviteController,
|
||||
inviteVerifyController,
|
||||
} = require("../controllers/authController");
|
||||
|
||||
//Auth routes
|
||||
@@ -30,8 +28,13 @@ router.put(
|
||||
verifyJWT,
|
||||
userEditController
|
||||
);
|
||||
router.get("/users/admin", checkAdminController);
|
||||
router.get("/users", verifyJWT, verifyAdmin, getAllUsersController);
|
||||
router.get("/users/superadmin", checkSuperadminController);
|
||||
router.get(
|
||||
"/users",
|
||||
verifyJWT,
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
getAllUsersController
|
||||
);
|
||||
router.delete(
|
||||
"/user/:userId",
|
||||
verifyJWT,
|
||||
@@ -39,9 +42,6 @@ router.delete(
|
||||
deleteUserController
|
||||
);
|
||||
|
||||
router.post("/invite", verifyJWT, inviteController);
|
||||
router.post("/invite/verify", inviteVerifyController);
|
||||
|
||||
//Recovery routes
|
||||
router.post("/recovery/request", recoveryRequestController);
|
||||
router.post("/recovery/validate", validateRecoveryTokenController);
|
||||
|
||||
@@ -9,13 +9,9 @@ router.post(
|
||||
checkController.createCheck
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:monitorId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
checkController.getChecks
|
||||
);
|
||||
router.get("/:monitorId", checkController.getChecks);
|
||||
|
||||
router.get("/user/:userId", checkController.getUserChecks);
|
||||
router.get("/team/:teamId", checkController.getTeamChecks);
|
||||
|
||||
router.delete(
|
||||
"/:monitorId",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const router = require("express").Router();
|
||||
const { verifyJWT } = require("../middleware/verifyJWT");
|
||||
const { isAllowed } = require("../middleware/isAllowed");
|
||||
|
||||
const {
|
||||
inviteController,
|
||||
inviteVerifyController,
|
||||
} = require("../controllers/inviteController");
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
verifyJWT,
|
||||
inviteController
|
||||
);
|
||||
router.post(
|
||||
"/verify",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
inviteVerifyController
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,25 +1,34 @@
|
||||
const router = require("express").Router();
|
||||
const monitorController = require("../controllers/monitorController");
|
||||
const { verifyOwnership } = require("../middleware/verifyOwnership");
|
||||
const Monitor = require("../models/Monitor");
|
||||
const { isAllowed } = require("../middleware/isAllowed");
|
||||
|
||||
router.get("/", monitorController.getAllMonitors);
|
||||
router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
|
||||
router.get("/certificate/:monitorId", monitorController.getMonitorCertificate);
|
||||
router.get("/:monitorId", monitorController.getMonitorById);
|
||||
router.get("/user/:userId", monitorController.getMonitorsByUserId);
|
||||
router.get("/team/:teamId", monitorController.getMonitorsByTeamId);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
monitorController.createMonitor
|
||||
);
|
||||
|
||||
router.post("/", monitorController.createMonitor);
|
||||
router.delete(
|
||||
"/:monitorId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
monitorController.deleteMonitor
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/:monitorId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
monitorController.editMonitor
|
||||
);
|
||||
|
||||
router.delete("/all", monitorController.deleteAllMonitors);
|
||||
router.delete(
|
||||
"/all",
|
||||
isAllowed(["superadmin"]),
|
||||
monitorController.deleteAllMonitors
|
||||
);
|
||||
module.exports = router;
|
||||
@@ -17,6 +17,9 @@ const errorMessages = {
|
||||
VERIFY_OWNER_NOT_FOUND: "Document not found",
|
||||
VERIFY_OWNER_UNAUTHORIZED: "Unauthorized access",
|
||||
|
||||
//Permissions Middleware
|
||||
INSUFFICIENT_PERMISSIONS: "Insufficient permissions",
|
||||
|
||||
//DB Errors
|
||||
DB_USER_EXISTS: "User already exists",
|
||||
DB_USER_NOT_FOUND: "User not found",
|
||||
|
||||
+22
-16
@@ -5,8 +5,11 @@ const joi = require("joi");
|
||||
//****************************************
|
||||
|
||||
const roleValidatior = (role) => (value, helpers) => {
|
||||
if (!value.includes(role)) {
|
||||
throw new joi.ValidationError(`You do not have ${role} authorization`);
|
||||
const hasRole = role.some((role) => value.includes(role));
|
||||
if (!hasRole) {
|
||||
throw new Joi.ValidationError(
|
||||
`You do not have the required authorization. Required roles: ${roles.join(", ")}`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -36,7 +39,7 @@ const loginValidation = joi.object({
|
||||
),
|
||||
});
|
||||
|
||||
const registerValidation = joi.object({
|
||||
const registrationBodyValidation = joi.object({
|
||||
firstName: joi
|
||||
.string()
|
||||
.required()
|
||||
@@ -66,9 +69,10 @@ const registerValidation = joi.object({
|
||||
profileImage: joi.any(),
|
||||
role: joi
|
||||
.array()
|
||||
.items(joi.string().valid("admin", "user"))
|
||||
.items(joi.string().valid("superadmin", "admin", "user"))
|
||||
.min(1)
|
||||
.required(),
|
||||
teamId: joi.string().allow("").required(),
|
||||
});
|
||||
|
||||
const editUserParamValidation = joi.object({
|
||||
@@ -123,7 +127,7 @@ const deleteUserParamValidation = joi.object({
|
||||
});
|
||||
|
||||
const inviteRoleValidation = joi.object({
|
||||
roles: joi.custom(roleValidatior("admin")).required(),
|
||||
roles: joi.custom(roleValidatior(["admin", "superadmin"])).required(),
|
||||
});
|
||||
|
||||
const inviteBodyValidation = joi.object({
|
||||
@@ -132,6 +136,7 @@ const inviteBodyValidation = joi.object({
|
||||
"string.email": "Must be a valid email address",
|
||||
}),
|
||||
role: joi.array().required(),
|
||||
teamId: joi.string().required(),
|
||||
});
|
||||
|
||||
const inviteVerifciationBodyValidation = joi.object({
|
||||
@@ -155,11 +160,11 @@ const getMonitorByIdQueryValidation = joi.object({
|
||||
normalize: joi.boolean(),
|
||||
});
|
||||
|
||||
const getMonitorsByUserIdValidation = joi.object({
|
||||
userId: joi.string().required(),
|
||||
const getMonitorsByTeamIdValidation = joi.object({
|
||||
teamId: joi.string().required(),
|
||||
});
|
||||
|
||||
const getMonitorsByUserIdQueryValidation = joi.object({
|
||||
const getMonitorsByTeamIdQueryValidation = joi.object({
|
||||
status: joi.boolean(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
@@ -175,6 +180,7 @@ const getMonitorsByUserIdQueryValidation = joi.object({
|
||||
const createMonitorBodyValidation = joi.object({
|
||||
_id: joi.string(),
|
||||
userId: joi.string().required(),
|
||||
teamId: joi.string().required(),
|
||||
name: joi.string().required(),
|
||||
description: joi.string().required(),
|
||||
type: joi.string().required(),
|
||||
@@ -265,11 +271,11 @@ const getChecksQueryValidation = joi.object({
|
||||
rowsPerPage: joi.number(),
|
||||
});
|
||||
|
||||
const getUserChecksParamValidation = joi.object({
|
||||
userId: joi.string().required(),
|
||||
const getTeamChecksParamValidation = joi.object({
|
||||
teamId: joi.string().required(),
|
||||
});
|
||||
|
||||
const getUserChecksQueryValidation = joi.object({
|
||||
const getTeamChecksQueryValidation = joi.object({
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
dateRange: joi.string().valid("day", "week", "month"),
|
||||
@@ -331,7 +337,7 @@ const getMaintenanceWindowsByMonitorIdParamValidation = joi.object({
|
||||
module.exports = {
|
||||
roleValidatior,
|
||||
loginValidation,
|
||||
registerValidation,
|
||||
registrationBodyValidation,
|
||||
recoveryValidation,
|
||||
recoveryTokenValidation,
|
||||
newPasswordValidation,
|
||||
@@ -341,8 +347,8 @@ module.exports = {
|
||||
createMonitorBodyValidation,
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByUserIdValidation,
|
||||
getMonitorsByUserIdQueryValidation,
|
||||
getMonitorsByTeamIdValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
editMonitorBodyValidation,
|
||||
editUserParamValidation,
|
||||
editUserBodyValidation,
|
||||
@@ -358,8 +364,8 @@ module.exports = {
|
||||
createCheckBodyValidation,
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
getUserChecksParamValidation,
|
||||
getUserChecksQueryValidation,
|
||||
getTeamChecksParamValidation,
|
||||
getTeamChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
deleteUserParamValidation,
|
||||
getPageSpeedCheckParamValidation,
|
||||
|
||||
Reference in new issue
Block a user