Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-integrations

This commit is contained in:
Skorpios committed 2025-01-26 13:37:48 -08:00
commit d83773abd1
35 files changed
+1830 -1810

No files matched your search

+4 -4
View File
@@ -1,4 +1,4 @@
**(when sending your PR, only remove this line. Do not remove any lines below. Check all the relevant items before sending your PR)**
**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
## Describe your changes
@@ -8,10 +8,10 @@ Briefly describe the changes you made and their purpose.
Mention the issue number(s) this PR addresses (e.g., #123).
## Please ensure all items are checked off before requesting a review:
## Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks.
- [ ] I deployed the application locally.
- [ ] I have performed a self-review and testing of my code.
- [ ] (Do not skip this or your PR will be closed) I deployed the application locally.
- [ ] (Do not skip this or your PR will be closed) I have performed a self-review and testing of my code.
- [ ] I have included the issue # in the PR.
- [ ] I have labelled the PR correctly.
- [ ] The issue I am working on is assigned to me.
+3 -3
View File
@@ -1,9 +1,9 @@
# Contributing to Uptime Manager application
# Contributing to Checkmate
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how the Uptime Manager project handles them. Please make sure to read the relevant section before making your contribution. It will make it much easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how the Checkmate project handles them. Please make sure to read the relevant section before making your contribution. It will make it much easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
@@ -77,7 +77,7 @@ Once it's filed:
This section guides you through submitting an enhancement suggestion for the application, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community understand your enhancements and find related suggestions.
- Make sure that you are using the latest version.
- Make sure you run the code locally. The Uptime Manager [documentation](https://bluewavelabs.gitbook.io/uptime-manager) covers it.
- Make sure you run the code locally. The Checkmate [documentation](https://bluewavelabs.gitbook.io/checkmate) covers it.
- Find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
+17 -17
View File
@@ -15,8 +15,8 @@
"@mui/lab": "6.0.0-beta.24",
"@mui/material": "6.4.1",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.24.0",
"@mui/x-date-pickers": "7.24.0",
"@mui/x-data-grid": "7.24.1",
"@mui/x-date-pickers": "7.24.1",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
@@ -1390,15 +1390,15 @@
"license": "MIT"
},
"node_modules/@mui/x-charts": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.24.0.tgz",
"integrity": "sha512-X/zN0GxPHJC24QckFrtEbIK0HYoGwZgpNC5CV9cvRGvHphDFuNZk9xg9fJwIg1i3PPZHXRO3E+L/ee6fJ9ufhw==",
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.24.1.tgz",
"integrity": "sha512-OdTS/nXaANPe4AoUFIDD4LlID8kK/00q+uqVOCkVClEvFQeAkj3pBaghdS4hY7rVqsCgsm+yOStQVJa9G2MR+Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-charts-vendor": "7.20.0",
"@mui/x-internals": "7.24.0",
"@mui/x-internals": "7.24.1",
"@react-spring/rafz": "^9.7.5",
"@react-spring/web": "^9.7.5",
"clsx": "^2.1.1",
@@ -1448,14 +1448,14 @@
}
},
"node_modules/@mui/x-data-grid": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.24.0.tgz",
"integrity": "sha512-goYTKDp+e+dXw7E+WndWUhWXTjX3aTqN8W2dCKhXnmE9Gu8dFwG6Azl7GK9l2m5YHGuqYmpWqcSG9etLdwYaVg==",
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.24.1.tgz",
"integrity": "sha512-4sYTbMwsDotuTd2Cwa2JGTPXPWQs8RGJvocAKnIsNOzNdZNMrikE//HO35snriK8s4dauAApY7RVbeisjpVT+A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.24.0",
"@mui/x-internals": "7.24.1",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1"
@@ -1485,14 +1485,14 @@
}
},
"node_modules/@mui/x-date-pickers": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.24.0.tgz",
"integrity": "sha512-oBM9Yp2H3tJ7qoHB4APQJYxZG4rz6JD4CwLzbzD9o3r+E1HGpGSLhwK3rDEz9VEjbOq8893Z2TGYLLWoyjeFXQ==",
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.24.1.tgz",
"integrity": "sha512-ykQugMQHuQKBk3kViW/r0PpubtHQOlrd54bgbdafgTMCeM2VpXvv4zimzOu5IGnM6wEN8hupC7EXZbkrT6x46w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.24.0",
"@mui/x-internals": "7.24.1",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1551,9 +1551,9 @@
}
},
"node_modules/@mui/x-internals": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.0.tgz",
"integrity": "sha512-lYa/XLltxNMY8YAFDopIHrXda2EAoqMCilyGMuPMz+WTG+b+StlUKqtj8cgFPQ/sa5dQ2fR7R3KJdjLREKUrlQ==",
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.1.tgz",
"integrity": "sha512-9BvJzpLJnS9BDphvkiv6v0QOLxbnu8jhwcexFjtCQ2ZyxtVuVsWzGZ2npT9sGOil7+eaFDmWnJtea/tgrPvSwQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
+2 -2
View File
@@ -18,8 +18,8 @@
"@mui/lab": "6.0.0-beta.24",
"@mui/material": "6.4.1",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.24.0",
"@mui/x-date-pickers": "7.24.0",
"@mui/x-data-grid": "7.24.1",
"@mui/x-date-pickers": "7.24.1",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
@@ -16,7 +16,7 @@ import { BaseLabel } from "../Label";
const DEFAULT_CODE = 9999; // Default code for unknown status
const handleStatusCode = (status) => {
if (status >= 100 && status < 600) {
if (status) {
return status;
}
return DEFAULT_CODE;
@@ -104,6 +104,14 @@ const Search = ({
"& .MuiOutlinedInput-root": {
paddingY: 0,
},
"& .MuiAutocomplete-tag": {
// CAIO_REVIEW
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.lowContrast,
},
"& .MuiChip-deleteIcon": {
color: theme.palette.primary.contrastText, // CAIO_REVIEW
},
}}
/>
{error && (
@@ -165,6 +173,7 @@ const Search = ({
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
{
backgroundColor: theme.palette.primary.lowContrast,
color: "red",
},
"& li.MuiAutocomplete-option:hover:not([aria-selected='true'])": {
color: theme.palette.primary.main,
@@ -5,11 +5,11 @@ import { TablePaginationActions } from "./Actions";
import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react";
Pagination.propTypes = {
monitorCount: PropTypes.number.isRequired, // Total number of items for pagination.
itemCount: PropTypes.number.isRequired, // Total number of items for pagination.
page: PropTypes.number.isRequired, // Current page index.
rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page.
handleChangePage: PropTypes.func.isRequired, // Function to handle page changes.
handleChangeRowsPerPage: PropTypes.func.isRequired, // Function to handle changes in rows per page.
handleChangeRowsPerPage: PropTypes.func, // Function to handle changes in rows per page.
};
const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
@@ -18,6 +18,7 @@ const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
* Pagination component for table navigation with customized styling and behavior.
*
* @param {object} props - Component properties.
* @param {string} props.paginationLabel - Label for the pagination.
* @param {number} props.monitorCount - Total number of monitors to paginate.
* @param {number} props.page - Current page index (0-based).
* @param {number} props.rowsPerPage - Number of rows to display per page.
@@ -26,7 +27,8 @@ const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
* @returns {JSX.Element} The Pagination component.
*/
function Pagination({
monitorCount,
paginationLabel,
itemCount,
page,
rowsPerPage,
handleChangePage,
@@ -35,7 +37,7 @@ function Pagination({
const theme = useTheme();
const start = page * rowsPerPage + 1;
const end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount);
const end = Math.min(page * rowsPerPage + rowsPerPage, itemCount);
const range = `${start} - ${end}`;
return (
@@ -51,11 +53,11 @@ function Pagination({
variant="body2"
sx={{ opacity: 0.7 }}
>
Showing {range} of {monitorCount} monitor(s)
Showing {range} of {itemCount} {paginationLabel}
</Typography>
<TablePagination
component="div"
count={monitorCount}
count={itemCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
@@ -121,4 +123,4 @@ function Pagination({
);
}
export { Pagination };
export default Pagination;
+3 -2
View File
@@ -82,11 +82,12 @@ const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => {
</TableRow>
) : (
data.map((row) => {
const key = row.id || row._id || Math.random();
return (
<TableRow
key={row.id}
key={key}
sx={config?.rowSX ?? {}}
onClick={() => config?.onRowClick(row)}
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
>
{headers.map((header, index) => {
return (
@@ -1,8 +1,6 @@
import PropTypes from "prop-types";
import { Pagination, PaginationItem, Typography, Box } from "@mui/material";
import { Typography, Box } from "@mui/material";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { networkService } from "../../../main";
@@ -16,7 +14,9 @@ import { HttpStatusLabel } from "../../../Components/HttpStatusLabel";
import { Empty } from "./Empty/Empty";
import { IncidentSkeleton } from "./Skeleton/Skeleton";
import DataTable from "../../../Components/Table";
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
import Pagination from "../../../Components/Table/TablePagination";
const IncidentTable = ({ monitors, selectedMonitor, filter, dateRange }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
@@ -24,19 +24,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
const mode = useSelector((state) => state.ui.mode);
const [checks, setChecks] = useState([]);
const [checksCount, setChecksCount] = useState(0);
const [paginationController, setPaginationController] = useState({
page: 0,
rowsPerPage: 14,
});
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setPaginationController((prevPaginationController) => ({
...prevPaginationController,
page: 0,
}));
}, [filter, selectedMonitor]);
useEffect(() => {
const fetchPage = async () => {
if (!monitors || Object.keys(monitors).length === 0) {
@@ -51,10 +42,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
teamId: user.teamId,
sortOrder: "desc",
limit: null,
dateRange: null,
dateRange,
filter: filter,
page: paginationController.page,
rowsPerPage: paginationController.rowsPerPage,
page: page,
rowsPerPage: rowsPerPage,
});
} else {
res = await networkService.getChecksByMonitor({
@@ -62,12 +53,13 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
monitorId: selectedMonitor,
sortOrder: "desc",
limit: null,
dateRange: null,
dateRange,
filter: filter,
page: paginationController.page,
rowsPerPage: paginationController.rowsPerPage,
page,
rowsPerPage,
});
}
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
@@ -77,21 +69,14 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
}
};
fetchPage();
}, [
authToken,
user,
monitors,
selectedMonitor,
filter,
paginationController.page,
paginationController.rowsPerPage,
]);
}, [authToken, user, monitors, selectedMonitor, filter, page, rowsPerPage, dateRange]);
const handlePageChange = (_, newPage) => {
setPaginationController({
...paginationController,
page: newPage - 1, // 0-indexed
});
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(event.target.value);
};
const headers = [
@@ -134,28 +119,6 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
{ id: "message", content: "Message", render: (row) => row.message },
];
let paginationComponent = <></>;
if (checksCount > paginationController.rowsPerPage) {
paginationComponent = (
<Pagination
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
page={paginationController.page + 1} //0-indexed
onChange={handlePageChange}
shape="rounded"
renderItem={(item) => (
<PaginationItem
slots={{
previous: ArrowBackRoundedIcon,
next: ArrowForwardRoundedIcon,
}}
{...item}
/>
)}
sx={{ mt: "auto" }}
/>
);
}
let sharedStyles = {
border: 1,
borderColor: theme.palette.primary.lowContrast,
@@ -198,8 +161,14 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
headers={headers}
data={checks}
/>
{paginationComponent}
<Pagination
paginationLabel="incidents"
itemCount={checksCount}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handlePageChange}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</>
)}
</>
@@ -210,6 +179,7 @@ IncidentTable.propTypes = {
monitors: PropTypes.object.isRequired,
selectedMonitor: PropTypes.string.isRequired,
filter: PropTypes.string.isRequired,
dateRange: PropTypes.string.isRequired,
};
export default IncidentTable;
-11
View File
@@ -1,11 +0,0 @@
.incidents h1.MuiTypography-root {
font-size: var(--env-var-font-size-large);
font-weight: 600;
}
.incidents button.MuiButtonBase-root,
.incidents p.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
.incidents button.MuiButtonBase-root {
min-height: 34px;
}
+115 -45
View File
@@ -8,7 +8,6 @@ import { useTheme } from "@emotion/react";
import Select from "../../Components/Inputs/Select";
import IncidentTable from "./IncidentTable";
import SkeletonLayout from "./skeleton";
import "./index.css";
const Incidents = () => {
const theme = useTheme();
@@ -21,6 +20,7 @@ const Incidents = () => {
// TODO do something with these filters
const [filter, setFilter] = useState("all");
const [dateRange, setDateRange] = useState("hour");
useEffect(() => {
const fetchMonitors = async () => {
@@ -77,62 +77,132 @@ const Incidents = () => {
<>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={theme.spacing(6)}
>
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
Incidents for
</Typography>
<Select
id="incidents-select-monitor"
placeholder="All servers"
value={selectedMonitor}
onChange={handleSelect}
items={Object.values(monitors)}
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
}}
/>
<ButtonGroup
sx={{
ml: "auto",
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
borderColor: theme.palette.primary.lowContrast,
},
}}
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
Incidents for
</Typography>
<Select
id="incidents-select-monitor"
placeholder="All servers"
value={selectedMonitor}
onChange={handleSelect}
items={Object.values(monitors)}
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
}}
/>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Button
variant="group"
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
All
</Button>
<Button
variant="group"
filled={(filter === "down").toString()}
onClick={() => setFilter("down")}
Filter by:
</Typography>
<ButtonGroup
sx={{
ml: "auto",
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
borderColor: theme.palette.primary.lowContrast,
},
}}
>
Down
</Button>
<Button
variant="group"
filled={(filter === "resolve").toString()}
onClick={() => setFilter("resolve")}
<Button
variant="group"
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
>
All
</Button>
<Button
variant="group"
filled={(filter === "down").toString()}
onClick={() => setFilter("down")}
>
Down
</Button>
<Button
variant="group"
filled={(filter === "resolve").toString()}
onClick={() => setFilter("resolve")}
>
Cannot resolve
</Button>
</ButtonGroup>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
Cannot Resolve
</Button>
</ButtonGroup>
Show:
</Typography>
<ButtonGroup
sx={{
ml: "auto",
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
borderColor: theme.palette.primary.lowContrast,
},
}}
>
<Button
variant="group"
filled={(dateRange === "hour").toString()}
onClick={() => setDateRange("hour")}
>
Last hour
</Button>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Last day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Last week
</Button>
<Button
variant="group"
filled={(dateRange === "all").toString()}
onClick={() => setDateRange("all")}
>
All
</Button>
</ButtonGroup>
</Stack>
</Stack>
<IncidentTable
monitors={monitors}
selectedMonitor={selectedMonitor}
filter={filter}
dateRange={dateRange}
/>
</>
)}
+4 -3
View File
@@ -13,7 +13,7 @@ import { Box, Button, IconButton, Stack } from "@mui/material";
import Breadcrumbs from "../../Components/Breadcrumbs";
import { StatusLabel } from "../../Components/Label";
import { Heading } from "../../Components/Heading";
import { Pagination } from "../../Components/Table/TablePagination/index.jsx";
import Pagination from "../../Components/Table/TablePagination/index.jsx";
// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { networkService } from "../../Utils/NetworkService.js";
import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx";
@@ -150,7 +150,7 @@ function Infrastructure() {
const monitorsAsRows = monitors.map((monitor) => {
const processor =
((monitor.checks[0]?.cpu?.usage_frequency ?? 0) / 1000).toFixed(2) + " GHz";
((monitor.checks[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz";
const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100;
const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100;
const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100;
@@ -269,7 +269,8 @@ function Infrastructure() {
/>
<Pagination
monitorCount={summary?.totalMonitors ?? 0}
itemCount={summary?.totalMonitors ?? 0}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
@@ -412,7 +412,7 @@ const CreateMaintenance = () => {
backgroundColor: theme.palette.accent.light, // Hover background
},
"&.Mui-disabled": {
color: theme.palette.primary.lowContrast, // Disabled day color
color: theme.palette.primary.ContrastTextTertiary, // Disabled day color
},
},
"& .MuiDayCalendar-weekDayLabel": {
@@ -1,34 +1,16 @@
import PropTypes from "prop-types";
import {
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Paper,
Box,
TablePagination,
Stack,
Typography,
Button,
} from "@mui/material";
import { Box } from "@mui/material";
import DataTable from "../../../Components/Table";
import Pagination from "../../../Components/Table/TablePagination";
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 { memo } 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).
*
@@ -41,65 +23,6 @@ import dayjs from "dayjs";
*
* @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,
@@ -111,13 +34,102 @@ const MaintenanceTable = ({
updateCallback,
}) => {
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
const theme = useTheme();
console.log(rowsPerPage);
const dispatch = useDispatch();
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
dispatch(
setRowsPerPage({
value: parseInt(event.target.value, 10),
table: "maintenance",
})
);
setPage(0);
};
const headers = [
{
id: "name",
content: (
<Box onClick={() => handleSort("name")}>
Maintenance Window Name
<span
style={{
visibility: sort.field === "name" ? "visible" : "hidden",
}}
>
{sort.order === "asc" ? (
<ArrowUpwardRoundedIcon />
) : (
<ArrowDownwardRoundedIcon />
)}
</span>
</Box>
),
render: (row) => row.name,
},
{
id: "status",
content: (
<Box onClick={() => handleSort("status")}>
{" "}
Status
<span
style={{
visibility: sort.field === "active" ? "visible" : "hidden",
}}
>
{sort.order === "asc" ? (
<ArrowUpwardRoundedIcon />
) : (
<ArrowDownwardRoundedIcon />
)}
</span>
</Box>
),
render: (row) => {
const status = row.active ? "up" : "paused";
const text = row.active ? "active" : "paused";
return (
<StatusLabel
status={status}
text={text}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
id: "nextWindow",
content: "Next window",
render: (row) => {
return getTimeToNextWindow(row.start, row.end, row.repeat);
},
},
{
id: "repeat",
content: "Repeat",
render: (row) => {
return row.repeat === 0 ? "N/A" : formatDurationRounded(row.repeat);
},
},
{
id: "actions",
content: "Actions",
render: (row) => (
<ActionsMenu
maintenanceWindow={row}
updateCallback={updateCallback}
/>
),
},
];
const getTimeToNextWindow = (startTime, endTime, repeat) => {
//1. Advance time closest to next window as possible
const now = dayjs();
@@ -153,26 +165,6 @@ const MaintenanceTable = ({
}
};
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) {
@@ -183,165 +175,21 @@ const MaintenanceTable = ({
setSort({ field, order });
};
console.log(handleChangePage);
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.primary.main,
},
},
},
}}
sx={{
mt: theme.spacing(6),
color: theme.palette.primary.contrastTextSecondary,
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.3,
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
}}
/>
</Stack>
<DataTable
headers={headers}
data={maintenanceWindows}
/>
<Pagination
itemCount={maintenanceWindowCount}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</>
);
};
+10 -10
View File
@@ -108,16 +108,16 @@ const CreateMonitor = () => {
}
if (monitor.type === "http") {
const checkEndpointAction = await dispatch(
checkEndpointResolution({ authToken, monitorURL: form.url })
);
if (checkEndpointAction.meta.requestStatus === "rejected") {
createToast({
body: "The endpoint you entered doesn't resolve. Check the URL again.",
});
setErrors({ url: "The entered URL is not reachable." });
return;
}
// const checkEndpointAction = await dispatch(
// checkEndpointResolution({ authToken, monitorURL: form.url })
// );
// if (checkEndpointAction.meta.requestStatus === "rejected") {
// createToast({
// body: "The endpoint you entered doesn't resolve. Check the URL again.",
// });
// setErrors({ url: "The entered URL is not reachable." });
// return;
// }
}
form = {
+3 -2
View File
@@ -5,7 +5,7 @@ import SkeletonLayout from "./skeleton";
import Fallback from "./fallback";
import StatusBox from "./StatusBox";
import UptimeDataTable from "./UptimeDataTable";
import { Pagination } from "../../../Components/Table/TablePagination";
import Pagination from "../../../Components/Table/TablePagination";
// Utils
import { useTheme } from "@emotion/react";
@@ -225,7 +225,8 @@ const UptimeMonitors = () => {
triggerUpdate={triggerUpdate}
/>
<Pagination
monitorCount={totalMonitors}
itemCount={totalMonitors}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
-5
View File
@@ -103,11 +103,6 @@ const monitorValidation = joi.object({
"(?:(?:https?|ftp):\\/\\/)?" +
// user:pass BasicAuth (optional)
"(?:" +
// IP address exclusion
// private & local networks
"(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
"(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
"(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
+5 -6
View File
@@ -44,7 +44,7 @@ class CheckController {
}
};
getChecks = async (req, res, next) => {
getChecksByMonitor = async (req, res, next) => {
try {
await getChecksParamValidation.validateAsync(req.params);
await getChecksQueryValidation.validateAsync(req.query);
@@ -54,19 +54,18 @@ class CheckController {
}
try {
const checks = await this.db.getChecks(req);
const checksCount = await this.db.getChecksCount(req);
const result = await this.db.getChecksByMonitor(req);
return res.success({
msg: successMessages.CHECK_GET,
data: { checksCount, checks },
data: result,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getChecks"));
}
};
getTeamChecks = async (req, res, next) => {
getChecksByTeam = async (req, res, next) => {
try {
await getTeamChecksParamValidation.validateAsync(req.params);
await getTeamChecksQueryValidation.validateAsync(req.query);
@@ -75,7 +74,7 @@ class CheckController {
return;
}
try {
const checkData = await this.db.getTeamChecks(req);
const checkData = await this.db.getChecksByTeam(req);
return res.success({
msg: successMessages.CHECK_GET,
+12 -1
View File
@@ -20,7 +20,7 @@ import { getTokenFromHeaders } from "../utils/utils.js";
import logger from "../utils/logger.js";
import { handleError, handleValidationError } from "./controllerUtils.js";
import axios from "axios";
import seedDb from "../db/mongo/utils/seedDb.js";
const SERVICE_NAME = "monitorController";
class MonitorController {
@@ -495,6 +495,17 @@ class MonitorController {
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
}
};
seedDb = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = this.settingsService.getSettings();
const { _id, teamId } = jwt.verify(token, jwtSecret);
await seedDb(_id, teamId);
} catch (error) {
next(handleError(error, SERVICE_NAME, "seedDb"));
}
};
}
export default MonitorController;
+9
View File
@@ -12,6 +12,13 @@ const BaseCheckSchema = mongoose.Schema({
immutable: true,
index: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
index: true,
},
/**
* Status of the check (true for up, false for down).
*
@@ -69,6 +76,8 @@ const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: tr
CheckSchema.index({ createdAt: 1 });
CheckSchema.index({ monitorId: 1, createdAt: 1 });
CheckSchema.index({ monitorId: 1, createdAt: -1 });
CheckSchema.index({ teamId: 1, createdAt: -1 });
CheckSchema.index({ teamId: 1 });
export default mongoose.model("Check", CheckSchema);
export { BaseCheckSchema };
+81 -91
View File
@@ -2,11 +2,15 @@ import Check from "../../models/Check.js";
import Monitor from "../../models/Monitor.js";
import User from "../../models/User.js";
import logger from "../../../utils/logger.js";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
all: undefined,
};
/**
@@ -56,41 +60,6 @@ const createCheck = async (checkData) => {
}
};
const getChecksCount = async (req) => {
const monitorId = req.params.monitorId;
const dateRange = req.query.dateRange;
const filter = req.query.filter;
// Build query
const checksQuery = { monitorId: monitorId };
// Filter checks by "day", "week", or "month"
if (dateRange !== undefined) {
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
}
if (filter !== undefined) {
checksQuery.status = false;
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
checksQuery.statusCode = 5000;
break;
default:
logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecksCount",
});
break;
}
}
const count = await Check.countDocuments(checksQuery);
return count;
};
/**
* Get all checks for a monitor
* @async
@@ -98,33 +67,31 @@ const getChecksCount = async (req) => {
* @returns {Promise<Array<Check>>}
* @throws {Error}
*/
const getChecks = async (req) => {
const getChecksByMonitor = async (req) => {
try {
const { monitorId } = req.params;
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
// Default limit to 0 if not provided
limit = limit === undefined ? 0 : limit;
let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
// Match
const matchStage = {
monitorId: ObjectId.createFromHexString(monitorId),
status: false,
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
},
}),
};
// Default sort order is newest -> oldest
sortOrder = sortOrder === "asc" ? 1 : -1;
// Build query
const checksQuery = { monitorId: monitorId };
// Filter checks by "day", "week", or "month"
if (dateRange !== undefined) {
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
}
// Filter checks by status
if (filter !== undefined) {
checksQuery.status = false;
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
checksQuery.statusCode = 5000;
matchStage.statusCode = 5000;
break;
default:
logger.warn({
@@ -136,16 +103,33 @@ const getChecks = async (req) => {
}
}
// Need to skip and limit here
//Sort
sortOrder = sortOrder === "asc" ? 1 : -1;
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const checks = await Check.find(checksQuery)
.skip(skip)
.limit(rowsPerPage)
.sort({ createdAt: sortOrder });
return checks;
const checks = await Check.aggregate([
{ $match: matchStage },
{ $sort: { createdAt: sortOrder } },
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
checksCount: { $arrayElemAt: ["$summary.checksCount", 0] },
checks: "$checks",
},
},
]);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecks";
@@ -153,64 +137,71 @@ const getChecks = async (req) => {
}
};
const getTeamChecks = async (req) => {
const getChecksByTeam = async (req) => {
try {
let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const { teamId } = req.params;
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
// Get monitorIDs
const userMonitors = await Monitor.find({ teamId: teamId }).select("_id");
//Build check query
// Default limit to 0 if not provided
limit = limit === undefined ? 0 : limit;
// Default sort order is newest -> oldest
sortOrder = sortOrder === "asc" ? 1 : -1;
const checksQuery = { monitorId: { $in: userMonitors } };
const matchStage = {
teamId: ObjectId.createFromHexString(teamId),
status: false,
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
},
}),
};
// Add filter to match stage
if (filter !== undefined) {
checksQuery.status = false;
switch (filter) {
case "all":
break;
case "down":
checksQuery.status = false;
break;
case "resolve":
checksQuery.statusCode = 5000;
matchStage.statusCode = 5000;
break;
default:
logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getTeamChecks",
method: "getChecksByTeam",
});
break;
}
}
if (dateRange !== undefined) {
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
}
sortOrder = sortOrder === "asc" ? 1 : -1;
// Skip and limit for pagination
// pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const checksCount = await Check.countDocuments(checksQuery);
const checks = await Check.aggregate([
{ $match: matchStage },
{ $sort: { createdAt: sortOrder } },
const checks = await Check.find(checksQuery)
.skip(skip)
.limit(rowsPerPage)
.sort({ createdAt: sortOrder })
.select(["monitorId", "status", "responseTime", "statusCode", "message"]);
return { checksCount, checks };
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
checksCount: { $arrayElemAt: ["$summary.checksCount", 0] },
checks: "$checks",
},
},
]);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getTeamChecks";
error.method = "getChecksByTeam";
throw error;
}
};
@@ -298,9 +289,8 @@ const updateChecksTTL = async (teamId, ttl) => {
export {
createCheck,
getChecksCount,
getChecks,
getTeamChecks,
getChecksByMonitor,
getChecksByTeam,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
+57
View File
@@ -0,0 +1,57 @@
import Monitor from "../../models/Monitor.js";
import Check from "../../models/Check.js";
const generateRandomUrl = () => {
const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"];
const paths = ["api", "status", "health", "ping", "check"];
return `https://${domains[Math.floor(Math.random() * domains.length)]}/${paths[Math.floor(Math.random() * paths.length)]}`;
};
const generateChecks = (monitorId, teamId, count) => {
const checks = [];
const endTime = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago
const startTime = new Date(endTime - count * 60 * 1000); // count minutes before endTime
for (let i = 0; i < count; i++) {
const timestamp = new Date(startTime.getTime() + i * 60 * 1000);
const status = Math.random() > 0.05; // 95% chance of being up
checks.push({
monitorId,
teamId,
status,
responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms
createdAt: timestamp,
updatedAt: timestamp,
});
}
return checks;
};
const seedDb = async (userId, teamId) => {
try {
console.log("Deleting all monitors and checks");
await Monitor.deleteMany({});
await Check.deleteMany({});
console.log("Adding monitors");
for (let i = 0; i < 300; i++) {
const monitor = await Monitor.create({
name: `Monitor ${i}`,
url: generateRandomUrl(),
type: "http",
userId,
teamId,
interval: 60000,
active: false,
});
console.log(`Adding monitor and checks for monitor ${i}`);
const checks = generateChecks(monitor._id, teamId, 10000);
await Check.insertMany(checks);
}
} catch (error) {
console.error(error);
}
};
export default seedDb;
+14 -14
View File
@@ -11,7 +11,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.35.0",
"bullmq": "5.37.0",
"cors": "^2.8.5",
"dockerode": "4.0.4",
"dotenv": "^16.4.5",
@@ -324,9 +324,9 @@
"dev": true
},
"node_modules/@eslint/js": {
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz",
"integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==",
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1748,9 +1748,9 @@
}
},
"node_modules/bullmq": {
"version": "5.35.0",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.35.0.tgz",
"integrity": "sha512-4wOFPv5oVkcK5A11cwsk5Zzc7Hz7xFe2rHIQfsg0UmWjtwTj/nc7+vPjcMGML6jdl7WejuHXnMeop4uxxo/3WA==",
"version": "5.37.0",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.37.0.tgz",
"integrity": "sha512-h/wf979+9uROyYpB8oTE44Py6JERCluCSd+ZFpCZlPsYh+wxAkqrfHsHHHKBgsNJp9odWLIY4SG+280EXzXiCQ==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
@@ -3071,9 +3071,9 @@
}
},
"node_modules/eslint": {
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz",
"integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==",
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3082,7 +3082,7 @@
"@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.10.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.18.0",
"@eslint/js": "9.19.0",
"@eslint/plugin-kit": "^0.2.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -5848,9 +5848,9 @@
"license": "MIT"
},
"node_modules/nodemailer": {
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz",
"integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
+1 -1
View File
@@ -18,7 +18,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.35.0",
"bullmq": "5.37.0",
"cors": "^2.8.5",
"dockerode": "4.0.4",
"dotenv": "^16.4.5",
+2 -2
View File
@@ -11,7 +11,7 @@ class CheckRoutes {
}
initRoutes() {
this.router.get("/:monitorId", this.checkController.getChecks);
this.router.get("/:monitorId", this.checkController.getChecksByMonitor);
this.router.post(
"/:monitorId",
verifyOwnership(Monitor, "monitorId"),
@@ -23,7 +23,7 @@ class CheckRoutes {
this.checkController.deleteChecks
);
this.router.get("/team/:teamId", this.checkController.getTeamChecks);
this.router.get("/team/:teamId", this.checkController.getChecksByTeam);
this.router.delete(
"/team/:teamId",
+2
View File
@@ -75,6 +75,8 @@ class MonitorRoutes {
isAllowed(["admin", "superadmin"]),
this.monitorController.addDemoMonitors
);
this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb);
}
getRouter() {
+1
View File
@@ -5,6 +5,7 @@ const QUEUE_LOOKUP = {
hardware: "hardware",
http: "uptime",
ping: "uptime",
port: "uptime",
docker: "uptime",
pagespeed: "pagespeed",
};
+1
View File
@@ -131,6 +131,7 @@ class NetworkService {
const httpResponse = {
monitorId: job.data._id,
teamId: job.data.teamId,
type: job.data.type,
responseTime,
payload: response?.data,
+2 -1
View File
@@ -83,10 +83,11 @@ class StatusService {
* @returns {Object} The check object.
*/
buildCheck = (networkResponse) => {
const { monitorId, type, status, responseTime, code, message, payload } =
const { monitorId, teamId, type, status, responseTime, code, message, payload } =
networkResponse;
const check = {
monitorId,
teamId,
status,
statusCode: code,
responseTime,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -131,7 +131,7 @@ const getMonitorByIdQueryValidation = joi.object({
status: joi.boolean(),
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("day", "week", "month"),
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
numToDisplay: joi.number(),
normalize: joi.boolean(),
});
@@ -166,7 +166,7 @@ const getMonitorStatsByIdQueryValidation = joi.object({
status: joi.string(),
limit: joi.number(),
sortOrder: joi.string().valid("asc", "desc"),
dateRange: joi.string().valid("day", "week", "month", "all"),
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
numToDisplay: joi.number(),
normalize: joi.boolean(),
});
@@ -217,7 +217,7 @@ const getHardwareDetailsByIdParamValidation = joi.object({
});
const getHardwareDetailsByIdQueryValidation = joi.object({
dateRange: joi.string().valid("day", "week", "month"),
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
});
//****************************************
@@ -288,7 +288,7 @@ const getChecksParamValidation = joi.object({
const getChecksQueryValidation = joi.object({
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("day", "week", "month"),
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
filter: joi.string().valid("all", "down", "resolve"),
page: joi.number(),
rowsPerPage: joi.number(),
@@ -301,7 +301,7 @@ const getTeamChecksParamValidation = joi.object({
const getTeamChecksQueryValidation = joi.object({
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("day", "week", "month"),
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
filter: joi.string().valid("all", "down", "resolve"),
page: joi.number(),
rowsPerPage: joi.number(),
Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

+35
View File
@@ -0,0 +1,35 @@
---
icon: clock-nine
---
# Maintenance mode
The Maintenance mode allows you to manage and schedule maintenance periods for monitored URLs. During a maintenance window, no pings or HTTP requests are sent to the specified URLs, preventing downtime alerts during scheduled maintenance activities.
### Creating a new maintenance window
When you click the **"Create Maintenance Window"** button, you will see a page similar to this:
<figure><img src="../.gitbook/assets/Screenshot 2024-10-10 at 10.37.36 PM.png" alt=""><figcaption></figcaption></figure>
Here's how to set up a new maintenance window:
**General settings:**
* **Maintenance repeat**: Select how often the maintenance window should repeat (e.g., don't repeat, daily, weekly).
* **Date**: Select the date the maintenance window should start.
* **Start time**: Set the time of day the window begins.&#x20;
* **Duration**: Specify how long the maintenance window should last (in seconds, minutes, or hours).
**Monitor related settings:**
* **Friendly Name**: Provide a descriptive name for the maintenance window (e.g., "Maintenance at 10:37 PM for 30 minutes").
* **Add Monitors**: Select the specific monitors (URLs) for which this maintenance window will apply by typing and selecting from the list.
Once all fields are filled, click **Create maintenance** to save the new window.
### Editing a maintenance window
* To edit an existing window, click the gear icon under **Actions** in the table view.
* Update the required fields and save your changes.
+6
View File
@@ -0,0 +1,6 @@
{
"name": "bluewave-uptime",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}