Merge branch 'develop' into feat/flow-docs

This commit is contained in:
Alex Holliday
2024-10-10 07:15:21 +08:00
70 changed files with 3056 additions and 843 deletions

View File

@@ -20,13 +20,13 @@ import NewPasswordConfirmed from "./Pages/Auth/NewPasswordConfirmed";
import ProtectedRoute from "./Components/ProtectedRoute";
import Details from "./Pages/Monitors/Details";
import AdvancedSettings from "./Pages/AdvancedSettings";
// import Maintenance from "./Pages/Maintenance";
import Maintenance from "./Pages/Maintenance";
import withAdminCheck from "./HOC/withAdminCheck";
import withAdminProp from "./HOC/withAdminProp";
import Configure from "./Pages/Monitors/Configure";
import PageSpeed from "./Pages/PageSpeed";
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
// import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow";
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenance";
import PageSpeedDetails from "./Pages/PageSpeed/Details";
import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
import { ThemeProvider } from "@emotion/react";
@@ -45,7 +45,7 @@ function App() {
const MonitorDetailsWithAdminProp = withAdminProp(Details);
const PageSpeedWithAdminProp = withAdminProp(PageSpeed);
const PageSpeedDetailsWithAdminProp = withAdminProp(PageSpeedDetails);
// const MaintenanceWithAdminProp = withAdminProp(Maintenance);
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
const SettingsWithAdminProp = withAdminProp(Settings);
const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
const mode = useSelector((state) => state.ui.mode);
@@ -105,14 +105,14 @@ function App() {
path="integrations"
element={<ProtectedRoute Component={Integrations} />}
/>
{/* <Route
<Route
path="maintenance"
element={<ProtectedRoute Component={MaintenanceWithAdminProp} />}
/> */}
{/* <Route
path="/maintenance/create"
/>
<Route
path="/maintenance/create/:maintenanceWindowId?"
element={<CreateNewMaintenanceWindow />}
/> */}
/>
<Route
path="settings"
element={<ProtectedRoute Component={SettingsWithAdminProp} />}

View File

@@ -1,5 +1,12 @@
import PropTypes from "prop-types";
import { Box, ListItem, Autocomplete, TextField } from "@mui/material";
import {
Box,
ListItem,
Autocomplete,
TextField,
Stack,
Typography,
} from "@mui/material";
import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
@@ -15,21 +22,52 @@ import SearchIcon from "../../../assets/icons/search.svg?react";
* @param {Object} props.sx - Additional styles to apply to the component
* @returns {JSX.Element} The rendered Search component
*/
const SearchAdornment = () => {
const theme = useTheme();
return (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
);
};
const Search = ({
id,
options,
filteredBy,
secondaryLabel,
value,
inputValue,
handleInputChange,
handleChange,
sx,
multiple = false,
isAdorned = true,
error,
disabled,
}) => {
const theme = useTheme();
return (
<Autocomplete
multiple={multiple}
id={id}
inputValue={value}
value={value}
inputValue={inputValue}
onInputChange={(_, newValue) => {
handleInputChange(newValue);
}}
@@ -38,45 +76,45 @@ const Search = ({
}}
fullWidth
freeSolo
disabled={disabled}
disableClearable
options={options}
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
<TextField
{...params}
placeholder="Type to search"
InputProps={{
...params.InputProps,
startAdornment: (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
),
}}
sx={{
"& fieldset": {
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
<Stack>
<TextField
{...params}
error={Boolean(error)}
placeholder="Type to search"
InputProps={{
...params.InputProps,
...(isAdorned && { startAdornment: <SearchAdornment /> }),
}}
sx={{
"& fieldset": {
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
}}
/>
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
borderColor: theme.palette.border.light,
},
}}
/>
{error && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.text}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</Stack>
)}
filterOptions={(options, { inputValue }) => {
const filtered = options.filter((option) =>
@@ -103,7 +141,8 @@ const Search = ({
: {}
}
>
{option[filteredBy]}
{option[filteredBy] +
(secondaryLabel ? ` (${option[secondaryLabel]})` : "")}
</ListItem>
);
}}
@@ -139,12 +178,18 @@ const Search = ({
Search.propTypes = {
id: PropTypes.string,
multiple: PropTypes.bool,
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
secondaryLabel: PropTypes.string,
value: PropTypes.array,
inputValue: PropTypes.string.isRequired,
handleInputChange: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isAdorned: PropTypes.bool,
sx: PropTypes.object,
error: PropTypes.string,
disabled: PropTypes.bool,
};
export default Search;

View File

@@ -34,6 +34,7 @@ import "./index.css";
*
* <Select
* id="frequency-id"
* name="my-name"
* label="Check frequency"
* placeholder="Select frequency"
* value={value}
@@ -51,6 +52,7 @@ const Select = ({
items,
onChange,
sx,
name = "",
}) => {
const theme = useTheme();
const itemStyles = {
@@ -77,6 +79,7 @@ const Select = ({
value={value}
onChange={onChange}
displayEmpty
name={name}
inputProps={{ id: id }}
IconComponent={KeyboardArrowDownIcon}
MenuProps={{ disableScrollLock: true }}
@@ -127,6 +130,7 @@ const Select = ({
Select.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
isHidden: PropTypes.bool,

View File

@@ -55,7 +55,7 @@ const menu = [
},
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
// { name: "Status pages", path: "status", icon: <StatusPages /> },
// { name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
{
name: "Account",

View File

@@ -9,6 +9,9 @@ const initialState = {
team: {
rowsPerPage: 5,
},
maintenance: {
rowsPerPage: 5,
},
sidebar: {
collapsed: false,
},

View File

@@ -0,0 +1,3 @@
.create-maintenance button {
height: 35px;
}

View File

@@ -0,0 +1,570 @@
import { Box, Button, duration, Stack, Typography } from "@mui/material";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
import { ConfigBox } from "./styled";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import LoadingButton from "@mui/lab/LoadingButton";
import dayjs from "dayjs";
import Select from "../../../Components/Inputs/Select";
import Field from "../../../Components/Inputs/Field";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import CalendarIcon from "../../../assets/icons/calendar.svg?react";
import "./index.css";
import Search from "../../../Components/Inputs/Search";
import { networkService } from "../../../main";
import { logger } from "../../../Utils/Logger";
import {
MS_PER_SECOND,
MS_PER_MINUTE,
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../Utils/timeUtils";
import { useNavigate, useParams } from "react-router-dom";
const getDurationAndUnit = (durationInMs) => {
if (durationInMs % MS_PER_DAY === 0) {
return {
duration: (durationInMs / MS_PER_DAY).toString(),
durationUnit: "days",
};
} else if (durationInMs % MS_PER_HOUR === 0) {
return {
duration: (durationInMs / MS_PER_HOUR).toString(),
durationUnit: "hours",
};
} else if (durationInMs % MS_PER_MINUTE === 0) {
return {
duration: (durationInMs / MS_PER_MINUTE).toString(),
durationUnit: "minutes",
};
} else {
return {
duration: (durationInMs / MS_PER_SECOND).toString(),
durationUnit: "seconds",
};
}
};
const MS_LOOKUP = {
seconds: MS_PER_SECOND,
minutes: MS_PER_MINUTE,
hours: MS_PER_HOUR,
days: MS_PER_DAY,
weeks: MS_PER_WEEK,
};
const REPEAT_LOOKUP = {
none: 0,
daily: MS_PER_DAY,
weekly: MS_PER_DAY * 7,
};
const REVERSE_REPEAT_LOOKUP = {
0: "none",
[MS_PER_DAY]: "daily",
[MS_PER_WEEK]: "weekly",
};
const repeatConfig = [
{ _id: 0, name: "Don't repeat", value: "none" },
{
_id: 1,
name: "Repeat daily",
value: "daily",
},
{ _id: 2, name: "Repeat weekly", value: "weekly" },
];
const durationConfig = [
{ _id: 0, name: "seconds" },
{ _id: 1, name: "minutes" },
{ _id: 2, name: "hours" },
{
_id: 3,
name: "days",
},
];
const getValueById = (config, id) => {
const item = config.find((config) => config._id === id);
return item ? (item.value ? item.value : item.name) : null;
};
const getIdByValue = (config, name) => {
const item = config.find((config) => {
if (config.value) {
return config.value === name;
} else {
return config.name === name;
}
});
return item ? item._id : null;
};
const CreateMaintenance = () => {
const { maintenanceWindowId } = useParams();
const navigate = useNavigate();
const theme = useTheme();
const { user, authToken } = useSelector((state) => state.auth);
const [monitors, setMonitors] = useState([]);
const [search, setSearch] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [form, setForm] = useState({
repeat: "none",
startDate: dayjs(),
startTime: dayjs(),
duration: "",
durationUnit: "seconds",
name: "",
monitors: [],
});
const [errors, setErrors] = useState({});
useEffect(() => {
const fetchMonitors = async () => {
setIsLoading(true);
try {
const response = await networkService.getMonitorsByTeamId({
authToken: authToken,
teamId: user.teamId,
limit: -1,
types: ["http", "ping", "pagespeed"],
});
const monitors = response.data.data.monitors;
setMonitors(monitors);
if (maintenanceWindowId === undefined) {
return;
}
const res = await networkService.getMaintenanceWindowById({
authToken: authToken,
maintenanceWindowId: maintenanceWindowId,
});
const maintenanceWindow = res.data.data;
const { name, start, end, repeat, monitorId } = maintenanceWindow;
const startTime = dayjs(start);
const endTime = dayjs(end);
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
const monitor = monitors.find((monitor) => monitor._id === monitorId);
setForm({
...form,
name,
repeat: REVERSE_REPEAT_LOOKUP[repeat],
startDate: startTime,
startTime,
duration,
durationUnit,
monitors: monitor ? [monitor] : [],
});
} catch (error) {
createToast({ body: "Failed to fetch data" });
logger.error("Failed to fetch monitors", error);
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, user]);
const buildErrors = (prev, id, error) => {
const updatedErrors = { ...prev };
if (error) {
updatedErrors[id] = error.details[0].message;
} else {
delete updatedErrors[id];
}
return updatedErrors;
};
const handleSearch = (value) => {
setSearch(value);
};
const handleSelectMonitors = (monitors) => {
setForm({ ...form, monitors });
const { error } = maintenanceWindowValidation.validate(
{ monitors },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, "monitors", error);
});
};
const handleFormChange = (key, value) => {
setForm({ ...form, [key]: value });
const { error } = maintenanceWindowValidation.validate(
{ [key]: value },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
const handleTimeChange = (key, newTime) => {
setForm({ ...form, [key]: newTime });
const { error } = maintenanceWindowValidation.validate(
{ [key]: newTime },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
const handleSubmit = async () => {
const { error } = maintenanceWindowValidation.validate(form, {
abortEarly: false,
});
// If errors, return early
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
logger.error(error);
return;
}
// Build timestamp for maintenance window from startDate and startTime
const start = dayjs(form.startDate)
.set("hour", form.startTime.hour())
.set("minute", form.startTime.minute());
// Build end timestamp for maintenance window
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
const durationInMs = form.duration * MS_MULTIPLIER;
const end = start.add(durationInMs);
// Get repeat value in milliseconds
const repeat = REPEAT_LOOKUP[form.repeat];
const submit = {
monitors: form.monitors.map((monitor) => monitor._id),
name: form.name,
start: start.toISOString(),
end: end.toISOString(),
repeat,
};
if (repeat === 0) {
submit.expiry = end;
}
const requestConfig = { authToken: authToken, maintenanceWindow: submit };
if (maintenanceWindowId !== undefined) {
requestConfig.maintenanceWindowId = maintenanceWindowId;
}
const request =
maintenanceWindowId === undefined
? networkService.createMaintenanceWindow(requestConfig)
: networkService.editMaintenanceWindow(requestConfig);
try {
setIsLoading(true);
await request;
createToast({
body: "Successfully created maintenance window",
});
navigate("/maintenance");
} catch (error) {
createToast({
body: `Failed to ${
maintenanceWindowId === undefined ? "create" : "edit"
} maintenance window`,
});
logger.error(error);
} finally {
setIsLoading(false);
}
};
return (
<Box className="create-maintenance">
<Breadcrumbs
list={[
{ name: "maintenance", path: "/maintenance" },
{
name: maintenanceWindowId === undefined ? "create" : "edit",
path: `/maintenance/create`,
},
]}
/>
<Stack
component="form"
noValidate
spellCheck="false"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Box>
<Typography component="h1" variant="h1">
<Typography component="span" fontSize="inherit">
{`${maintenanceWindowId === undefined ? "Create a" : "Edit"}`}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
maintenance{" "}
</Typography>
<Typography component="span" fontSize="inherit">
window
</Typography>
</Typography>
<Typography component="h2" variant="body2" fontSize={14}>
Your pings won&apos;t be sent during this time frame
</Typography>
</Box>
<ConfigBox direction="row">
<Typography component="h2" variant="h2">
General Settings
</Typography>
<Box
px={theme.spacing(14)}
borderLeft={1}
borderLeftColor={theme.palette.border.light}
>
<Select
id="repeat"
name="maintenance-repeat"
label="Maintenance Repeat"
value={getIdByValue(repeatConfig, form.repeat)}
onChange={(event) => {
handleFormChange(
"repeat",
getValueById(repeatConfig, event.target.value)
);
}}
items={repeatConfig}
/>
<Stack gap={theme.spacing(2)} mt={theme.spacing(16)}>
<Typography component="h3">Date</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
id="startDate"
disablePast
disableHighlightToday
value={form.startDate}
slots={{ openPickerIcon: CalendarIcon }}
slotProps={{
switchViewButton: { sx: { display: "none" } },
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
height: 34,
p: 0,
pr: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.border.dark,
},
},
},
inputAdornment: { sx: { ml: 0, px: 3 } },
openPickerButton: {
sx: {
py: 0,
mr: 0,
"& path": {
stroke: theme.palette.other.icon,
strokeWidth: 1.1,
},
"&:hover": { backgroundColor: "transparent" },
},
},
}}
sx={{}}
onChange={(newDate) => {
handleTimeChange("startDate", newDate);
}}
error={errors["startDate"]}
/>
</LocalizationProvider>
</Stack>
</Box>
</ConfigBox>
<ConfigBox>
<Stack direction="row">
<Box>
<Typography component="h2" variant="h2">
Start time
</Typography>
<Typography>
All dates and times are in GMT+0 time zone.
</Typography>
</Box>
<Stack direction="row">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MobileTimePicker
id="startTime"
value={form.startTime}
onChange={(newTime) => {
handleTimeChange("startTime", newTime);
}}
slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
height: 34,
p: 0,
pl: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.border.dark,
},
},
},
}}
error={errors["startTime"]}
/>
</LocalizationProvider>
</Stack>
</Stack>
<Stack direction="row">
<Box>
<Typography component="h2" variant="h2">
Duration
</Typography>
</Box>
<Stack direction="row" spacing={theme.spacing(8)}>
<Field
type="number"
id="duration"
value={form.duration}
onChange={(event) => {
handleFormChange("duration", event.target.value);
}}
error={errors["duration"]}
/>
<Select
id="durationUnit"
value={getIdByValue(durationConfig, form.durationUnit)}
items={durationConfig}
onChange={(event) => {
handleFormChange(
"durationUnit",
getValueById(durationConfig, event.target.value)
);
}}
error={errors["durationUnit"]}
/>
</Stack>
</Stack>
</ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
fontSize={16}
my={theme.spacing(6)}
>
Monitor related settings
</Typography>
<ConfigBox>
<Stack direction="row">
<Box>
<Typography component="h2" variant="h2">
Friendly name
</Typography>
</Box>
<Box>
<Field
id="name"
placeholder="Maintenance at __ : __ for ___ minutes"
value={form.name}
onChange={(event) => {
handleFormChange("name", event.target.value);
}}
error={errors["name"]}
/>
</Box>
</Stack>
<Stack direction="row">
<Box>
<Typography component="h2" variant="h2">
Add monitors
</Typography>
</Box>
<Box>
<Search
id={"monitors"}
multiple={true}
isAdorned={false}
options={monitors ? monitors : []}
filteredBy="name"
secondaryLabel={"type"}
inputValue={search}
value={form.monitors}
handleInputChange={handleSearch}
handleChange={handleSelectMonitors}
error={errors["monitors"]}
disabled={maintenanceWindowId !== undefined}
/>
</Box>
</Stack>
</ConfigBox>
</Box>
<Box ml="auto" display="inline-block">
<Button
variant="contained"
color="secondary"
onClick={() => navigate("/maintenance")}
sx={{ mr: theme.spacing(6) }}
>
Cancel
</Button>
<LoadingButton
loading={isLoading}
variant="contained"
color="primary"
onClick={handleSubmit}
disabled={false}
>
{`${
maintenanceWindowId === undefined
? "Create maintenance"
: "Edit maintenance"
}`}
</LoadingButton>
</Box>
</Stack>
</Box>
);
};
export default CreateMaintenance;

View File

@@ -0,0 +1,25 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
backgroundColor: theme.palette.background.main,
"& > *": { padding: theme.spacing(14) },
"& > :first-of-type, & > .MuiStack-root > div:first-of-type": {
flex: 0.6,
},
"& > div:last-of-type, & > .MuiStack-root > div:last-of-type": {
flex: 1,
},
"& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) },
"& > .MuiStack-root > div:last-of-type": {
paddingLeft: theme.spacing(14),
},
"& h2": { fontSize: 13.5, fontWeight: 500 },
"& h3, & p": {
color: theme.palette.text.tertiary,
},
"& h3": { fontWeight: 500 },
}));

View File

@@ -1,71 +0,0 @@
.maintenance-options .MuiStack-root .MuiStack-root.select-wrapper,
.maintenance-options .MuiStack-root .field {
width: 60%;
max-width: 380px;
}
.date-picker .MuiOutlinedInput-root,
.time-picker .MuiOutlinedInput-root {
height: 34px;
font-size: var(--env-var-font-size-medium);
}
.date-picker .MuiOutlinedInput-root {
width: 140px;
}
.time-picker .MuiOutlinedInput-root {
width: 90px;
}
.maintenance-options .MuiInputAdornment-outlined .MuiIconButton-root svg {
width: 20px;
}
.duration-config .field-text .MuiTextField-root,
.maintenance-options .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;
}

View File

@@ -1,298 +0,0 @@
import { Box, Button, Stack, Typography } from "@mui/material";
import "./index.css";
import { useState } from "react";
import Back from "../../../assets/icons/left-arrow-long.svg?react";
import Select from "../../../Components/Inputs/Select";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
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";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { logger } from "../../../Utils/Logger";
import { createToast } from "../../../Utils/toastUtils";
import { useTheme } from "@emotion/react";
const directory = {
title: "Create a maintenance window",
description: "Your pings wont be sent in this time frame.",
};
const repeatOptions = [
{ _id: 1, name: "Don't repeat" },
{ _id: 2, name: "Repeat daily" },
{ _id: 3, name: "Repeat weekly" },
];
const configOptionTitle = (title, description) => {
return (
<Stack width="40%" gap={1}>
<Typography
style={{
fontWeight: 600,
fontSize: "var(--env-var-font-size-medium)",
}}
>
{title}
</Typography>
{description && (
<Typography
style={{
fontSize: "var(--env-var-font-size-small)",
}}
>
{description}
</Typography>
)}
</Stack>
);
};
const durationOptions = [
{ _id: "minutes", name: "minutes" },
{ _id: "hours", name: "hours" },
{ _id: "days", name: "days" },
];
const CreateNewMaintenanceWindow = () => {
const theme = useTheme();
const [values, setValues] = useState({
repeat: 1,
date: dayjs(),
startTime: dayjs(),
duration: "60",
unit: "minutes",
displayName: "",
AddMonitors: "",
});
const [errors, setErrors] = useState({});
const handleChange = (event, name) => {
const { value } = event.target;
setValues((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = async () => {
const data = {
repeat: values.repeat,
date: values.date.format("YYYY-MM-DD"),
startTime: values.startTime.format("HH:mm"),
duration: values.duration,
unit: values.unit,
displayName: values.displayName,
addMonitors: values.AddMonitors,
};
const { error } = maintenanceWindowValidation.validate(data, {
abortEarly: false,
});
logger.log("error: ", error);
if (!error || error.details.length === 0) {
setErrors({});
} else {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data",
});
logger.error("Validation errors:", error.details);
}
logger.log("Submitting data: ", data);
};
const configOptions = [
{
title: "Repeat",
component: (
<Select
onChange={(e) => handleChange(e, "repeat")}
id="repeat-mode"
items={repeatOptions}
value={values.repeat}
/>
),
},
{
title: "Date",
component: (
<LocalizationProvider
className="date-localization-provider"
dateAdapter={AdapterDayjs}
>
<DatePicker
className="date-picker"
defaultValue={values.date}
onChange={(date) =>
handleChange({ target: { value: date } }, "date")
}
/>
</LocalizationProvider>
),
},
{
title: "Start time",
component: (
<LocalizationProvider
className="time-localization-provider"
dateAdapter={AdapterDayjs}
>
<MobileTimePicker
className="time-picker"
defaultValue={values.startTime}
onChange={(time) =>
handleChange({ target: { value: time } }, "startTime")
}
/>
</LocalizationProvider>
),
},
{
title: "Duration",
component: (
<Stack
className="duration-config"
gap={theme.spacing(5)}
direction="row"
>
<Field
id="duration-value"
placeholder="60"
onChange={(e) => handleChange(e, "duration")}
value={values.duration}
error={errors.duration}
type="number"
/>
<Select
onChange={(e) => handleChange(e, "unit")}
id="duration-unit"
items={durationOptions}
value={values.unit}
/>
</Stack>
),
},
{
title: "Display name",
component: (
<Field
id="display-name"
placeholder="Maintanence at __ : __ for ___ minutes"
value={values.displayName}
onChange={(e) => handleChange(e, "displayName")}
error={errors.displayName}
/>
),
},
{
title: "Add monitors",
component: (
<Stack
className="add-monitors-fields"
sx={{ width: "60%", maxWidth: "380px" }}
gap={theme.spacing(5)}
>
<Field
id="add-monitors"
placeholder="Start typing to search for current monitors"
value={values.AddMonitors}
onChange={(e) => handleChange(e, "AddMonitors")}
error={errors.addMonitors}
/>
<Typography
sx={{
width: "fit-content",
fontSize: "var(--env-var-font-size-small)",
borderBottom: `1px dashed ${theme.palette.primary.main}`,
paddingBottom: "4px",
}}
>
Add all monitors to this maintenance window
</Typography>
</Stack>
),
},
];
return (
<div className="create-maintenance-window">
<Stack gap={theme.spacing(10)}>
<Button
variant="contained"
color="secondary"
sx={{
width: "100px",
height: "30px",
gap: "10px",
}}
>
<Back />
Back
</Button>
<Box>
<Typography
sx={{
fontSize: "var(--env-var-font-size-large)",
fontWeight: 600,
color: theme.palette.text.secondary,
}}
>
{directory.title}
</Typography>
<Typography sx={{ fontSize: "var(--env-var-font-size-medium)" }}>
{directory.description}
</Typography>
</Box>
<Stack
className="maintenance-options"
gap={theme.spacing(20)}
paddingY={theme.spacing(15)}
paddingX={theme.spacing(20)}
sx={{
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.main,
}}
>
{configOptions.map((item, index) => (
<Stack key={index} display="-webkit-inline-box">
{item.title && configOptionTitle(item.title, item.description)}
{item.component && item.component}
</Stack>
))}
</Stack>
<Stack justifyContent="end" direction="row" marginTop={3}>
<Button
variant="text"
color="info"
sx={{
"&:hover": {
backgroundColor: "transparent",
},
}}
>
Cancel
</Button>
<Button variant="contained" color="primary" onClick={handleSubmit}>
Create
</Button>
</Stack>
</Stack>
</div>
);
};
export default CreateNewMaintenanceWindow;

View File

@@ -0,0 +1,229 @@
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import {
Button,
IconButton,
Menu,
MenuItem,
Modal,
Stack,
Typography,
} from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { logger } from "../../../../Utils/Logger";
import Settings from "../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
const ActionsMenu = ({ isAdmin, maintenanceWindow, updateCallback }) => {
maintenanceWindow;
const { authToken } = useSelector((state) => state.auth);
const [anchorEl, setAnchorEl] = useState(null);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const theme = useTheme();
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
try {
setIsLoading(true);
await networkService.deleteMaintenanceWindow({
authToken,
maintenanceWindowId: maintenanceWindow._id,
});
updateCallback();
createToast({ body: "Maintenance window deleted successfully." });
} catch (error) {
createToast({ body: "Failed to delete maintenance window." });
logger.error("Failed to delete maintenance window", error);
} finally {
setIsLoading(false);
}
setIsOpen(false);
};
const handlePause = async () => {
try {
setIsLoading(true);
const data = {
active: !maintenanceWindow.active,
};
await networkService.editMaintenanceWindow({
authToken,
maintenanceWindowId: maintenanceWindow._id,
maintenanceWindow: data,
});
updateCallback();
} catch (error) {
logger.error(error);
createToast({ body: "Failed to pause maintenance window." });
} finally {
setIsLoading(false);
}
};
const handleEdit = () => {
navigate(`/maintenance/create/${maintenanceWindow._id}`);
};
const openMenu = (event) => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const openRemove = (e) => {
closeMenu(e);
setIsOpen(true);
};
const closeMenu = (e) => {
e.stopPropagation();
setAnchorEl(null);
};
const navigate = useNavigate();
return (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(event);
}}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
>
<Settings />
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => closeMenu(e)}
disableScrollLock
slotProps={{
paper: {
sx: {
"& ul": { p: theme.spacing(2.5) },
"& li": { m: 0 },
"& li:last-of-type": {
color: theme.palette.error.text,
},
},
},
}}
>
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
handleEdit();
}}
>
Edit
</MenuItem>
<MenuItem
onClick={(e) => {
handlePause();
closeMenu(e);
e.stopPropagation();
}}
>
{`${maintenanceWindow.active === true ? "Pause" : "Resume"}`}
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
>
Remove
</MenuItem>
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
aria-describedby="delete-monitor-confirmation"
open={isOpen}
onClose={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
>
<Stack
gap={theme.spacing(2)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography id="modal-delete-maintenance" component="h2" variant="h2">
Do you really want to remove this maintenance window?
</Typography>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Button
variant="text"
color="info"
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
>
Cancel
</Button>
<LoadingButton
loading={isLoading}
variant="contained"
color="error"
onClick={(e) => {
e.stopPropagation(e);
handleRemove(e);
}}
>
Delete
</LoadingButton>
</Stack>
</Stack>
</Modal>
</>
);
};
ActionsMenu.propTypes = {
maintenanceWindow: PropTypes.object,
isAdmin: PropTypes.bool,
updateCallback: PropTypes.func,
};
export default ActionsMenu;

View File

@@ -0,0 +1,363 @@
import PropTypes from "prop-types";
import {
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Paper,
Box,
TablePagination,
Stack,
Typography,
Button,
} from "@mui/material";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import ActionsMenu from "./ActionsMenu";
import { useState, useEffect, memo, useCallback, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react";
import { formatDurationRounded } from "../../../Utils/timeUtils";
import { StatusLabel } from "../../../Components/Label";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import dayjs from "dayjs";
/**
* Component for pagination actions (first, previous, next, last).
*
* @component
* @param {Object} props
* @param {number} props.count - Total number of items.
* @param {number} props.page - Current page number.
* @param {number} props.rowsPerPage - Number of rows per page.
* @param {function} props.onPageChange - Callback function to handle page change.
*
* @returns {JSX.Element} Pagination actions component.
*/
const TablePaginationActions = (props) => {
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box sx={{ flexShrink: 0, ml: "24px" }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrowDouble />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrowDouble />
</Button>
</Box>
);
};
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
const MaintenanceTable = ({
page,
setPage,
sort,
setSort,
maintenanceWindows,
maintenanceWindowCount,
updateCallback,
}) => {
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
const theme = useTheme();
const dispatch = useDispatch();
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const getTimeToNextWindow = (startTime, endTime, repeat) => {
//1. Advance time closest to next window as possible
const now = dayjs();
let start = dayjs(startTime);
let end = dayjs(endTime);
if (repeat > 0) {
// Advance time closest to next window as possible
while (start.isBefore(now) && end.isBefore(now)) {
start = start.add(repeat, "milliseconds");
end = end.add(repeat, "milliseconds");
}
}
//Check if we are in a window
if (now.isAfter(start) && now.isBefore(end)) {
return "In maintenance window";
}
if (start.isAfter(now)) {
const diffInMinutes = start.diff(now, "minutes");
const diffInHours = start.diff(now, "hours");
const diffInDays = start.diff(now, "days");
if (diffInMinutes < 60) {
return diffInMinutes + " minutes";
} else if (diffInHours < 24) {
return diffInHours + " hours";
} else if (diffInDays < 7) {
return diffInDays + " days";
} else {
return diffInDays + " days";
}
}
};
const handleChangeRowsPerPage = (event) => {
dispatch(
setRowsPerPage({
value: parseInt(event.target.value, 10),
table: "maintenance",
})
);
setPage(0);
};
/**
* Helper function to calculate the range of displayed rows.
* @returns {string}
*/
const getRange = () => {
let start = page * rowsPerPage + 1;
let end = Math.min(
page * rowsPerPage + rowsPerPage,
maintenanceWindowCount
);
return `${start} - ${end}`;
};
const handleSort = async (field) => {
let order = "";
if (sort.field !== field) {
order = "desc";
} else {
order = sort.order === "asc" ? "desc" : "asc";
}
setSort({ field, order });
};
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell
sx={{ cursor: "pointer" }}
onClick={() => handleSort("name")}
>
<Box>
Maintenance Window Name
<span
style={{
visibility: sort.field === "name" ? "visible" : "hidden",
}}
>
{sort.order === "asc" ? (
<ArrowUpwardRoundedIcon />
) : (
<ArrowDownwardRoundedIcon />
)}
</span>
</Box>
</TableCell>
<TableCell
sx={{ cursor: "pointer" }}
onClick={() => handleSort("status")}
>
{" "}
<Box width="max-content">
{" "}
Status
<span
style={{
visibility:
sort.field === "active" ? "visible" : "hidden",
}}
>
{sort.order === "asc" ? (
<ArrowUpwardRoundedIcon />
) : (
<ArrowDownwardRoundedIcon />
)}
</span>
</Box>
</TableCell>
<TableCell>Next Window</TableCell>
<TableCell>Repeat</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{maintenanceWindows.map((maintenanceWindow) => {
const text = maintenanceWindow.active ? "active" : "paused";
const status = maintenanceWindow.active ? "up" : "paused";
return (
<TableRow key={maintenanceWindow._id}>
<TableCell>{maintenanceWindow.name}</TableCell>
<TableCell>
<StatusLabel
status={status}
text={text}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
{getTimeToNextWindow(
maintenanceWindow.start,
maintenanceWindow.end,
maintenanceWindow.repeat
)}
</TableCell>
<TableCell>
{maintenanceWindow.repeat === 0
? "N/A"
: formatDurationRounded(maintenanceWindow.repeat)}
</TableCell>
<TableCell>
<ActionsMenu
maintenanceWindow={maintenanceWindow}
updateCallback={updateCallback}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
>
<Typography px={theme.spacing(2)} variant="body2" sx={{ opacity: 0.7 }}>
Showing {getRange()} of {maintenanceWindowCount} maintenance window(s)
</Typography>
<TablePagination
component="div"
count={maintenanceWindowCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 15, 25]}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
}
slotProps={{
select: {
MenuProps: {
keepMounted: true,
disableScrollLock: true,
PaperProps: {
className: "pagination-dropdown",
sx: {
mt: 0,
mb: theme.spacing(2),
},
},
transformOrigin: { vertical: "bottom", horizontal: "left" },
anchorOrigin: { vertical: "top", horizontal: "left" },
sx: { mt: theme.spacing(-2) },
},
inputProps: { id: "pagination-dropdown" },
IconComponent: SelectorVertical,
sx: {
ml: theme.spacing(4),
mr: theme.spacing(12),
minWidth: theme.spacing(20),
textAlign: "left",
"&.Mui-focused > div": {
backgroundColor: theme.palette.background.main,
},
},
},
}}
sx={{
mt: theme.spacing(6),
color: theme.palette.text.secondary,
"& svg path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.3,
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
}}
/>
</Stack>
</>
);
};
MaintenanceTable.propTypes = {
isAdmin: PropTypes.bool,
page: PropTypes.number,
setPage: PropTypes.func,
rowsPerPage: PropTypes.number,
setRowsPerPage: PropTypes.func,
sort: PropTypes.object,
setSort: PropTypes.func,
maintenanceWindows: PropTypes.array,
maintenanceWindowCount: PropTypes.number,
updateCallback: PropTypes.func,
};
const MemoizedMaintenanceTable = memo(MaintenanceTable);
export default MemoizedMaintenanceTable;

View File

@@ -1,36 +0,0 @@
.maintenance-checklist-main {
max-width: 350px;
min-height: 390px;
margin: auto;
padding: 5px;
}
.maintenance-image {
width: 280px;
height: 150px;
background-color: whitesmoke;
border-radius: 10px;
}
.maintenance-title {
color: var(--secondary-color-light);
font-size: 16px;
font-weight: bold;
margin-top: 40px;
margin-bottom: 30px;
}
.checklist-item {
font-size: 13px;
color: var(--secondary-color-light);
display: flex;
margin-bottom: 10px;
}
.checklist-item-text {
margin-left: 10px;
}
.maintenance-checklist-main .maintenance-checklist-button {
margin-top: 30px;
}

View File

@@ -1,10 +1,48 @@
import { Box } from "@mui/material";
import { Box, Stack, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import Fallback from "../../Components/Fallback";
import { useState, useEffect } from "react";
import "./index.css";
import MaintenanceTable from "./MaintenanceTable";
import { useSelector } from "react-redux";
import { networkService } from "../../main";
import Breadcrumbs from "../../Components/Breadcrumbs";
import { useNavigate } from "react-router-dom";
const Maintenance = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const { authToken } = useSelector((state) => state.auth);
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
const [maintenanceWindows, setMaintenanceWindows] = useState([]);
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
const [page, setPage] = useState(0);
const [sort, setSort] = useState({});
const [updateTrigger, setUpdateTrigger] = useState(false);
const handleActionMenuDelete = () => {
setUpdateTrigger((prev) => !prev);
};
useEffect(() => {
const fetchMaintenanceWindows = async () => {
try {
const response = await networkService.getMaintenanceWindowsByTeamId({
authToken: authToken,
page: page,
rowsPerPage: rowsPerPage,
});
const { maintenanceWindows, maintenanceWindowCount } =
response.data.data;
setMaintenanceWindows(maintenanceWindows);
setMaintenanceWindowCount(maintenanceWindowCount);
} catch (error) {
console.log(error);
}
};
fetchMaintenanceWindows();
}, [authToken, page, rowsPerPage, updateTrigger]);
return (
<Box
@@ -21,16 +59,52 @@ const Maintenance = ({ isAdmin }) => {
},
}}
>
<Fallback
title="maintenance window"
checks={[
"Mark your maintenance periods",
"Eliminate any misunderstandings",
"Stop sending alerts in maintenance windows",
]}
link="/maintenance/create"
isAdmin={isAdmin}
/>
{maintenanceWindows.length > 0 && (
<Stack gap={theme.spacing(8)}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Breadcrumbs
list={[{ name: "maintenance", path: "/maintenance" }]}
/>
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/maintenance/create");
}}
sx={{ fontWeight: 500 }}
>
Create maintenance window
</Button>
</Stack>
<MaintenanceTable
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
sort={sort}
setSort={setSort}
maintenanceWindows={maintenanceWindows}
maintenanceWindowCount={maintenanceWindowCount}
updateCallback={handleActionMenuDelete}
/>
</Stack>
)}
{isAdmin && maintenanceWindows.length === 0 && (
<Fallback
title="maintenance window"
checks={[
"Mark your maintenance periods",
"Eliminate any misunderstandings",
"Stop sending alerts in maintenance windows",
]}
link="/maintenance/create"
isAdmin={isAdmin}
/>
)}
</Box>
);
};

View File

@@ -1,34 +0,0 @@
.create-monitor p.MuiTypography-root,
.create-monitor button.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
}
.create-monitor h6.MuiTypography-root,
.create-monitor .MuiBox-root .field + p.MuiTypography-root {
font-size: var(--env-var-font-size-small-plus);
opacity: 0.8;
}
.create-monitor button.MuiButtonBase-root {
height: 34px;
}
.create-monitor .error-container {
position: relative;
}
.create-monitor .error-container p.MuiTypography-root.input-error {
opacity: 0.8;
position: absolute;
top: 0;
}
.create-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
}
.create-monitor span.MuiTypography-root.input-error {
position: absolute;
top: 100%;
}
.create-monitor .MuiStack-root .MuiButtonGroup-root button {
font-size: var(--env-var-font-size-small);
height: 28px;
padding: 8px;
}

View File

@@ -112,7 +112,7 @@ const CreateMonitor = () => {
{ [name]: value },
{ abortEarly: false }
);
console.log(error);
setErrors((prev) => {
const updatedErrors = { ...prev };
if (error) updatedErrors[name] = error.details[0].message;

View File

@@ -136,7 +136,7 @@ const Monitors = ({ isAdmin }) => {
<Search
options={monitorState?.monitorsSummary?.monitors ?? []}
filteredBy="name"
value={search}
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>

View File

@@ -1,5 +1,7 @@
import store from "../store";
const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL || "debug";
const NO_OP = () => {};
class Logger {
constructor() {
let logLevel = LOG_LEVEL;
@@ -11,29 +13,37 @@ class Logger {
}
updateLogLevel(logLevel) {
const NO_OP = () => {};
if (logLevel === "none") {
this.info = NO_OP;
this.error = NO_OP;
this.warn = NO_OP;
this.log = NO_OP;
return;
}
this.error = console.error.bind(console);
if (logLevel === "error") {
this.error = console.error.bind(console);
this.info = NO_OP;
this.warn = NO_OP;
this.log = NO_OP;
return;
}
this.warn = console.warn.bind(console);
if (logLevel === "warn") {
this.error = console.error.bind(console);
this.warn = console.warn.bind(console);
this.info = NO_OP;
this.log = NO_OP;
return;
}
if (logLevel === "info") {
this.error = console.error.bind(console);
this.warn = console.warn.bind(console);
this.info = console.info.bind(console);
this.log = NO_OP;
return;
}
this.log = console.log.bind(console);
}
cleanup() {

View File

@@ -1,25 +1,35 @@
import axios from "axios";
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
import { clearAuthState } from "../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
import { logger } from "./Logger";
class NetworkService {
constructor(store) {
constructor(store, dispatch, navigate) {
this.store = store;
this.dispatch = dispatch;
this.navigate = navigate;
let baseURL = BASE_URL;
this.axiosInstance = axios.create();
this.setBaseUrl(baseURL);
this.unsubscribe = store.subscribe(() => {
const state = store.getState();
if (BASE_URL === undefined && state.settings.apiBaseUrl) {
if (BASE_URL !== undefined) {
baseURL = BASE_URL;
} else if (state?.settings?.apiBaseUrl ?? null) {
baseURL = state.settings.apiBaseUrl;
} else {
baseURL = FALLBACK_BASE_URL;
}
this.setBaseUrl(baseURL);
});
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
logger.error(error);
if (error.response && error.response.status === 401) {
logger.error("Invalid token received");
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
navigate("/login");
}
return Promise.reject(error);
}
@@ -120,7 +130,7 @@ class NetworkService {
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} config.teamId - The ID of the team whose monitors are to be retrieved.
* @param {number} [config.limit] - The maximum number of monitors to retrieve.
* @param {number} [config.limit] - The maximum number of checks to retrieve. 0 for all, -1 for none
* @param {Array<string>} [config.types] - The types of monitors to retrieve.
* @param {string} [config.status] - The status of the monitors to retrieve.
* @param {string} [config.checkOrder] - The order in which to sort the retrieved monitors.
@@ -680,6 +690,155 @@ class NetworkService {
},
});
}
/**
* ************************************
* Creates a maintenance window
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {Object} config.maintenanceWindow - The maintenance window object to be sent in the request body.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async createMaintenanceWindow(config) {
return this.axiosInstance.post(
`/maintenance-window`,
config.maintenanceWindow,
{
headers: {
Authorization: `Bearer ${config.authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
* Edits a maintenance window
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {Object} config.maintenanceWindowId - The maintenance window id.
* @param {Object} config.maintenanceWindow - The maintenance window object to be sent in the request body.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async editMaintenanceWindow(config) {
return this.axiosInstance.put(
`/maintenance-window/${config.maintenanceWindowId}`,
config.maintenanceWindow,
{
headers: {
Authorization: `Bearer ${config.authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
* Get maintenance window by id
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} [config.maintenanceWindowId] - The id of the maintenance window to delete.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async getMaintenanceWindowById(config) {
const { authToken, maintenanceWindowId } = config;
return this.axiosInstance.get(
`/maintenance-window/${maintenanceWindowId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
* Get maintenance windows by teamId
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} [config.active] - The status of the maintenance windows to retrieve.
* @param {number} [config.page] - The page number for pagination.
* @param {number} [config.rowsPerPage] - The number of rows per page for pagination.
* @param {string} [config.field] - The field to sort by.
* @param {string} [config.order] - The order in which to sort the field.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async getMaintenanceWindowsByTeamId(config) {
const { authToken, active, page, rowsPerPage, field, order } = config;
const params = new URLSearchParams();
if (active) params.append("status", active);
if (page) params.append("page", page);
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
if (field) params.append("field", field);
if (order) params.append("order", order);
return this.axiosInstance.get(
`/maintenance-window/team?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
/**
* ************************************
* Delete maintenance window by id
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} [config.maintenanceWindowId] - The id of the maintenance window to delete.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*
*/
async deleteMaintenanceWindow(config) {
const { authToken, maintenanceWindowId } = config;
return this.axiosInstance.delete(
`/maintenance-window/${maintenanceWindowId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
}
export default NetworkService;
let networkService;
export const setNetworkService = (service) => {
networkService = service;
};
export { networkService };

View File

@@ -0,0 +1,15 @@
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { setNetworkService } from "./NetworkService";
import NetworkService from "./NetworkService";
import { store } from "../store";
const NetworkServiceProvider = ({ children }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const networkService = new NetworkService(store, dispatch, navigate);
setNetworkService(networkService);
return children;
};
export default NetworkServiceProvider;

View File

@@ -4,6 +4,12 @@ import timezone from "dayjs/plugin/timezone";
dayjs.extend(utc);
dayjs.extend(timezone);
export const MS_PER_SECOND = 1000;
export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
export const MS_PER_DAY = 24 * MS_PER_HOUR;
export const MS_PER_WEEK = MS_PER_DAY * 7;
export const formatDuration = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);

View File

@@ -1,4 +1,5 @@
import joi from "joi";
import dayjs from "dayjs";
const nameSchema = joi
.string()
@@ -113,50 +114,33 @@ const imageValidation = joi.object({
}),
});
const maintenanceWindowValidation = joi.object({
repeat: joi.number().valid(1, 2, 3).required().messages({
"number.base": "Repeat must be a number.",
"any.only": "Repeat must be one of [1, 2, 3].",
"any.required": "Repeat is required.",
}),
date: joi.date().required().messages({
"date.base": "Date must be a valid date.",
"any.required": "Date is required.",
}),
startTime: joi.string().required().messages({
"string.base": "Start time must be a valid time.",
"any.required": "Start time is required.",
}),
duration: joi.number().required().messages({
"number.empty": "duration is required.",
"number.base": "Duration must be a number.",
"any.required": "Duration is required.",
}),
unit: joi.string().valid("minutes", "hours", "days").required().messages({
"string.base": "Unit must be a string.",
"any.only": "Unit must be one of ['minutes', 'hours', 'days'].",
"any.required": "Unit is required.",
}),
displayName: joi.string().max(50).required().messages({
"string.empty": "Display name is required.",
"string.max": "Display name must be less than 50 characters long",
}),
addMonitors: joi.string().max(50).required().messages({
"string.empty": "Add monitors is required.",
"string.max": "Add monitors must be less than 50 characters long",
}),
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
"string.empty": "TTL is required",
}),
});
const dayjsValidator = (value, helpers) => {
if (!dayjs(value).isValid()) {
return helpers.error("any.invalid");
}
return value;
};
const maintenanceWindowValidation = joi.object({
repeat: joi.string(),
startDate: joi.custom(dayjsValidator, "Day.js date validation"),
startTime: joi.custom(dayjsValidator, "Day.js date validation"),
duration: joi.number(),
durationUnit: joi.string(),
name: joi.string(),
monitors: joi.array().min(1),
});
export {
credentials,
imageValidation,
monitorValidation,
maintenanceWindowValidation,
settingsValidation,
maintenanceWindowValidation,
};

View File

@@ -0,0 +1,3 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5 8.33342H1.5M12.3333 1.66675V5.00008M5.66667 1.66675V5.00008M5.5 18.3334H12.5C13.9001 18.3334 14.6002 18.3334 15.135 18.0609C15.6054 17.8212 15.9878 17.4388 16.2275 16.9684C16.5 16.4336 16.5 15.7335 16.5 14.3334V7.33342C16.5 5.93328 16.5 5.23322 16.2275 4.69844C15.9878 4.22803 15.6054 3.84558 15.135 3.6059C14.6002 3.33341 13.9001 3.33341 12.5 3.33341H5.5C4.09987 3.33341 3.3998 3.33341 2.86502 3.6059C2.39462 3.84558 2.01217 4.22803 1.77248 4.69844C1.5 5.23322 1.5 5.93328 1.5 7.33341V14.3334C1.5 15.7335 1.5 16.4336 1.77248 16.9684C2.01217 17.4388 2.39462 17.8212 2.86502 18.0609C3.3998 18.3334 4.09987 18.3334 5.5 18.3334Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 830 B

View File

@@ -1,18 +1,21 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { BrowserRouter as Router, HashRouter } from "react-router-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import { persistor, store } from "./store";
import { PersistGate } from "redux-persist/integration/react";
import NetworkService from "./Utils/NetworkService.js";
export const networkService = new NetworkService(store);
import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx";
import { networkService } from "./Utils/NetworkService";
export { networkService };
ReactDOM.createRoot(document.getElementById("root")).render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Router>
<App />
<NetworkServiceProvider>
<App />
</NetworkServiceProvider>
</Router>
</PersistGate>
</Provider>

222
README.md
View File

@@ -19,7 +19,15 @@ BlueWave Uptime is an open source server monitoring application used to track th
See [BlueWave Uptime](https://uptime-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1!
## Questions & Ideas
## User's guide
Usage instructions can be found [here](https://bluewavelabs.gitbook.io/uptime-manager).
## Installation
See installation instructions in [Uptime Manager documentation portal](https://bluewavelabs.gitbook.io/uptime-manager/quickstart).
## Questions & ideas
We've just launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you!
@@ -78,215 +86,3 @@ Also check other developer and contributor-friendly projects of BlueWave:
- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom)
- [VerifyWise](https://github.com/bluewave-labs/verifywise)
## Getting Started
- Clone this repository to your local machine
1. [Quickstart for Users](#user-quickstart)
2. [Quickstart for Developers](#dev-quickstart)
3. [Manual Install](#manual-install)
- [Install Client](#install-client)
- [Environment](#env-vars-client)
- [Start Client](#start-client)
- [Install Server](#install-server)
- [Environment](#env-vars-server)
- [Database](#databases)
- [(Optional) Dockerised Databases](#optional-docker-databases)
- [Start Server](#start-server)
4. [API Documentation](#api-documentation)
5. [Error Handling](#error-handling)
6. [Contributors](#contributors)
---
## <u> Quickstart for Users</u><a id="user-quickstart"></a>
1. Download our [Docker Compose File](/Docker/dist/docker-compose.yaml)
2. Run `docker compose up` to start the application
3. Application is running at `http://localhost`
## <u>Quickstart for Developers</u> <a id="dev-quickstart"></a>
<span style="color: red; font-weight: bold;">MAKE SURE YOU CD TO THE SPECIFIED DIRECTORIES AS PATHS IN COMMANDS ARE RELATIVE</span>
### Cloning and Initial Setup
1. Clone this repository
2. Checkout the `develop` branch `git checkout develop`
### Docker Images Setup
3. <span style="color: red; font-weight: bold;">CD</span> to the `Docker` directory
4. Run `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
5. Run `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
### Server Setup
6. <span style="color: red; font-weight: bold;">CD</span> to `Server` directory, run `npm install`
7. While in `Server` directory, create a `.env` file with the [required environmental variables](#env-vars-server)
8. While in the `Server` directory, run `npm run dev`
### Client Setup
9. <span style="color: red; font-weight: bold;">CD</span> to `Client` directory `run npm install`
10. While in the `Client` directory, create a `.env` file with the [required environmental variables](#env-vars-client)
11. While in the `Client` cirectory run `npm run dev`
### Access Application
12. Client is running at `localhost:5173`
13. Server is running at `localhost:5000`
---
## <u>Manual Install</u> <a id="manual-install"></a>
---
### Client Installation<a id="install-client"></a>
1. Change directory to the `Client` directory
2. Install all dependencies by running `npm install`
---
### Environmental Variables <a id="env-vars-client"></a>
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
| --------------------- | ----------------- | --------- | ------------------ | ---------------------------------- |
| VITE_APP_API_BASE_URL | Required | `string` | Base URL of server | {host}/api/v1 |
| VITE_APP_LOG_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| |
| VITE_APP_DEMO | Optional | `boolean` | Demo server or not | `true`\|`false` \| |
---
### Starting Development Server<a id="start-client"></a>
1. Run `npm run dev` to start the development server.
---
### Server Installation<a id="install-server"></a>
1. Change directory to the `Server` directory
2. Install all dependencies by running `npm install`
---
### Environmental Variables <a id="env-vars-server"></a>
Configure the server with the following environmental variables:
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
| --------------------- | ----------------- | --------- | ---------------------------------------- | ------------------------------------------------ |
| CLIENT_HOST | Required | `string` | Frontend Host | |
| JWT_SECRET | Required | `string` | JWT secret | |
| DB_TYPE | Optional | `string` | Specify DB to use | `MongoDB \| FakeDB` |
| DB_CONNECTION_STRING | Required | `string` | Specifies URL for MongoDB Database | |
| PORT | Optional | `integer` | Specifies Port for Server | |
| LOGIN_PAGE_URL | Required | `string` | Login url to be used in emailing service | |
| REDIS_HOST | Required | `string` | Host address for Redis database | |
| REDIS_PORT | Required | `integer` | Port for Redis database | |
| TOKEN_TTL | Optional | `string` | Time for token to live | In vercel/ms format https://github.com/vercel/ms |
| PAGESPEED_API_KEY | Optional | `string` | API Key for PageSpeed requests | |
| SYSTEM_EMAIL_HOST | Required | `string` | Host to send System Emails From | |
| SYSTEM_EMAIL_PORT | Required | `number` | Port for System Email Host | |
| SYSTEM_EMAIL_ADDRESS | Required | `string` | System Email Address | |
| SYSTEM_EMAIL_PASSWORD | Required | `string` | System Email Password | |
---
### Databases <a id="databases"></a>
This project requires two databases:
1. **Main Application Database:** The project uses MongoDB for its primary database, with a MongoDB Docker image provided for easy setup.
2. **Redis for Queue Management:** A Redis database is used for the PingServices queue system, and a Redis Docker image is included for deployment.
You may use the included Dockerfiles to spin up databases quickly if you wish.
#### (Optional) Dockerised Databases <a id="optional-docker-databases"></a>
Dockerfiles for the server and databases are located in the `Docker` directory
<details>
<summary><b>MongoDB Image</b></summary>
Location: `Docker/mongoDB.Dockerfile`
The `Docker/mongo/data` directory should be mounted to the MongoDB container in order to persist data.
From the `Docker` directory run
1. Build the image: `docker build -f mongoDB.Dockerfile -t uptime_database_mongo .`
2. Run the docker image: `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
</details>
<details>
<summary><b>Redis Image</b></summary>
Location `Docker/redis.Dockerfile`
the `Docker/redis/data` directory should be mounted to the Redis container in order to persist data.
From the `Docker` directory run
1. Build the image: `docker build -f redis.Dockerfile -t uptime_redis .`
2. Run the image: `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
</details>
---
### Starting the Development Server <a id="start-server"></a>
- run `npm run dev` to start the development server
OR
- run `node index.js` to start server
---
## <u>API Documentation</u><a id="api-documentation"></a>
Our API is documented in accordance with the [OpenAPI spec](https://www.openapis.org/).
You can see the documentation on your local development server at [http://localhost:{port}/api-docs](htt>>p://localhost:5000/api-docs)
You can also view the documentation on our demo server at [https://uptime-demo.bluewavelabs.ca/api-docs](https://uptime-demo.bluewavelabs.ca/api-docs)
## <u>Error handling</u>
Errors are returned in a standard format:
`{"success": false, "msg": "No token provided"}`
Errors are handled by error handling middleware and should be thrown with the following parameters
| Name | Type | Default | Notes |
| ------- | --------- | ---------------------- | ------------------------------------ |
| status | `integer` | 500 | Standard HTTP codes |
| message | `string` | "Something went wrong" | An error message |
| service | `string` | "Unknown Service" | Name of service that threw the error |
Example:
```
const myRoute = async(req, res, next) => {
try{
const result = myRiskyOperationHere();
}
catch(error){
error.status = 404
error.message = "Resource not found"
error.service = service name
next(error)
return;
}
}
```
Errors should not be handled at the controller level and should be left to the middleware to handle.

View File

@@ -1,17 +1,19 @@
const {
createMaintenanceWindowParamValidation,
createMaintenanceWindowBodyValidation,
getMaintenanceWindowsByUserIdParamValidation,
editMaintenanceWindowByIdParamValidation,
editMaintenanceByIdWindowBodyValidation,
getMaintenanceWindowByIdParamValidation,
getMaintenanceWindowsByMonitorIdParamValidation,
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
} = require("../validation/joi");
const {successMessages} = require("../utils/messages")
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const { successMessages } = require("../utils/messages");
const SERVICE_NAME = "maintenanceWindowController";
const createMaintenanceWindow = async (req, res, next) => {
const createMaintenanceWindows = async (req, res, next) => {
try {
await createMaintenanceWindowParamValidation.validateAsync(req.params);
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
@@ -21,23 +23,26 @@ const createMaintenanceWindow = async (req, res, next) => {
next(error);
return;
}
try {
const data = {
monitorId: req.params.monitorId,
...req.body,
};
if (data.oneTime === true) {
data.expiry = data.end;
}
const maintenanceWindow = await req.db.createMaintenanceWindow(data);
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const monitorIds = req.body.monitors;
const dbTransactions = monitorIds.map((monitorId) => {
return req.db.createMaintenanceWindow({
teamId,
monitorId,
name: req.body.name,
active: req.body.active ? req.body.active : true,
repeat: req.body.repeat,
start: req.body.start,
end: req.body.end,
});
});
await Promise.all(dbTransactions);
return res.status(201).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_CREATE,
data: maintenanceWindow,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
@@ -48,11 +53,9 @@ const createMaintenanceWindow = async (req, res, next) => {
}
};
const getMaintenanceWindowsByUserId = async (req, res, next) => {
const getMaintenanceWindowById = async (req, res, next) => {
try {
await getMaintenanceWindowsByUserIdParamValidation.validateAsync(
req.params
);
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
@@ -62,13 +65,47 @@ const getMaintenanceWindowsByUserId = async (req, res, next) => {
return;
}
try {
const maintenanceWindows = await req.db.getMaintenanceWindowsByUserId(
req.params.userId
const maintenanceWindow = await req.db.getMaintenanceWindowById(
req.params.id
);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID,
data: maintenanceWindow,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMaintenanceWindowById")
: null;
next(error);
}
};
const getMaintenanceWindowsByTeamId = async (req, res, next) => {
try {
await getMaintenanceWindowsByTeamIdQueryValidation.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 token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const maintenanceWindows = await req.db.getMaintenanceWindowsByTeamId(
teamId,
req.query
);
return res.status(201).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM,
data: maintenanceWindows,
});
} catch (error) {
@@ -112,8 +149,67 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
next(error);
}
};
module.exports = {
createMaintenanceWindow,
getMaintenanceWindowsByUserId,
getMaintenanceWindowsByMonitorId,
const deleteMaintenanceWindow = async (req, res, next) => {
try {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
await req.db.deleteMaintenanceWindowById(req.params.id);
return res.status(201).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_DELETE,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "deleteMaintenanceWindow")
: null;
}
};
const editMaintenanceWindow = async (req, res, next) => {
try {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
const editedMaintenanceWindow = await req.db.editMaintenanceWindowById(
req.params.id,
req.body
);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_EDIT,
data: editedMaintenanceWindow,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "editMaintenanceWindow")
: null;
}
};
module.exports = {
createMaintenanceWindows,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenanceWindow,
editMaintenanceWindow,
};

View File

@@ -6,7 +6,7 @@ const mongoose = require("mongoose");
* @typedef {Object} MaintenanceWindow
* @property {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor. This is a reference to the Monitor model and is immutable.
* @property {Boolean} active - Indicates whether the maintenance window is active.
* @property {Boolean} oneTime - Indicates whether the maintenance window is a one-time event.
* @property {Number} repeat - Indicates how often this maintenance window should repeat.
* @property {Date} start - The start date and time of the maintenance window.
* @property {Date} end - The end date and time of the maintenance window.
* @property {Date} expiry - The expiry date and time of the maintenance window. This is used for MongoDB's TTL index to automatically delete the document at this time. This field is set to the same value as `end` when `oneTime` is `true`.
@@ -16,12 +16,12 @@ const mongoose = require("mongoose");
* let maintenanceWindow = new MaintenanceWindow({
* monitorId: monitorId,
* active: active,
* oneTime: oneTime,
* repeat: repeat,
* start: start,
* end: end,
* });
*
* if (oneTime) {
* if (repeat === 0) {
* maintenanceWindow.expiry = end;
* }
*
@@ -34,16 +34,20 @@ const MaintenanceWindow = mongoose.Schema(
ref: "Monitor",
immutable: true,
},
userId: {
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
ref: "Team",
immutable: true,
},
active: {
type: Boolean,
default: true,
},
oneTime: {
type: Boolean,
name: {
type: String,
},
repeat: {
type: Number,
},
start: {
type: Date,

View File

@@ -118,12 +118,14 @@ const {
//****************************************
const {
createMaintenanceWindow,
getMaintenanceWindowsByUserId,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenaceWindowById,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
} = require("./modules/maintenaceWindowModule");
editMaintenanceWindowById,
} = require("./modules/maintenanceWindowModule");
//****************************************
// Notifications
@@ -181,11 +183,13 @@ module.exports = {
getPageSpeedChecks,
deletePageSpeedChecksByMonitorId,
createMaintenanceWindow,
getMaintenanceWindowsByUserId,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowById,
getMaintenanceWindowsByMonitorId,
deleteMaintenaceWindowById,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
editMaintenanceWindowById,
createNotification,
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,

View File

@@ -36,7 +36,7 @@ const createMaintenanceWindow = async (maintenanceWindowData) => {
if (maintenanceWindowData.oneTime) {
maintenanceWindow.expiry = maintenanceWindowData.end;
}
const result = maintenanceWindow.save();
const result = await maintenanceWindow.save();
return result;
} catch (error) {
error.service = SERVICE_NAME;
@@ -45,22 +45,60 @@ const createMaintenanceWindow = async (maintenanceWindowData) => {
}
};
const getMaintenanceWindowById = async (maintenanceWindowId) => {
try {
const maintenanceWindow = await MaintenanceWindow.findById({
_id: maintenanceWindowId,
});
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowById";
throw error;
}
};
/**
* Asynchronously retrieves all MaintenanceWindow documents associated with a specific user ID.
* Asynchronously retrieves all MaintenanceWindow documents associated with a specific team ID.
* @async
* @function getMaintenanceWindowByUserId
* @param {String} userId - The ID of the user.
* @param {String} teamId - The ID of the team.
* @param {Object} query - The request body.
* @returns {Promise<Array<MaintenanceWindow>>} An array of MaintenanceWindow documents.
* @throws {Error} If there is an error retrieving the documents.
* @example
* getMaintenanceWindowByUserId('userId')
* getMaintenanceWindowByUserId(teamId)
* .then(maintenanceWindows => console.log(maintenanceWindows))
* .catch(error => console.error(error));
*/
const getMaintenanceWindowsByUserId = async (userId) => {
const getMaintenanceWindowsByTeamId = async (teamId, query) => {
try {
const maintenanceWindows = await MaintenanceWindow.find({ userId: userId });
return maintenanceWindows;
let { active, page, rowsPerPage, field, order } = query || {};
const maintenanceQuery = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
const maintenanceWindowCount =
await MaintenanceWindow.countDocuments(maintenanceQuery);
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
// Sorting
let sort = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
}
const maintenanceWindows = await MaintenanceWindow.find(maintenanceQuery)
.skip(skip)
.limit(rowsPerPage)
.sort(sort);
return { maintenanceWindows, maintenanceWindowCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowByUserId";
@@ -96,23 +134,23 @@ const getMaintenanceWindowsByMonitorId = async (monitorId) => {
/**
* Asynchronously deletes a MaintenanceWindow document by its ID.
* @async
* @function deleteMaintenaceWindowById
* @function deleteMaintenanceWindowById
* @param {mongoose.Schema.Types.ObjectId} maintenanceWindowId - The ID of the MaintenanceWindow document to delete.
* @returns {Promise<MaintenanceWindow>} The deleted MaintenanceWindow document.
* @throws {Error} If there is an error deleting the document.
* @example
* deleteMaintenaceWindowById('maintenanceWindowId')
* deleteMaintenanceWindowById('maintenanceWindowId')
* .then(maintenanceWindow => console.log(maintenanceWindow))
* .catch(error => console.error(error));
*/
const deleteMaintenaceWindowById = async (maintenanceWindowId) => {
const deleteMaintenanceWindowById = async (maintenanceWindowId) => {
try {
const maintenanceWindow =
await MaintenanceWindow.findByIdAndDelete(maintenanceWindowId);
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMaintenaceWindowById";
error.method = "deleteMaintenanceWindowById";
throw error;
}
};
@@ -163,11 +201,32 @@ const deleteMaintenanceWindowByUserId = async (userId) => {
}
};
const editMaintenanceWindowById = async (
maintenanceWindowId,
maintenanceWindowData
) => {
console.log(maintenanceWindowData);
try {
const editedMaintenanceWindow = MaintenanceWindow.findByIdAndUpdate(
maintenanceWindowId,
maintenanceWindowData,
{ new: true }
);
return editedMaintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMaintenanceWindowById";
throw error;
}
};
module.exports = {
createMaintenanceWindow,
getMaintenanceWindowsByUserId,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenaceWindowById,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
editMaintenanceWindowById,
};

View File

@@ -38,6 +38,11 @@ const verifyJWT = (req, res, next) => {
const { jwtSecret } = req.settingsService.getSettings();
jwt.verify(parsedToken, jwtSecret, (err, decoded) => {
if (err) {
if (err.name === "TokenExpiredError") {
res
.status(401)
.json({ success: false, msg: errorMessages.EXPIRED_AUTH_TOKEN });
}
return res
.status(401)
.json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN });

View File

@@ -32,6 +32,17 @@
}
}
},
{
"url": "http://localhost/{API_PATH}",
"description": "Distribution Local Development Server",
"variables": {
"API_PATH": {
"description": "API Base Path",
"enum": ["api/v1"],
"default": "api/v1"
}
}
},
{
"url": "https://uptime-demo.bluewavelabs.ca/{API_PATH}",
"description": "Bluewave Demo Server",

View File

@@ -3,21 +3,20 @@ const maintenanceWindowController = require("../controllers/maintenanceWindowCon
const { verifyOwnership } = require("../middleware/verifyOwnership");
const Monitor = require("../db/models/Monitor");
router.post("/", maintenanceWindowController.createMaintenanceWindows);
router.get(
"/monitor/:monitorId",
verifyOwnership(Monitor, "monitorId"),
maintenanceWindowController.getMaintenanceWindowsByMonitorId
);
router.post(
"/monitor/:monitorId",
verifyOwnership(Monitor, "monitorId"),
maintenanceWindowController.createMaintenanceWindow
);
router.get("/team/", maintenanceWindowController.getMaintenanceWindowsByTeamId);
router.get(
"/user/:userId",
maintenanceWindowController.getMaintenanceWindowsByUserId
);
router.get("/:id", maintenanceWindowController.getMaintenanceWindowById);
router.put("/:id", maintenanceWindowController.editMaintenanceWindow);
router.delete("/:id", maintenanceWindowController.deleteMaintenanceWindow);
module.exports = router;

View File

@@ -77,14 +77,21 @@ class JobQueue {
const monitorId = job.data._id;
const maintenanceWindows =
await this.db.getMaintenanceWindowsByMonitorId(monitorId);
// Check for active maintenance window:
const maintenanceWindowActive = maintenanceWindows.reduce(
(acc, window) => {
if (window.active) {
const start = new Date(window.start);
const end = new Date(window.end);
if (start < new Date() && end > new Date()) {
const now = new Date();
const repeatInterval = window.repeat || 0;
while ((start < now) & (repeatInterval !== 0)) {
start.setTime(start.getTime() + repeatInterval);
end.setTime(end.getTime() + repeatInterval);
}
if (start < now && end > now) {
return true;
}
}
@@ -139,9 +146,7 @@ class JobQueue {
return { jobs, load };
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getWorkerStats")
: null;
error.method === undefined ? (error.method = "getWorkerStats") : null;
throw error;
}
}

View File

@@ -12,6 +12,7 @@ const errorMessages = {
UNKNOWN_SERVICE: "Unknown service",
NO_AUTH_TOKEN: "No auth token provided",
INVALID_AUTH_TOKEN: "Invalid auth token",
EXPIRED_AUTH_TOKEN: "Token expired",
//Ownership Middleware
VERIFY_OWNER_NOT_FOUND: "Document not found",
@@ -92,8 +93,12 @@ const successMessages = {
JOB_QUEUE_RESUME_JOB: "Job resumed successfully",
//Maintenance Window Controller
MAINTENANCE_WINDOW_GET_BY_ID: "Got Maintenance Window by Id successfully",
MAINTENANCE_WINDOW_CREATE: "Maintenance Window created successfully",
MAINTENANCE_WINDOW_GET_BY_USER: "Got Maintenance Windows by User successfully",
MAINTENANCE_WINDOW_GET_BY_TEAM:
"Got Maintenance Windows by Team successfully",
MAINTENANCE_WINDOW_DELETE: "Maintenance Window deleted successfully",
MAINTENANCE_WINDOW_EDIT: "Maintenance Window edited successfully",
//Ping Operations
PING_SUCCESS: "Success",

View File

@@ -1,5 +1,7 @@
const exp = require("constants");
const joi = require("joi");
const { normalize } = require("path");
const { start } = require("repl");
//****************************************
// Custom Validators
@@ -361,27 +363,51 @@ const deletePageSpeedCheckParamValidation = joi.object({
//****************************************
// MaintenanceWindowValidation
//****************************************
const createMaintenanceWindowParamValidation = joi.object({
monitorId: joi.string().required(),
});
const createMaintenanceWindowBodyValidation = joi.object({
userId: joi.string().required(),
active: joi.boolean().required(),
oneTime: joi.boolean().required(),
monitors: joi.array().items(joi.string()).required(),
name: joi.string().required(),
active: joi.boolean(),
start: joi.date().required(),
end: joi.date().required(),
repeat: joi.number().required(),
expiry: joi.date(),
});
const getMaintenanceWindowsByUserIdParamValidation = joi.object({
userId: joi.string().required(),
const getMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
const getMaintenanceWindowsByTeamIdQueryValidation = joi.object({
active: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
field: joi.string(),
order: joi.string().valid("asc", "desc"),
});
const getMaintenanceWindowsByMonitorIdParamValidation = joi.object({
monitorId: joi.string().required(),
});
const deleteMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
const editMaintenanceWindowByIdParamValidation = joi.object({
id: joi.string().required(),
});
const editMaintenanceByIdWindowBodyValidation = joi.object({
active: joi.boolean(),
name: joi.string(),
repeat: joi.number(),
start: joi.date(),
end: joi.date(),
expiry: joi.date(),
monitors: joi.array(),
});
//****************************************
// SettingsValidation
//****************************************
@@ -447,9 +473,12 @@ module.exports = {
createPageSpeedCheckParamValidation,
deletePageSpeedCheckParamValidation,
createPageSpeedCheckBodyValidation,
createMaintenanceWindowParamValidation,
createMaintenanceWindowBodyValidation,
getMaintenanceWindowsByUserIdParamValidation,
getMaintenanceWindowByIdParamValidation,
getMaintenanceWindowsByTeamIdQueryValidation,
getMaintenanceWindowsByMonitorIdParamValidation,
deleteMaintenanceWindowByIdParamValidation,
editMaintenanceWindowByIdParamValidation,
editMaintenanceByIdWindowBodyValidation,
updateAppSettingsBodyValidation,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

63
docs/README.md Normal file
View File

@@ -0,0 +1,63 @@
---
icon: hand-wave
cover: .gitbook/assets/Group 3763.png
coverY: 0
layout:
cover:
visible: true
size: full
title:
visible: true
description:
visible: false
tableOfContents:
visible: true
outline:
visible: true
pagination:
visible: true
---
# Welcome to Uptime Manager
BlueWave Uptime Manager is an open-source server monitoring application used to track the operational status and performance of servers and websites.&#x20;
It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
## Demo
We have a [demo](https://uptime-demo.bluewavelabs.ca/) where you can test how the Uptime Manager works. The username is [uptimedemo@demo.com](mailto:uptimedemo@demo.com) and the password is Demouser1!
## Questions & ideas
If you have a question, head over to GitHub's [discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page.
## Features
* [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
* [x] E-mail notifications
* [ ] Scheduled maintenance (in the works)
## **Roadmap (short term)**
* [ ] Memory, disk and CPU monitoring
* [ ] 3rd party integrations
* [ ] DNS monitoring
* [ ] SSL monitoring
## **Roadmap (long term)**
* [ ] Status pages
## Tech stack
* [ReactJs](https://react.dev/)
* [MUI (React framework)](https://mui.com/)
* [Node.js](https://nodejs.org/en)
* [MongoDB](https://mongodb.com)

19
docs/SUMMARY.md Normal file
View File

@@ -0,0 +1,19 @@
# Table of contents
* [Welcome to Uptime Manager](README.md)
## USER'S GUIDE
* [Installing Uptime Manager](users-guide/quickstart.md)
* [Using Uptime Manager](users-guide/using-uptime-manager.md)
* [Creating a new monitor](users-guide/creating-a-new-monitor.md)
* [Pagespeed monitoring](users-guide/pagespeed-monitoring.md)
* [Incidents page](users-guide/incidents-page.md)
* [Server settings](users-guide/server-settings.md)
* [User settings](users-guide/user-settings.md)
## DEVELOPER'S GUIDE
* [Contributing to the code](developers-guide/contributing-to-the-code.md)
* [General project structure](developers-guide/general-project-structure.md)
* [High level overview](developers-guide/high-level-overview.md)

View File

@@ -0,0 +1,31 @@
---
icon: plus
---
# Creating a new monitor
Creating a new monitor involves a few steps, mentioned below.&#x20;
### General settings
* **URL to monitor:** Enter the full URL of the website or service you want to monitor.
* **Display name:** Optionally, provide a custom name for your monitor. This helps identify it easily in your dashboard.
### Checks to perform
* **Website monitoring:** This option uses HTTP(s) to monitor your website or API endpoint. You can choose between HTTPS and HTTP protocols.
* **Ping monitoring:** Checks whether your server is available. This option is currently unselected.
### Incident notifications&#x20;
When there's a new incident, you can choose how to be notified:
* Notify via SMS (coming soon)
* Notify via email (to the email address you logged in with)
* Notify via email to multiple addresses (coming soon)
### Advanced settings
* **Check frequency:** Set how often the system should check your monitor. The current setting is 1 minute.
After configuring all settings, click the "Create monitor" button at the bottom right to set up your new monitor.

View File

@@ -0,0 +1,18 @@
---
icon: list-check
---
# Contributing to the code
We generally follow the [gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) workflow model. If youre not familiar with it, the general steps are
1. Create a feature branch in your local depository and make your changes.
2. Push your branch to the remote repository on Github.
3. Open a pull request.
4. The rest of the team will review the pull request and either approve or request changes.
5. If changes are requested, make changes and push.
6. Project maintainer will merge the branch, closing the pull request and deleting the remote branch.
### Git
If you are inexperienced with Git or need a refresher please visit our [Git Quick Start Guide](https://github.com/ajhollid/bluewave\_collaborative\_git) to help get up to speed. If youd like to go further in depth, this [Git for Professionals](https://youtu.be/Uszj\_k0DGsg?si=6rOWEQOMxmwhnb-K) is a good resource.\

View File

@@ -0,0 +1,48 @@
---
icon: diagram-project
---
# General project structure
The Uptime Manager product uses the MERN stack, which is to say that the project uses:
* React on the Front End via Vite
* Express on the Back End via NodeJS
* MongoDB for data with Mongoose for data access
### Front end
The project uses the [Material UI Components](https://mui.com/material-ui/all-components/) (MUI) which allows us to build with a minimum of fuss. The library is highly customisable and the project makes heavy use of the Theme concept and follows MUIs paradigm to avoid writing excessive CSS.\
\
The overriding goal on the Front Eed is to write maintainable and scalable code. If you find yourself writing lots of CSS to customize a component or are having to set a value often like font size or color you probably should be using the theme.\
\
When making changes to the Front end please always keep future developers in mind. Will they know how to make changes to your code? Is your code modular? If a dev makes changes elsewhere in the app will your component be affected? If the team makes a theme change like font size or primary color will your component be updated as well?&#x20;
### Back end
The back end of this project is not especially complex and is built around Express. The back end is a RESTful API and the [documentation can be found here](https://uptime-demo.bluewavelabs.ca/api-docs).
The application consists of several main conceptual models:
1. User
2. Monitor
3. Check
4. Notification
There are several supporting models as well.
1. AppSettings
2. InviteToken
3. Team
All requests are based around these models and manipulation of their data.\
\
In general the models interact in this fashion:
* A `User` has many `Monitors`
* A `Monitor` has many `Checks`
* A `Monitor` has many `Notifications`
A `User` can create a `Monitor`. `Monitors` are enqueued in the `JobQueue`, and their target URLs are queried when they are dequeued. When the query is complete, a `Check` is created that records information about the request.\
\
A `User` may attach a `Notification` to their `Monitor`, if the `Monitor`s status changes then the user will be notified by the method specified in the `Notification`

View File

@@ -0,0 +1,57 @@
---
icon: dove
---
# High level overview
The figure below shows a high level architecture of the Uptime Manager.
<figure><img src="../.gitbook/assets/Screenshot 2024-10-04 at 9.28.27AM.png" alt=""><figcaption></figcaption></figure>
### Typical auth request overview
The following diagram describes a typical request to the /auth endpoints.
<figure><img src="../.gitbook/assets/Screenshot 2024-10-04 at 9.30.30AM.png" alt=""><figcaption></figcaption></figure>
### Typical monitor request overview
The following diagram describes a typical request to the `/monitors` endpoints.
<figure><img src="../.gitbook/assets/Screenshot 2024-10-04 at 12.13.55PM.png" alt=""><figcaption></figcaption></figure>
### JobQueue
The heart of this application is a `JobQueue` class that wraps a BullMQ `Queue`. \
\
A `Monitor` is considered a job, when one is created it is enqueued in the `JobQueue`.\
\
Jobs are handled by a pool of workers in the `JobQueue` and their tasks are executed in the order in which they are enqueued.\
\
Workers are scaled up and down based on the jobs/worker ratio as jobs are enqueued and dequeued.
#### **High level overview of the JobQueue**
<figure><img src="../.gitbook/assets/Screenshot 2024-10-04 at 12.17.44PM.png" alt=""><figcaption></figcaption></figure>
### SSL
SSL is handled by LetsEncrypt and Certbot. This works by Nginx and Certbot sharing the same volume where the certificates are held. The following snippet from the docker-compose.yaml file shows how this works.\
<figure><img src="../.gitbook/assets/Screenshot 2024-10-04 at 12.20.52PM.png" alt=""><figcaption></figcaption></figure>
Please see [this guide](https://phoenixnap.com/kb/letsencrypt-docker) for more information on this setup.
\
\
\
\
\
\

13
docs/incidents-page.md Normal file
View File

@@ -0,0 +1,13 @@
---
icon: brake-warning
---
# Incidents page
This page shows a list of incidents of all the servers you monitor.&#x20;
<figure><img src=".gitbook/assets/Screenshot 2024-10-03 at 11.54.31PM.png" alt=""><figcaption></figcaption></figure>
Page includes a table where Monitor name, Status (down or cannot resolve), date and time of the incident, status code and corresponding message is shown.
By default, the page shows all incidents from all servers. There is also a dropdown button above the table where you can select a server name.

View File

@@ -0,0 +1,89 @@
---
icon: gauge-min
---
# Pagespeed monitoring
Under Dashboard > Pagespeed, you can see an overview of pagespeed monitors for different websites. This dashboard helps you view optimal website performance by providing clear, actionable insights into various aspects of your site's speed and user experience.
<figure><img src=".gitbook/assets/Screenshot 2024-10-03 at 11.37.47PM (1).png" alt=""><figcaption></figcaption></figure>
When you add a new page speed monitor, it is added here. Click on the "Create new" button to add a new page speed.&#x20;
Here, both monitors are shown as "Live (Collecting Data)" with a green dot, indicating they are actively monitoring. Each monitor has a small graph, representing recent pagespeed performance over time.
### Details of a pagespeed monitor
<figure><img src=".gitbook/assets/Screenshot 2024-10-03 at 11.41.55PM.png" alt=""><figcaption></figcaption></figure>
When you click on a pagespeed card, you'll see an overview of data collected using Google's PageSpeed Monitor API.&#x20;
### Score history graph
Shows performance trends over the past 24 hours. Four colored lines represent different metrics:
* Accessibility (blue)
* Best Practices (orange)
* Performance (green)
* Search Engine Optimization (purple)
You can toggle these metrics on/off using the checkboxes on the right
### Performance report
You'll see an overall score of your server's pagespeed.&#x20;
### Performance metrics
* **Cumulative Layout Shift:** Measures visual stability
* **Speed Index:** How quickly content is visually displayed
* **First Contentful Paint:** Time until the first content is rendered
* **Largest Contentful Paint:** Time until the largest content element is rendered
* **Total Blocking Time:** Sum of all time periods between FCP and Time to Interactive
### Creating a new pagespeed&#x20;
When you are on a pagespeed screen, click on the "Create pagespeed" button. You'll see a screen similar to this:&#x20;
<figure><img src=".gitbook/assets/Screenshot 2024-10-03 at 11.47.41PM.png" alt=""><figcaption></figcaption></figure>
This page allows you to set up a new pagespeed monitor for your website. Follow these steps to configure your monitor:
#### General settings
* **URL to monitor:** Enter the full URL of the website you want to monitor (e.g., [https://google.com](https://google.com)).
* **Display name (optional):** Enter a custom name for your monitor to easily identify it in your dashboard.
#### Checks to perform
Here, you can choose between HTTPS (recommended for secure sites) or HTTP protocols.
#### Incident notifications&#x20;
Select how you want to be notified when there's a new incident:
* Notify via SMS (coming soon)
* Notify via email (to your email address)
* Also notify via email to multiple addresses (coming soon)
#### Advanced settings
**Check frequency:** Choose how often you want the system to check your website's pagespeed. The default is set to 3 minutes.
After configuring all settings, click the "Create monitor" button at the bottom right of the page.
#### Tips
* Ensure the URL you enter is correct and accessible.
* Choose a descriptive display name if you're monitoring multiple sites.
* Consider the trade-off between check frequency and resource usage. More frequent checks provide more data but may use more resources.
* Remember to enable your preferred notification methods to stay informed about incidents.
After creating the monitor, you'll be able to view its performance data in your dashboard. This will help you track your website's pagespeed and optimize its performance over time.

176
docs/quickstart.md Normal file
View File

@@ -0,0 +1,176 @@
---
icon: sign-posts-wrench
---
# Installing Uptime Manager
## Quickstart for users (quick method) <a href="#user-quickstart" id="user-quickstart"></a>
1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/blob/develop/Docker/dist/docker-compose.yaml)
2. Run `docker compose up` to start the application
3. Now the application is running at `http://localhost`
## Quickstart for developers <a href="#dev-quickstart" id="dev-quickstart"></a>
{% hint style="info" %}
Make sure you change the directory to the specified directories, as paths in commands are relative.
{% endhint %}
### Cloning and initial setup
1. Clone this repository.
2. Checkout the `develop` branch `git checkout develop`
### Setting up Docker images
3. Change directory to the `Docker/dev` directory
4. Run `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
5. Run `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
### Server setup
6. CD to `Server` directory, and run `npm install`
7. While in `Server` directory, create a `.env` file with the required environmental variables
8. While in the `Server` directory, run `npm run dev`
### Client setup
9. CD to `Client` directory `run npm install`
10. While in the `Client` directory, create a `.env` file with the required environmental variables
11. While in the `Client` cirectory run `npm run dev`
### Access the application
12. Client is now running at `localhost:5173`
13. Server is now running at `localhost:5000`
## Manual installation <a href="#manual-install" id="manual-install"></a>
### Client installation <a href="#install-client" id="install-client"></a>
1. Change directory to the `Client` directory
2. Install all dependencies by running `npm install`
#### Environment variables <a href="#env-vars-client" id="env-vars-client"></a>
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
| ------------------------- | ----------------- | --------- | ------------------ | ---------------------------------- |
| VITE\_APP\_API\_BASE\_URL | Required | `string` | Base URL of server | {host}/api/v1 |
| VITE\_APP\_LOG\_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| |
| VITE\_APP\_DEMO | Optional | `boolean` | Demo server or not | `true`\|`false` \| |
#### Starting the development server <a href="#start-client" id="start-client"></a>
1. Run `npm run dev` to start the development server.
### Server Installation <a href="#install-server" id="install-server"></a>
1. Change the directory to the `Server` directory
2. Install all dependencies by running `npm install`
#### Environment variables <a href="#env-vars-server" id="env-vars-server"></a>
Configure the server with the following environmental variables:
<table><thead><tr><th width="239">ENV Variable Name</th><th width="149">Required/Optional</th><th width="116">Type</th><th>Description</th><th>Accepted Values</th></tr></thead><tbody><tr><td>CLIENT_HOST</td><td>Required</td><td><code>string</code></td><td>Frontend Host</td><td></td></tr><tr><td>JWT_SECRET</td><td>Required</td><td><code>string</code></td><td>JWT secret</td><td></td></tr><tr><td>DB_TYPE</td><td>Optional</td><td><code>string</code></td><td>Specify DB to use</td><td><code>MongoDB | FakeDB</code></td></tr><tr><td>DB_CONNECTION_STRING</td><td>Required</td><td><code>string</code></td><td>Specifies URL for MongoDB Database</td><td></td></tr><tr><td>PORT</td><td>Optional</td><td><code>integer</code></td><td>Specifies Port for Server</td><td></td></tr><tr><td>LOGIN_PAGE_URL</td><td>Required</td><td><code>string</code></td><td>Login url to be used in emailing service</td><td></td></tr><tr><td>REDIS_HOST</td><td>Required</td><td><code>string</code></td><td>Host address for Redis database</td><td></td></tr><tr><td>REDIS_PORT</td><td>Required</td><td><code>integer</code></td><td>Port for Redis database</td><td></td></tr><tr><td>TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for token to live</td><td>In vercel/ms format https://github.com/vercel/ms</td></tr><tr><td>PAGESPEED_API_KEY</td><td>Optional</td><td><code>string</code></td><td>API Key for PageSpeed requests</td><td></td></tr><tr><td>SYSTEM_EMAIL_HOST</td><td>Required</td><td><code>string</code></td><td>Host to send System Emails From</td><td></td></tr><tr><td>SYSTEM_EMAIL_PORT</td><td>Required</td><td><code>number</code></td><td>Port for System Email Host</td><td></td></tr><tr><td>SYSTEM_EMAIL_ADDRESS</td><td>Required</td><td><code>string</code></td><td>System Email Address</td><td></td></tr><tr><td>SYSTEM_EMAIL_PASSWORD</td><td>Required</td><td><code>string</code></td><td>System Email Password</td><td></td></tr></tbody></table>
***
#### Databases <a href="#databases" id="databases"></a>
This project requires two databases:
1. **Main application database:** The project uses MongoDB for its primary database, with a MongoDB Docker image provided for easy setup.
2. **Redis for queue management:** A Redis database is used for the PingServices queue system, and a Redis Docker image is included for deployment.
You may use the included Dockerfiles to spin up databases quickly if you wish.
**(Optional) Dockerised databases**
Dockerfiles for the server and databases are located in the `Docker` directory
<details>
<summary>MongoDB Image</summary>
Location: `Docker/mongoDB.Dockerfile`
The `Docker/mongo/data` directory should be mounted to the MongoDB container in order to persist data.
From the `Docker` directory run
1. Build the image: `docker build -f mongoDB.Dockerfile -t uptime_database_mongo .`
2. Run the docker image: `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
</details>
<details>
<summary>Redis Image</summary>
Location `Docker/redis.Dockerfile`
the `Docker/redis/data` directory should be mounted to the Redis container in order to persist data.
From the `Docker` directory run
1. Build the image: `docker build -f redis.Dockerfile -t uptime_redis .`
2. Run the image: `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
</details>
***
#### Starting the development derver <a href="#start-server" id="start-server"></a>
* run `npm run dev` to start the development server
or,
* run `node index.js` to start server
***
### API documentation <a href="#api-documentation" id="api-documentation"></a>
Our API is documented in accordance with the [OpenAPI spec](https://www.openapis.org/).
You can see the documentation on your local development server at http://localhost:{port}/api-docs
You can also view the documentation on our demo server at [https://uptime-demo.bluewavelabs.ca/api-docs](https://uptime-demo.bluewavelabs.ca/api-docs)
### Error handling
Errors are returned in a standard format:
`{"success": false, "msg": "No token provided"}`
Errors are handled by error handling middleware and should be thrown with the following parameters
| Name | Type | Default | Notes |
| ------- | --------- | ---------------------- | ------------------------------------ |
| status | `integer` | 500 | Standard HTTP codes |
| message | `string` | "Something went wrong" | An error message |
| service | `string` | "Unknown Service" | Name of service that threw the error |
Example:
```
const myRoute = async(req, res, next) => {
try{
const result = myRiskyOperationHere();
}
catch(error){
error.status = 404
error.message = "Resource not found"
error.service = service name
next(error)
return;
}
}
```
Errors should not be handled at the controller level and should be left to the middleware to handle.

35
docs/server-settings.md Normal file
View File

@@ -0,0 +1,35 @@
---
icon: screwdriver-wrench
---
# Server settings
Under the Settings page, you can configure the server's behaviour.&#x20;
### General settings
Display timezone: The timezone of the dashboard you publicly display.
### History and monitoring
Define here for how long you want to keep the data. You can also remove all past data. You can also clear all stats. This is irreversible.
### Demo monitors
Here you can add and remove demo monitors. It will load roughly 300 demo monitors at the same time so you can test your server.
### Advanced settings&#x20;
Here you can setup the following:&#x20;
**Client settings:** The URL of the APIs and debug level. The more verbose the debug level, the more log is written on the server.
**Email settings:** Set your host email settings here. These settings are used for sending system emails.
Server settings: Several server settings can be done here. Alternatively, you can add a pagespeed API key to bypass Google's limitations (albeit they are generous about it)
\

45
docs/user-settings.md Normal file
View File

@@ -0,0 +1,45 @@
---
icon: user-pen
---
# User settings
This screen allows you to manage your personal information and account settings within the application.
### Profile section
<figure><img src=".gitbook/assets/Screenshot 2024-10-04 at 12.17.57AM.png" alt=""><figcaption></figcaption></figure>
Enter your first name, last name and email here. You can also upload or delete a profile photo. If you want to delete an account, this can also be done here. Note that that this is irreversible and cannot be undone.
### Password section
This screen allows you to update your password for your account.
<figure><img src=".gitbook/assets/Screenshot 2024-10-04 at 12.16.53AM.png" alt=""><figcaption></figcaption></figure>
In order to change your password, you need to enter your password, and the new password twice. The new password must meet the specified criteria (e.g., minimum length, uppercase letter, number, symbol).
**Additional notes:**
* Your new password will take effect immediately.
* For security reasons, it is recommended to choose a strong password that is difficult to guess.
### Team section
This screen allows you to manage your team members within the application.
<figure><img src=".gitbook/assets/Screenshot 2024-10-04 at 12.14.45AM.png" alt=""><figcaption></figcaption></figure>
Here you can see a list of all users in your system. A user can be either admin or a member, where an admin can create or delete a monitor, and a user can only view them.&#x20;
To invite a team member,&#x20;
* Click the "Invite a team member" button.
* Enter the email address of the person you want to invite.
* Select their desired role (Administrator or Member).
* Click "Invite" to send the invitation.
The invited team member will receive an email invitation to join your team.
Admin also has the option to edit or remove existing team members.

View File

@@ -0,0 +1,31 @@
---
icon: plus
---
# Creating a new monitor
Creating a new monitor involves a few steps, mentioned below.&#x20;
### General settings
* **URL to monitor:** Enter the full URL of the website or service you want to monitor.
* **Display name:** Optionally, provide a custom name for your monitor. This helps identify it easily in your dashboard.
### Checks to perform
* **Website monitoring:** This option uses HTTP(s) to monitor your website or API endpoint. You can choose between HTTPS and HTTP protocols.
* **Ping monitoring:** Checks whether your server is available. This option is currently unselected.
### Incident notifications&#x20;
When there's a new incident, you can choose how to be notified:
* Notify via SMS (coming soon)
* Notify via email (to the email address you logged in with)
* Notify via email to multiple addresses (coming soon)
### Advanced settings
* **Check frequency:** Set how often the system should check your monitor. The current setting is 1 minute.
After configuring all settings, click the "Create monitor" button at the bottom right to set up your new monitor.

View File

@@ -0,0 +1,13 @@
---
icon: brake-warning
---
# Incidents page
This page shows a list of incidents of all the servers you monitor.&#x20;
<figure><img src="../.gitbook/assets/Screenshot 2024-10-03 at 11.54.31PM.png" alt=""><figcaption></figcaption></figure>
Page includes a table where Monitor name, Status (down or cannot resolve), date and time of the incident, status code and corresponding message is shown.
By default, the page shows all incidents from all servers. There is also a dropdown button above the table where you can select a server name.

View File

@@ -0,0 +1,89 @@
---
icon: gauge-min
---
# Pagespeed monitoring
Under Dashboard > Pagespeed, you can see an overview of pagespeed monitors for different websites. This dashboard helps you view optimal website performance by providing clear, actionable insights into various aspects of your site's speed and user experience.
<figure><img src="../.gitbook/assets/Screenshot 2024-10-03 at 11.37.47PM (1).png" alt=""><figcaption></figcaption></figure>
When you add a new page speed monitor, it is added here. Click on the "Create new" button to add a new page speed.&#x20;
Here, both monitors are shown as "Live (Collecting Data)" with a green dot, indicating they are actively monitoring. Each monitor has a small graph, representing recent pagespeed performance over time.
### Details of a pagespeed monitor
<figure><img src="../.gitbook/assets/Screenshot 2024-10-03 at 11.41.55PM.png" alt=""><figcaption></figcaption></figure>
When you click on a pagespeed card, you'll see an overview of data collected using Google's PageSpeed Monitor API.&#x20;
### Score history graph
Shows performance trends over the past 24 hours. Four colored lines represent different metrics:
* Accessibility (blue)
* Best Practices (orange)
* Performance (green)
* Search Engine Optimization (purple)
You can toggle these metrics on/off using the checkboxes on the right
### Performance report
You'll see an overall score of your server's pagespeed.&#x20;
### Performance metrics
* **Cumulative Layout Shift:** Measures visual stability
* **Speed Index:** How quickly content is visually displayed
* **First Contentful Paint:** Time until the first content is rendered
* **Largest Contentful Paint:** Time until the largest content element is rendered
* **Total Blocking Time:** Sum of all time periods between FCP and Time to Interactive
### Creating a new pagespeed&#x20;
When you are on a pagespeed screen, click on the "Create pagespeed" button. You'll see a screen similar to this:&#x20;
<figure><img src="../.gitbook/assets/Screenshot 2024-10-03 at 11.47.41PM.png" alt=""><figcaption></figcaption></figure>
This page allows you to set up a new pagespeed monitor for your website. Follow these steps to configure your monitor:
#### General settings
* **URL to monitor:** Enter the full URL of the website you want to monitor (e.g., [https://google.com](https://google.com)).
* **Display name (optional):** Enter a custom name for your monitor to easily identify it in your dashboard.
#### Checks to perform
Here, you can choose between HTTPS (recommended for secure sites) or HTTP protocols.
#### Incident notifications&#x20;
Select how you want to be notified when there's a new incident:
* Notify via SMS (coming soon)
* Notify via email (to your email address)
* Also notify via email to multiple addresses (coming soon)
#### Advanced settings
**Check frequency:** Choose how often you want the system to check your website's pagespeed. The default is set to 3 minutes.
After configuring all settings, click the "Create monitor" button at the bottom right of the page.
#### Tips
* Ensure the URL you enter is correct and accessible.
* Choose a descriptive display name if you're monitoring multiple sites.
* Consider the trade-off between check frequency and resource usage. More frequent checks provide more data but may use more resources.
* Remember to enable your preferred notification methods to stay informed about incidents.
After creating the monitor, you'll be able to view its performance data in your dashboard. This will help you track your website's pagespeed and optimize its performance over time.

View File

@@ -0,0 +1,176 @@
---
icon: sign-posts-wrench
---
# Installing Uptime Manager
## Quickstart for users (quick method) <a href="#user-quickstart" id="user-quickstart"></a>
1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/blob/develop/Docker/dist/docker-compose.yaml)
2. Run `docker compose up` to start the application
3. Now the application is running at `http://localhost`
## Quickstart for developers <a href="#dev-quickstart" id="dev-quickstart"></a>
{% hint style="info" %}
Make sure you change the directory to the specified directories, as paths in commands are relative.
{% endhint %}
### Cloning and initial setup
1. Clone this repository.
2. Checkout the `develop` branch `git checkout develop`
### Setting up Docker images
3. Change directory to the `Docker/dev` directory
4. Run `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
5. Run `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
### Server setup
6. CD to `Server` directory, and run `npm install`
7. While in `Server` directory, create a `.env` file with the required environmental variables
8. While in the `Server` directory, run `npm run dev`
### Client setup
9. CD to `Client` directory `run npm install`
10. While in the `Client` directory, create a `.env` file with the required environmental variables
11. While in the `Client` cirectory run `npm run dev`
### Access the application
12. Client is now running at `localhost:5173`
13. Server is now running at `localhost:5000`
## Manual installation <a href="#manual-install" id="manual-install"></a>
### Client installation <a href="#install-client" id="install-client"></a>
1. Change directory to the `Client` directory
2. Install all dependencies by running `npm install`
#### Environment variables <a href="#env-vars-client" id="env-vars-client"></a>
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
| ------------------------- | ----------------- | --------- | ------------------ | ---------------------------------- |
| VITE\_APP\_API\_BASE\_URL | Required | `string` | Base URL of server | {host}/api/v1 |
| VITE\_APP\_LOG\_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| |
| VITE\_APP\_DEMO | Optional | `boolean` | Demo server or not | `true`\|`false` \| |
#### Starting the development server <a href="#start-client" id="start-client"></a>
1. Run `npm run dev` to start the development server.
### Server Installation <a href="#install-server" id="install-server"></a>
1. Change the directory to the `Server` directory
2. Install all dependencies by running `npm install`
#### Environment variables <a href="#env-vars-server" id="env-vars-server"></a>
Configure the server with the following environmental variables:
<table><thead><tr><th width="239">ENV Variable Name</th><th width="149">Required/Optional</th><th width="116">Type</th><th>Description</th><th>Accepted Values</th></tr></thead><tbody><tr><td>CLIENT_HOST</td><td>Required</td><td><code>string</code></td><td>Frontend Host</td><td></td></tr><tr><td>JWT_SECRET</td><td>Required</td><td><code>string</code></td><td>JWT secret</td><td></td></tr><tr><td>DB_TYPE</td><td>Optional</td><td><code>string</code></td><td>Specify DB to use</td><td><code>MongoDB | FakeDB</code></td></tr><tr><td>DB_CONNECTION_STRING</td><td>Required</td><td><code>string</code></td><td>Specifies URL for MongoDB Database</td><td></td></tr><tr><td>PORT</td><td>Optional</td><td><code>integer</code></td><td>Specifies Port for Server</td><td></td></tr><tr><td>LOGIN_PAGE_URL</td><td>Required</td><td><code>string</code></td><td>Login url to be used in emailing service</td><td></td></tr><tr><td>REDIS_HOST</td><td>Required</td><td><code>string</code></td><td>Host address for Redis database</td><td></td></tr><tr><td>REDIS_PORT</td><td>Required</td><td><code>integer</code></td><td>Port for Redis database</td><td></td></tr><tr><td>TOKEN_TTL</td><td>Optional</td><td><code>string</code></td><td>Time for token to live</td><td>In vercel/ms format https://github.com/vercel/ms</td></tr><tr><td>PAGESPEED_API_KEY</td><td>Optional</td><td><code>string</code></td><td>API Key for PageSpeed requests</td><td></td></tr><tr><td>SYSTEM_EMAIL_HOST</td><td>Required</td><td><code>string</code></td><td>Host to send System Emails From</td><td></td></tr><tr><td>SYSTEM_EMAIL_PORT</td><td>Required</td><td><code>number</code></td><td>Port for System Email Host</td><td></td></tr><tr><td>SYSTEM_EMAIL_ADDRESS</td><td>Required</td><td><code>string</code></td><td>System Email Address</td><td></td></tr><tr><td>SYSTEM_EMAIL_PASSWORD</td><td>Required</td><td><code>string</code></td><td>System Email Password</td><td></td></tr></tbody></table>
***
#### Databases <a href="#databases" id="databases"></a>
This project requires two databases:
1. **Main application database:** The project uses MongoDB for its primary database, with a MongoDB Docker image provided for easy setup.
2. **Redis for queue management:** A Redis database is used for the PingServices queue system, and a Redis Docker image is included for deployment.
You may use the included Dockerfiles to spin up databases quickly if you wish.
**(Optional) Dockerised databases**
Dockerfiles for the server and databases are located in the `Docker` directory
<details>
<summary>MongoDB Image</summary>
Location: `Docker/mongoDB.Dockerfile`
The `Docker/mongo/data` directory should be mounted to the MongoDB container in order to persist data.
From the `Docker` directory run
1. Build the image: `docker build -f mongoDB.Dockerfile -t uptime_database_mongo .`
2. Run the docker image: `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
</details>
<details>
<summary>Redis Image</summary>
Location `Docker/redis.Dockerfile`
the `Docker/redis/data` directory should be mounted to the Redis container in order to persist data.
From the `Docker` directory run
1. Build the image: `docker build -f redis.Dockerfile -t uptime_redis .`
2. Run the image: `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
</details>
***
#### Starting the development derver <a href="#start-server" id="start-server"></a>
* run `npm run dev` to start the development server
or,
* run `node index.js` to start server
***
### API documentation <a href="#api-documentation" id="api-documentation"></a>
Our API is documented in accordance with the [OpenAPI spec](https://www.openapis.org/).
You can see the documentation on your local development server at http://localhost:{port}/api-docs
You can also view the documentation on our demo server at [https://uptime-demo.bluewavelabs.ca/api-docs](https://uptime-demo.bluewavelabs.ca/api-docs)
### Error handling
Errors are returned in a standard format:
`{"success": false, "msg": "No token provided"}`
Errors are handled by error handling middleware and should be thrown with the following parameters
| Name | Type | Default | Notes |
| ------- | --------- | ---------------------- | ------------------------------------ |
| status | `integer` | 500 | Standard HTTP codes |
| message | `string` | "Something went wrong" | An error message |
| service | `string` | "Unknown Service" | Name of service that threw the error |
Example:
```
const myRoute = async(req, res, next) => {
try{
const result = myRiskyOperationHere();
}
catch(error){
error.status = 404
error.message = "Resource not found"
error.service = service name
next(error)
return;
}
}
```
Errors should not be handled at the controller level and should be left to the middleware to handle.

View File

@@ -0,0 +1,35 @@
---
icon: screwdriver-wrench
---
# Server settings
Under the Settings page, you can configure the server's behaviour.&#x20;
### General settings
Display timezone: The timezone of the dashboard you publicly display.
### History and monitoring
Define here for how long you want to keep the data. You can also remove all past data. You can also clear all stats. This is irreversible.
### Demo monitors
Here you can add and remove demo monitors. It will load roughly 300 demo monitors at the same time so you can test your server.
### Advanced settings&#x20;
Here you can setup the following:&#x20;
**Client settings:** The URL of the APIs and debug level. The more verbose the debug level, the more log is written on the server.
**Email settings:** Set your host email settings here. These settings are used for sending system emails.
Server settings: Several server settings can be done here. Alternatively, you can add a pagespeed API key to bypass Google's limitations (albeit they are generous about it)
\

View File

@@ -0,0 +1,45 @@
---
icon: user-pen
---
# User settings
This screen allows you to manage your personal information and account settings within the application.
### Profile section
<figure><img src="../.gitbook/assets/Screenshot 2024-10-04 at 12.17.57AM.png" alt=""><figcaption></figcaption></figure>
Enter your first name, last name and email here. You can also upload or delete a profile photo. If you want to delete an account, this can also be done here. Note that that this is irreversible and cannot be undone.
### Password section
This screen allows you to update your password for your account.
<figure><img src="../.gitbook/assets/Screenshot 2024-10-04 at 12.16.53AM.png" alt=""><figcaption></figcaption></figure>
In order to change your password, you need to enter your password, and the new password twice. The new password must meet the specified criteria (e.g., minimum length, uppercase letter, number, symbol).
**Additional notes:**
* Your new password will take effect immediately.
* For security reasons, it is recommended to choose a strong password that is difficult to guess.
### Team section
This screen allows you to manage your team members within the application.
<figure><img src="../.gitbook/assets/Screenshot 2024-10-04 at 12.14.45AM.png" alt=""><figcaption></figcaption></figure>
Here you can see a list of all users in your system. A user can be either admin or a member, where an admin can create or delete a monitor, and a user can only view them.&#x20;
To invite a team member,&#x20;
* Click the "Invite a team member" button.
* Enter the email address of the person you want to invite.
* Select their desired role (Administrator or Member).
* Click "Invite" to send the invitation.
The invited team member will receive an email invitation to join your team.
Admin also has the option to edit or remove existing team members.

View File

@@ -0,0 +1,80 @@
---
icon: computer-mouse
description: >-
This user guide helps new users navigate and understand the Uptime Manager
dashboard layout and functionality.
---
# Using Uptime Manager
## **General Overview**
The Uptime Manager is an open-source server monitoring tool designed to track the uptime, downtime, and performance of servers, websites, or web applications.&#x20;
It provides real-time alerts, status updates, and detailed response time metrics.
### **Sidebar menu**&#x20;
The sidebar on the left contains the following sections:
* **Dashboard**: The main section that shows an overview of your monitored services, including their status and performance. It includes "Monitors" and "Pagespeed"
* **Incidents**: A section where alerts and incidents regarding the monitored services are logged. This helps users investigate issues and track downtime history.
* **Account**: User-related settings and account information are accessible here.
* **Support**: Links to customer support or documentation to help troubleshoot or ask for assistance.
* **Settings**: Provides customization options for the applications behavior, monitoring frequency, and notification preferences.
**User info section**
At the bottom of the sidebar, youll see the current logged-in user alongside with the role.
### **Dashboard content**
The main section of the dashboard displays an overview of the monitored services, including their current status and key performance indicators.
The dashboard offers a summary of your uptime monitors:
* **Up**: The number of monitored services that are currently operational.
* **Down**: The number of monitored services that are currently experiencing issues.
* **Paused**: The number of services that have their monitoring paused.
**Actively monitoring table**
This section lists all the services being monitored, showing key details for each service:
<figure><img src="../.gitbook/assets/Screenshot 2024-10-03 at 10.56.43PM.png" alt=""><figcaption></figcaption></figure>
* **Host**: The name and URL of the service being monitored.
* **Status**: The current status of the service, indicated by color-coded icons:
* Green: **Up** (operational)
* Red: **Down** (non-operational)
* Yellow: **Paused** (monitoring paused for this service)
* **Response time**: A visual graph showing the performance trends of the service over time. Each bar represents the response time of the service at a given interval. Green bars indicate a healthy response time, while red bars indicate slower or problematic performance.
* **Type**: Indicates the type of service being monitored (either Ping or HTTP).
* **Actions**: This column has settings icons that allow the user to configure the monitoring details of the specific service.
At the top-right corner of the dashboard, theres a button labeled **Create monitor**. This allows users to add a new server or website to monitor.
A search bar is available above the list of monitored services, enabling users to quickly locate specific services by name or URL.
**Status indicators**
* **Green**: Indicates the service is currently up and operational.
* **Red**: Indicates the service is down or experiencing problems.
* **Yellow**: Indicates that monitoring for this service is paused.
Each service has a graph in the **Response Time** column that displays its performance history over time. This allows you to easily visualize any spikes or drops in performance.
The gear icon next to each monitored service allows for quick access to configuration settings or additional monitoring options.
### Table actions&#x20;
<figure><img src="../.gitbook/assets/Screenshot 2024-10-03 at 10.57.45PM.png" alt="" width="375"><figcaption></figcaption></figure>
When you click on the gear icon, you'll see a few options that correspond to that host:&#x20;
1. **Open site**: Opens the monitored website or server in a new browser tab.
2. **Details**: Displays historical uptime and performance metrics for the service.
3. **Incidents**: Shows a log of all incidents related to the service, including downtime and performance issues.
4. **Configure**: Allows modification of monitoring parameters like check frequency and alert preferences.
5. **Clone**: Duplicates the current services monitoring settings for a new service.
6. **Remove**: Removes the service from the monitored list and stops all monitoring activities.

View File

@@ -0,0 +1,80 @@
---
description: >-
This user guide helps new users navigate and understand the Uptime Manager
dashboard layout and functionality.
icon: computer-mouse
---
# Using Uptime Manager
## **General Overview**
The Uptime Manager is an open-source server monitoring tool designed to track the uptime, downtime, and performance of servers, websites, or web applications.&#x20;
It provides real-time alerts, status updates, and detailed response time metrics.
### **Sidebar menu**&#x20;
The sidebar on the left contains the following sections:
* **Dashboard**: The main section that shows an overview of your monitored services, including their status and performance. It includes "Monitors" and "Pagespeed"
* **Incidents**: A section where alerts and incidents regarding the monitored services are logged. This helps users investigate issues and track downtime history.
* **Account**: User-related settings and account information are accessible here.
* **Support**: Links to customer support or documentation to help troubleshoot or ask for assistance.
* **Settings**: Provides customization options for the applications behavior, monitoring frequency, and notification preferences.
**User info section**
At the bottom of the sidebar, youll see the current logged-in user alongside with the role.
### **Dashboard content**
The main section of the dashboard displays an overview of the monitored services, including their current status and key performance indicators.
The dashboard offers a summary of your uptime monitors:
* **Up**: The number of monitored services that are currently operational.
* **Down**: The number of monitored services that are currently experiencing issues.
* **Paused**: The number of services that have their monitoring paused.
**Actively monitoring table**
This section lists all the services being monitored, showing key details for each service:
<figure><img src=".gitbook/assets/Screenshot 2024-10-03 at 10.56.43PM.png" alt=""><figcaption></figcaption></figure>
* **Host**: The name and URL of the service being monitored.
* **Status**: The current status of the service, indicated by color-coded icons:
* Green: **Up** (operational)
* Red: **Down** (non-operational)
* Yellow: **Paused** (monitoring paused for this service)
* **Response time**: A visual graph showing the performance trends of the service over time. Each bar represents the response time of the service at a given interval. Green bars indicate a healthy response time, while red bars indicate slower or problematic performance.
* **Type**: Indicates the type of service being monitored (either Ping or HTTP).
* **Actions**: This column has settings icons that allow the user to configure the monitoring details of the specific service.
At the top-right corner of the dashboard, theres a button labeled **Create monitor**. This allows users to add a new server or website to monitor.
A search bar is available above the list of monitored services, enabling users to quickly locate specific services by name or URL.
**Status indicators**
* **Green**: Indicates the service is currently up and operational.
* **Red**: Indicates the service is down or experiencing problems.
* **Yellow**: Indicates that monitoring for this service is paused.
Each service has a graph in the **Response Time** column that displays its performance history over time. This allows you to easily visualize any spikes or drops in performance.
The gear icon next to each monitored service allows for quick access to configuration settings or additional monitoring options.
### Table actions&#x20;
<figure><img src=".gitbook/assets/Screenshot 2024-10-03 at 10.57.45PM.png" alt="" width="375"><figcaption></figcaption></figure>
When you click on the gear icon, you'll see a few options that correspond to that host:&#x20;
1. **Open site**: Opens the monitored website or server in a new browser tab.
2. **Details**: Displays historical uptime and performance metrics for the service.
3. **Incidents**: Shows a log of all incidents related to the service, including downtime and performance issues.
4. **Configure**: Allows modification of monitoring parameters like check frequency and alert preferences.
5. **Clone**: Duplicates the current services monitoring settings for a new service.
6. **Remove**: Removes the service from the monitored list and stops all monitoring activities.