41 files changed
+1828
-584
No files matched your search
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- Browser [e.g. Chrome, Safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
+11
-13
@@ -1,16 +1,14 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="./bluewave_favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BlueWave Uptime</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BlueWave Uptime</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -26,6 +26,7 @@ import PageSpeed from "./Pages/PageSpeed";
|
||||
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
|
||||
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow";
|
||||
import PageSpeedDetails from "./Pages/PageSpeed/Details";
|
||||
import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
|
||||
|
||||
function App() {
|
||||
const AdminCheckedRegister = withAdminCheck(Register);
|
||||
@@ -103,6 +104,10 @@ function App() {
|
||||
path="pagespeed/:monitorId"
|
||||
element={<ProtectedRoute Component={PageSpeedDetails} />}
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/configure/:monitorId"
|
||||
element={<ProtectedRoute Component={PageSpeedConfigure} />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route exact path="/login" element={<Login />} />
|
||||
|
||||
@@ -79,7 +79,6 @@ const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
|
||||
MonitorDetailsAreaChart.propTypes = {
|
||||
checks: PropTypes.array,
|
||||
filter: PropTypes.string,
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
|
||||
@@ -12,7 +12,7 @@ import Visibility from "@mui/icons-material/Visibility";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import Button from "../../Button";
|
||||
import "./index.css";
|
||||
import { useState } from "react";
|
||||
import { forwardRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
@@ -30,140 +30,151 @@ import { useState } from "react";
|
||||
* @param {function} props.onChange - Function called on input change.
|
||||
* @param {string} [props.error] - Error message to display for the input field.
|
||||
* @param {boolean} [props.disabled] - Indicates if the input field is disabled.
|
||||
* @param {React.Ref} [ref] - Ref forwarded to the underlying `TextField` component. Allows for direct interactions such as focusing.
|
||||
*/
|
||||
|
||||
const Field = ({
|
||||
type = "text",
|
||||
id,
|
||||
label,
|
||||
https,
|
||||
isRequired,
|
||||
isOptional,
|
||||
optionalLabel,
|
||||
hasCopy,
|
||||
autoComplete,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
disabled,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const Field = forwardRef(
|
||||
(
|
||||
{
|
||||
type = "text",
|
||||
id,
|
||||
label,
|
||||
https,
|
||||
isRequired,
|
||||
isOptional,
|
||||
optionalLabel,
|
||||
hasCopy,
|
||||
autoComplete,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
disabled,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// TODO - are we using this feature anywhere ?
|
||||
const [copy, setCopy] = useState(false);
|
||||
const handleCopy = () => {
|
||||
setCopy(true);
|
||||
setTimeout(() => setCopy(false), 1000);
|
||||
};
|
||||
// TODO - are we using this feature anywhere ?
|
||||
const [copy, setCopy] = useState(false);
|
||||
const handleCopy = () => {
|
||||
setCopy(true);
|
||||
setTimeout(() => setCopy(false), 1000);
|
||||
};
|
||||
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<Stack gap={theme.gap.xs} className={`field field-${type}`}>
|
||||
{label && (
|
||||
<Typography component="h3">
|
||||
{label}
|
||||
{isRequired ? <span className="field-required">*</span> : ""}
|
||||
{isOptional ? (
|
||||
<span className="field-optional">
|
||||
{optionalLabel || "(optional)"}
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<TextField
|
||||
type={type === "password" ? (isVisible ? "text" : type) : type}
|
||||
id={id}
|
||||
autoComplete={autoComplete}
|
||||
placeholder={placeholder}
|
||||
multiline={type === "description"}
|
||||
rows={type === "description" ? 4 : 1}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
sx={
|
||||
type === "url"
|
||||
? {
|
||||
"& .MuiInputBase-root": { padding: 0 },
|
||||
"& .MuiStack-root": {
|
||||
borderTopLeftRadius: `${theme.shape.borderRadius}px`,
|
||||
borderBottomLeftRadius: `${theme.shape.borderRadius}px`,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: type === "url" && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="100%"
|
||||
sx={{
|
||||
borderRight: `solid 1px ${theme.palette.section.borderColor}`,
|
||||
backgroundColor: "#f9f9fa",
|
||||
pl: theme.gap.medium,
|
||||
}}
|
||||
>
|
||||
<Typography component="h5" sx={{ lineHeight: 1 }}>
|
||||
{https ? "https" : "http"}://
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
endAdornment:
|
||||
type === "password" ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setVisible((show) => !show)}
|
||||
tabIndex={-1}
|
||||
sx={{
|
||||
color: theme.palette.section.borderColor,
|
||||
padding: `calc(${theme.gap.xs} / 2)`,
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!isVisible ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
return (
|
||||
<Stack gap={theme.gap.xs} className={`field field-${type}`}>
|
||||
{label && (
|
||||
<Typography component="h3">
|
||||
{label}
|
||||
{isRequired ? <span className="field-required">*</span> : ""}
|
||||
{isOptional ? (
|
||||
<span className="field-optional">
|
||||
{optionalLabel || "(optional)"}
|
||||
</span>
|
||||
) : (
|
||||
hasCopy && (
|
||||
<InputAdornment className="copy" position="end">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label={copy ? "Copied" : "Copy"}
|
||||
img={<ContentCopyIcon />}
|
||||
onClick={handleCopy}
|
||||
""
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<TextField
|
||||
type={type === "password" ? (isVisible ? "text" : type) : type}
|
||||
id={id}
|
||||
autoComplete={autoComplete}
|
||||
placeholder={placeholder}
|
||||
multiline={type === "description"}
|
||||
rows={type === "description" ? 4 : 1}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
inputRef={ref}
|
||||
sx={
|
||||
type === "url"
|
||||
? {
|
||||
"& .MuiInputBase-root": { padding: 0 },
|
||||
"& .MuiStack-root": {
|
||||
borderTopLeftRadius: `${theme.shape.borderRadius}px`,
|
||||
borderBottomLeftRadius: `${theme.shape.borderRadius}px`,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: type === "url" && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="100%"
|
||||
sx={{
|
||||
borderRight: `solid 1px ${theme.palette.section.borderColor}`,
|
||||
backgroundColor: "#f9f9fa",
|
||||
pl: theme.gap.medium,
|
||||
}}
|
||||
>
|
||||
<Typography component="h5" sx={{ lineHeight: 1 }}>
|
||||
{https ? "https" : "http"}://
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
endAdornment:
|
||||
type === "password" ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setVisible((show) => !show)}
|
||||
tabIndex={-1}
|
||||
sx={{
|
||||
borderLeft: `solid 1px ${theme.palette.section.borderColor}`,
|
||||
lineHeight: 0,
|
||||
color: theme.palette.section.borderColor,
|
||||
padding: `calc(${theme.gap.xs} / 2)`,
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{!isVisible ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<Typography component="span" className="input-error" mt={theme.gap.xs}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
) : (
|
||||
hasCopy && (
|
||||
<InputAdornment className="copy" position="end">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label={copy ? "Copied" : "Copy"}
|
||||
img={<ContentCopyIcon />}
|
||||
onClick={handleCopy}
|
||||
sx={{
|
||||
borderLeft: `solid 1px ${theme.palette.section.borderColor}`,
|
||||
lineHeight: 0,
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</InputAdornment>
|
||||
)
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
mt={theme.gap.xs}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Field.propTypes = {
|
||||
type: PropTypes.oneOf(["text", "password", "url", "email", "description"]),
|
||||
|
||||
@@ -352,7 +352,7 @@ const ProfilePanel = () => {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
width: 500,
|
||||
bgcolor: "white",
|
||||
border: "solid 1px #f2f2f2",
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
|
||||
@@ -68,6 +68,58 @@ export const getPageSpeedByUserId = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const updatePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/updatePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
// notifications: monitor.notifications,
|
||||
};
|
||||
const res = await axiosInstance.put(
|
||||
`/monitors/${monitor._id}`,
|
||||
updatedFields,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deletePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/deletePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const res = await axiosInstance.delete(`/monitors/${monitor._id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const pageSpeedMonitorSlice = createSlice({
|
||||
name: "pageSpeedMonitor",
|
||||
initialState,
|
||||
@@ -137,6 +189,44 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create page speed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(updatePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updatePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updatePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update page speed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deletePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deletePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deletePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete page speed monitor";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ export const updateUptimeMonitor = createAsyncThunk(
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const res = await axiosInstance.put(
|
||||
`/monitors/edit/${monitor._id}`,
|
||||
`/monitors/${monitor._id}`,
|
||||
updatedFields,
|
||||
{
|
||||
headers: {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
.home-layout * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.home-layout {
|
||||
display: grid;
|
||||
position: relative;
|
||||
@@ -10,7 +7,7 @@
|
||||
grid-template-columns: var(--env-var-side-bar-width) auto;
|
||||
grid-template-rows: var(--env-var-nav-bar-height) auto;
|
||||
background-color: var(--env-var-color-30);
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.home-layout aside {
|
||||
|
||||
+334
-126
@@ -1,18 +1,221 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import "./index.css";
|
||||
import Logomark from "../../assets/Images/bwl-logo-2.svg?react";
|
||||
import Button from "../../Components/Button";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { login } from "../../Features/Auth/authSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import Button from "../../Components/Button";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import { Checkbox, FormControlLabel, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import Mail from "../../assets/icons/mail.svg?react";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Displays the initial landing page.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const LandingPage = ({ onContinue }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} alignItems="center" textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>We are pleased to see you again!</Typography>
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Continue with Email"
|
||||
img={<Mail />}
|
||||
onClick={onContinue}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& svg": {
|
||||
mr: theme.gap.small,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
By continuing, you agree to our{" "}
|
||||
<Typography component="span">Terms of Service</Typography> and{" "}
|
||||
<Typography component="span">Privacy Policy.</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the first step of the login process.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.form - Form state object.
|
||||
* @param {Object} props.errors - Object containing form validation errors.
|
||||
* @param {Function} props.onSubmit - Callback function to handle form submission.
|
||||
* @param {Function} props.onChange - Callback function to handle form input changes.
|
||||
* @param {Function} props.onBack - Callback function to handle "Back" button click.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Field
|
||||
type="email"
|
||||
id="login-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="john.doe@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={onChange}
|
||||
error={errors.email}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the second step of the login process, including a password input field.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} props.form - Form state object.
|
||||
* @param {Object} props.errors - Object containing form validation errors.
|
||||
* @param {Function} props.onSubmit - Callback function to handle form submission.
|
||||
* @param {Function} props.onChange - Callback function to handle form input changes.
|
||||
* @param {Function} props.onBack - Callback function to handle "Back" button click.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your password</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Field
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.password && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Box textAlign="center">
|
||||
<Typography className="forgot-p" display="inline-block">
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
sx={{ userSelect: "none" }}
|
||||
onClick={() => navigate("/forgot-password")}
|
||||
>
|
||||
Reset password
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -29,6 +232,7 @@ const Login = () => {
|
||||
password: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
axiosInstance
|
||||
@@ -43,47 +247,6 @@ const Login = () => {
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { error } = credentials.validate(form, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
// validation errors
|
||||
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.",
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(login(form));
|
||||
if (action.payload.success) {
|
||||
navigate("/monitors");
|
||||
createToast({
|
||||
body: "Welcome back! You're successfully logged in.",
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
@@ -105,91 +268,136 @@ const Login = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (step === 1) {
|
||||
const { error } = credentials.validate(
|
||||
{ email: form.email },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
createToast({ body: error.details[0].message });
|
||||
} else {
|
||||
setStep(2);
|
||||
}
|
||||
} else if (step === 2) {
|
||||
const { error } = credentials.validate(form, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
// validation errors
|
||||
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.",
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(login(form));
|
||||
if (action.payload.success) {
|
||||
navigate("/monitors");
|
||||
createToast({
|
||||
body: "Welcome back! You're successfully logged in.",
|
||||
});
|
||||
} else {
|
||||
setErrors({
|
||||
password: "The password you provided does not match our records",
|
||||
});
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<Stack className="login-page" overflow="hidden">
|
||||
<img
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
/>
|
||||
<form className="login-form" onSubmit={handleSubmit}>
|
||||
<Stack gap={theme.gap.small} alignItems="center">
|
||||
<Logomark alt="BlueWave Uptime Icon" />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.xl }}>
|
||||
Login to Your Account
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.large} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Field
|
||||
type="email"
|
||||
id="login-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.gap.large}
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
p={theme.gap.xl}
|
||||
pb={theme.gap.triplexl}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.otherColors.graishWhite,
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
padding: {
|
||||
xs: theme.gap.large,
|
||||
sm: theme.gap.xl,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{step === 0 ? (
|
||||
<LandingPage onContinue={() => setStep(1)} />
|
||||
) : step === 1 ? (
|
||||
<StepOne
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
onBack={() => setStep(0)}
|
||||
/>
|
||||
<Field
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
/>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
sx={{
|
||||
padding: 0,
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Remember me"
|
||||
) : (
|
||||
step === 2 && (
|
||||
<StepTwo
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(1)}
|
||||
/>
|
||||
<Typography component="span" onClick={() => navigate("/forgot-password")}>
|
||||
Forgot password
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.ml} mt={theme.gap.large}>
|
||||
<Button
|
||||
type="submit"
|
||||
level="primary"
|
||||
label="Continue"
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
gap="5px"
|
||||
mt={theme.gap.ml}
|
||||
>
|
||||
<Typography component="p" sx={{ alignSelf: "center" }}>
|
||||
Don't have an account?
|
||||
</Typography>
|
||||
<Typography component="span"
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
>
|
||||
Sign up
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.gap.large}>
|
||||
<Typography display="inline-block">Don't have an account? —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Sign Up
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
/* SHARED */
|
||||
/* ////// */
|
||||
.register-page,
|
||||
.login-page,
|
||||
.set-new-password-page,
|
||||
.password-confirmed-page,
|
||||
.forgot-password-page,
|
||||
@@ -12,7 +11,6 @@
|
||||
height: var(--env-var-height-1);
|
||||
}
|
||||
.register-page h1.MuiTypography-root,
|
||||
.login-page h1.MuiTypography-root,
|
||||
.set-new-password-page h1.MuiTypography-root,
|
||||
.password-confirmed-page h1.MuiTypography-root,
|
||||
.forgot-password-page h1.MuiTypography-root,
|
||||
@@ -24,8 +22,6 @@
|
||||
.register-page p.MuiTypography-root,
|
||||
.register-page span,
|
||||
.register-page button,
|
||||
.login-page p.MuiTypography-root,
|
||||
.login-page span,
|
||||
.login-page button,
|
||||
.set-new-password-page p.MuiTypography-root,
|
||||
.set-new-password-page button,
|
||||
@@ -38,13 +34,11 @@
|
||||
.check-email-page span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.register-page p.MuiTypography-root,
|
||||
.login-page p.MuiTypography-root {
|
||||
.register-page p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.register-page button:not(.MuiIconButton-root),
|
||||
.login-page button:not(.MuiIconButton-root),
|
||||
.set-new-password-page button:not(.MuiIconButton-root),
|
||||
.password-confirmed-page button:not(.MuiIconButton-root),
|
||||
.forgot-password-page button:not(.MuiIconButton-root),
|
||||
@@ -54,12 +48,10 @@
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.register-page svg rect,
|
||||
.login-page svg rect {
|
||||
.register-page svg rect {
|
||||
fill: none;
|
||||
}
|
||||
.register-form,
|
||||
.login-form {
|
||||
.register-form {
|
||||
margin-top: 95px;
|
||||
}
|
||||
|
||||
@@ -77,7 +69,6 @@
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
|
||||
.login-page .MuiFormControlLabel-root .MuiButtonBase-root,
|
||||
.set-new-password-page button:not(.MuiIconButton-root) svg,
|
||||
.password-confirmed-page button:not(.MuiIconButton-root) svg,
|
||||
.forgot-password-page button svg,
|
||||
@@ -108,19 +99,6 @@
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.login-page .MuiFormControlLabel-root {
|
||||
margin: 0;
|
||||
}
|
||||
.login-page .MuiFormControlLabel-root span:not(.Mui-checked) svg {
|
||||
fill: var(--env-var-color-4);
|
||||
}
|
||||
.login-page .MuiFormControlLabel-root svg {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.login-page .MuiFormControlLabel-root .MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* /////////// */
|
||||
/* Check Email */
|
||||
@@ -128,3 +106,70 @@
|
||||
.check-email-page span.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* */
|
||||
.login-page {
|
||||
height: 100vh;
|
||||
}
|
||||
.login-page h1 {
|
||||
font-size: 37px;
|
||||
font-weight: 600;
|
||||
color: var(--env-var-color-3);
|
||||
}
|
||||
.login-page button:not(.MuiIconButton-root),
|
||||
.login-page .field h3.MuiTypography-root,
|
||||
.login-page .field input,
|
||||
.login-page .field .input-error,
|
||||
.login-page p,
|
||||
.login-page span {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
.login-page p {
|
||||
color: var(--env-var-color-2-light);
|
||||
}
|
||||
.login-page .field h3.MuiTypography-root,
|
||||
.login-page .field input {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
|
||||
.login-page p + span {
|
||||
color: var(--env-var-color-3);
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
.login-page p > span {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms;
|
||||
}
|
||||
.login-page p > span:hover {
|
||||
color: var(--env-var-color-2);
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
.login-page p + span:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.login-page button:not(.MuiIconButton-root) {
|
||||
user-select: none;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 1;
|
||||
}
|
||||
.login-page button:not(.MuiIconButton-root),
|
||||
.login-page .field .MuiInputBase-root:has(input) {
|
||||
height: 38px;
|
||||
}
|
||||
.login-page .field svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.login-page > .MuiStack-root:nth-of-type(1) {
|
||||
height: var(--env-var-nav-bar-height);
|
||||
}
|
||||
.login-page .background-pattern-svg {
|
||||
top: 14%;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import { StatusLabel } from "../../../Components/Label";
|
||||
|
||||
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
const [paginationController, setPaginationController] = useState({
|
||||
page: 0,
|
||||
rowsPerPage: 12,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
let url = `/checks/${selectedMonitor}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
|
||||
|
||||
if (selectedMonitor === "0") {
|
||||
url = `/checks/user/${user._id}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
|
||||
}
|
||||
|
||||
const res = await axiosInstance.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
}, [
|
||||
authToken,
|
||||
user,
|
||||
selectedMonitor,
|
||||
filter,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage,
|
||||
]);
|
||||
|
||||
const handlePageChange = (_, newPage) => {
|
||||
console.log(newPage);
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage - 1, // 0-indexed
|
||||
});
|
||||
};
|
||||
|
||||
let paginationComponent = <></>;
|
||||
if (checksCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
|
||||
page={paginationController.page + 1} //0-indexed
|
||||
onChange={handlePageChange}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Monitor Name</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(check.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{check.statusCode}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentTable.propTypes = {
|
||||
monitors: PropTypes.object.isRequired,
|
||||
selectedMonitor: PropTypes.string.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default IncidentTable;
|
||||
@@ -14,6 +14,9 @@
|
||||
height: 34px;
|
||||
border-color: var(--env-var-color-16);
|
||||
}
|
||||
.incidents .MuiPagination-root {
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
|
||||
/* remove later */
|
||||
.incidents .MuiSelect-select {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Typography, ButtonGroup, Stack, Box, Skeleton } from "@mui/material";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import { ButtonGroup, Stack, Skeleton, Typography } from "@mui/material";
|
||||
import Button from "../../Components/Button";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import BasicTable from "../../Components/BasicTable";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
|
||||
import IncidentTable from "./IncidentTable";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
@@ -40,8 +36,8 @@ const SkeletonLayout = () => {
|
||||
|
||||
const Incidents = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
const [monitors, setMonitors] = useState({});
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -49,21 +45,11 @@ const Incidents = () => {
|
||||
// TODO do something with these filters
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
let data = {
|
||||
cols: [
|
||||
{ id: 1, name: "Status" },
|
||||
{ id: 2, name: "Timestamp" },
|
||||
{ id: 3, name: "Monitor" },
|
||||
{ id: 4, name: "Message" },
|
||||
],
|
||||
rows: [],
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchIncidents = async () => {
|
||||
const fetchMonitors = async () => {
|
||||
setLoading(true);
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${authState.user._id}?status=false`,
|
||||
`/monitors/user/${authState.user._id}?status=false&limit=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
@@ -82,141 +68,70 @@ const Incidents = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
fetchIncidents();
|
||||
fetchMonitors();
|
||||
}, [authState]);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
const handleSelect = (event) => {
|
||||
setSelectedMonitor(event.target.value);
|
||||
};
|
||||
let incidents = [];
|
||||
let hasAnyIncidents = false;
|
||||
let hasSpecificIncidents = false;
|
||||
if (selectedMonitor === "0") {
|
||||
Object.values(monitors).forEach((monitor) => {
|
||||
incidents = incidents.concat(monitor.checks);
|
||||
});
|
||||
if (incidents.length !== 0) hasAnyIncidents = true;
|
||||
hasSpecificIncidents = hasAnyIncidents;
|
||||
} else {
|
||||
const monitor = monitors[selectedMonitor];
|
||||
incidents = monitor.checks;
|
||||
if (incidents.length !== 0) hasSpecificIncidents = true;
|
||||
hasAnyIncidents = true;
|
||||
}
|
||||
incidents = incidents
|
||||
.sort((a, b) => {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||
})
|
||||
.filter((incident) => {
|
||||
if (filter === "all") return true;
|
||||
else if (filter === "resolve") return incident.statusCode === 5000;
|
||||
else if (filter === "down") return incident.status === false;
|
||||
})
|
||||
.map((incident, idx) => {
|
||||
return {
|
||||
id: idx,
|
||||
data: [
|
||||
{
|
||||
id: idx + 0,
|
||||
data: <StatusLabel status="down" text="Down" />,
|
||||
},
|
||||
{ id: idx + 1, data: new Date(incident.createdAt).toLocaleString() },
|
||||
{ id: idx + 2, data: monitors[incident.monitorId].name },
|
||||
{ id: idx + 3, data: incident.statusCode },
|
||||
],
|
||||
};
|
||||
});
|
||||
data.rows = incidents;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack className="incidents" gap={theme.gap.large}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : hasAnyIncidents ? (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
|
||||
<Typography component="h1">Incident history for: </Typography>
|
||||
<Select
|
||||
id="incidents-select-monitor"
|
||||
placeholder="All servers"
|
||||
value={selectedMonitor}
|
||||
onChange={handleSelect}
|
||||
items={Object.values(monitors)}
|
||||
/>
|
||||
{hasAnyIncidents && hasSpecificIncidents && (
|
||||
<ButtonGroup sx={{ ml: "auto" }}>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="All"
|
||||
onClick={() => setFilter("all")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "all" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Down"
|
||||
onClick={() => setFilter("down")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "down" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Cannot Resolve"
|
||||
onClick={() => setFilter("resolve")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "resolve" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</Stack>
|
||||
{hasAnyIncidents && hasSpecificIncidents ? (
|
||||
<BasicTable data={data} paginated={true} rowsPerPage={12} />
|
||||
) : (
|
||||
<Typography alignSelf="center" mt={theme.gap.xxl}>
|
||||
The monitor you have selected, has no recorded incidents yet.
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Box>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
px: theme.gap.ml,
|
||||
mb: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
<Stack className="incidents" gap={theme.gap.large}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
|
||||
<Typography display="inline-block" component="h1">
|
||||
Incidents for
|
||||
</Typography>
|
||||
<Select
|
||||
id="incidents-select-monitor"
|
||||
placeholder="All servers"
|
||||
value={selectedMonitor}
|
||||
onChange={handleSelect}
|
||||
items={Object.values(monitors)}
|
||||
/>
|
||||
<Box
|
||||
padding={theme.gap.xxl}
|
||||
gap={theme.gap.small}
|
||||
border={1}
|
||||
borderRadius={`${theme.shape.borderRadius}px`}
|
||||
borderColor={theme.palette.otherColors.graishWhite}
|
||||
backgroundColor={theme.palette.otherColors.white}
|
||||
>
|
||||
<Typography textAlign="center">No incidents, yet.</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
<ButtonGroup sx={{ ml: "auto" }}>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="All"
|
||||
onClick={() => setFilter("all")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "all" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Down"
|
||||
onClick={() => setFilter("down")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "down" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Cannot Resolve"
|
||||
onClick={() => setFilter("resolve")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "resolve" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<IncidentTable
|
||||
monitors={monitors}
|
||||
selectedMonitor={selectedMonitor}
|
||||
filter={filter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.maintenance-options > .MuiStack-root > .MuiStack-root.select-wrapper {
|
||||
width: 380px;
|
||||
.maintenance-options .MuiStack-root .MuiStack-root.select-wrapper,
|
||||
.maintenance-options .MuiStack-root .field {
|
||||
width: 60%;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.date-picker .MuiOutlinedInput-root,
|
||||
@@ -16,6 +18,54 @@
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.MuiInputAdornment-outlined .MuiIconButton-root svg {
|
||||
.maintenance-options .MuiInputAdornment-outlined .MuiIconButton-root svg {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.duration-config .field-text .MuiTextField-root,
|
||||
.duration-config .field {
|
||||
width: 70px;
|
||||
min-width: 70px;
|
||||
max-width: 70px;
|
||||
}
|
||||
|
||||
.duration-config > .MuiStack-root.select-wrapper {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.duration-config
|
||||
> .MuiStack-root.select-wrapper
|
||||
> .MuiInputBase-root:has(> .MuiSelect-select) {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.duration-config > .select-wrapper .select-component > .MuiSelect-select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.maintenance-options .add-monitors-fields .field.field-text {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.MuiPopover-paper.MuiMenu-paper.select-dropdown
|
||||
.MuiMenu-list
|
||||
.MuiButtonBase-root {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.date-picker
|
||||
.MuiOutlinedInput-root
|
||||
.MuiInputAdornment-outlined
|
||||
.MuiIconButton-root {
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.create-maintenance-window .MuiStack-root .MuiStack-root > button {
|
||||
width: 110px;
|
||||
height: 34px;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
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";
|
||||
|
||||
const directory = {
|
||||
title: "Create a maintenance window",
|
||||
@@ -45,9 +46,21 @@ const configOptionTitle = (title, description) => {
|
||||
);
|
||||
};
|
||||
|
||||
const durationOptions = [
|
||||
{ _id: "minutes", name: "minutes" },
|
||||
{ _id: "hours", name: "hours" },
|
||||
{ _id: "days", name: "days" },
|
||||
];
|
||||
|
||||
const CreateNewMaintenanceWindow = () => {
|
||||
const [values, setValues] = useState({
|
||||
repeat: 1,
|
||||
date: dayjs(),
|
||||
startTime: dayjs(),
|
||||
duration: "60",
|
||||
unit: "minutes",
|
||||
friendlyName: "",
|
||||
AddMonitors: "",
|
||||
});
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
@@ -77,22 +90,90 @@ const CreateNewMaintenanceWindow = () => {
|
||||
className="date-localization-provider"
|
||||
dateAdapter={AdapterDayjs}
|
||||
>
|
||||
<DatePicker className="date-picker" defaultValue={dayjs()} />
|
||||
<DatePicker
|
||||
className="date-picker"
|
||||
defaultValue={values.date}
|
||||
onChange={(date) =>
|
||||
handleChange({ target: { value: date } }, "date")
|
||||
}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Start time",
|
||||
description: "Your pings won’t be sent in this time frame.",
|
||||
component: (
|
||||
<LocalizationProvider
|
||||
className="time-localization-provider"
|
||||
dateAdapter={AdapterDayjs}
|
||||
>
|
||||
<MobileTimePicker className="time-picker" defaultValue={dayjs()} />
|
||||
<MobileTimePicker
|
||||
className="time-picker"
|
||||
defaultValue={values.startTime}
|
||||
onChange={(time) =>
|
||||
handleChange({ target: { value: time } }, "startTime")
|
||||
}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Duration",
|
||||
component: (
|
||||
<Stack className="duration-config" gap={2} direction="row">
|
||||
<Field
|
||||
id="duration-value"
|
||||
placeholder="60"
|
||||
onChange={(e) => handleChange(e, "duration")}
|
||||
value={values.duration}
|
||||
/>
|
||||
<Select
|
||||
onChange={(e) => handleChange(e, "unit")}
|
||||
id="duration-unit"
|
||||
items={durationOptions}
|
||||
value={values.unit}
|
||||
/>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Friendly name",
|
||||
component: (
|
||||
<Field
|
||||
id="friendly-name"
|
||||
placeholder="Maintanence at __ : __ for ___ minutes"
|
||||
value={values.friendlyName}
|
||||
onChange={(e) => handleChange(e, "friendlyName")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Add monitors",
|
||||
component: (
|
||||
<Stack
|
||||
className="add-monitors-fields"
|
||||
sx={{ width: "60%", maxWidth: "380px" }}
|
||||
gap={2}
|
||||
>
|
||||
<Field
|
||||
id="add-monitors"
|
||||
placeholder="Start typing to search for current monitors"
|
||||
value={values.AddMonitors}
|
||||
onChange={(e) => handleChange(e, "AddMonitors")}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
width: "fit-content",
|
||||
fontSize: "var(--env-var-font-size-small)",
|
||||
borderBottom: "1px dashed var(--env-var-color-3)",
|
||||
paddingBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Add all monitors to this maintenance window
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -127,22 +208,46 @@ const CreateNewMaintenanceWindow = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
className="maintenance-options"
|
||||
gap={5}
|
||||
paddingY={4}
|
||||
paddingX={8}
|
||||
paddingBottom={10}
|
||||
sx={{
|
||||
border: "1px solid var(--env-var-color-16)",
|
||||
borderRadius: "var(--env-var-radius-1)",
|
||||
backgroundColor: "var(--env-var-color-0)",
|
||||
}}
|
||||
className="maintenance-options"
|
||||
>
|
||||
{configOptions.map((item, index) => (
|
||||
<Stack key={index} display="-webkit-inline-box">
|
||||
{configOptionTitle(item.title, item.description)}
|
||||
{item.title && configOptionTitle(item.title, item.description)}
|
||||
{item.component && item.component}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack justifyContent="end" direction="row" marginTop={3}>
|
||||
<Button
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent",
|
||||
boxShadow: "none",
|
||||
},
|
||||
}}
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
/>
|
||||
<Button
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: "#1570EF",
|
||||
boxShadow: "none",
|
||||
},
|
||||
}}
|
||||
level="primary"
|
||||
label="Create"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -460,10 +460,10 @@ const Configure = () => {
|
||||
}}
|
||||
>
|
||||
<Typography id="modal-delete-monitor" component="h2">
|
||||
Really delete this monitor?
|
||||
Do you really want to delete this monitor?
|
||||
</Typography>
|
||||
<Typography id="delete-monitor-confirmation">
|
||||
Once deleted, this monitor cannot be retrieved back.
|
||||
Once deleted, this monitor cannot be retrieved.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
@@ -6,16 +6,19 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TablePagination,
|
||||
PaginationItem,
|
||||
Pagination,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import axiosInstance from "../../../../Utils/axiosConfig";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
|
||||
const PaginationTable = ({ monitorId, filter }) => {
|
||||
const PaginationTable = ({ monitorId, dateRange }) => {
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
@@ -28,7 +31,7 @@ const PaginationTable = ({ monitorId, filter }) => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/checks/${monitorId}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
|
||||
`/checks/${monitorId}?sortOrder=desc&dateRange=${dateRange}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -45,7 +48,7 @@ const PaginationTable = ({ monitorId, filter }) => {
|
||||
}, [
|
||||
authToken,
|
||||
monitorId,
|
||||
filter,
|
||||
dateRange,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage,
|
||||
]);
|
||||
@@ -53,69 +56,82 @@ const PaginationTable = ({ monitorId, filter }) => {
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage,
|
||||
page: newPage - 1, // 0-indexed
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
rowsPerPage: parseInt(event.target.value, 10),
|
||||
page: 0,
|
||||
});
|
||||
};
|
||||
let paginationComponent = <></>;
|
||||
if (checksCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
|
||||
page={paginationController.page + 1} //0-indexed
|
||||
onChange={handlePageChange}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(check.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{check.statusCode}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
onPageChange={handlePageChange}
|
||||
page={paginationController.page}
|
||||
count={checksCount}
|
||||
rowsPerPage={paginationController.rowsPerPage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(check.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{check.statusCode}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
PaginationTable.propTypes = {
|
||||
monitorId: PropTypes.string.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
dateRange: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PaginationTable;
|
||||
@@ -113,13 +113,13 @@ const DetailsPage = () => {
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [filter, setFilter] = useState("day");
|
||||
const [dateRange, setDateRange] = useState("day");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchMonitor = useCallback(async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=asc&filter=${filter}&numToDisplay=50&normalize=true`,
|
||||
`/monitors/${monitorId}?sortOrder=asc&filter=${dateRange}&numToDisplay=50&normalize=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -131,7 +131,7 @@ const DetailsPage = () => {
|
||||
console.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found");
|
||||
}
|
||||
}, [authToken, monitorId, navigate, filter]);
|
||||
}, [authToken, monitorId, navigate, dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMonitor();
|
||||
@@ -301,44 +301,43 @@ const DetailsPage = () => {
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Day"
|
||||
onClick={() => setFilter("day")}
|
||||
onClick={() => setDateRange("day")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "day" && theme.palette.otherColors.fillGray,
|
||||
dateRange === "day" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Week"
|
||||
onClick={() => setFilter("week")}
|
||||
onClick={() => setDateRange("week")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "week" && theme.palette.otherColors.fillGray,
|
||||
dateRange === "week" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Month"
|
||||
onClick={() => setFilter("month")}
|
||||
onClick={() => setDateRange("month")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "month" &&
|
||||
dateRange === "month" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<MonitorDetailsAreaChart
|
||||
checks={monitor.checks}
|
||||
filter={filter}
|
||||
/>
|
||||
<MonitorDetailsAreaChart checks={monitor.checks} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.ml}>
|
||||
<Typography component="h2">History</Typography>
|
||||
{/* TODO New Table */}
|
||||
<PaginationTable monitorId={monitorId} filter={filter} />
|
||||
<PaginationTable monitorId={monitorId} dateRange={dateRange} />
|
||||
{/* <BasicTable data={data} paginated={true} /> */}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
.monitors p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.monitors h2:has(+ p),
|
||||
.monitors p.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
.configure-pagespeed form > .MuiStack-root:nth-child(2) {
|
||||
background-color: var(--env-var-color-8);
|
||||
border: solid 1px var(--env-var-color-6);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-default-2);
|
||||
}
|
||||
.configure-pagespeed h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
}
|
||||
.configure-pagespeed h1.MuiTypography-root,
|
||||
.configure-pagespeed h2.MuiTypography-root {
|
||||
color: var(--env-var-color-1);
|
||||
}
|
||||
.configure-pagespeed span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.configure-pagespeed p.MuiTypography-root,
|
||||
.configure-pagespeed h3.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.configure-pagespeed h1.MuiTypography-root,
|
||||
.configure-pagespeed h3.MuiTypography-root,
|
||||
.configure-pagespeed h2.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.configure-pagespeed p.MuiTypography-root,
|
||||
.configure-pagespeed h3.MuiTypography-root,
|
||||
.configure-pagespeed h3.MuiTypography-root > span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.configure-pagespeed .MuiBox-root > .field + p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.configure-pagespeed .MuiBox-root > .field + p.MuiTypography-root,
|
||||
.configure-pagespeed h3.MuiTypography-root > span.MuiTypography-root {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.configure-pagespeed .checkbox-wrapper {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.configure-pagespeed .MuiInputBase-root:has(> .Mui-disabled) {
|
||||
background-color: var(--env-var-color-13);
|
||||
}
|
||||
/* to be removed */
|
||||
.configure-pagespeed .section-disabled {
|
||||
opacity: 0.4;
|
||||
background-color: #f1f1f2;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.configure-pagespeed button {
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
|
||||
body:has(.configure-pagespeed) .select-dropdown .MuiMenuItem-root,
|
||||
.configure-pagespeed .select-wrapper .select-component > .MuiSelect-select {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.configure-pagespeed .field,
|
||||
.configure-pagespeed .section-disabled,
|
||||
.configure-pagespeed .select-wrapper,
|
||||
.configure-pagespeed h3.MuiTypography-root {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
deletePageSpeed,
|
||||
getPageSpeedByUserId,
|
||||
updatePageSpeed,
|
||||
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
|
||||
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack gap={theme.gap.xl} mt={theme.gap.medium} maxWidth="1000px">
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.gap.small }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="50%" height={18} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.medium}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={500} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PageSpeedConfigure = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const frequencies = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
{ _id: 20, name: "20 minutes" },
|
||||
{ _id: 60, name: "1 hour" },
|
||||
{ _id: 1440, name: "1 day" },
|
||||
{ _id: 10080, name: "1 week" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
if (!data) {
|
||||
console.error("Error fetching pagespeed monitor of id: " + monitorId);
|
||||
navigate("/not-found");
|
||||
}
|
||||
setMonitor({
|
||||
...data,
|
||||
});
|
||||
}, [monitorId]);
|
||||
|
||||
const handleChange = (event, id) => {
|
||||
let { value } = event.target;
|
||||
if (id === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
setMonitor((prev) => ({ ...prev, [id]: value }));
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [id]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
if (error) newErrors[id] = error.details[0].message;
|
||||
else delete newErrors[id];
|
||||
return newErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(
|
||||
updatePageSpeed({ authToken, monitor: monitor })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor updated successfully!" });
|
||||
dispatch(getPageSpeedByUserId(authToken));
|
||||
} else {
|
||||
createToast({ body: "Failed to update monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(deletePageSpeed({ authToken, monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/pagespeed");
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
return (
|
||||
<Box className="configure-pagespeed">
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
mb: theme.gap.large,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<form
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
onSubmit={handleSave}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.gap.large,
|
||||
maxWidth: "1000px",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" gap={theme.gap.small}>
|
||||
{monitor?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
mb={theme.gap.xs}
|
||||
sx={{ lineHeight: 1 }}
|
||||
>
|
||||
{monitor?.url}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: monitor?.status
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-24)",
|
||||
}}
|
||||
>
|
||||
Your pagespeed monitor is {monitor?.status ? "live" : "down"}.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box alignSelf="flex-end" ml="auto">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Pause"
|
||||
animate="rotate180"
|
||||
img={<PauseCircleOutlineIcon />}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
pl: theme.gap.small,
|
||||
pr: theme.gap.medium,
|
||||
"& svg": {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="error"
|
||||
label="Remove"
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
px: theme.gap.ml,
|
||||
ml: theme.gap.medium,
|
||||
}}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">Monitor friendly name</Typography>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
placeholder="Example monitor"
|
||||
value={monitor?.name || ""}
|
||||
onChange={(event) => handleChange(event, "name")}
|
||||
error={errors.name}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">URL</Typography>
|
||||
<Field
|
||||
type="url"
|
||||
id="monitor-url"
|
||||
placeholder="random.website.com"
|
||||
value={monitor?.url?.replace("http://", "") || ""}
|
||||
onChange={(event) => handleChange(event, "url")}
|
||||
error={errors.url}
|
||||
disabled={true}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">Check frequency</Typography>
|
||||
<Select
|
||||
id="monitor-frequency"
|
||||
items={frequencies}
|
||||
value={monitor?.interval / MS_PER_MINUTE || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">
|
||||
Incidents notifications{" "}
|
||||
<Typography component="span">(coming soon)</Typography>
|
||||
</Typography>
|
||||
<Stack className="section-disabled">
|
||||
<Typography mb={theme.gap.small}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Notify via email (to gorkem.cetin@bluewavelabs.ca)"
|
||||
isChecked={false}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-emails"
|
||||
label="Notify via email to following emails"
|
||||
isChecked={false}
|
||||
/>
|
||||
<Box mx={`calc(${theme.gap.ml} * 2)`}>
|
||||
<Field
|
||||
id="notify-emails-list"
|
||||
placeholder="notifications@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
error=""
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
type="submit"
|
||||
level="primary"
|
||||
label="Save"
|
||||
onClick={handleSave}
|
||||
sx={{ px: theme.gap.large, mt: theme.gap.large }}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-pagespeed-monitor"
|
||||
aria-describedby="delete-pagespeed-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-pagespeed-monitor" component="h2">
|
||||
Do you really want to delete this monitor?
|
||||
</Typography>
|
||||
<Typography id="delete-pagespeed-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={handleRemove} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeedConfigure;
|
||||
@@ -139,7 +139,7 @@ const SkeletonLayout = () => {
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack direction="row" gap={theme.gap.small}>
|
||||
<Skeleton variant="circular" style={{ minWidth: 24, minHeight: 24 }} />
|
||||
<Box width="80%">
|
||||
<Box width="85%">
|
||||
<Skeleton variant="rounded" width="50%" height={24} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
@@ -289,7 +289,7 @@ const PageSpeedDetails = () => {
|
||||
return props;
|
||||
};
|
||||
|
||||
const pieSize = { width: 200, height: 200 };
|
||||
const pieSize = { width: 210, height: 200 };
|
||||
const pieData = getPieData(audits);
|
||||
const colorMap = getColors(performance);
|
||||
|
||||
@@ -321,7 +321,7 @@ const PageSpeedDetails = () => {
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" gap={theme.gap.small}>
|
||||
<GreenCheck />
|
||||
{monitor?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
|
||||
@@ -98,6 +98,7 @@ const theme = createTheme({
|
||||
large: "24px",
|
||||
xl: "40px",
|
||||
xxl: "60px",
|
||||
triplexl: "120px"
|
||||
},
|
||||
alert: {
|
||||
info: {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" fill="#1570EF"/>
|
||||
<path d="M4.43466 24V9.45455H10.0028C11.054 9.45455 11.9276 9.62026 12.6236 9.9517C13.3243 10.2784 13.8475 10.7259 14.1932 11.294C14.5436 11.8622 14.7188 12.5062 14.7188 13.2259C14.7188 13.8177 14.6051 14.3243 14.3778 14.7457C14.1506 15.1624 13.8452 15.5009 13.4616 15.7614C13.0781 16.0218 12.6496 16.2088 12.1761 16.3224V16.4645C12.6922 16.4929 13.187 16.6515 13.6605 16.9403C14.1387 17.2244 14.5294 17.6269 14.8324 18.1477C15.1354 18.6686 15.2869 19.2983 15.2869 20.0369C15.2869 20.7898 15.1046 21.4669 14.7401 22.0682C14.3755 22.6648 13.8262 23.1359 13.0923 23.4815C12.3584 23.8272 11.4351 24 10.3224 24H4.43466ZM7.0696 21.7983H9.90341C10.8598 21.7983 11.5488 21.616 11.9702 21.2514C12.3963 20.8821 12.6094 20.4086 12.6094 19.831C12.6094 19.4001 12.5028 19.0118 12.2898 18.6662C12.0767 18.3158 11.7737 18.0412 11.3807 17.8423C10.9877 17.6387 10.5189 17.5369 9.97443 17.5369H7.0696V21.7983ZM7.0696 15.6406H9.67614C10.1307 15.6406 10.5402 15.5578 10.9048 15.392C11.2694 15.2216 11.5559 14.9825 11.7642 14.6747C11.9773 14.3622 12.0838 13.9929 12.0838 13.5668C12.0838 13.0033 11.8849 12.5393 11.4872 12.1747C11.0942 11.8101 10.5095 11.6278 9.73295 11.6278H7.0696V15.6406ZM25.5482 9.45455H28.1832V18.9574C28.1832 19.9991 27.937 20.9152 27.4445 21.706C26.9568 22.4967 26.2703 23.1146 25.3849 23.5597C24.4995 24 23.4649 24.2202 22.2812 24.2202C21.0927 24.2202 20.0558 24 19.1704 23.5597C18.285 23.1146 17.5984 22.4967 17.1107 21.706C16.623 20.9152 16.3792 19.9991 16.3792 18.9574V9.45455H19.0141V18.7372C19.0141 19.3433 19.1467 19.883 19.4119 20.3565C19.6817 20.83 20.0605 21.2017 20.5482 21.4716C21.0359 21.7367 21.6136 21.8693 22.2812 21.8693C22.9488 21.8693 23.5264 21.7367 24.0141 21.4716C24.5066 21.2017 24.8853 20.83 25.1505 20.3565C25.4156 19.883 25.5482 19.3433 25.5482 18.7372V9.45455Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,4 @@
|
||||
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5 18L14.8571 12M9.14286 12L2.50003 18M2 7L10.1649 12.7154C10.8261 13.1783 11.1567 13.4097 11.5163 13.4993C11.8339 13.5785 12.1661 13.5785 12.4837 13.4993C12.8433 13.4097 13.1739 13.1783 13.8351 12.7154L22 7M6.8 20H17.2C18.8802 20 19.7202 20 20.362 19.673C20.9265 19.3854 21.3854 18.9265 21.673 18.362C22 17.7202 22 16.8802 22 15.2V8.8C22 7.11984 22 6.27976 21.673 5.63803C21.3854 5.07354 20.9265 4.6146 20.362 4.32698C19.7202 4 18.8802 4 17.2 4H6.8C5.11984 4 4.27976 4 3.63803 4.32698C3.07354 4.6146 2.6146 5.07354 2.32698 5.63803C2 6.27976 2 7.11984 2 8.8V15.2C2 16.8802 2 17.7202 2.32698 18.362C2.6146 18.9265 3.07354 19.3854 3.63803 19.673C4.27976 20 5.11984 20 6.8 20Z" stroke="#475467" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 876 B |
@@ -1,5 +1,9 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
@@ -17,8 +21,10 @@
|
||||
|
||||
--env-var-font-familt-1: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
|
||||
--env-var-color-0: #fff;
|
||||
--env-var-color-1: #101828;
|
||||
--env-var-color-2: #475467;
|
||||
--env-var-color-2-light: #838c99;
|
||||
--env-var-color-3: #1570ef;
|
||||
|
||||
--env-var-color-4: #d0d5dd;
|
||||
|
||||
@@ -20,18 +20,22 @@ BlueWave Uptime is an open source server monitoring application used to track th
|
||||
|
||||
## Features
|
||||
|
||||
* Completely open source, deployable on your servers
|
||||
* Website monitoring
|
||||
* Port monitoring
|
||||
* Ping monitoring
|
||||
* Incidents at a glance
|
||||
* Status pages
|
||||
* Page speed monitoring (in the works)
|
||||
- [x] Completely open source, deployable on your servers
|
||||
- [x] Website monitoring
|
||||
- [x] Port monitoring
|
||||
- [x] Ping monitoring
|
||||
- [x] Incidents at a glance
|
||||
- [x] Page speed monitoring
|
||||
- [ ] Scheduled maintenance (in the works)
|
||||
- [ ] Status pages (in the works)
|
||||
|
||||
**Roadmap:**
|
||||
* 3rd party integrations
|
||||
* Domain monitoring
|
||||
* SSL monitoring
|
||||
**Roadmap (short term):**
|
||||
- [ ] 3rd party integrations
|
||||
- [ ] DNS monitoring
|
||||
- [ ] SSL monitoring
|
||||
|
||||
**Roadmap (long term):**
|
||||
- [ ] Memory, disk and CPU monitoring using OpenTelemetry
|
||||
|
||||
## Tech stack
|
||||
|
||||
@@ -76,29 +80,41 @@ Made with [contrib.rocks](https://contrib.rocks).
|
||||
###### Auth
|
||||
- <code>POST</code> [/api/v1/auth/register](#post-register)
|
||||
- <code>POST</code> [/api/v1/auth/login](#post-login)
|
||||
- <code>POST</code> [/api/v1/auth/user/{userId}](#post-auth-user-edit-id)
|
||||
- <code>PUT</code> [/api/v1/auth/user/{userId}](#post-auth-user-edit-id)
|
||||
- <code>GET</code> [/api/v1/auth/users](#get-all-users-id)
|
||||
- <code>GET</code> [/api/v1/auth/users/admin](#get-all-admin-users-id)
|
||||
- <code>POST</code> [/api/v1/auth/invite](#post-auth-invite-id)
|
||||
- <code>POST</code> [/api/v1/auth/invite/verify/](#post-auth-invite-verify-id)
|
||||
- <code>POST</code> [/api/v1/auth/recovery/request](#post-auth-recovery-request-id)
|
||||
- <code>POST</code> [/api/v1/auth/recovery/validate](#post-auth-recovery-validate-id)
|
||||
- <code>POST</code> [/api/v1/auth/recovery/reset](#post-auth-recovery-reset-id)
|
||||
###### Monitors
|
||||
- <code>GET</code> [/api/v1/monitors](#get-monitors)
|
||||
- <code>GET</code> [/api/v1/monitor/{id}](#get-monitor-id)
|
||||
- <code>GET</code> [/api/v1/monitors/{monitorId}](#get-monitor-id)
|
||||
- <code>GET</code> [/api/v1/monitors/user/{userId}?limit](#get-monitors-user-userid)
|
||||
- <code>POST</code> [/api/v1/monitors](#post-monitors)
|
||||
- <code>POST</code> [/api/v1/monitors/delete/{monitorId}](#post-monitors-del-id)
|
||||
- <code>POST</code> [/api/v1/monitors/edit/{monitorId}](#post-monitors-edit-id)
|
||||
- <code>PUT</code> [/api/v1/monitors/{monitorId}](#put-monitors-id)
|
||||
- <code>DELETE</code> [/api/v1/monitors/{monitorId}](#delete-monitors-id)
|
||||
- <code>DELETE</code> [/api/v1/monitors/all](#delete-all-monitors)
|
||||
###### Checks
|
||||
- <code>POST</code> [/api/v1/checks/{monitorId}](#post-checks)
|
||||
- <code>GET</code> [/api/v1/checks/{monitorId}](#get-checks)
|
||||
- <code>POST</code> [/api/v1/checks/delete/{monitorId}](#delete-checks)
|
||||
- <code>DELETE</code> [/api/v1/checks/{monitorId}](#delete-checks)
|
||||
###### Alerts
|
||||
- <code>POST</code> [/api/v1/alerts/{monitorId}](#create-alert)
|
||||
- <code>GET</code> [/api/v1/alerts/user/{userId}](#get-alerts-user-id)
|
||||
- <code>GET</code> [/api/v1/alerts/monitor/{monitorId}](#get-alerts-monitor-id)
|
||||
- <code>GET</code> [/api/v1/alerts/{alertId}](#get-alert-alert-id)
|
||||
- <code>POST</code> [/api/v1/alerts/edit/{alertId}](#edit-alert)
|
||||
- <code>POST</code> [/api/v1/alerts/delete/{alertId}](#delete-alert)
|
||||
- <code>PUT</code> [/api/v1/alerts/{alertId}](#edit-alert)
|
||||
- <code>DELETE</code> [/api/v1/alerts/{alertId}](#delete-alert)
|
||||
###### Page Speed
|
||||
- <code>POST</code> [/api/v1/pagespeed/:monitorId](#post-pagespeed)
|
||||
- <code>GET</code> [/api/v1/pagespeed/:monitorId](#get-pagespeed)
|
||||
- <code>DELETE</code> [/api/v1/pagespeed/:monitorId](#delete-pagespeed)
|
||||
###### Maintenance Window
|
||||
- <code>POST</code> [/api/v1/maintenance-window/:monitorId](#post-maintenance-window)
|
||||
- <code>GET</code> [/api/v1/maintenance-window/user/:userId](#get-maintenance-window-by-userid)
|
||||
- <code>GET</code> [/api/v1/maintenance-window/monitor/:monitorId](#get-maintenance-window-by-monitor-id)
|
||||
1. [Error Handling](#error-handling)
|
||||
1. [Contributors](#contributors)
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Security Policy
|
||||
|
||||
If you find a vulnerability, send it to hello@bluewavelabs.ca
|
||||
|
||||
Please do not create an issue for security vulnerabilities.
|
||||
|
||||
@@ -3,6 +3,8 @@ const {
|
||||
createCheckBodyValidation,
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
getUserChecksParamValidation,
|
||||
getUserChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
} = require("../validation/joi");
|
||||
const { successMessages } = require("../utils/messages");
|
||||
@@ -60,6 +62,31 @@ const getChecks = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getUserChecks = async (req, res, next) => {
|
||||
try {
|
||||
await getUserChecksParamValidation.validateAsync(req.params);
|
||||
await getUserChecksQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const checkData = await req.db.getUserChecks(req);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.CHECK_GET,
|
||||
data: checkData,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteChecks = async (req, res, next) => {
|
||||
try {
|
||||
await deleteChecksParamValidation.validateAsync(req.params);
|
||||
@@ -88,5 +115,6 @@ const deleteChecks = async (req, res, next) => {
|
||||
module.exports = {
|
||||
createCheck,
|
||||
getChecks,
|
||||
getUserChecks,
|
||||
deleteChecks,
|
||||
};
|
||||
@@ -90,6 +90,7 @@ const {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
getUserChecks,
|
||||
deleteChecks,
|
||||
} = require("./modules/checkModule");
|
||||
|
||||
@@ -148,6 +149,7 @@ module.exports = {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
getUserChecks,
|
||||
deleteChecks,
|
||||
createAlert,
|
||||
getAlertsByUserId,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const Check = require("../../../models/Check");
|
||||
const filterLookup = {
|
||||
const Monitor = require("../../../models/Monitor");
|
||||
const mongoose = require("mongoose");
|
||||
const dateRangeLookup = {
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a check for a monitor
|
||||
* @async
|
||||
@@ -28,13 +31,32 @@ const createCheck = async (checkData) => {
|
||||
|
||||
const getChecksCount = async (req) => {
|
||||
const monitorId = req.params.monitorId;
|
||||
const dateRange = req.query.dateRange;
|
||||
const filter = req.query.filter;
|
||||
console.log(filter);
|
||||
// Build query
|
||||
const checksQuery = { monitorId: monitorId };
|
||||
// Filter checks by "day", "week", or "month"
|
||||
if (filter !== undefined) {
|
||||
checksQuery.createdAt = { $gte: filterLookup[filter] };
|
||||
if (dateRange !== undefined) {
|
||||
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
|
||||
}
|
||||
|
||||
if (filter !== undefined) {
|
||||
checksQuery.status = false;
|
||||
switch (filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "down":
|
||||
break;
|
||||
case "resolve":
|
||||
checksQuery.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
console.log("default");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const count = await Check.countDocuments(checksQuery);
|
||||
return count;
|
||||
};
|
||||
@@ -50,23 +72,34 @@ const getChecksCount = async (req) => {
|
||||
const getChecks = async (req) => {
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
let { sortOrder, limit, filter, page, rowsPerPage } = req.query;
|
||||
|
||||
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
|
||||
// Default limit to 0 if not provided
|
||||
if (limit === undefined) limit = 0;
|
||||
limit = limit === "undefined" ? 0 : limit;
|
||||
|
||||
// Default sort order is newest -> oldest
|
||||
if (sortOrder === "asc") {
|
||||
sortOrder = 1;
|
||||
} else if (sortOrder === "desc") {
|
||||
sortOrder = -1;
|
||||
} else sortOrder = -1;
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Build query
|
||||
const checksQuery = { monitorId: monitorId };
|
||||
// Filter checks by "day", "week", or "month"
|
||||
if (dateRange !== undefined) {
|
||||
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
|
||||
}
|
||||
// Fitler checks by status
|
||||
if (filter !== undefined) {
|
||||
checksQuery.createdAt = { $gte: filterLookup[filter] };
|
||||
checksQuery.status = false;
|
||||
switch (filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "down":
|
||||
break;
|
||||
case "resolve":
|
||||
checksQuery.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
console.log("default");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Need to skip and limit here
|
||||
@@ -84,8 +117,61 @@ const getChecks = async (req) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getUserChecks = async (req) => {
|
||||
const { userId } = req.params;
|
||||
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
|
||||
|
||||
// Get monitorIDs
|
||||
const userMonitors = await Monitor.find({ userId: userId });
|
||||
const monitorIds = userMonitors.map((monitor) => monitor._id);
|
||||
|
||||
//Build check query
|
||||
// Default limit to 0 if not provided
|
||||
limit = limit === "undefined" ? 0 : limit;
|
||||
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
checksQuery = { monitorId: { $in: monitorIds } };
|
||||
|
||||
if (filter !== undefined) {
|
||||
checksQuery.status = false;
|
||||
switch (filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "down":
|
||||
break;
|
||||
case "resolve":
|
||||
checksQuery.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
console.log("default");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dateRange !== undefined) {
|
||||
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
|
||||
}
|
||||
|
||||
// Skip and limit for pagination
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
const checksCount = await Check.countDocuments(checksQuery);
|
||||
|
||||
const checks = await Check.find(checksQuery)
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage)
|
||||
.sort({ createdAt: sortOrder });
|
||||
|
||||
return { checksCount, checks };
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all checks for a monitor
|
||||
* Delete all checks for a user
|
||||
* @async
|
||||
* @param {string} monitorId
|
||||
* @returns {number}
|
||||
@@ -100,4 +186,10 @@ const deleteChecks = async (monitorId) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
module.exports = { createCheck, getChecksCount, getChecks, deleteChecks };
|
||||
module.exports = {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
getUserChecks,
|
||||
deleteChecks,
|
||||
};
|
||||
@@ -81,14 +81,15 @@ const getMonitorById = async (req, res) => {
|
||||
// If more than numToDisplay checks, pick every nth check
|
||||
if (numToDisplay !== undefined && checks && checks.length > numToDisplay) {
|
||||
const n = Math.ceil(checks.length / numToDisplay);
|
||||
checks = checks.filter((_, index) => index % n === 0);
|
||||
checks = checks.filter(
|
||||
(_, index) => index % n === 0 || index === checks.length - 1
|
||||
);
|
||||
}
|
||||
|
||||
// Normalize checks if requested
|
||||
if (normalize) {
|
||||
checks = NormalizeData(checks, 10, 100);
|
||||
if (normalize !== undefined) {
|
||||
checks = NormalizeData(checks, 1, 100);
|
||||
}
|
||||
|
||||
const notifications = await Notification.find({ monitorId: monitor._id });
|
||||
const monitorWithChecks = { ...monitor.toObject(), checks, notifications };
|
||||
return monitorWithChecks;
|
||||
@@ -148,7 +149,7 @@ const getMonitorsByUserId = async (req, res) => {
|
||||
.limit(limit);
|
||||
|
||||
//Normalize checks if requested
|
||||
if (normalize === true) {
|
||||
if (normalize !== undefined) {
|
||||
checks = NormalizeData(checks, 10, 100);
|
||||
}
|
||||
|
||||
|
||||
+13
-12
@@ -4,7 +4,7 @@ const Notification = require("../models/Notification");
|
||||
|
||||
/**
|
||||
* Check Schema for MongoDB collection.
|
||||
*
|
||||
*
|
||||
* Represents a check associated with a monitor, storing information
|
||||
* about the status and response of a particular check event.
|
||||
*/
|
||||
@@ -12,7 +12,7 @@ const CheckSchema = mongoose.Schema(
|
||||
{
|
||||
/**
|
||||
* Reference to the associated Monitor document.
|
||||
*
|
||||
*
|
||||
* @type {mongoose.Schema.Types.ObjectId}
|
||||
*/
|
||||
monitorId: {
|
||||
@@ -22,7 +22,7 @@ const CheckSchema = mongoose.Schema(
|
||||
},
|
||||
/**
|
||||
* Status of the check (true for up, false for down).
|
||||
*
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
status: {
|
||||
@@ -30,23 +30,23 @@ const CheckSchema = mongoose.Schema(
|
||||
},
|
||||
/**
|
||||
* Response time of the check in milliseconds.
|
||||
*
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
responseTime: {
|
||||
type: Number,
|
||||
type: Number,
|
||||
},
|
||||
/**
|
||||
* HTTP status code received during the check.
|
||||
*
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
statusCode: {
|
||||
type: Number,
|
||||
type: Number,
|
||||
},
|
||||
/**
|
||||
* Message or description of the check result.
|
||||
*
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
message: {
|
||||
@@ -54,7 +54,7 @@ const CheckSchema = mongoose.Schema(
|
||||
},
|
||||
/**
|
||||
* Expiry date of the check, auto-calculated to expire after 30 days.
|
||||
*
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
expiry: {
|
||||
@@ -70,7 +70,7 @@ const CheckSchema = mongoose.Schema(
|
||||
|
||||
/**
|
||||
* Pre-save middleware to handle status change notifications.
|
||||
*
|
||||
*
|
||||
* This middleware checks if the status of the monitor associated
|
||||
* with the check has changed and sends notifications if necessary.
|
||||
*/
|
||||
@@ -79,7 +79,9 @@ CheckSchema.pre("save", async function (next) {
|
||||
const monitor = await mongoose.model("Monitor").findById(this.monitorId);
|
||||
|
||||
if (monitor) {
|
||||
const notifications = await Notification.find({ monitorId: this.monitorId });
|
||||
const notifications = await Notification.find({
|
||||
monitorId: this.monitorId,
|
||||
});
|
||||
|
||||
// Check if there are any notifications
|
||||
if (notifications && notifications.length > 0) {
|
||||
@@ -128,5 +130,4 @@ CheckSchema.pre("save", async function (next) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = mongoose.model("Check", CheckSchema);
|
||||
@@ -29,14 +29,14 @@ router.get(
|
||||
alertController.getAlertById
|
||||
);
|
||||
// Edit
|
||||
router.post(
|
||||
"/edit/:alertId",
|
||||
router.put(
|
||||
"/:alertId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
alertController.editAlert
|
||||
);
|
||||
//Delete
|
||||
router.post(
|
||||
"/delete/:alertId",
|
||||
router.delete(
|
||||
"/:alertId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
alertController.deleteAlert
|
||||
);
|
||||
|
||||
@@ -15,10 +15,12 @@ router.get(
|
||||
checkController.getChecks
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/delete/:monitorId",
|
||||
router.delete(
|
||||
"/:monitorId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
checkController.deleteChecks
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
router.get("/user/:userId", checkController.getUserChecks);
|
||||
|
||||
module.exports = router;
|
||||
@@ -14,10 +14,10 @@ router.delete(
|
||||
monitorController.deleteMonitor
|
||||
);
|
||||
router.put(
|
||||
"/edit/:monitorId",
|
||||
"/:monitorId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
monitorController.editMonitor
|
||||
);
|
||||
|
||||
router.delete("/delete/all", monitorController.deleteAllMonitors);
|
||||
router.delete("/all", monitorController.deleteAllMonitors);
|
||||
module.exports = router;
|
||||
@@ -235,7 +235,21 @@ const getChecksParamValidation = joi.object({
|
||||
const getChecksQueryValidation = joi.object({
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
filter: joi.string().valid("day", "week", "month"),
|
||||
dateRange: joi.string().valid("day", "week", "month"),
|
||||
filter: joi.string().valid("all", "down", "resolve"),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
});
|
||||
|
||||
const getUserChecksParamValidation = joi.object({
|
||||
userId: joi.string().required(),
|
||||
});
|
||||
|
||||
const getUserChecksQueryValidation = joi.object({
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
dateRange: joi.string().valid("day", "week", "month"),
|
||||
filter: joi.string().valid("all", "down", "resolve"),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
});
|
||||
@@ -320,6 +334,8 @@ module.exports = {
|
||||
createCheckBodyValidation,
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
getUserChecksParamValidation,
|
||||
getUserChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
deleteUserParamValidation,
|
||||
getPageSpeedCheckParamValidation,
|
||||
|
||||
Reference in new issue
Block a user