Merge branch 'develop' into feat/fe/statuspage-3

This commit is contained in:
Shemy Gan committed 2025-01-28 15:05:29 -05:00
commit 12b14e1938
110 files changed
+3944 -4077

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.
+1
View File
@@ -0,0 +1 @@
package-lock=false
+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.
+31 -31
View File
@@ -16,9 +16,9 @@
"@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",
"@reduxjs/toolkit": "2.5.0",
"@mui/x-data-grid": "7.24.1",
"@mui/x-date-pickers": "7.24.1",
"@reduxjs/toolkit": "2.5.1",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"immutability-helper": "^3.1.1",
@@ -31,7 +31,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
"recharts": "2.15.0",
"recharts": "2.15.1",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
@@ -1419,15 +1419,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",
@@ -1477,14 +1477,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"
@@ -1514,14 +1514,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",
@@ -1580,9 +1580,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",
@@ -1720,9 +1720,9 @@
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
"integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz",
"integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==",
"license": "MIT",
"dependencies": {
"immer": "^10.0.3",
@@ -5552,9 +5552,9 @@
}
},
"node_modules/react-smooth": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz",
"integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
"license": "MIT",
"dependencies": {
"fast-equals": "^5.0.1",
@@ -5562,8 +5562,8 @@
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-toastify": {
@@ -5596,16 +5596,16 @@
}
},
"node_modules/recharts": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
"integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
"integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^18.3.1",
"react-smooth": "^4.0.0",
"react-smooth": "^4.0.4",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
+4 -4
View File
@@ -19,9 +19,9 @@
"@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",
"@reduxjs/toolkit": "2.5.0",
"@mui/x-data-grid": "7.24.1",
"@mui/x-date-pickers": "7.24.1",
"@reduxjs/toolkit": "2.5.1",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"immutability-helper": "^3.1.1",
@@ -34,7 +34,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
"recharts": "2.15.0",
"recharts": "2.15.1",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
-360
View File
@@ -1,360 +0,0 @@
/* TODO
Should we delete this?
*/
import PropTypes from "prop-types";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
import {
TableContainer,
Paper,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
TablePagination,
Box,
Typography,
Stack,
Button,
} from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { setRowsPerPage } from "../../Features/UI/uiSlice";
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 "./index.css";
/**
* 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,
};
/**
* BasicTable Component
* Renders a table with optional pagination.
*
* @component
* @param {Object} props - Component props.
* @param {Object} props.data - Data for the table including columns and rows.
* @param {Array} props.data.cols - Array of objects for column headers.
* @param {number} props.data.cols[].id - Unique identifier for the column.
* @param {string} props.data.cols[].name - Name of the column to display as header.
* @param {Array} props.data.rows - Array of row objects.
* @param {number} props.data.rows[].id - Unique identifier for the row.
* @param {Array} props.data.rows[].data - Array of cell data objects for the row.
* @param {number} props.data.rows[].data[].id - Unique identifier for the cell.
* @param {JSX.Element} props.data.rows[].data[].data - The content to display in the cell.
* @param {function} props.data.rows.data.handleClick - Function to call when the row is clicked.
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
* @param {number} props.rowsPerPage- Number of rows per page (table).
* @param {string} props.emptyMessage - Message to display when there is no data.
* @example
* const data = {
* cols: [
* { id: 1, name: "First Col" },
* { id: 2, name: "Second Col" },
* { id: 3, name: "Third Col" },
* { id: 4, name: "Fourth Col" },
* ],
* rows: [
* {
* id: 1,
* data: [
* { id: 1, data: <div>Data for Row 1 Col 1</div> },
* { id: 2, data: <div>Data for Row 1 Col 2</div> },
* { id: 3, data: <div>Data for Row 1 Col 3</div> },
* { id: 4, data: <div>Data for Row 1 Col 4</div> },
* ],
* },
* {
* id: 2,
* data: [
* { id: 5, data: <div>Data for Row 2 Col 1</div> },
* { id: 6, data: <div>Data for Row 2 Col 2</div> },
* { id: 7, data: <div>Data for Row 2 Col 3</div> },
* { id: 8, data: <div>Data for Row 2 Col 4</div> },
* ],
* },
* ],
* };
*
* <BasicTable data={data} rows={rows} paginated={true} />
*/
const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => {
const DEFAULT_ROWS_PER_PAGE = 5;
const theme = useTheme();
const dispatch = useDispatch();
const uiState = useSelector((state) => state.ui);
let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE;
const [page, setPage] = useState(0);
useEffect(() => {
setPage(0);
}, [data]);
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
dispatch(
setRowsPerPage({
value: parseInt(event.target.value, 10),
table: table,
})
);
setPage(0);
};
let displayData = [];
if (data && data.rows) {
let rows = reversed ? [...data.rows].reverse() : data.rows;
displayData = paginated
? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
: rows;
}
if (!data || !data.cols || !data.rows) {
return <div>No data</div>;
}
/**
* 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, data.rows.length);
return `${start} - ${end}`;
};
return (
<>
<TableContainer component={Paper}>
<Table
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(12),
},
"& th": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
/*
TODO all general styling for th should live here
*/
},
"& td": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
/*
TODO all general styling for td should live here
*/
},
}}
>
<TableHead>
<TableRow>
{data.cols.map((col) => (
<TableCell key={col.id}>{col.name}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{displayData.map((row) => {
return (
<TableRow
sx={{
cursor: row.handleClick ? "pointer" : "default",
"&:hover": {
filter: "brightness(.75)",
opacity: 0.75,
transition: "filter 0.3s ease, opacity 0.3s ease",
},
}}
key={row.id}
onClick={row.handleClick ? row.handleClick : null}
>
{row.data.map((cell) => {
return <TableCell key={cell.id}>{cell.data}</TableCell>;
})}
</TableRow>
);
})}
{displayData.length === 0 && (
<TableRow>
<TableCell
sx={{ textAlign: "center" }}
colSpan={data.cols.length}
>
{emptyMessage}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{paginated && (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
sx={{
"& p": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Typography
px={theme.spacing(2)}
fontSize={12}
sx={{ opacity: 0.7 }}
>
Showing {getRange()} of {data.rows.length} monitor(s)
</Typography>
<TablePagination
component="div"
count={data.rows.length}
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,
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>
)}
</>
);
};
BasicTable.propTypes = {
data: PropTypes.object.isRequired,
paginated: PropTypes.bool,
reversed: PropTypes.bool,
rowPerPage: PropTypes.number,
table: PropTypes.string,
emptyMessage: PropTypes.string,
};
export default BasicTable;
@@ -0,0 +1,29 @@
import { Box } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const CircularCount = ({ count }) => {
const theme = useTheme();
return (
<Box
component="span"
color={theme.palette.tertiary.contrastText}
border={2}
borderColor={theme.palette.accent.main}
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "50rem",
fontSize: "12px",
fontWeight: 500,
}}
>
{count}
</Box>
);
};
CircularCount.propTypes = {
count: PropTypes.number,
};
export default CircularCount;
@@ -0,0 +1,33 @@
import { Stack, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import SkeletonLayout from "./skeleton";
const CreateMonitorHeader = ({ isAdmin, shouldRender, path }) => {
const navigate = useNavigate();
if (!isAdmin) return null;
if (!shouldRender) return <SkeletonLayout />;
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
>
<Button
variant="contained"
color="accent"
onClick={() => navigate(path)}
>
Create new
</Button>
</Stack>
);
};
export default CreateMonitorHeader;
CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
shouldRender: PropTypes.bool.isRequired,
path: PropTypes.string.isRequired,
};
@@ -0,0 +1,19 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
>
<Skeleton
variant="rectangular"
width={100}
height={36}
/>
</Stack>
);
};
export default SkeletonLayout;
+23
View File
@@ -0,0 +1,23 @@
import PropTypes from "prop-types";
const Dot = ({ color = "gray", size = "4px" }) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
}}
/>
);
};
Dot.propTypes = {
color: PropTypes.string,
size: PropTypes.string,
};
export default Dot;
+58 -47
View File
@@ -26,58 +26,69 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false }) =>
const mode = useSelector((state) => state.ui.mode);
return (
<Stack
className={`fallback__${title.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
<Box
className="page-speed"
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
borderStyle="dashed"
backgroundColor={theme.palette.primary.main}
overflow="hidden"
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={"300px"}
zIndex={1}
className={`fallback__${title.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
<Typography
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={"300px"}
zIndex={1}
>
<Typography
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
>
Let's create your first{title}
</Button>
)}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
>
Let's create your {title}
</Button>
)}
</Stack>
</Box>
);
};
@@ -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;
@@ -113,6 +113,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 && (
@@ -174,6 +182,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,
@@ -0,0 +1,40 @@
import { Stack } from "@mui/material";
import { Heading } from "../Heading";
import CircularCount from "../CircularCount";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import SkeletonLayout from "./skeleton";
const MonitorCountHeader = ({
shouldRender = true,
monitorCount,
heading,
sx,
children,
}) => {
const theme = useTheme();
if (!shouldRender) return <SkeletonLayout />;
return (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
sx={{ ...sx }}
>
<Heading component="h2">{heading}</Heading>
<CircularCount count={monitorCount} />
{children}
</Stack>
);
};
MonitorCountHeader.propTypes = {
shouldRender: PropTypes.bool,
monitorCount: PropTypes.number,
heading: PropTypes.string,
children: PropTypes.node,
sx: PropTypes.object,
};
export default MonitorCountHeader;
@@ -0,0 +1,26 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Skeleton
variant="text"
width={100}
height={32}
/>
<Skeleton
variant="circular"
width={40}
height={40}
/>
</Stack>
);
};
export default SkeletonLayout;
+1 -1
View File
@@ -1,7 +1,7 @@
import { Box, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import useUtils from "../../Pages/Uptime/utils";
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
/**
* StatBox Component
@@ -5,11 +5,12 @@ 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.
paginationLabel: PropTypes.string, // Label for the 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 +19,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 +28,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 +38,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 +54,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 +124,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) {
@@ -48,26 +39,29 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
if (selectedMonitor === "0") {
res = await networkService.getChecksByTeam({
authToken: authToken,
status: false,
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({
authToken: authToken,
status: false,
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 +71,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 +121,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 +163,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 +181,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}
/>
</>
)}
@@ -1,6 +1,6 @@
import { Box, Stack, Typography } from "@mui/material";
import TextInput from "../../../../Components/Inputs/TextInput";
import Checkbox from "../../../../Components/Inputs/Checkbox";
import TextInput from "../../../../../Components/Inputs/TextInput";
import Checkbox from "../../../../../Components/Inputs/Checkbox";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
@@ -22,7 +22,7 @@ import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import { CustomThreshold } from "./CustomThreshold";
import { CustomThreshold } from "./Components/CustomThreshold";
const SELECT_VALUES = [
{ _id: 0.25, name: "15 seconds" },
@@ -8,7 +8,7 @@ import AreaChart from "../../../Components/Charts/AreaChart";
import { useSelector } from "react-redux";
import { networkService } from "../../../main";
import PulseDot from "../../../Components/Animated/PulseDot";
import useUtils from "../../Uptime/utils";
import useUtils from "../../Uptime/Monitors/Hooks/useUtils";
import { useNavigate } from "react-router-dom";
import Empty from "./empty";
import { logger } from "../../../Utils/Logger";
@@ -626,7 +626,7 @@ const InfrastructureDetails = () => {
</Stack>
<Stack
direction={"row"}
height={chartContainerHeight} // FE team HELP!
// height={chartContainerHeight} // FE team HELP! Possibly no longer needed?
gap={theme.spacing(8)} // FE team HELP!
flexWrap="wrap" // //FE team HELP! Better way to do this?
sx={{
+15 -17
View File
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { /* useDispatch, */ useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import useUtils from "../Uptime/utils.jsx";
import useUtils from "../Uptime/Monitors/Hooks/useUtils.jsx";
import { jwtDecode } from "jwt-decode";
import SkeletonLayout from "./skeleton";
import Fallback from "../../Components/Fallback";
@@ -13,11 +13,11 @@ 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";
import Host from "../Uptime/Home/host.jsx";
import Host from "../Uptime/Monitors/Components/Host/index.jsx";
import { useIsAdmin } from "../../Hooks/useIsAdmin.js";
import { InfrastructureMenu } from "./components/Menu";
@@ -137,20 +137,18 @@ function Infrastructure() {
id: "actions",
content: "Actions",
render: (row) => (
<IconButton>
<InfrastructureMenu
monitor={row}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
</IconButton>
<InfrastructureMenu
monitor={row}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
),
},
];
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;
@@ -162,10 +160,9 @@ function Infrastructure() {
monitor.uptimePercentage < 0.25
? theme.palette.error.main
: monitor.uptimePercentage < 0.5
? theme.palette.percentage.uptimeFair
: monitor.uptimePercentage < 0.75
? theme.palette.percentage.uptimeGood
: theme.palette.success.lowContrast;
? theme.palette.warning.main
: theme.palette.success.main;
return {
id: monitor._id,
name: monitor.name,
@@ -243,7 +240,7 @@ function Infrastructure() {
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "50%",
borderRadius: "10000px",
fontSize: "12px",
fontWeight: 500,
}}
@@ -269,7 +266,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}
/>
</>
);
};
@@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import LoadingButton from "@mui/lab/LoadingButton";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import SkeletonLayout from "./skeleton";
import useUtils from "../../Uptime/utils";
import useUtils from "../../Uptime/Monitors/Hooks/useUtils";
import "./index.css";
import Dialog from "../../../Components/Dialog";
+1 -1
View File
@@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import PagespeedDetailsAreaChart from "./Charts/AreaChart";
import Checkbox from "../../../Components/Inputs/Checkbox";
import PieChart from "./Charts/PieChart";
import useUtils from "../../Uptime/utils";
import useUtils from "../../Uptime/Monitors/Hooks/useUtils";
import "./index.css";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import StatBox from "../../../Components/StatBox";
@@ -1,15 +1,16 @@
import PropTypes from "prop-types";
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
import { StatusLabel } from "../../Components/Label";
// import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react";
import PageSpeedIcon from "../../../../../assets/icons/page-speed.svg?react";
import { StatusLabel } from "../../../../../Components/Label";
import { Box, Grid, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router";
import { useTheme } from "@emotion/react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils";
import useUtils from "../Uptime/utils";
import { formatDateWithTz, formatDurationSplit } from "../../../../../Utils/timeUtils";
import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
import { useState } from "react";
import IconBox from "../../Components/IconBox";
import IconBox from "../../../../../Components/IconBox";
/**
* CustomToolTip displays a tooltip with formatted date and score information.
* @param {Object} props
@@ -0,0 +1,22 @@
import { Grid, Grid2 } from "@mui/material";
import Card from "../Card";
const MonitorGrid = ({ shouldRender, monitors }) => {
if (!shouldRender) return null;
return (
<Grid
container
spacing={12}
>
{monitors?.map((monitor) => (
<Card
monitor={monitor}
key={monitor._id}
/>
))}
</Grid>
);
};
export default MonitorGrid;
@@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
const useMonitorsFetch = ({ authToken, teamId }) => {
const [isLoading, setIsLoading] = useState(true);
const [monitors, setMonitors] = useState([]);
const [summary, setSummary] = useState({});
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authToken,
teamId: teamId,
limit: 10,
types: ["pagespeed"],
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
if (res?.data?.data?.filteredMonitors) {
setMonitors(res.data.data.filteredMonitors);
setSummary(res.data.data.summary);
}
} catch (error) {
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, teamId]);
return { isLoading, monitors, summary };
};
export default useMonitorsFetch;
@@ -0,0 +1,66 @@
// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Box } from "@mui/material";
import CreateMonitorHeader from "../../../Components/CreateMonitorHeader";
import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorGrid from "./Components/MonitorGrid";
import Fallback from "../../../Components/Fallback";
// Utils
import { useTheme } from "@emotion/react";
import { useSelector } from "react-redux";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import useMonitorsFetch from "./Hooks/useMonitorsFetch";
// Constants
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
const PageSpeed = () => {
const theme = useTheme();
const isAdmin = useIsAdmin();
const { user, authToken } = useSelector((state) => state.auth);
const { isLoading, monitors, summary } = useMonitorsFetch({
authToken: authToken,
teamId: user.teamId,
});
if (!isLoading && monitors?.length === 0) {
return (
<Fallback
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
link="/pagespeed/create"
isAdmin={isAdmin}
/>
);
}
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
path="/pagespeed/create"
/>
<MonitorCountHeader
shouldRender={!isLoading}
monitorCount={summary?.totalMonitors}
heading="PageSpeed monitors"
sx={{ mb: theme.spacing(8) }}
/>
<MonitorGrid
size={6}
shouldRender={!isLoading}
monitors={monitors}
/>
</Stack>
);
};
export default PageSpeed;
-8
View File
@@ -1,8 +0,0 @@
.page-speed .label {
padding: 7px;
height: 24px;
font-size: var(--env-var-font-size-small);
}
.page-speed:not(:has([class*="fallback__"])) button {
min-height: 34px;
}
-147
View File
@@ -1,147 +0,0 @@
import { Box, Button, Grid, Stack } from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useSelector } from "react-redux";
import Fallback from "../../Components/Fallback";
import "./index.css";
import { useNavigate } from "react-router";
import PropTypes from "prop-types";
import Breadcrumbs from "../../Components/Breadcrumbs";
import SkeletonLayout from "./skeleton";
import Card from "./card";
import { networkService } from "../../main";
import { Heading } from "../../Components/Heading";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
const PageSpeed = () => {
const theme = useTheme();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const { user, authToken } = useSelector((state) => state.auth);
const [isLoading, setIsLoading] = useState(true);
const [monitors, setMonitors] = useState([]);
const [summary, setSummary] = useState({});
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authToken,
teamId: user.teamId,
limit: 10,
types: ["pagespeed"],
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
if (res?.data?.data?.filteredMonitors) {
setMonitors(res.data.data.filteredMonitors);
setSummary(res.data.data.summary);
}
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, user.teamId]);
// will show skeletons only on initial load
// since monitor state is being added to redux persist, there's no reason to display skeletons on every render
let isActuallyLoading = isLoading && monitors?.length === 0;
return (
<Box
className="page-speed"
sx={{
':has(> [class*="fallback__"])': {
position: "relative",
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
borderStyle: "dashed",
backgroundColor: theme.palette.primary.main,
overflow: "hidden",
},
}}
>
{isActuallyLoading ? (
<SkeletonLayout />
) : monitors?.length !== 0 ? (
<Stack gap={theme.spacing(8)}>
<Box>
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
<Stack
direction="row"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
>
{isAdmin && (
<Button
variant="contained"
color="accent"
onClick={() => navigate("/pagespeed/create")}
sx={{ fontWeight: 500, whiteSpace: "nowrap" }}
>
Create new
</Button>
)}
</Stack>
</Box>
<Stack
direction="row"
sx={{
alignItems: "center",
gap: ".25rem",
flexWrap: "wrap",
}}
>
<Heading component="h2">PageSpeed monitors</Heading>
{/* TODO Correct the class current-monitors-counter, there are some unnecessary things there */}
<Box
component="span"
className="current-monitors-counter"
color={theme.palette.primary.contrastText}
border={1}
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.tertiary.main}
>
{summary?.totalMonitors ?? 0}
</Box>
</Stack>
<Grid
container
spacing={theme.spacing(12)}
>
{monitors?.map((monitor) => (
<Card
monitor={monitor}
key={monitor._id}
/>
))}
</Grid>
</Stack>
) : (
<Fallback
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
link="/pagespeed/create"
isAdmin={isAdmin}
/>
)}
</Box>
);
};
PageSpeed.propTypes = {
isAdmin: PropTypes.bool,
};
export default PageSpeed;
-73
View File
@@ -1,73 +0,0 @@
import { Box, Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
/**
* Renders a skeleton layout.
*
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack gap={theme.spacing(2)}>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.spacing(12)}
>
<Box width="80%">
<Skeleton
variant="rounded"
width="25%"
height={24}
/>
<Skeleton
variant="rounded"
width="50%"
height={19.5}
sx={{ mt: theme.spacing(2) }}
/>
</Box>
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(12)}
>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
</Stack>
</Stack>
);
};
export default SkeletonLayout;
+2 -2
View File
@@ -266,7 +266,7 @@ const Settings = () => {
open={isOpen.deleteStats}
theme={theme}
title="Do you want to clear all stats?"
description="Once deleted, this monitor cannot be retrieved."
description="Once deleted, your monitors cannot be retrieved."
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel="Yes, clear all stats"
onConfirm={handleClearStats}
@@ -279,7 +279,7 @@ const Settings = () => {
<Box>
<Typography component="h1">Demo monitors</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Here you can add and remove demo monitors
Here you can add and remove demo monitors.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
@@ -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 = {
@@ -235,53 +235,6 @@ const CreateMonitor = () => {
monitor
</Typography>
</Typography>
<ConfigBox>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the type of monitor.
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
type={monitor.type === "http" ? "url" : "text"}
id="monitor-url"
startAdornment={
monitor.type === "http" ? <HttpAdornment https={https} /> : null
}
label={monitorTypeMaps[monitor.type].label || "URL to monitor"}
https={https}
placeholder={monitorTypeMaps[monitor.type].placeholder || ""}
value={monitor.url}
onChange={(event) => handleChange(event, "url")}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<TextInput
type="number"
id="monitor-port"
label="Port to monitor"
placeholder="5173"
value={monitor.port}
onChange={(event) => handleChange(event, "port")}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port"}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={monitorTypeMaps[monitor.type].namePlaceholder || ""}
value={monitor.name}
onChange={(event) => handleChange(event, "name")}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Checks to perform</Typography>
@@ -363,6 +316,52 @@ const CreateMonitor = () => {
)}
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the type of monitor.
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
type={monitor.type === "http" ? "url" : "text"}
id="monitor-url"
startAdornment={
monitor.type === "http" ? <HttpAdornment https={https} /> : null
}
label={monitorTypeMaps[monitor.type].label || "URL to monitor"}
https={https}
placeholder={monitorTypeMaps[monitor.type].placeholder || ""}
value={monitor.url}
onChange={(event) => handleChange(event, "url")}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<TextInput
type="number"
id="monitor-port"
label="Port to monitor"
placeholder="5173"
value={monitor.port}
onChange={(event) => handleChange(event, "port")}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port"}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={monitorTypeMaps[monitor.type].namePlaceholder || ""}
value={monitor.name}
onChange={(event) => handleChange(event, "name")}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Incident notifications</Typography>
@@ -0,0 +1,140 @@
// Components
import { Stack, Typography, Box } from "@mui/material";
import ChartBox from "../Charts/ChartBox";
import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react";
import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react";
import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react";
import UpBarChart from "../Charts/UpBarChart";
import DownBarChart from "../Charts/DownBarChart";
import ResponseGaugeChart from "../Charts/ResponseGaugeChart";
import SkeletonLayout from "./skeleton";
// Utils
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const ChartBoxes = ({
shouldRender = true,
monitor,
dateRange,
uiTimezone,
dateFormat,
hoveredUptimeData,
setHoveredUptimeData,
hoveredIncidentsData,
setHoveredIncidentsData,
}) => {
const theme = useTheme();
if (!shouldRender) {
return <SkeletonLayout />;
}
return (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
<ChartBox
icon={<UptimeIcon />}
header="Uptime"
>
<Stack justifyContent="space-between">
<Box position="relative">
<Typography>Total Checks</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
: (monitor?.groupedUpChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
<Box>
<Typography>
{hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"}
</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
: Math.floor(
((monitor?.upChecks?.totalChecks ?? 0) /
(monitor?.totalChecks ?? 1)) *
100
)}
<Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"}
</Typography>
</Typography>
</Box>
</Stack>
<UpBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredUptimeData}
/>
</ChartBox>
<ChartBox
icon={<IncidentsIcon />}
header="Incidents"
>
<Box position="relative">
<Typography component="span">
{hoveredIncidentsData !== null
? hoveredIncidentsData.totalChecks
: (monitor?.groupedDownChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
<DownBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredIncidentsData}
/>
</ChartBox>
<ChartBox
icon={<AverageResponseIcon />}
header="Average Response Time"
>
<ResponseGaugeChart avgResponseTime={monitor.avgResponseTime ?? 0} />
</ChartBox>
</Stack>
);
};
export default ChartBoxes;
ChartBoxes.propTypes = {
monitor: PropTypes.object.isRequired,
dateRange: PropTypes.string.isRequired,
uiTimezone: PropTypes.string.isRequired,
dateFormat: PropTypes.string.isRequired,
hoveredUptimeData: PropTypes.object,
setHoveredUptimeData: PropTypes.func,
hoveredIncidentsData: PropTypes.object,
setHoveredIncidentsData: PropTypes.func,
};
@@ -0,0 +1,30 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -0,0 +1,75 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../../../../Components/IconBox";
import PropTypes from "prop-types";
const ChartBox = ({ children, icon, header, height = "300px" }) => {
const theme = useTheme();
return (
<Stack
sx={{
justifyContent: "space-between",
flex: "1 30%",
gap: theme.spacing(8),
height,
minWidth: 250,
padding: theme.spacing(8),
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 4,
backgroundColor: theme.palette.primary.main,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& .MuiBox-root:not(.area-tooltip) p": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 13,
},
"& .MuiBox-root > span": {
color: theme.palette.primary.contrastText,
fontSize: 20,
"& span": {
opacity: 0.8,
marginLeft: 2,
fontSize: 15,
},
},
"& .MuiStack-root": {
flexDirection: "row",
gap: theme.spacing(6),
},
"& .MuiStack-root:first-of-type": {
alignItems: "center",
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& path": {
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
);
};
export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node.isRequired,
header: PropTypes.string.isRequired,
height: PropTypes.string,
};
@@ -1,6 +1,6 @@
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../Utils/timeUtils";
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
@@ -34,8 +34,8 @@ CustomLabels.propTypes = {
x: PropTypes.number.isRequired,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
firstDataPoint: PropTypes.object.isRequired,
lastDataPoint: PropTypes.object.isRequired,
firstDataPoint: PropTypes.object,
lastDataPoint: PropTypes.object,
type: PropTypes.string.isRequired,
};
@@ -40,9 +40,9 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => {
y={0}
width="100%"
height="100%"
firstDataPoint={monitor.groupedDownChecks?.[0] ?? {}}
firstDataPoint={monitor?.groupedDownChecks?.[0] ?? {}}
lastDataPoint={
monitor.groupedDownChecks?.[monitor.groupedDownChecks.length - 1] ?? {}
monitor?.groupedDownChecks?.[monitor?.groupedDownChecks?.length - 1] ?? {}
}
type={type}
/>
@@ -53,7 +53,7 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => {
maxBarSize={7}
background={{ fill: "transparent" }}
>
{monitor.groupedDownChecks.map((entry, index) => {
{monitor?.groupedDownChecks?.map((entry, index) => {
return (
<Cell
key={`cell-${entry.time}`}
@@ -0,0 +1,31 @@
import ChartBox from "./ChartBox";
import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart";
import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
import SkeletonLayout from "./ResponseTimeChartSkeleton";
import PropTypes from "prop-types";
const ResponseTImeChart = ({ shouldRender = true, monitor, dateRange }) => {
if (!shouldRender) {
return <SkeletonLayout />;
}
return (
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
>
<MonitorDetailsAreaChart
checks={monitor.groupedChecks ?? []}
dateRange={dateRange}
/>
</ChartBox>
);
};
ResponseTImeChart.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object,
dateRange: PropTypes.string,
};
export default ResponseTImeChart;
@@ -0,0 +1,12 @@
import { Skeleton } from "@mui/material";
const ResponseTimeChartSkeleton = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
);
};
export default ResponseTimeChartSkeleton;
@@ -28,7 +28,7 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => {
<BarChart
width="100%"
height="100%"
data={monitor.groupedUpChecks}
data={monitor?.groupedUpChecks}
onMouseEnter={() => {
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 });
@@ -49,8 +49,10 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => {
y={0}
width="100%"
height="100%"
firstDataPoint={monitor.groupedUpChecks[0]}
lastDataPoint={monitor.groupedUpChecks[monitor.groupedUpChecks.length - 1]}
firstDataPoint={monitor?.groupedUpChecks?.[0]}
lastDataPoint={
monitor?.groupedUpChecks?.[monitor?.groupedUpChecks?.length - 1]
}
type={type}
/>
}
@@ -60,7 +62,7 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => {
maxBarSize={7}
background={{ fill: "transparent" }}
>
{monitor.groupedUpChecks.map((entry, index) => {
{monitor?.groupedUpChecks?.map((entry, index) => {
const themeColor = getThemeColor(entry.avgResponseTime);
return (
<Cell
@@ -0,0 +1,40 @@
import { Button, Box } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
const ConfigButton = ({ shouldRender, monitorId }) => {
const theme = useTheme();
const navigate = useNavigate();
if (!shouldRender) return null;
return (
<Box alignSelf="flex-end">
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/uptime/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
/* Should always be contrastText for the button color */
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
</Box>
);
};
ConfigButton.propTypes = {
shouldRender: PropTypes.bool,
monitorId: PropTypes.string,
};
export default ConfigButton;
@@ -0,0 +1,55 @@
import { Stack, Typography } from "@mui/material";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import Dot from "../../../../../Components/Dot";
import { useTheme } from "@emotion/react";
import useUtils from "../../../Monitors/Hooks/useUtils";
import { formatDurationRounded } from "../../../../../Utils/timeUtils";
import ConfigButton from "../ConfigButton";
import SkeletonLayout from "./skeleton";
import PropTypes from "prop-types";
const MonitorHeader = ({ shouldRender = true, isAdmin, monitor }) => {
const theme = useTheme();
const { statusColor, statusMsg, determineState } = useUtils();
console.log(shouldRender);
if (!shouldRender) {
return <SkeletonLayout />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Stack>
<Typography variant="h1">{monitor.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(2)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography variant="h2">
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Stack>
<ConfigButton
shouldRender={isAdmin}
monitorId={monitor._id}
/>
</Stack>
);
};
MonitorHeader.propTypes = {
shouldRender: PropTypes.bool,
isAdmin: PropTypes.bool,
monitor: PropTypes.object,
};
export default MonitorHeader;
@@ -0,0 +1,23 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -0,0 +1,89 @@
import ChartBox from "../Charts/ChartBox";
import PropTypes from "prop-types";
import HistoryIcon from "../../../../../assets/icons/history-icon.svg?react";
import Table from "../../../../../Components/Table";
import TablePagination from "../../../../../Components/Table/TablePagination";
import { StatusLabel } from "../../../../../Components/Label";
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
import SkeletonLayout from "./skeleton";
const ResponseTable = ({
shouldRender = true,
checks,
checksCount,
uiTimezone,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}) => {
if (!shouldRender) {
return <SkeletonLayout />;
}
const headers = [
{
id: "status",
content: "Status",
render: (row) => {
const status = row.status === true ? "up" : "down";
return (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
id: "date",
content: "Date & Time",
render: (row) =>
formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone),
},
{
id: "statusCode",
content: "Status code",
render: (row) => (row.statusCode ? row.statusCode : "N/A"),
},
{
id: "message",
content: "Message",
render: (row) => row.message,
},
];
return (
<ChartBox
icon={<HistoryIcon />}
header="Response Times"
height="100%"
>
<Table
headers={headers}
data={checks}
/>
<TablePagination
page={page}
handleChangePage={setPage}
rowsPerPage={rowsPerPage}
handleChangeRowsPerPage={setRowsPerPage}
itemCount={checksCount}
/>
</ChartBox>
);
};
ResponseTable.propTypes = {
shouldRender: PropTypes.bool,
checks: PropTypes.array.isRequired,
checksCount: PropTypes.number.isRequired,
uiTimezone: PropTypes.string.isRequired,
page: PropTypes.number.isRequired,
setPage: PropTypes.func.isRequired,
rowsPerPage: PropTypes.number.isRequired,
setRowsPerPage: PropTypes.func.isRequired,
};
export default ResponseTable;
@@ -0,0 +1,13 @@
import { Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
);
};
export default SkeletonLayout;
@@ -0,0 +1,83 @@
// Components
import { Stack, Typography } from "@mui/material";
import StatBox from "../../../../../Components/StatBox";
import SkeletonLayout from "./skeleton";
// Utils
import { useTheme } from "@mui/material/styles";
import useUtils from "../../../Monitors/Hooks/useUtils";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
import PropTypes from "prop-types";
const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => {
// Utils
const theme = useTheme();
const { determineState } = useUtils();
if (!shouldRender) {
return <SkeletonLayout />;
}
const { time: streakTime, units: streakUnits } = getHumanReadableDuration(
monitor?.uptimeStreak
);
const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration(
monitor?.timeSinceLastCheck
);
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
gradient={true}
status={determineState(monitor)}
heading={"active for"}
subHeading={
<>
{streakTime}
<Typography component="span">{streakUnits}</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{lastCheckTime}
<Typography component="span">{lastCheckUnits}</Typography>
<Typography component="span">{"ago"}</Typography>
</>
}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitor?.latestResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
</Stack>
);
};
StatusBoxes.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object,
certificateExpiry: PropTypes.string,
};
export default StatusBoxes;
@@ -0,0 +1,30 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -0,0 +1,58 @@
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
import { useTheme } from "@emotion/react";
import SkeletonLayout from "./skeleton";
import PropTypes from "prop-types";
const TimeFramePicker = ({ shouldRender = true, dateRange, setDateRange }) => {
const theme = useTheme();
if (!shouldRender) {
return <SkeletonLayout />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "day" ? "24 hours" : dateRange === "week" ? "7 days" : "30 days"}.
</Typography>
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
</Stack>
);
};
TimeFramePicker.propTypes = {
shouldRender: PropTypes.bool,
dateRange: PropTypes.string,
setDateRange: PropTypes.func,
};
export default TimeFramePicker;
@@ -0,0 +1,23 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -0,0 +1,46 @@
import { logger } from "../../../../Utils/Logger";
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { formatDateWithTz } from "../../../../Utils/timeUtils";
const useCertificateFetch = ({
monitor,
authToken,
monitorId,
certificateDateFormat,
uiTimezone,
}) => {
const [certificateExpiry, setCertificateExpiry] = useState("N/A");
const [certificateIsLoading, setCertificateIsLoading] = useState(false);
useEffect(() => {
const fetchCertificate = async () => {
if (monitor?.type !== "http") {
return;
}
try {
setCertificateIsLoading(true);
const res = await networkService.getCertificateExpiry({
authToken: authToken,
monitorId: monitorId,
});
if (res?.data?.data?.certificateDate) {
const date = res.data.data.certificateDate;
setCertificateExpiry(
formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A"
);
}
} catch (error) {
setCertificateExpiry("N/A");
logger.error(error);
} finally {
setCertificateIsLoading(false);
}
};
fetchCertificate();
}, [authToken, monitorId, certificateDateFormat, uiTimezone, monitor]);
return { certificateExpiry, certificateIsLoading };
};
export default useCertificateFetch;
@@ -0,0 +1,45 @@
import { useState } from "react";
import { useEffect } from "react";
import { logger } from "../../../../Utils/Logger";
import { networkService } from "../../../../main";
export const useChecksFetch = ({
authToken,
monitorId,
dateRange,
page,
rowsPerPage,
}) => {
const [checks, setChecks] = useState([]);
const [checksCount, setChecksCount] = useState(0);
const [checksAreLoading, setChecksAreLoading] = useState(false);
useEffect(() => {
const fetchChecks = async () => {
try {
setChecksAreLoading(true);
const res = await networkService.getChecksByMonitor({
authToken: authToken,
monitorId: monitorId,
sortOrder: "desc",
limit: null,
dateRange: dateRange,
filter: null,
page: page,
rowsPerPage: rowsPerPage,
});
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
logger.error(error);
} finally {
setChecksAreLoading(false);
}
};
fetchChecks();
}, [authToken, monitorId, dateRange, page, rowsPerPage]);
return { checks, checksCount, checksAreLoading };
};
export default useChecksFetch;
@@ -0,0 +1,34 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { logger } from "../../../../Utils/Logger";
import { useNavigate } from "react-router-dom";
export const useMonitorFetch = ({ authToken, monitorId, dateRange }) => {
const [monitorIsLoading, setMonitorsIsLoading] = useState(false);
const [monitor, setMonitor] = useState({});
const navigate = useNavigate();
useEffect(() => {
const fetchMonitors = async () => {
try {
setMonitorsIsLoading(true);
const res = await networkService.getUptimeDetailsById({
authToken: authToken,
monitorId: monitorId,
dateRange: dateRange,
normalize: true,
});
setMonitor(res?.data?.data ?? {});
} catch (error) {
logger.error(error);
navigate("/not-found", { replace: true });
} finally {
setMonitorsIsLoading(false);
}
};
fetchMonitors();
}, [authToken, dateRange, monitorId, navigate]);
return { monitor, monitorIsLoading };
};
export default useMonitorFetch;
@@ -1,163 +0,0 @@
import PropTypes from "prop-types";
import {
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
PaginationItem,
Pagination,
Paper,
} from "@mui/material";
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { networkService } from "../../../../main";
import { StatusLabel } from "../../../../Components/Label";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
import { logger } from "../../../../Utils/Logger";
import { formatDateWithTz } from "../../../../Utils/timeUtils";
import { useTheme } from "@emotion/react";
const PaginationTable = ({ monitorId, dateRange }) => {
const theme = useTheme();
const { authToken } = useSelector((state) => state.auth);
const [checks, setChecks] = useState([]);
const [checksCount, setChecksCount] = useState(0);
const [paginationController, setPaginationController] = useState({
page: 0,
rowsPerPage: 5,
});
const uiTimezone = useSelector((state) => state.ui.timezone);
useEffect(() => {
setPaginationController((prevPaginationController) => ({
...prevPaginationController,
page: 0,
}));
}, [dateRange]);
useEffect(() => {
const fetchPage = async () => {
try {
const res = await networkService.getChecksByMonitor({
authToken: authToken,
monitorId: monitorId,
sortOrder: "desc",
limit: null,
dateRange: dateRange,
filter: null,
page: paginationController.page,
rowsPerPage: paginationController.rowsPerPage,
});
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
logger.error(error);
}
};
fetchPage();
}, [
authToken,
monitorId,
dateRange,
paginationController.page,
paginationController.rowsPerPage,
]);
const handlePageChange = (_, newPage) => {
setPaginationController({
...paginationController,
page: newPage - 1, // 0-indexed
});
};
let paginationComponent = <></>;
if (checksCount > paginationController.rowsPerPage) {
paginationComponent = (
<Pagination
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
page={paginationController.page + 1} //0-indexed
onChange={handlePageChange}
shape="rounded"
renderItem={(item) => (
<PaginationItem
slots={{
previous: ArrowBackRoundedIcon,
next: ArrowForwardRoundedIcon,
}}
{...item}
/>
)}
/>
);
}
return (
<>
<TableContainer component={Paper}>
<Table
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(8),
},
"& :is(th)": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
fontSize: "12px",
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
},
}}
>
<TableHead>
<TableRow>
<TableCell>Status</TableCell>
<TableCell>Date & Time</TableCell>
<TableCell>Status Code</TableCell>
<TableCell>Message</TableCell>
</TableRow>
</TableHead>
<TableBody>
{checks.map((check) => {
const status = check.status === true ? "up" : "down";
return (
<TableRow key={check._id}>
<TableCell>
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
{formatDateWithTz(
check.createdAt,
"ddd, MMMM D, YYYY, HH:mm A",
uiTimezone
)}
</TableCell>
<TableCell>{check.statusCode ? check.statusCode : "N/A"}</TableCell>
<TableCell>{check.message}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
{paginationComponent}
</>
);
};
PaginationTable.propTypes = {
monitorId: PropTypes.string.isRequired,
dateRange: PropTypes.string.isRequired,
};
export default PaginationTable;
Whitespace-only changes.
+117 -465
View File
@@ -1,476 +1,128 @@
import PropTypes from "prop-types";
import { useEffect, useState, useCallback } from "react";
import { Box, Button, Stack, Tooltip, Typography, useTheme } from "@mui/material";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { networkService } from "../../../main";
import { logger } from "../../../Utils/Logger";
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
import ButtonGroup from "@mui/material/ButtonGroup";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import UptimeIcon from "../../../assets/icons/uptime-icon.svg?react";
import ResponseTimeIcon from "../../../assets/icons/response-time-icon.svg?react";
import AverageResponseIcon from "../../../assets/icons/average-response-icon.svg?react";
import IncidentsIcon from "../../../assets/icons/incidents.svg?react";
import HistoryIcon from "../../../assets/icons/history-icon.svg?react";
import PaginationTable from "./PaginationTable";
// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import { ChartBox } from "./styled";
import SkeletonLayout from "./skeleton";
import "./index.css";
import useUtils from "../utils";
import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import IconBox from "../../../Components/IconBox";
import StatBox from "../../../Components/StatBox";
import UpBarChart from "./Charts/UpBarChart";
import DownBarChart from "./Charts/DownBarChart";
import ResponseGaugeChart from "./Charts/ResponseGaugeChart";
/**
* Details page component displaying monitor details and related information.
* @component
*/
const DetailsPage = () => {
const theme = useTheme();
const { statusColor, statusMsg, determineState } = useUtils();
const isAdmin = useIsAdmin();
const [monitor, setMonitor] = useState({});
const { monitorId } = useParams();
const { authToken } = useSelector((state) => state.auth);
const [dateRange, setDateRange] = useState("day");
const [certificateExpiry, setCertificateExpiry] = useState("N/A");
const navigate = useNavigate();
import MonitorHeader from "./Components/MonitorHeader";
import StatusBoxes from "./Components/StatusBoxes";
import TimeFramePicker from "./Components/TimeFramePicker";
import ChartBoxes from "./Components/ChartBoxes";
import ResponseTimeChart from "./Components/Charts/ResponseTimeChart";
import ResponseTable from "./Components/ResponseTable";
// MUI Components
import { Stack } from "@mui/material";
const certificateDateFormat = "MMM D, YYYY h A";
const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
// Utils
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import useMonitorFetch from "./Hooks/useMonitorFetch";
import useCertificateFetch from "./Hooks/useCertificateFetch";
import useChecksFetch from "./Hooks/useChecksFetch";
// Constants
const BREADCRUMBS = [
{ name: "uptime", path: "/uptime" },
{ name: "details", path: "" },
// { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy
];
const certificateDateFormat = "MMM D, YYYY h A";
const UptimeDetails = () => {
// Redux state
const { authToken } = useSelector((state) => state.auth);
const uiTimezone = useSelector((state) => state.ui.timezone);
const fetchMonitor = useCallback(async () => {
try {
const res = await networkService.getUptimeDetailsById({
authToken: authToken,
monitorId: monitorId,
dateRange: dateRange,
normalize: true,
});
setMonitor(res?.data?.data ?? {});
} catch (error) {
logger.error(error);
navigate("/not-found", { replace: true });
}
}, [authToken, monitorId, navigate, dateRange]);
useEffect(() => {
fetchMonitor();
}, [fetchMonitor]);
useEffect(() => {
const fetchCertificate = async () => {
if (monitor?.type !== "http") {
return;
}
try {
const res = await networkService.getCertificateExpiry({
authToken: authToken,
monitorId: monitorId,
});
if (res?.data?.data?.certificateDate) {
const date = res.data.data.certificateDate;
setCertificateExpiry(
formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A"
);
}
} catch (error) {
setCertificateExpiry("N/A");
console.error(error);
}
};
fetchCertificate();
}, [authToken, monitorId, monitor, uiTimezone, dateFormat]);
const splitDuration = (duration) => {
const { time, format } = formatDurationSplit(duration);
return (
<>
{time}
<Typography component="span">{format}</Typography>
</>
);
};
let loading = Object.keys(monitor).length === 0;
// Local state
const [dateRange, setDateRange] = useState("day");
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
// Utils
const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
const { monitorId } = useParams();
const theme = useTheme();
const isAdmin = useIsAdmin();
const { monitor, monitorIsLoading } = useMonitorFetch({
authToken,
monitorId,
dateRange,
});
const { certificateExpiry, certificateIsLoading } = useCertificateFetch({
monitor,
authToken,
monitorId,
certificateDateFormat,
uiTimezone,
});
const { checks, checksCount, checksAreLoading } = useChecksFetch({
authToken,
monitorId,
dateRange,
page,
rowsPerPage,
});
// Handlers
const handlePageChange = (_, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(event.target.value);
};
const BREADCRUMBS = [
{ name: "uptime", path: "/uptime" },
{ name: "details", path: `/uptime/${monitorId}` },
];
return (
<Box className="monitor-details">
{loading ? (
<SkeletonLayout />
) : (
<>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
gap={theme.spacing(10)}
mt={theme.spacing(10)}
>
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Typography
component="h1"
variant="h1"
>
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
{/* TODO there is a tooltip at BarChart component. Wrap the Tooltip on our own component */}
<Tooltip
title={statusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -8],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
px: theme.spacing(4),
py: theme.spacing(3),
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
/* TODO Font size should point to theme */
fontSize: 12,
fontWeight: 500,
},
},
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="h2"
>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
mt={theme.spacing(1)}
ml={theme.spacing(6)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: 4,
height: 4,
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: -9,
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{/* Checking every {formatDurationRounded(monitor?.interval)}. */}
</Typography>
</Stack>
</Box>
<Stack
direction="row"
height={34}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
{isAdmin && (
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/uptime/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
/* Should always be contrastText for the button color */
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
)}
</Stack>
</Stack>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
/* sx={getStatusStyles(determineState(monitor))} */
/* statusStyles[determineState(monitor)] */
gradient={true}
status={determineState(monitor)}
heading={"active for"}
subHeading={splitDuration(monitor?.uptimeStreak)}
/>
<StatBox
heading="last check"
subHeading={splitDuration(monitor?.timeSinceLastCheck)}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitor?.latestResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
</Stack>
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "day"
? "24 hours"
: dateRange === "week"
? "7 days"
: "30 days"}
.
</Typography>
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
</Stack>
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
<ChartBox>
<Stack>
<IconBox>
<UptimeIcon />
</IconBox>
<Typography component="h2">Uptime</Typography>
</Stack>
<Stack justifyContent="space-between">
<Box position="relative">
<Typography>Total Checks</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
: (monitor?.groupedUpChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(
hoveredUptimeData._id,
dateFormat,
uiTimezone
)}
</Typography>
)}
</Box>
<Box>
<Typography>
{hoveredUptimeData !== null
? "Avg Response Time"
: "Uptime Percentage"}
</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
: Math.floor(
((monitor?.upChecks?.totalChecks ?? 0) /
(monitor?.totalChecks ?? 1)) *
100
)}
<Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"}
</Typography>
</Typography>
</Box>
</Stack>
<UpBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredUptimeData}
/>
</ChartBox>
<ChartBox>
<Stack>
<IconBox>
<IncidentsIcon />
</IconBox>
<Typography component="h2">Incidents</Typography>
</Stack>
<Box position="relative">
<Typography>Total Incidents</Typography>
<Typography component="span">
{hoveredIncidentsData !== null
? hoveredIncidentsData.totalChecks
: (monitor?.groupedDownChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredIncidentsData !== null &&
hoveredIncidentsData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(
hoveredIncidentsData._id,
dateFormat,
uiTimezone
)}
</Typography>
)}
</Box>
<DownBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredIncidentsData}
/>
</ChartBox>
<ChartBox justifyContent="space-between">
<Stack>
<IconBox>
<AverageResponseIcon />
</IconBox>
<Typography component="h2">Average Response Time</Typography>
</Stack>
<ResponseGaugeChart avgResponseTime={monitor.avgResponseTime ?? 0} />
</ChartBox>
<ChartBox sx={{ padding: 0 }}>
<Stack
pt={theme.spacing(8)}
pl={theme.spacing(8)}
>
<IconBox>
<ResponseTimeIcon />
</IconBox>
<Typography component="h2">Response Times</Typography>
</Stack>
<MonitorDetailsAreaChart
checks={monitor.groupedChecks ?? []}
dateRange={dateRange}
/>
</ChartBox>
<ChartBox
gap={theme.spacing(8)}
sx={{
flex: "100%",
height: "fit-content",
"& nav": { mt: theme.spacing(12) },
}}
>
<Stack mb={theme.spacing(8)}>
<IconBox>
<HistoryIcon />
</IconBox>
<Typography
component="h2"
color={theme.palette.primary.contrastTextSecondary}
>
History
</Typography>
</Stack>
<PaginationTable
monitorId={monitorId}
dateRange={dateRange}
/>
</ChartBox>
</Stack>
</Box>
</Stack>
</>
)}
</Box>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorHeader
isAdmin={isAdmin}
shouldRender={!monitorIsLoading}
monitor={monitor}
/>
<StatusBoxes
shouldRender={!monitorIsLoading}
monitor={monitor}
certificateExpiry={certificateExpiry}
/>
<TimeFramePicker
shouldRender={!monitorIsLoading}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<ChartBoxes
shouldRender={!monitorIsLoading}
monitor={monitor}
uiTimezone={uiTimezone}
dateRange={dateRange}
dateFormat={dateFormat}
hoveredUptimeData={hoveredUptimeData}
setHoveredUptimeData={setHoveredUptimeData}
hoveredIncidentsData={hoveredIncidentsData}
setHoveredIncidentsData={setHoveredIncidentsData}
/>
<ResponseTimeChart
shouldRender={!monitorIsLoading}
monitor={monitor}
dateRange={dateRange}
/>
<ResponseTable
shouldRender={!checksAreLoading}
checks={checks}
uiTimezone={uiTimezone}
page={page}
setPage={handlePageChange}
rowsPerPage={rowsPerPage}
setRowsPerPage={handleChangeRowsPerPage}
checksCount={checksCount}
/>
</Stack>
);
};
DetailsPage.propTypes = {
isAdmin: PropTypes.bool,
};
export default DetailsPage;
export default UptimeDetails;
@@ -1,120 +0,0 @@
import { Box, Skeleton, Stack, useTheme } from "@mui/material";
/**
* Renders a skeleton layout.
*
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
const theme = useTheme();
return (
<>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(6)}
>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
/>
<Skeleton
variant="rounded"
width="50%"
height={18}
sx={{ mt: theme.spacing(4) }}
/>
</Box>
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(12)}
>
<Skeleton
variant="rounded"
width="100%"
height={80}
/>
<Skeleton
variant="rounded"
width="100%"
height={80}
/>
<Skeleton
variant="rounded"
width="100%"
height={80}
/>
</Stack>
<Box>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.spacing(8)}
>
<Skeleton
variant="rounded"
width="20%"
height={24}
sx={{ alignSelf: "flex-end" }}
/>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
</Stack>
<Box sx={{ height: "200px" }}>
<Skeleton
variant="rounded"
width="100%"
height="100%"
/>
</Box>
</Box>
<Stack gap={theme.spacing(8)}>
<Skeleton
variant="rounded"
width="20%"
height={24}
/>
<Skeleton
variant="rounded"
width="100%"
height={200}
/>
<Skeleton
variant="rounded"
width="100%"
height={50}
/>
</Stack>
</Stack>
</>
);
};
export default SkeletonLayout;
@@ -1,45 +0,0 @@
import { Stack, styled } from "@mui/material";
export const ChartBox = styled(Stack)(({ theme }) => ({
flex: "1 30%",
gap: theme.spacing(8),
height: 300,
minWidth: 250,
padding: theme.spacing(8),
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 4,
backgroundColor: theme.palette.primary.main,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& .MuiBox-root:not(.area-tooltip) p": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 13,
},
"& .MuiBox-root > span": {
color: theme.palette.primary.contrastText,
fontSize: 20,
"& span": {
opacity: 0.8,
marginLeft: 2,
fontSize: 15,
},
},
"& .MuiStack-root": {
flexDirection: "row",
gap: theme.spacing(6),
},
"& .MuiStack-root:first-of-type": {
alignItems: "center",
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& path": {
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}));
@@ -1,47 +0,0 @@
import { Skeleton } from "@mui/material";
import DataTable from "../../../../../Components/Table";
const ROWS_NUMBER = 7;
const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
const TableSkeleton = () => {
/* TODO Skeleton does not follow light and dark theme */
const headers = [
{
id: "name",
content: "Host",
render: () => <Skeleton />,
},
{
id: "status",
content: "Status",
render: () => <Skeleton />,
},
{
id: "responseTime",
content: "Response Time",
render: () => <Skeleton />,
},
{
id: "type",
content: "Type",
render: () => <Skeleton />,
},
{
id: "actions",
content: "Actions",
render: () => <Skeleton />,
},
];
return (
<DataTable
headers={headers}
data={ROWS_ARRAY}
/>
);
};
export { TableSkeleton };
-59
View File
@@ -1,59 +0,0 @@
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
import PropTypes from "prop-types";
const Fallback = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const mode = useSelector((state) => state.ui.mode);
return (
<Stack
alignItems="center"
backgroundColor={theme.palette.primary.main}
p={theme.spacing(30)}
pt={theme.spacing(25)}
gap={theme.spacing(2)}
border={1}
borderRadius={theme.shape.borderRadius}
borderColor={theme.palette.primary.lowContrast}
color={theme.palette.primary.contrastTextSecondary}
>
<Box pb={theme.spacing(20)}>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
component="h2"
variant="h2"
fontWeight={500}
>
No monitors found to display
</Typography>
<Typography variant="body1">
It looks like you don’t have any monitors set up yet.
</Typography>
{isAdmin && (
<Button
variant="contained"
color="accent"
onClick={() => {
navigate("/uptime/create");
}}
sx={{ mt: theme.spacing(12) }}
>
Create your first monitor
</Button>
)}
</Stack>
);
};
Fallback.propTypes = {
isAdmin: PropTypes.bool,
};
export default Fallback;
-6
View File
@@ -1,6 +0,0 @@
/* TODO take these from here and declare using emotion. Plus, the values should live in the theme */
.monitors .MuiStack-root > button:not(.MuiIconButton-root) {
font-size: var(--env-var-font-size-medium);
height: var(--env-var-height-2);
min-width: fit-content;
}
-243
View File
@@ -1,243 +0,0 @@
// Components
import { Box, Stack, Button } from "@mui/material";
import Greeting from "../../../Utils/greeting";
import SkeletonLayout from "./skeleton";
import Fallback from "./fallback";
import StatusBox from "./StatusBox";
import UptimeDataTable from "./UptimeDataTable";
import { Pagination } from "../../../Components/Table/TablePagination";
// Utils
import { useTheme } from "@emotion/react";
import { useEffect, useState, useCallback, useMemo } from "react";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import useDebounce from "../../../Utils/debounce";
import { networkService } from "../../../main";
const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }];
const UptimeMonitors = () => {
// Redux state
const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage);
// Local state
const [sort, setSort] = useState({});
const [search, setSearch] = useState("");
const [page, setPage] = useState(0);
const [isSearching, setIsSearching] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
const [monitors, setMonitors] = useState([]);
const [filteredMonitors, setFilteredMonitors] = useState([]);
const [monitorsSummary, setMonitorsSummary] = useState({});
// Utils
const debouncedFilter = useDebounce(search, 500);
const dispatch = useDispatch();
const theme = useTheme();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const authState = useSelector((state) => state.auth);
const fetchParams = useMemo(
() => ({
authToken: authState.authToken,
teamId: authState.user.teamId,
sort: { field: sort.field, order: sort.order },
filter: debouncedFilter,
page,
rowsPerPage,
}),
[authState.authToken, authState.user.teamId, sort, debouncedFilter, page, rowsPerPage]
);
const getMonitorWithPercentage = useCallback((monitor, theme) => {
let uptimePercentage = "";
let percentageColor = "";
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
? "0"
: (monitor.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.error.main
: monitor.uptimePercentage < 0.5
? theme.palette.warning.main
: monitor.uptimePercentage < 0.75
? theme.palette.success.main
: theme.palette.success.main;
}
return {
id: monitor._id,
name: monitor.name,
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
percentageColor,
monitor: monitor,
};
}, []);
const fetchMonitors = useCallback(async () => {
try {
setIsLoading(true);
const config = fetchParams;
const res = await networkService.getMonitorsByTeamId({
authToken: config.authToken,
teamId: config.teamId,
limit: 25,
types: ["http", "ping", "docker", "port"],
page: config.page,
rowsPerPage: config.rowsPerPage,
filter: config.filter,
field: config.sort.field,
order: config.sort.order,
});
const { monitors, filteredMonitors, summary } = res.data.data;
const mappedMonitors = filteredMonitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(monitors);
setFilteredMonitors(mappedMonitors);
setMonitorsSummary(summary);
} catch (error) {
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
setIsSearching(false);
}
}, [fetchParams, getMonitorWithPercentage, theme]);
useEffect(() => {
fetchMonitors();
}, [fetchMonitors, monitorUpdateTrigger]);
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
dispatch(
setRowsPerPage({
value: parseInt(event.target.value, 10),
table: "monitors",
})
);
setPage(0);
};
const triggerUpdate = useCallback(() => {
setMonitorUpdateTrigger((prev) => !prev);
}, []);
const totalMonitors = monitorsSummary?.totalMonitors ?? 0;
const hasMonitors = totalMonitors > 0;
const canAddMonitor = isAdmin && hasMonitors;
return (
<Stack
className="monitors"
gap={theme.spacing(8)}
>
<Box>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
gap={theme.spacing(6)}
>
{canAddMonitor && (
<>
<Button
variant="contained"
color="accent"
onClick={() => {
navigate("/uptime/create");
}}
sx={{
fontWeight: 500,
whiteSpace: "nowrap",
/* TODO IMPORTANT this should be applied to all buttons */
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.contrastText}`,
outlineOffset: 4,
},
}}
>
Create new
</Button>
</>
)}
</Stack>
<Greeting type="uptime" />
</Box>
{
<>
{!isLoading && !hasMonitors && <Fallback isAdmin={isAdmin} />}
{isLoading ? (
<SkeletonLayout />
) : (
hasMonitors && (
<>
<Stack
gap={theme.spacing(8)}
direction="row"
justifyContent="space-between"
>
<StatusBox
title="up"
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title="down"
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title="paused"
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
<UptimeDataTable
isAdmin={isAdmin}
isLoading={isLoading}
filteredMonitors={filteredMonitors}
monitors={monitors}
monitorCount={totalMonitors}
sort={sort}
setSort={setSort}
debouncedSearch={debouncedFilter}
setSearch={setSearch}
isSearching={isSearching}
setIsSearching={setIsSearching}
setIsLoading={setIsLoading}
triggerUpdate={triggerUpdate}
/>
<Pagination
monitorCount={totalMonitors}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</>
)
)}
</>
}
</Stack>
);
};
export default UptimeMonitors;
@@ -2,16 +2,16 @@ import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { createToast } from "../../../../../Utils/toastUtils";
import { logger } from "../../../../../Utils/Logger";
import { IconButton, Menu, MenuItem } from "@mui/material";
import {
deleteUptimeMonitor,
pauseUptimeMonitor,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Settings from "../../../assets/icons/settings-bold.svg?react";
} from "../../../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Settings from "../../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import Dialog from "../../../Components/Dialog";
import Dialog from "../../../../../Components/Dialog";
const ActionsMenu = ({
monitor,
@@ -1,5 +1,7 @@
import { Box, Typography } from "@mui/material";
import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import Dot from "../../../../../Components/Dot";
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
@@ -13,44 +15,33 @@ import PropTypes from "prop-types";
* @returns {React.ElementType} Returns a div element with the host details.
*/
const Host = ({ url, title, percentageColor, percentage }) => {
const noTitle = title === undefined || title === url;
const theme = useTheme();
return (
<Box className="host">
<Box
display="inline-block"
<Stack>
<Stack
direction="row"
position="relative"
sx={{
fontWeight: 500,
"&:before": {
position: "absolute",
content: `""`,
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "gray",
opacity: 0.8,
right: "-10px",
top: "42%",
},
}}
alignItems="center"
gap={theme.spacing(4)}
>
{title}
</Box>
{percentageColor && percentage && (
<Typography
component="span"
sx={{
color: percentageColor,
fontWeight: 500,
/* TODO point font weight to theme */
ml: "15px",
}}
>
{percentage}%
</Typography>
)}
{!noTitle && <Box sx={{ opacity: 0.6 }}>{url}</Box>}
</Box>
{percentageColor && percentage && (
<>
<Dot />
<Typography
component="span"
sx={{
color: percentageColor,
fontWeight: 500,
}}
>
{percentage}%
</Typography>
</>
)}
</Stack>
<span style={{ opacity: 0.6 }}>{url}</span>
</Stack>
);
};
@@ -0,0 +1,46 @@
import { CircularProgress, Box } from "@mui/material";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const LoadingSpinner = ({ shouldRender }) => {
const theme = useTheme();
if (shouldRender === false) {
return;
}
return (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.primary.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="50%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.accent.main,
}}
/>
</Box>
</>
);
};
LoadingSpinner.propTypes = {
shouldRender: PropTypes.bool,
};
export default LoadingSpinner;
@@ -0,0 +1,43 @@
import { useState } from "react";
import Search from "../../../../../Components/Inputs/Search";
import { Box } from "@mui/material";
import useDebounce from "../../Hooks/useDebounce";
import { useEffect } from "react";
import PropTypes from "prop-types";
const SearchComponent = ({ monitors, onSearchChange, setIsSearching }) => {
const [localSearch, setLocalSearch] = useState("");
const debouncedSearch = useDebounce(localSearch, 500);
useEffect(() => {
onSearchChange(debouncedSearch);
setIsSearching(false);
}, [debouncedSearch, onSearchChange, setIsSearching]);
const handleSearch = (value) => {
setLocalSearch(value);
setIsSearching(true);
};
return (
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitors}
filteredBy="name"
inputValue={localSearch}
handleInputChange={handleSearch}
/>
</Box>
);
};
SearchComponent.propTypes = {
monitors: PropTypes.array,
onSearchChange: PropTypes.func,
setIsSearching: PropTypes.func,
};
export default SearchComponent;
@@ -0,0 +1,36 @@
import PropTypes from "prop-types";
import { Stack } from "@mui/material";
import StatusBox from "./statusBox";
import { useTheme } from "@emotion/react";
import SkeletonLayout from "./skeleton";
const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
const theme = useTheme();
if (!shouldRender) return <SkeletonLayout shouldRender={shouldRender} />;
return (
<Stack
gap={theme.spacing(8)}
direction="row"
justifyContent="space-between"
>
<StatusBox
title="up"
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title="down"
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title="paused"
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
);
};
StatusBoxes.propTypes = {
monitorsSummary: PropTypes.object,
};
export default StatusBoxes;
@@ -0,0 +1,31 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(12)}
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,9 +1,9 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Arrow from "../../../assets/icons/top-right-arrow.svg?react";
import Background from "../../../assets/Images/background-grid.svg?react";
import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react";
import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react";
import Background from "../../../../../assets/Images/background-grid.svg?react";
import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react";
const StatusBox = ({ title, value }) => {
const theme = useTheme();
@@ -52,47 +52,48 @@ const StatusBox = ({ title, value }) => {
overflow="hidden"
>
<Box
sx={{
position: "absolute",
top: "-10%",
left: "5%",
pointerEvents: "none",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
position="absolute"
top="-10%"
left="5%"
>
<Background />
</Box>
<Box
textTransform="uppercase"
fontSize={15}
letterSpacing={0.5}
color={theme.palette.primary.contrastTextTertiary}
mb={theme.spacing(8)}
>
{title}
</Box>
{icon}
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontWeight={600}
color={color}
gap="2px"
>
{value}
<Typography
component="span"
fontSize={20}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{ opacity: 0.3 }}
<Stack direction="column">
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
#
</Typography>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
{icon}
</Stack>
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontWeight={600}
color={color}
gap="2px"
>
{value}
<Typography
fontSize={20}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{
opacity: 0.3,
}}
>
#
</Typography>
</Stack>
</Stack>
</Box>
);
@@ -1,57 +1,53 @@
// Components
import { Box, Stack, CircularProgress } from "@mui/material";
import Search from "../../../../Components/Inputs/Search";
import { Heading } from "../../../../Components/Heading";
import DataTable from "../../../../Components/Table";
import { Heading } from "../../../../../Components/Heading";
import DataTable from "../../../../../Components/Table";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import Host from "../host";
import { StatusLabel } from "../../../../Components/Label";
import BarChart from "../../../../Components/Charts/BarChart";
import ActionsMenu from "../actionsMenu";
import Host from "../Host";
import { StatusLabel } from "../../../../../Components/Label";
import BarChart from "../../../../../Components/Charts/BarChart";
import ActionsMenu from "../ActionsMenu";
import { useState } from "react";
import SearchComponent from "../SearchComponent";
import CircularCount from "../../../../../Components/CircularCount";
import LoadingSpinner from "../LoadingSpinner";
import MonitorCountHeader from "../../../../../Components/MonitorCountHeader";
import UptimeDataTableSkeleton from "./skeleton";
// Utils
import { useTheme } from "@emotion/react";
import useUtils from "../../utils";
import { useState, memo, useCallback } from "react";
import useUtils from "../../Hooks/useUtils";
import { useNavigate } from "react-router-dom";
import "../index.css";
import PropTypes from "prop-types";
const SearchComponent = memo(
({ monitors, debouncedSearch, onSearchChange, setIsSearching }) => {
const [localSearch, setLocalSearch] = useState(debouncedSearch);
const handleSearch = useCallback(
(value) => {
setIsSearching(true);
setLocalSearch(value);
onSearchChange(value);
},
[onSearchChange, setIsSearching]
);
const MonitorDataTable = ({ shouldRender, isSearching, headers, filteredMonitors }) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitors}
filteredBy="name"
inputValue={localSearch}
handleInputChange={handleSearch}
/>
</Box>
);
}
);
SearchComponent.displayName = "SearchComponent";
SearchComponent.propTypes = {
monitors: PropTypes.array,
debouncedSearch: PropTypes.string,
onSearchChange: PropTypes.func,
setIsSearching: PropTypes.func,
if (!shouldRender) return null;
return (
<Box position="relative">
<LoadingSpinner shouldRender={isSearching} />
<DataTable
headers={headers}
data={filteredMonitors}
config={{
rowSX: {
cursor: "pointer",
"&:hover td": {
backgroundColor: theme.palette.tertiary.main,
transition: "background-color .3s ease",
},
},
onRowClick: (row) => {
navigate(`/uptime/${row.id}`);
},
emptyView: "No monitors found",
}}
/>
</Box>
);
};
/**
@@ -79,31 +75,32 @@ SearchComponent.propTypes = {
* @param {string} props.search - Current search query
* @param {Function} props.setSearch - Callback to update search query
* @param {boolean} props.isSearching - Whether a search is in progress
* @param {Function} props.setIsSearching - Callback to update search state
* @param {Function} props.setIsLoading - Callback to update loading state
* @param {Function} props.triggerUpdate - Callback to trigger a data refresh
* @returns {JSX.Element} Rendered component
*/
const UptimeDataTable = ({
isAdmin,
isLoading,
monitors,
filteredMonitors,
monitorCount,
sort,
setSort,
debouncedSearch,
setSearch,
isSearching,
setIsSearching,
setIsLoading,
triggerUpdate,
}) => {
const { determineState } = useUtils();
const UptimeDataTable = (props) => {
// Utils
const {
isAdmin,
setIsLoading,
monitors,
filteredMonitors,
monitorCount,
sort,
setSort,
setSearch,
triggerUpdate,
monitorsAreLoading,
} = props;
const { determineState } = useUtils();
const theme = useTheme();
const navigate = useNavigate();
// Local state
const [isSearching, setIsSearching] = useState(false);
// Handlers
const handleSort = (field) => {
let order = "";
if (sort.field !== field) {
@@ -195,7 +192,9 @@ const UptimeDataTable = ({
id: "type",
content: "Type",
render: (row) => (
<span style={{ textTransform: "uppercase" }}>{row.monitor.type}</span>
<span style={{ textTransform: "uppercase" }}>
{row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type}
</span>
),
},
{
@@ -212,100 +211,45 @@ const UptimeDataTable = ({
),
},
];
return (
<Box
flex={1}
py={theme.spacing(8)}
>
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
gap={theme.spacing(2)}
<MonitorCountHeader
shouldRender={!monitorsAreLoading}
monitorCount={monitorCount}
heading="Uptime monitors"
sx={{ mb: theme.spacing(8) }}
>
<Heading component="h2">Uptime monitors</Heading>
{/* TODO Same as the one in Infrastructure. Create component */}
<Box
component="span"
color={theme.palette.tertiary.contrastText}
border={2}
borderColor={theme.palette.accent.main}
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "50%",
fontSize: "12px",
fontWeight: 500,
}}
>
{monitorCount}
</Box>
<SearchComponent
monitorsAreLoading={monitorsAreLoading}
monitors={monitors}
debouncedSearch={debouncedSearch}
onSearchChange={setSearch}
setIsSearching={setIsSearching}
/>
</Stack>
<Box position="relative">
{(isSearching || isLoading) && (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.primary.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="50%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.accent.main,
}}
/>
</Box>
</>
)}
<DataTable
headers={headers}
data={filteredMonitors}
config={{
rowSX: {
cursor: "pointer",
"&:hover td": {
backgroundColor: theme.palette.tertiary.main,
transition: "background-color .3s ease",
},
},
onRowClick: (row) => {
navigate(`/uptime/${row.id}`);
},
emptyView: "No monitors found",
}}
/>
</Box>
</MonitorCountHeader>
<MonitorDataTable
shouldRender={!monitorsAreLoading}
isSearching={isSearching}
headers={headers}
filteredMonitors={filteredMonitors}
/>
<UptimeDataTableSkeleton shouldRender={monitorsAreLoading} />
</Box>
);
};
const MemoizedUptimeDataTable = memo(UptimeDataTable);
export default MemoizedUptimeDataTable;
UptimeDataTable.propTypes = {
isSearching: PropTypes.bool,
setIsSearching: PropTypes.func,
setSort: PropTypes.func,
setSearch: PropTypes.func,
setIsLoading: PropTypes.func,
triggerUpdate: PropTypes.func,
debouncedSearch: PropTypes.string,
onSearchChange: PropTypes.func,
isAdmin: PropTypes.bool,
isLoading: PropTypes.bool,
monitors: PropTypes.array,
@@ -315,11 +259,6 @@ UptimeDataTable.propTypes = {
field: PropTypes.string,
order: PropTypes.oneOf(["asc", "desc"]),
}),
setSort: PropTypes.func,
debouncedSearch: PropTypes.string,
setSearch: PropTypes.func,
isSearching: PropTypes.bool,
setIsSearching: PropTypes.func,
setIsLoading: PropTypes.func,
triggerUpdate: PropTypes.func,
};
export default UptimeDataTable;
@@ -0,0 +1,21 @@
import { Skeleton } from "@mui/material";
import PropTypes from "prop-types";
const UptimeDataTableSkeleton = ({ shouldRender }) => {
if (!shouldRender) return null;
return (
<Skeleton
variant="rounded"
width="100%"
height="100%"
flex={1}
/>
);
};
UptimeDataTableSkeleton.propTypes = {
shouldRender: PropTypes.bool.isRequired,
};
export default UptimeDataTableSkeleton;
@@ -0,0 +1,18 @@
import { useState, useEffect } from "react";
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
@@ -0,0 +1,98 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
import { useTheme } from "@emotion/react";
const getMonitorWithPercentage = (monitor, theme) => {
let uptimePercentage = "";
let percentageColor = "";
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0 ? "0" : (monitor.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.error.main
: monitor.uptimePercentage < 0.5
? theme.palette.warning.main
: monitor.uptimePercentage < 0.75
? theme.palette.success.main
: theme.palette.success.main;
}
return {
id: monitor._id,
name: monitor.name,
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
percentageColor,
monitor: monitor,
};
};
export const useMonitorFetch = ({
authToken,
teamId,
limit,
page,
rowsPerPage,
filter,
field,
order,
triggerUpdate,
}) => {
const [monitorsAreLoading, setMonitorsAreLoading] = useState(false);
const [monitors, setMonitors] = useState([]);
const [filteredMonitors, setFilteredMonitors] = useState([]);
const [monitorsSummary, setMonitorsSummary] = useState({});
const theme = useTheme();
useEffect(() => {
const fetchMonitors = async () => {
try {
setMonitorsAreLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken,
teamId,
limit,
types: ["http", "ping", "docker", "port"],
page,
rowsPerPage,
filter,
field,
order,
});
const { monitors, filteredMonitors, summary } = res.data.data;
const mappedMonitors = filteredMonitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(monitors);
setFilteredMonitors(mappedMonitors);
setMonitorsSummary(summary);
} catch (error) {
createToast({
body: error.message,
});
} finally {
setMonitorsAreLoading(false);
}
};
fetchMonitors();
}, [
authToken,
teamId,
limit,
field,
filter,
order,
page,
rowsPerPage,
theme,
triggerUpdate,
]);
return { monitors, filteredMonitors, monitorsSummary, monitorsAreLoading };
};
export default useMonitorFetch;
+158
View File
@@ -0,0 +1,158 @@
// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Greeting from "../../../Utils/greeting";
import StatusBoxes from "./Components/StatusBoxes";
import UptimeDataTable from "./Components/UptimeDataTable";
import Pagination from "../../../Components/Table/TablePagination";
import CreateMonitorHeader from "../../../Components/CreateMonitorHeader";
import Fallback from "../../../Components/Fallback";
// MUI Components
import { Stack, Box, Button } from "@mui/material";
// Utils
import { useState, useCallback } from "react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import useMonitorsFetch from "./Hooks/useMonitorsFetch";
import { useSelector, useDispatch } from "react-redux";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import PropTypes from "prop-types";
const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }];
const CreateMonitorButton = ({ shouldRender }) => {
// Utils
const navigate = useNavigate();
if (shouldRender === false) {
return;
}
return (
<Box alignSelf="flex-end">
<Button
variant="contained"
color="accent"
onClick={() => {
navigate("/uptime/create");
}}
>
Create new
</Button>
</Box>
);
};
CreateMonitorButton.propTypes = {
shouldRender: PropTypes.bool,
};
const UptimeMonitors = () => {
// Redux state
const { authToken, user } = useSelector((state) => state.auth);
const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage);
// Local state
const [search, setSearch] = useState("");
const [page, setPage] = useState(0);
const [sort, setSort] = useState({});
const [isSearching, setIsSearching] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
// Utils
const theme = useTheme();
const isAdmin = useIsAdmin();
const dispatch = useDispatch();
// Handlers
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
dispatch(
setRowsPerPage({
value: parseInt(event.target.value, 10),
table: "monitors",
})
);
setPage(0);
};
const triggerUpdate = useCallback(() => {
setMonitorUpdateTrigger((prev) => !prev);
}, []);
const teamId = user.teamId;
const { monitorsAreLoading, monitors, filteredMonitors, monitorsSummary } =
useMonitorsFetch({
authToken,
teamId,
limit: 25,
page,
rowsPerPage: rowsPerPage,
filter: search,
field: sort.field,
order: sort.order,
triggerUpdate: monitorUpdateTrigger,
});
const totalMonitors = monitorsSummary?.totalMonitors ?? 0;
if (!isLoading && monitors?.length === 0) {
return (
<Fallback
vowelStart={true}
title="uptime monitor"
checks={["Check if a website or service is up and running"]}
link="/uptime/create"
isAdmin={isAdmin}
/>
);
}
return (
<Stack
className="monitors"
gap={theme.spacing(10)}
>
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
path="/uptime/create"
/>
<Greeting type="uptime" />
<StatusBoxes
monitorsSummary={monitorsSummary}
shouldRender={!monitorsAreLoading}
/>
<UptimeDataTable
isAdmin={isAdmin}
isLoading={isLoading}
setIsLoading={setIsLoading}
filteredMonitors={filteredMonitors}
monitors={monitors}
monitorCount={totalMonitors}
sort={sort}
setSort={setSort}
setSearch={setSearch}
isSearching={isSearching}
setIsSearching={setIsSearching}
monitorsAreLoading={monitorsAreLoading}
triggerUpdate={triggerUpdate}
/>
<Pagination
itemCount={totalMonitors}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Stack>
);
};
export default UptimeMonitors;
+45 -31
View File
@@ -1,36 +1,50 @@
import { Navigate, Route, Routes as LibRoutes } from "react-router";
import HomeLayout from "../Components/Layouts/HomeLayout";
import { Infrastructure } from "../Pages/Infrastructure";
import InfrastructureDetails from "../Pages/Infrastructure/Details";
import NotFound from "../Pages/NotFound";
import Login from "../Pages/Auth/Login/Login";
import Register from "../Pages/Auth/Register/Register";
import Account from "../Pages/Account";
import Monitors from "../Pages/Uptime/Home";
import CreateMonitor from "../Pages/Uptime/CreateUptime";
import CreateInfrastructureMonitor from "../Pages/Infrastructure/CreateMonitor";
// Auth
import AuthLogin from "../Pages/Auth/Login/Login";
import AuthRegister from "../Pages/Auth/Register/Register";
import AuthForgotPassword from "../Pages/Auth/ForgotPassword";
import AuthCheckEmail from "../Pages/Auth/CheckEmail";
import AuthSetNewPassword from "../Pages/Auth/SetNewPassword";
import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed";
// Uptime
import Uptime from "../Pages/Uptime/Monitors";
import UptimeDetails from "../Pages/Uptime/Details";
import UptimeCreate from "../Pages/Uptime/Create";
import UptimeConfigure from "../Pages/Uptime/Configure";
// PageSpeed
// import PageSpeed from "../Pages/PageSpeed";
import PageSpeed from "../Pages/PageSpeed/Monitors";
import PageSpeedCreate from "../Pages/PageSpeed/Create";
import PageSpeedDetails from "../Pages/PageSpeed/Details";
import PageSpeedConfigure from "../Pages/PageSpeed/Configure";
// Infrastructure
import InfrastructureCreate from "../Pages/Infrastructure/Create";
import InfrastructureDetails from "../Pages/Infrastructure/Details";
import Incidents from "../Pages/Incidents";
import Status from "../Pages/Status";
import Integrations from "../Pages/Integrations";
// Settings
import Account from "../Pages/Account";
import Settings from "../Pages/Settings";
import ForgotPassword from "../Pages/Auth/ForgotPassword";
import CheckEmail from "../Pages/Auth/CheckEmail";
import SetNewPassword from "../Pages/Auth/SetNewPassword";
import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed";
import ProtectedRoute from "../Components/ProtectedRoute";
import Details from "../Pages/Uptime/Details";
import Maintenance from "../Pages/Maintenance";
import Configure from "../Pages/Uptime/Configure";
import PageSpeed from "../Pages/PageSpeed";
import CreatePageSpeed from "../Pages/PageSpeed/CreatePageSpeed";
import ProtectedRoute from "../Components/ProtectedRoute";
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
import PageSpeedDetails from "../Pages/PageSpeed/Details";
import PageSpeedConfigure from "../Pages/PageSpeed/Configure";
import withAdminCheck from "../Components/HOC/withAdminCheck";
import CreateStatus from "../Pages/Status/CreateStatus";
const Routes = () => {
const AdminCheckedRegister = withAdminCheck(Register);
const AdminCheckedRegister = withAdminCheck(AuthRegister);
return (
<LibRoutes>
<Route
@@ -47,20 +61,20 @@ const Routes = () => {
/>
<Route
path="/uptime"
element={<Monitors />}
element={<Uptime />}
/>
<Route
path="/uptime/create/:monitorId?"
element={<CreateMonitor />}
element={<UptimeCreate />}
/>
<Route
path="/uptime/:monitorId/"
element={<Details />}
element={<UptimeDetails />}
/>
<Route
path="/uptime/configure/:monitorId/"
element={<Configure />}
element={<UptimeConfigure />}
/>
<Route
path="pagespeed"
@@ -68,7 +82,7 @@ const Routes = () => {
/>
<Route
path="pagespeed/create"
element={<CreatePageSpeed />}
element={<PageSpeedCreate />}
/>
<Route
path="pagespeed/:monitorId"
@@ -84,7 +98,7 @@ const Routes = () => {
/>
<Route
path="infrastructure/create"
element={<CreateInfrastructureMonitor />}
element={<InfrastructureCreate />}
/>
<Route
path="infrastructure/:monitorId"
@@ -136,7 +150,7 @@ const Routes = () => {
<Route
path="/login"
element={<Login />}
element={<AuthLogin />}
/>
<Route
@@ -147,24 +161,24 @@ const Routes = () => {
<Route
exact
path="/register/:token"
element={<Register />}
element={<AuthRegister />}
/>
<Route
path="/forgot-password"
element={<ForgotPassword />}
element={<AuthForgotPassword />}
/>
<Route
path="/check-email"
element={<CheckEmail />}
element={<AuthCheckEmail />}
/>
<Route
path="/set-new-password/:token"
element={<SetNewPassword />}
element={<AuthSetNewPassword />}
/>
<Route
path="/new-password-confirmed"
element={<NewPasswordConfirmed />}
element={<AuthNewPasswordConfirmed />}
/>
<Route
+2
View File
@@ -614,6 +614,7 @@ class NetworkService {
if (config.filter) params.append("filter", config.filter);
if (config.page) params.append("page", config.page);
if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage);
if (config.status !== undefined) params.append("status", config.status);
return this.axiosInstance.get(`/checks/${config.monitorId}?${params.toString()}`, {
headers: { Authorization: `Bearer ${config.authToken}` },
@@ -646,6 +647,7 @@ class NetworkService {
if (config.filter) params.append("filter", config.filter);
if (config.page) params.append("page", config.page);
if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage);
if (config.status !== undefined) params.append("status", config.status);
return this.axiosInstance.get(`/checks/team/${config.teamId}?${params.toString()}`, {
headers: { Authorization: `Bearer ${config.authToken}` },
});
+4 -3
View File
@@ -1,14 +1,15 @@
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { setNetworkService } from "./NetworkService";
import NetworkService from "./NetworkService";
import NetworkService, { 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);
if (!networkService) {
setNetworkService(new NetworkService(store, dispatch, navigate));
}
return children;
};
+19
View File
@@ -1,4 +1,5 @@
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import customParseFormat from "dayjs/plugin/customParseFormat";
@@ -6,6 +7,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(customParseFormat);
dayjs.extend(duration);
export const MS_PER_SECOND = 1000;
export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
@@ -75,6 +77,23 @@ export const formatDurationSplit = (ms) => {
: { time: 0, format: "seconds" };
};
export const getHumanReadableDuration = (ms) => {
const durationObj = dayjs.duration(ms);
if (durationObj.asDays() >= 1) {
const days = Math.floor(durationObj.asDays());
return { time: days, units: days === 1 ? "day" : "days" };
} else if (durationObj.asHours() >= 1) {
const hours = Math.floor(durationObj.asHours());
return { time: hours, units: hours === 1 ? "hour" : "hours" };
} else if (durationObj.asMinutes() >= 1) {
const minutes = Math.floor(durationObj.asMinutes());
return { time: minutes, units: minutes === 1 ? "minute" : "minutes" };
} else {
const seconds = Math.floor(durationObj.asSeconds());
return { time: seconds, units: seconds === 1 ? "second" : "seconds" };
}
};
export const formatDate = (date, customOptions) => {
const options = {
year: "numeric",
-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
+8 -9
View File
@@ -1,8 +1,7 @@
## **Announcing $5000 grant funding in partnership with [UpRock](https://uprock.com) to build distributed uptime monitoring - check [our web page](https://checkmate.so) for funding ideas**
![Frame 34](https://github.com/user-attachments/assets/4ecf2d6e-b3f9-47e9-9e31-b9cb8f3f00b9)
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
@@ -12,6 +11,7 @@
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
@@ -51,22 +51,21 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- Docker monitoring
- Ping monitoring
- SSL monitoring
- Port monitoring
- Incidents at a glance
- E-mail notifications
- Scheduled maintenance
**Short term roadmap:**
- Port monitoring (**complete**, waiting to be deployed to stable version) https://github.com/bluewave-labs/Checkmate/issues/1476
- Global (distributed) uptime checking on Solana network (**in progress**)
- Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593
- Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131
- Translations (i18n) (**in progress**)
- Better notification options (Webhooks, Discord, Telegram, Slack) https://github.com/bluewave-labs/Checkmate/issues/1545
- Server security monitoring
- Command line interface (CLI) https://github.com/bluewave-labs/Checkmate/issues/1558
- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545
- Command line interface (CLI) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1558
- JSON query monitoring https://github.com/bluewave-labs/Checkmate/issues/1573
- More configuration options
- Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546
- More configuration options
- DNS monitoring
## 🏗️ Screenshots
+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,
@@ -8,7 +8,6 @@ class DistributedUptimeController {
async resultsCallback(req, res, next) {
try {
console.log(req.body);
res.status(200).json({ message: "OK" });
} catch (error) {
throw handleError(error, SERVICE_NAME, "resultsCallback");
+13 -2
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 {
@@ -436,7 +436,7 @@ class MonitorController {
monitor.isActive = !monitor.isActive;
monitor.status = undefined;
monitor.save();
return res.ssuccess({
return res.success({
msg: monitor.isActive
? successMessages.MONITOR_RESUME
: successMessages.MONITOR_PAUSE,
@@ -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,32 @@ 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, status } = req.query;
status = typeof status !== "undefined" ? false : undefined;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
// Match
const matchStage = {
monitorId: ObjectId.createFromHexString(monitorId),
...(typeof status !== "undefined" && { status }),
...(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 +104,32 @@ 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,
+19 -2
View File
@@ -497,7 +497,6 @@ const getMonitorById = async (monitorId) => {
const getMonitorsByTeamId = async (req) => {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
@@ -512,7 +511,6 @@ const getMonitorsByTeamId = async (req) => {
}
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
const results = await Monitor.aggregate([
{ $match: matchStage },
@@ -839,3 +837,22 @@ export {
groupChecksByTime,
calculateGroupStats,
};
// limit 25
// page 1
// rowsPerPage 25
// filter undefined
// field name
// order asc
// skip 25
// sort { name: 1 }
// filteredMonitors []
// limit 25
// page NaN
// rowsPerPage 25
// filter undefined
// field name
// order asc
// skip 0
// sort { name: 1 }
+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;
+20 -124
View File
@@ -11,7 +11,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.34.10",
"bullmq": "5.38.0",
"cors": "^2.8.5",
"dockerode": "4.0.4",
"dotenv": "^16.4.5",
@@ -40,7 +40,7 @@
"eslint-plugin-mocha": "^10.5.0",
"esm": "3.2.25",
"globals": "^15.14.0",
"mocha": "11.0.1",
"mocha": "11.1.0",
"ngrok": "5.0.0-beta.2",
"nodemon": "3.1.9",
"prettier": "^3.3.3",
@@ -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.34.10",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.10.tgz",
"integrity": "sha512-ia6EzpQm1ZPq6GUBSLyfvzJrhdBTd1f3Gn2g9pFtLX4hBOob6QHmcmBzGgPlSCyr/i2Qfe4OdjS21bRd02srbw==",
"version": "5.38.0",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.38.0.tgz",
"integrity": "sha512-X6ApLQlSmX0LQ58xCpCoXE2nN57y8H9LD4EApJl9+8E6+FNWdKy6gykL8ofB9x4TcZeeGJhZOUZJAuhRV721bw==",
"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",
@@ -5450,9 +5450,9 @@
"license": "MIT"
},
"node_modules/mocha": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz",
"integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz",
"integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5473,8 +5473,8 @@
"strip-json-comments": "^3.1.1",
"supports-color": "^8.1.1",
"workerpool": "^6.5.1",
"yargs": "^16.2.0",
"yargs-parser": "^20.2.9",
"yargs": "^17.7.2",
"yargs-parser": "^21.1.1",
"yargs-unparser": "^2.0.0"
},
"bin": {
@@ -5485,35 +5485,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/mocha/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/mocha/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/mocha/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/mocha/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
@@ -5527,34 +5498,6 @@
"node": ">=10"
}
},
"node_modules/mocha/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mocha/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mocha/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -5571,53 +5514,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/mocha/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/mocha/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mocha/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/mongodb": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
@@ -5952,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"
+2 -2
View File
@@ -18,7 +18,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.34.10",
"bullmq": "5.38.0",
"cors": "^2.8.5",
"dockerode": "4.0.4",
"dotenv": "^16.4.5",
@@ -47,7 +47,7 @@
"eslint-plugin-mocha": "^10.5.0",
"esm": "3.2.25",
"globals": "^15.14.0",
"mocha": "11.0.1",
"mocha": "11.1.0",
"ngrok": "5.0.0-beta.2",
"nodemon": "3.1.9",
"prettier": "^3.3.3",
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff. Show more