mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-19 16:08:39 -05:00
Merge pull request #3008 from bluewave-labs/feat/v2/uptime-details
feat/v2/uptime details
This commit is contained in:
Generated
+28
@@ -26,6 +26,7 @@
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"mui-color-input": "^6.0.0",
|
||||
"pretty-ms": "9.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "7.63.0",
|
||||
@@ -5184,6 +5185,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-ms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
||||
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -5311,6 +5324,21 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
||||
"integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse-ms": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
|
||||
+1
-8
@@ -31,6 +31,7 @@
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"mui-color-input": "^6.0.0",
|
||||
"pretty-ms": "9.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "7.63.0",
|
||||
@@ -46,14 +47,6 @@
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"zod": "4.1.11"
|
||||
},
|
||||
"unusedDepencies": {
|
||||
"@solana/wallet-adapter-base": "0.9.25",
|
||||
"@solana/wallet-adapter-material-ui": "0.16.35",
|
||||
"@solana/wallet-adapter-react": "0.15.37",
|
||||
"@solana/wallet-adapter-react-ui": "0.9.37",
|
||||
"@solana/wallet-adapter-wallets": "0.19.34",
|
||||
"@solana/web3.js": "1.98.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.5.2",
|
||||
"@types/react": "^18.2.66",
|
||||
|
||||
+5
-10
@@ -9,7 +9,6 @@ import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||
import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
import { Routes } from "./Routes";
|
||||
import WalletProvider from "./Components/WalletProvider";
|
||||
import AppLayout from "@/Components/v1/Layouts/AppLayout";
|
||||
|
||||
function App() {
|
||||
@@ -24,16 +23,12 @@ function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
/* Extract Themeprovider, baseline and global styles to Styles */
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
<WalletProvider>
|
||||
<CssBaseline />
|
||||
|
||||
<AppLayout>
|
||||
<Routes />
|
||||
</AppLayout>
|
||||
<ToastContainer />
|
||||
</WalletProvider>
|
||||
<CssBaseline />
|
||||
<AppLayout>
|
||||
<Routes />
|
||||
</AppLayout>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { SxProps } from "@mui/material/styles";
|
||||
|
||||
type BaseBoxProps = React.PropsWithChildren<{ sx?: SxProps }>;
|
||||
|
||||
export const BaseBox: React.FC<BaseBoxProps> = ({ children, sx }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export const Dot = ({
|
||||
color = "gray",
|
||||
size = "4px",
|
||||
style,
|
||||
}: {
|
||||
color?: string;
|
||||
size?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
content: '""',
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
opacity: 0.8,
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
export const PulseDot = ({ color }: { color: string }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
width="26px"
|
||||
height="24px"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
minWidth="18px"
|
||||
minHeight="18px"
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: color,
|
||||
borderRadius: "50%",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "inherit",
|
||||
borderRadius: "50%",
|
||||
animation: "ripple 1.8s ease-out infinite",
|
||||
},
|
||||
"&::after": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "7px",
|
||||
height: "7px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.accent.contrastText,
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import type { PaletteKey } from "@/Utils/Theme/v2/theme";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
|
||||
type GradientBox = React.PropsWithChildren<{ palette?: PaletteKey }>;
|
||||
|
||||
export const GradientBox: React.FC<GradientBox> = ({ children, palette }) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const bg = palette
|
||||
? `linear-gradient(to bottom right, ${theme.palette[palette].main} 30%, ${theme.palette[palette].lowContrast} 70%)`
|
||||
: `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`;
|
||||
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
width: isSmall
|
||||
? `calc(50% - (1 * ${theme.spacing(8)} / 2))`
|
||||
: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
||||
|
||||
background: bg,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
type StatBoxProps = React.PropsWithChildren<{
|
||||
title: string;
|
||||
subtitle: string;
|
||||
palette?: PaletteKey;
|
||||
}>;
|
||||
|
||||
export const StatBox: React.FC<StatBoxProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
palette,
|
||||
children,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
|
||||
|
||||
return (
|
||||
<GradientBox palette={palette}>
|
||||
<Stack>
|
||||
<Typography color={textColor}>{title}</Typography>
|
||||
<Typography color={textColor}>{subtitle}</Typography>
|
||||
{children}
|
||||
</Stack>
|
||||
</GradientBox>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -11,15 +12,13 @@ type StatusBoxProps = React.PropsWithChildren<{}>;
|
||||
export const BGBox: React.FC<StatusBoxProps> = ({ children }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
flex={1}
|
||||
border={1}
|
||||
bgcolor={theme.palette.primary.main}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
p={theme.spacing(8)}
|
||||
overflow="hidden"
|
||||
<BaseBox
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
padding: theme.spacing(8),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
@@ -29,7 +28,7 @@ export const BGBox: React.FC<StatusBoxProps> = ({ children }) => {
|
||||
<Background />
|
||||
</Box>
|
||||
{children}
|
||||
</Box>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
import { getStatusPalette } from "@/Utils/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const StatusLabel = ({ status }: { status: MonitorStatus }) => {
|
||||
const theme = useTheme();
|
||||
const palette = getStatusPalette(status);
|
||||
const transformedText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
||||
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: theme.spacing(3, 5),
|
||||
color: theme.palette[palette].main,
|
||||
borderColor: theme.palette[palette].lowContrast,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={7}
|
||||
height={7}
|
||||
bgcolor={theme.palette[palette].lowContrast}
|
||||
borderRadius="50%"
|
||||
marginRight="5px"
|
||||
/>
|
||||
{transformedText}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,19 @@ import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import LastPageIcon from "@mui/icons-material/LastPage";
|
||||
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
||||
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
|
||||
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import TablePagination from "@mui/material/TablePagination";
|
||||
import type { TablePaginationProps } from "@mui/material/TablePagination";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
export type Header<T> = {
|
||||
id: number | string;
|
||||
content: React.ReactNode;
|
||||
@@ -87,3 +99,109 @@ export function DataTable<T extends { id?: string | number; _id?: string | numbe
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface TablePaginationActionsProps {
|
||||
count: number;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
||||
}
|
||||
|
||||
function TablePaginationActions(props: TablePaginationActionsProps) {
|
||||
const theme = useTheme();
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
|
||||
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
|
||||
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
|
||||
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
|
||||
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ flexShrink: 0, ml: 2.5 }}
|
||||
className="table-pagination-actions"
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const Pagination: React.FC<TablePaginationProps> = ({ ...props }) => {
|
||||
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TablePagination
|
||||
ActionsComponent={TablePaginationActions}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
{...props}
|
||||
sx={{
|
||||
"& .MuiTablePagination-toolbar": {
|
||||
display: isSmall ? "grid" : "flex",
|
||||
},
|
||||
"& .MuiTablePagination-selectLabel": {
|
||||
gridColumn: "1",
|
||||
gridRow: "1",
|
||||
justifySelf: "center",
|
||||
},
|
||||
"& .MuiTablePagination-select": {
|
||||
gridColumn: "2",
|
||||
gridRow: "1",
|
||||
justifySelf: "center",
|
||||
},
|
||||
"& .MuiTablePagination-displayedRows": {
|
||||
gridColumn: "2",
|
||||
gridRow: "2",
|
||||
justifySelf: "center ",
|
||||
},
|
||||
"& .table-pagination-actions": {
|
||||
gridColumn: "1",
|
||||
gridRow: "2",
|
||||
justifySelf: "center",
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
|
||||
export { BasePage } from "./BasePage";
|
||||
export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
|
||||
export { DataTable as Table } from "./Table";
|
||||
export { DataTable as Table, Pagination } from "./Table";
|
||||
export { GradientBox, StatBox } from "./StatBox";
|
||||
export { BaseBox } from "./BaseBox";
|
||||
export { StatusLabel } from "./StatusLabel";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import type { ButtonProps } from "@mui/material/Button";
|
||||
export const ButtonInput: React.FC<ButtonProps> = ({ ...props }) => {
|
||||
|
||||
export const ButtonInput: React.FC<ButtonProps> = ({ sx, ...props }) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
sx={{ textTransform: "none", height: 34, fontWeight: 400, borderRadius: 2 }}
|
||||
sx={{ textTransform: "none", height: 34, fontWeight: 400, borderRadius: 2, ...sx }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import type { ButtonGroupProps } from "@mui/material/ButtonGroup";
|
||||
export const ButtonGroupInput: React.FC<ButtonGroupProps> = ({
|
||||
orientation,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<ButtonGroup
|
||||
orientation={orientation}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export { ButtonInput as Button } from "./Button";
|
||||
export { ButtonGroupInput as ButtonGroup } from "./ButtonGroup";
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { BaseChart } from "./HistogramStatus";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react";
|
||||
import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
|
||||
|
||||
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const ChartAvgResponse = ({ avg, max }: { avg: number; max: number }) => {
|
||||
const theme = useTheme();
|
||||
const chartData = [
|
||||
{ name: "max", value: max - avg, color: "transparent" },
|
||||
{ name: "avg", value: avg, color: "red" },
|
||||
];
|
||||
|
||||
const palette = getResponseTimeColor(avg);
|
||||
const msg: Record<string, string> = {
|
||||
success: "Excellent",
|
||||
warning: "Average",
|
||||
danger: "Poor",
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseChart
|
||||
icon={<AverageResponseIcon />}
|
||||
title={"Average response time"}
|
||||
>
|
||||
<Stack
|
||||
height="100%"
|
||||
position={"relative"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={155}
|
||||
>
|
||||
<RadialBarChart
|
||||
cy="89%"
|
||||
data={chartData}
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={"120%"}
|
||||
outerRadius={"200%"}
|
||||
>
|
||||
<RadialBar
|
||||
dataKey="value"
|
||||
background={{ fill: theme.palette[palette].lowContrast }}
|
||||
>
|
||||
<Cell visibility={"hidden"} />
|
||||
<Cell fill={theme.palette[palette].main} />
|
||||
</RadialBar>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Typography variant="body2">Low</Typography>
|
||||
<Typography variant="body2">High</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
position="absolute"
|
||||
top={"50%"}
|
||||
right={"50%"}
|
||||
sx={{
|
||||
transform: "translate(50%, 0%)",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
textAlign={"center"}
|
||||
>
|
||||
{msg[palette]}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
textAlign={"center"}
|
||||
>{`${avg?.toFixed()}ms`}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import { BaseChart } from "./HistogramStatus";
|
||||
import { BaseBox } from "../DesignElements";
|
||||
import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
} from "recharts";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import {
|
||||
formatDateWithTz,
|
||||
tickDateFormatLookup,
|
||||
tooltipDateFormatLookup,
|
||||
} from "@/Utils/TimeUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { GroupedCheck } from "@/Types/Check";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
type XTickProps = {
|
||||
x: number;
|
||||
y: number;
|
||||
payload: { value: any };
|
||||
range: string;
|
||||
};
|
||||
|
||||
const XTick: React.FC<XTickProps> = ({ x, y, payload, range }) => {
|
||||
const format = tickDateFormatLookup(range);
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 10}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.primary.contrastTextTertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
type ResponseTimeToolTipProps = {
|
||||
active?: boolean | undefined;
|
||||
payload?: any[];
|
||||
label?: string;
|
||||
range: string;
|
||||
theme: any;
|
||||
uiTimezone: string;
|
||||
};
|
||||
|
||||
const ResponseTimeToolTip: React.FC<ResponseTimeToolTipProps> = ({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
range,
|
||||
theme,
|
||||
uiTimezone,
|
||||
}) => {
|
||||
if (!label) return null;
|
||||
if (!payload) return null;
|
||||
if (!active) return null;
|
||||
|
||||
const format = tooltipDateFormatLookup(range);
|
||||
const responseTime = Math.floor(payload?.[0]?.value || 0);
|
||||
return (
|
||||
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
|
||||
<Typography>{formatDateWithTz(label, format, uiTimezone)}</Typography>
|
||||
<Typography>Response time: {responseTime} ms</Typography>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChartResponseTime = ({
|
||||
checks,
|
||||
range,
|
||||
}: {
|
||||
checks: GroupedCheck[];
|
||||
range: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||
|
||||
return (
|
||||
<BaseChart
|
||||
icon={<ResponseTimeIcon />}
|
||||
title="Response times"
|
||||
>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={300}
|
||||
>
|
||||
<AreaChart data={checks?.slice().reverse()}>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="colorUv"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.accent.main}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.accent.light}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
dataKey="_id"
|
||||
tick={(props) => (
|
||||
<XTick
|
||||
{...props}
|
||||
range={range}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
content={(props) => (
|
||||
<ResponseTimeToolTip
|
||||
{...props}
|
||||
range={range}
|
||||
theme={theme}
|
||||
uiTimezone={uiTimezone}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="avgResponseTime"
|
||||
stroke={theme.palette.accent.main}
|
||||
fill="url(#colorUv)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { MonitorStatus } from "@/Components/v2/Monitors/MonitorStatus";
|
||||
import { ButtonGroup, Button } from "@/Components/v2/Inputs";
|
||||
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
|
||||
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
import BugReportOutlinedIcon from "@mui/icons-material/BugReportOutlined";
|
||||
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
import type { IMonitor } from "@/Types/Monitor";
|
||||
|
||||
export const HeaderControls = ({
|
||||
monitor,
|
||||
patch,
|
||||
isPatching,
|
||||
refetch,
|
||||
}: {
|
||||
monitor: IMonitor;
|
||||
patch: Function;
|
||||
isPatching: boolean;
|
||||
refetch: Function;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
spacing={isSmall ? theme.spacing(4) : 0}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<MonitorStatus monitor={monitor} />
|
||||
<Stack
|
||||
direction={"row"}
|
||||
spacing={theme.spacing(2)}
|
||||
>
|
||||
<ButtonGroup
|
||||
orientation={isSmall ? "vertical" : "horizontal"}
|
||||
fullWidth={isSmall}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
>
|
||||
<Button startIcon={<EmailIcon />}>{t("sendTestNotifications")}</Button>
|
||||
<Button startIcon={<BugReportOutlinedIcon />}>{t("menu.incidents")}</Button>
|
||||
<Button
|
||||
loading={isPatching}
|
||||
onClick={async () => {
|
||||
await patch();
|
||||
refetch();
|
||||
}}
|
||||
startIcon={
|
||||
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||
}
|
||||
>
|
||||
{monitor?.isActive ? t("pause") : t("resume")}
|
||||
</Button>
|
||||
<Button startIcon={<SettingsOutlinedIcon />}>{t("configure")}</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { ButtonGroup, Button } from "@/Components/v2/Inputs";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
export const HeaderRange = ({
|
||||
range,
|
||||
setRange,
|
||||
loading,
|
||||
}: {
|
||||
range: string;
|
||||
setRange: Function;
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(9)}
|
||||
direction={isSmall ? "column" : "row"}
|
||||
alignItems={"center"}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Typography variant="body2">{`Showing statistics for past ${range}`}</Typography>
|
||||
<ButtonGroup
|
||||
orientation={isSmall ? "vertical" : "horizontal"}
|
||||
fullWidth={isSmall}
|
||||
variant="contained"
|
||||
color={"primary"}
|
||||
>
|
||||
<Button
|
||||
color={range === "2h" ? "secondary" : "inherit"}
|
||||
onClick={() => setRange("2h")}
|
||||
loading={loading}
|
||||
>
|
||||
Recent
|
||||
</Button>
|
||||
<Button
|
||||
color={range === "24h" ? "secondary" : "inherit"}
|
||||
onClick={() => setRange("24h")}
|
||||
loading={loading}
|
||||
>
|
||||
Day
|
||||
</Button>
|
||||
<Button
|
||||
color={range === "7d" ? "secondary" : "inherit"}
|
||||
onClick={() => setRange("7d")}
|
||||
loading={loading}
|
||||
>
|
||||
7 days
|
||||
</Button>
|
||||
<Button
|
||||
color={range === "30d" ? "secondary" : "inherit"}
|
||||
onClick={() => setRange("30d")}
|
||||
loading={loading}
|
||||
>
|
||||
30 days
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
|
||||
import UptimeIcon from "@/assets/icons/uptime-icon.svg?react";
|
||||
import IncidentsIcon from "@/assets/icons/incidents.svg?react";
|
||||
|
||||
import type { GroupedCheck } from "@/Types/Check";
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
import { useState } from "react";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
|
||||
|
||||
const XLabel = ({
|
||||
p1,
|
||||
p2,
|
||||
range,
|
||||
}: {
|
||||
p1: GroupedCheck;
|
||||
p2: GroupedCheck;
|
||||
range: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||
const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D";
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
x={0}
|
||||
y="100%"
|
||||
dy={-3}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
fill={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{formatDateWithTz(p1._id, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
<text
|
||||
x="100%"
|
||||
y="100%"
|
||||
dy={-3}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
fill={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{formatDateWithTz(p2._id, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type BaseChartProps = React.PropsWithChildren<{
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
export const BaseChart: React.FC<BaseChartProps> = ({ children, icon, title }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
padding: theme.spacing(8),
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
flex={1}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<BaseBox
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 34,
|
||||
height: 34,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
"& svg": {
|
||||
width: 20,
|
||||
height: 20,
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</BaseBox>
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
</Stack>
|
||||
<Box flex={1}>{children}</Box>
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const HistogramStatus = ({
|
||||
checks,
|
||||
status,
|
||||
range,
|
||||
title,
|
||||
}: {
|
||||
checks: GroupedCheck[];
|
||||
status: MonitorStatus;
|
||||
range: string;
|
||||
title: string;
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||
|
||||
const icon = status === "up" ? <UptimeIcon /> : <IncidentsIcon />;
|
||||
const theme = useTheme();
|
||||
const [idx, setIdx] = useState<number | null>(null);
|
||||
const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D";
|
||||
|
||||
if (checks.length === 0) {
|
||||
return (
|
||||
<BaseChart
|
||||
icon={icon}
|
||||
title={title}
|
||||
>
|
||||
<Stack
|
||||
height={"100%"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
>
|
||||
<Typography variant="h2">
|
||||
{status === "up" ? "No checks yet" : "Great, no downtime yet!"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
}
|
||||
|
||||
const totalChecks = checks.reduce((count, check) => {
|
||||
return count + check.count;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<BaseChart
|
||||
icon={icon}
|
||||
title={title}
|
||||
>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Stack
|
||||
position="relative"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack>
|
||||
<Typography>Total checks</Typography>
|
||||
{idx ? (
|
||||
<Stack>
|
||||
<Typography variant="h2">{checks[idx].count}</Typography>
|
||||
<Typography
|
||||
position={"absolute"}
|
||||
top={"100%"}
|
||||
>
|
||||
{formatDateWithTz(checks[idx]._id, dateFormat, uiTimezone)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="h2">{totalChecks}</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={155}
|
||||
>
|
||||
<BarChart data={checks}>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<XLabel
|
||||
p1={checks[0]}
|
||||
p2={checks[checks.length - 1]}
|
||||
range={range}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="avgResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{checks?.map((groupedCheck, idx) => {
|
||||
const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime);
|
||||
return (
|
||||
<Cell
|
||||
onMouseEnter={() => setIdx(idx)}
|
||||
onMouseLeave={() => setIdx(null)}
|
||||
key={groupedCheck._id}
|
||||
fill={theme.palette[fillColor].main}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { IMonitor } from "@/Types/Monitor";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { PulseDot } from "@/Components/v2/DesignElements/PulseDot";
|
||||
import { Dot } from "@/Components/v2/DesignElements/Dot";
|
||||
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { typographyLevels } from "@/Utils/Theme/v2/palette";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
export const MonitorStatus = ({ monitor }: { monitor: IMonitor }) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
if (!monitor) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Stack>
|
||||
<Typography
|
||||
fontSize={typographyLevels.xl}
|
||||
fontWeight={500}
|
||||
color={theme.palette.primary.contrastText}
|
||||
overflow={"hidden"}
|
||||
textOverflow={"ellipsis"}
|
||||
whiteSpace={"nowrap"}
|
||||
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={getStatusColor(monitor.status, theme)} />
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontSize={typographyLevels.l}
|
||||
fontWeight={"bolder"}
|
||||
fontFamily={"monospace"}
|
||||
overflow={"hidden"}
|
||||
textOverflow={"ellipsis"}
|
||||
whiteSpace={"nowrap"}
|
||||
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
||||
>
|
||||
{formatUrl(monitor?.url)}
|
||||
</Typography>
|
||||
{!isSmall && (
|
||||
<>
|
||||
<Dot />
|
||||
<Typography>
|
||||
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import type { SWRConfiguration } from "swr";
|
||||
import type { AxiosRequestConfig } from "axios";
|
||||
import { get, post } from "@/Utils/ApiClient"; // your axios wrapper
|
||||
import { get, post, patch } from "@/Utils/ApiClient"; // your axios wrapper
|
||||
|
||||
export type ApiResponse = {
|
||||
message: string;
|
||||
@@ -20,7 +20,7 @@ export const useGet = <T,>(
|
||||
axiosConfig?: AxiosRequestConfig,
|
||||
swrConfig?: SWRConfiguration<T, Error>
|
||||
) => {
|
||||
const { data, error, isLoading, mutate } = useSWR<T>(
|
||||
const { data, error, isLoading, isValidating, mutate } = useSWR<T>(
|
||||
url,
|
||||
(url) => fetcher<T>(url, axiosConfig),
|
||||
swrConfig
|
||||
@@ -29,6 +29,7 @@ export const useGet = <T,>(
|
||||
return {
|
||||
response: data ?? null,
|
||||
loading: isLoading,
|
||||
isValidating,
|
||||
error: error?.message ?? null,
|
||||
refetch: mutate,
|
||||
};
|
||||
@@ -56,3 +57,26 @@ export const usePost = <B = any, R = any>(endpoint: string) => {
|
||||
|
||||
return { post: postFn, loading, error };
|
||||
};
|
||||
|
||||
export const usePatch = <B = any, R = any>(endpoint: string) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const patchFn = async (body: B, config?: AxiosRequestConfig): Promise<R | null> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await patch<R>(endpoint, body, config);
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||
setError(errMsg);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { patch: patchFn, loading, error };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Table, Pagination } from "@/Components/v2/DesignElements";
|
||||
import { StatusLabel } from "@/Components/v2/DesignElements";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import type { Header } from "@/Components/v2/DesignElements/Table";
|
||||
import type { Check } from "@/Types/Check";
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGet } from "@/Hooks/v2/UseApi";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
const headers: Header<Check>[] = [
|
||||
{
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status as MonitorStatus} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "date",
|
||||
content: t("date&Time"),
|
||||
render: (row) => {
|
||||
return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "statusCode",
|
||||
content: t("statusCode"),
|
||||
render: (row) => {
|
||||
return row.httpStatusCode || "N/A";
|
||||
},
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const CheckTable = ({ monitorId }: { monitorId: string }) => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const { t } = useTranslation();
|
||||
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||
const headers = getHeaders(t, uiTimezone);
|
||||
|
||||
const { response, error } = useGet<ApiResponse>(
|
||||
`/monitors/${monitorId}/checks?page=${page}&rowsPerPage=${rowsPerPage}`,
|
||||
{},
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
const checks = response?.data?.checks || [];
|
||||
const count = response?.data?.count || 0;
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
newPage: number
|
||||
) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleRowsPerPageChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
const value = Number(e.target.value);
|
||||
setPage(0);
|
||||
setRowsPerPage(value);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Table
|
||||
headers={headers}
|
||||
data={checks}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
count={count}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
onRowsPerPageChange={handleRowsPerPageChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { ConfigBox, BasePage } from "@/Components/v2/DesignElements";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import { RadioWithDescription } from "@/Components/v2/Inputs/RadioInput";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Button } from "@/Components/v2/Inputs";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import { Typography } from "@mui/material";
|
||||
import humanInterval from "human-interval";
|
||||
@@ -19,7 +19,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useGet, usePost } from "@/Hooks/v2/UseApi";
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
|
||||
const CreateUptimePage = () => {
|
||||
const UptimeCreatePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -259,4 +259,4 @@ const CreateUptimePage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUptimePage;
|
||||
export default UptimeCreatePage;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { BasePage } from "@/Components/v2/DesignElements";
|
||||
import { HeaderControls } from "@/Components/v2/Monitors/HeaderControls";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { StatBox } from "@/Components/v2/DesignElements";
|
||||
import { HistogramStatus } from "@/Components/v2/Monitors/HistogramStatus";
|
||||
import { ChartAvgResponse } from "@/Components/v2/Monitors/ChartAvgResponse";
|
||||
import { ChartResponseTime } from "@/Components/v2/Monitors/ChartResponseTime";
|
||||
import { HeaderRange } from "@/Components/v2/Monitors/HeaderRange";
|
||||
import { CheckTable } from "@/Pages/v2/Uptime/CheckTable";
|
||||
|
||||
import type { IMonitor } from "@/Types/Monitor";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useParams } from "react-router";
|
||||
import { useGet, usePatch, type ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
import { useState } from "react";
|
||||
import { getStatusPalette } from "@/Utils/MonitorUtils";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
|
||||
const UptimeDetailsPage = () => {
|
||||
const { id } = useParams();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
// Local state
|
||||
const [range, setRange] = useState("2h");
|
||||
|
||||
const { response, isValidating, error, refetch } = useGet<ApiResponse>(
|
||||
`/monitors/${id}?embedChecks=true&range=${range}`,
|
||||
|
||||
{},
|
||||
{ refreshInterval: 30000 }
|
||||
);
|
||||
|
||||
const {
|
||||
response: upResponse,
|
||||
isValidating: upIsValidating,
|
||||
error: upError,
|
||||
} = useGet<ApiResponse>(
|
||||
`/monitors/${id}?embedChecks=true&range=${range}&status=up`,
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
const {
|
||||
response: downResponse,
|
||||
error: downError,
|
||||
isValidating: downIsValidating,
|
||||
} = useGet<ApiResponse>(
|
||||
`/monitors/${id}?embedChecks=true&range=${range}&status=down`,
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
const {
|
||||
patch,
|
||||
loading: isPatching,
|
||||
error: postError,
|
||||
} = usePatch<ApiResponse>(`/monitors/${id}/active`);
|
||||
|
||||
const monitor: IMonitor = response?.data?.monitor;
|
||||
|
||||
if (!monitor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stats = response?.data?.stats || null;
|
||||
const avgResponseTime = stats?.avgResponseTime || 0;
|
||||
const maxResponseTime = stats?.maxResponseTime || 0;
|
||||
|
||||
const streakDuration = stats?.currentStreakStartedAt
|
||||
? Date.now() - stats?.currentStreakStartedAt
|
||||
: 0;
|
||||
|
||||
const lastChecked = stats?.lastCheckTimestamp
|
||||
? Date.now() - stats?.lastCheckTimestamp
|
||||
: -1;
|
||||
|
||||
const checks = response?.data?.checks || [];
|
||||
const upChecks = upResponse?.data?.checks ? [...upResponse.data.checks].reverse() : [];
|
||||
const downChecks = downResponse?.data?.checks
|
||||
? [...downResponse.data.checks].reverse()
|
||||
: [];
|
||||
|
||||
const palette = getStatusPalette(monitor.status);
|
||||
|
||||
if (error || upError || downError || postError) {
|
||||
console.error("Error fetching monitor data:", {
|
||||
error,
|
||||
upError,
|
||||
downError,
|
||||
postError,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<HeaderControls
|
||||
monitor={monitor}
|
||||
patch={patch}
|
||||
isPatching={isPatching}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<StatBox
|
||||
palette={palette}
|
||||
title="Active for"
|
||||
subtitle={prettyMilliseconds(streakDuration, { secondsDecimalDigits: 0 })}
|
||||
/>
|
||||
<StatBox
|
||||
title="Last check"
|
||||
subtitle={
|
||||
lastChecked >= 0
|
||||
? `${prettyMilliseconds(lastChecked, { secondsDecimalDigits: 0 })} ago`
|
||||
: "N/A"
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
title="Last response time"
|
||||
subtitle={stats?.lastResponseTime ? `${stats?.lastResponseTime} ms` : "N/A"}
|
||||
/>
|
||||
</Stack>
|
||||
<HeaderRange
|
||||
loading={isValidating || upIsValidating || downIsValidating}
|
||||
range={range}
|
||||
setRange={setRange}
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<HistogramStatus
|
||||
title="Uptime"
|
||||
status={"up"}
|
||||
checks={upChecks}
|
||||
range={range}
|
||||
/>
|
||||
<HistogramStatus
|
||||
title="Incidents"
|
||||
checks={downChecks}
|
||||
status={"down"}
|
||||
range={range}
|
||||
/>
|
||||
<ChartAvgResponse
|
||||
avg={avgResponseTime}
|
||||
max={maxResponseTime}
|
||||
/>
|
||||
</Stack>
|
||||
<ChartResponseTime
|
||||
checks={checks}
|
||||
range={range}
|
||||
/>
|
||||
<CheckTable monitorId={monitor._id} />
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeDetailsPage;
|
||||
@@ -1,16 +1,23 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table } from "@/Components/v2/DesignElements";
|
||||
import { HistogramResponseTime } from "@/Components/v2/Monitors/HistogramResponseTime";
|
||||
import type { Header } from "@/Components/v2/DesignElements/Table";
|
||||
import type { IMonitor } from "@/Types/Monitor";
|
||||
import { Table } from "@/Components/v2/DesignElements";
|
||||
import { ActionsMenu } from "@/Components/v2/ActionsMenu";
|
||||
import { StatusLabel } from "@/Components/v2/DesignElements";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { ActionsMenu } from "@/Components/v2/ActionsMenu";
|
||||
import type { ActionMenuItem } from "@/Components/v2/ActionsMenu";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const getActions = (theme: any): ActionMenuItem[] => {
|
||||
import type { IMonitor } from "@/Types/Monitor";
|
||||
import type { ActionMenuItem } from "@/Components/v2/ActionsMenu";
|
||||
const getActions = (
|
||||
theme: any,
|
||||
monitor: IMonitor,
|
||||
navigate: Function
|
||||
): ActionMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
@@ -24,7 +31,7 @@ const getActions = (theme: any): ActionMenuItem[] => {
|
||||
id: 2,
|
||||
label: "Details",
|
||||
action: () => {
|
||||
console.log("Open details");
|
||||
navigate(`${monitor._id}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -67,7 +74,7 @@ const getActions = (theme: any): ActionMenuItem[] => {
|
||||
];
|
||||
};
|
||||
|
||||
const getHeaders = (theme: any, t: Function) => {
|
||||
const getHeaders = (theme: any, t: Function, navigate: Function) => {
|
||||
const headers: Header<IMonitor>[] = [
|
||||
{
|
||||
id: "name",
|
||||
@@ -80,7 +87,7 @@ const getHeaders = (theme: any, t: Function) => {
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
render: (row) => {
|
||||
return row.status;
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -104,8 +111,8 @@ const getHeaders = (theme: any, t: Function) => {
|
||||
{
|
||||
id: "actions",
|
||||
content: t("actions"),
|
||||
render: () => {
|
||||
return <ActionsMenu items={getActions(theme)} />;
|
||||
render: (row) => {
|
||||
return <ActionsMenu items={getActions(theme, row, navigate)} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -116,8 +123,9 @@ export const MonitorTable = ({ monitors }: { monitors: IMonitor[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const navigate = useNavigate();
|
||||
|
||||
let headers = getHeaders(theme, t);
|
||||
let headers = getHeaders(theme, t, navigate);
|
||||
|
||||
if (isSmall) {
|
||||
headers = headers.filter((h) => h.id !== "histogram");
|
||||
|
||||
@@ -18,7 +18,7 @@ const UptimeMonitors = () => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const { response, loading } = useGet<ApiResponse>("/monitors?embedChecks=true");
|
||||
const { response, loading } = useGet<ApiResponse>("/monitors?embedChecks=true", {});
|
||||
const monitors = response?.data ?? ([] as IMonitor[]);
|
||||
|
||||
if (monitors.length === 0 && !loading) {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme";
|
||||
import AuthLoginV2 from "@/Pages/v2/Auth/Login";
|
||||
import AuthRegisterV2 from "@/Pages/v2/Auth/Register";
|
||||
import UptimeMonitorsPage from "@/Pages/v2/Uptime/UptimeMonitors";
|
||||
import CreateUptimePage from "@/Pages/v2/Uptime/Create";
|
||||
import UptimeCreatePage from "@/Pages/v2/Uptime/Create";
|
||||
import UptimeDetailsPage from "@/Pages/v2/Uptime/Details";
|
||||
import RootLayout from "@/Components/v2/Layouts/RootLayout";
|
||||
|
||||
const V2Routes = ({ mode = "light" }) => {
|
||||
@@ -34,9 +35,13 @@ const V2Routes = ({ mode = "light" }) => {
|
||||
path="uptime"
|
||||
element={<UptimeMonitorsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="uptime/:id"
|
||||
element={<UptimeDetailsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="uptime/create"
|
||||
element={<CreateUptimePage />}
|
||||
element={<UptimeCreatePage />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
export interface CheckTimingPhases {
|
||||
wait: number;
|
||||
dns: number;
|
||||
tcp: number;
|
||||
tls: number;
|
||||
request: number;
|
||||
firstByte: number;
|
||||
download: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface CheckTimings {
|
||||
start: string;
|
||||
socket: string;
|
||||
lookup: string;
|
||||
connect: string;
|
||||
secureConnect: string;
|
||||
response: string;
|
||||
end: string;
|
||||
phases: CheckTimingPhases;
|
||||
}
|
||||
|
||||
export interface Check {
|
||||
_id: string;
|
||||
monitorId: string;
|
||||
type: string;
|
||||
status: string;
|
||||
message: string;
|
||||
responseTime: number;
|
||||
httpStatusCode: number;
|
||||
ack: boolean;
|
||||
expiry: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
timings: CheckTimings;
|
||||
}
|
||||
|
||||
export interface GroupedCheck {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Check } from "@/Types/Check";
|
||||
export type MonitorStatus = "up" | "down" | "initializing";
|
||||
|
||||
export interface IMonitor {
|
||||
checks: Check[];
|
||||
@@ -9,7 +10,7 @@ export interface IMonitor {
|
||||
latestChecks: Check[];
|
||||
n: number;
|
||||
name: string;
|
||||
status: string;
|
||||
status: MonitorStatus;
|
||||
type: string;
|
||||
updatedAt: string;
|
||||
updatedBy: string;
|
||||
|
||||
@@ -18,4 +18,10 @@ export const post = <T>(
|
||||
config: AxiosRequestConfig = {}
|
||||
): Promise<AxiosResponse<T>> => api.post<T>(url, data, config);
|
||||
|
||||
export const patch = <T>(
|
||||
url: string,
|
||||
data: any,
|
||||
config: AxiosRequestConfig = {}
|
||||
): Promise<AxiosResponse<T>> => api.patch<T>(url, data, config);
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
import type { PaletteKey } from "./Theme/v2/theme";
|
||||
export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
|
||||
const paletteMap: Record<MonitorStatus, PaletteKey> = {
|
||||
up: "success",
|
||||
down: "error",
|
||||
initializing: "warning",
|
||||
};
|
||||
return paletteMap[status];
|
||||
};
|
||||
|
||||
export const getStatusColor = (status: MonitorStatus, theme: any): string => {
|
||||
const statusColors: Record<MonitorStatus, string> = {
|
||||
up: theme.palette.success.lowContrast,
|
||||
down: theme.palette.error.lowContrast,
|
||||
initializing: theme.palette.warning.lowContrast,
|
||||
};
|
||||
return statusColors[status];
|
||||
};
|
||||
|
||||
export const getResponseTimeColor = (responseTime: number): PaletteKey => {
|
||||
if (responseTime < 200) {
|
||||
return "success";
|
||||
} else if (responseTime < 300) {
|
||||
return "warning";
|
||||
} else {
|
||||
return "error";
|
||||
}
|
||||
};
|
||||
|
||||
export const formatUrl = (url: string, maxLength: number = 55) => {
|
||||
if (!url) return "";
|
||||
|
||||
const strippedUrl = url.replace(/^https?:\/\//, "");
|
||||
return strippedUrl.length > maxLength
|
||||
? `${strippedUrl.slice(0, maxLength)}…`
|
||||
: strippedUrl;
|
||||
};
|
||||
@@ -1,5 +1,12 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
import { lightPalette, darkPalette, typographyLevels } from "./palette";
|
||||
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
|
||||
export type PaletteKey = {
|
||||
[K in keyof Theme["palette"]]: Theme["palette"][K] extends { main: any } ? K : never;
|
||||
}[keyof Theme["palette"]];
|
||||
|
||||
const fontFamilyPrimary = '"Inter" , sans-serif';
|
||||
const shadow =
|
||||
"0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)";
|
||||
|
||||
@@ -23,3 +23,31 @@ export const formatDateWithTz = (timestamp: string, format: string, timezone: st
|
||||
const formattedDate = dayjs(timestamp).tz(timezone).format(format);
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
export const tickDateFormatLookup = (range: string) => {
|
||||
const tickFormatLookup: Record<string, string> = {
|
||||
"2h": "h:mm A",
|
||||
"24h": "h:mm A",
|
||||
"7d": "MM/D, h:mm A",
|
||||
"30d": "ddd. M/D",
|
||||
};
|
||||
const format = tickFormatLookup[range];
|
||||
if (format === undefined) {
|
||||
return "";
|
||||
}
|
||||
return format;
|
||||
};
|
||||
|
||||
export const tooltipDateFormatLookup = (range: string) => {
|
||||
const dateFormatLookup: Record<string, string> = {
|
||||
"2h": "ddd. MMMM D, YYYY, hh:mm A",
|
||||
"24h": "ddd. MMMM D, YYYY, hh:mm A",
|
||||
"7d": "ddd. MMMM D, YYYY, hh:mm A",
|
||||
"30d": "ddd. MMMM D, YYYY",
|
||||
};
|
||||
const format = dateFormatLookup[range];
|
||||
if (format === undefined) {
|
||||
return "";
|
||||
}
|
||||
return format;
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"ignore": ["src/locales/*", "*.log", "node_modules/*"],
|
||||
"watch": ["src/**/*.js", "*.json"],
|
||||
"ext": "js,json"
|
||||
"watch": ["src/**/*.ts", "src/**/*.js", "*.json"],
|
||||
"ext": "ts,js,json"
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const initializeControllers = (services) => {
|
||||
controllers.authControllerV2 = new AuthControllerV2(services.authServiceV2, services.inviteServiceV2);
|
||||
controllers.inviteControllerV2 = new InviteControllerV2(services.inviteServiceV2);
|
||||
controllers.maintenanceControllerV2 = new MaintenanceControllerV2(services.maintenanceServiceV2);
|
||||
controllers.monitorControllerV2 = new MonitorControllerV2(services.monitorServiceV2);
|
||||
controllers.monitorControllerV2 = new MonitorControllerV2(services.monitorServiceV2, services.checkServiceV2);
|
||||
controllers.notificationChannelControllerV2 = new NotificationChannelControllerV2(services.notificationChannelServiceV2);
|
||||
controllers.queueControllerV2 = new QueueControllerV2(services.jobQueueV2);
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ import { Request, Response, NextFunction } from "express";
|
||||
import ApiError from "../../utils/ApiError.js";
|
||||
import MonitorService from "../../service/v2/business/MonitorService.js";
|
||||
import { MonitorType } from "../../db/v2/models/monitors/Monitor.js";
|
||||
import CheckService from "../../service/v2/business/CheckService.js";
|
||||
class MonitorController {
|
||||
private monitorService: MonitorService;
|
||||
constructor(monitorService: MonitorService) {
|
||||
private checkService: CheckService;
|
||||
constructor(monitorService: MonitorService, checkService: CheckService) {
|
||||
this.monitorService = monitorService;
|
||||
this.checkService = checkService;
|
||||
}
|
||||
|
||||
create = async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -25,6 +28,86 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
getAll = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
let monitors;
|
||||
if (req.query.embedChecks === "true") {
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const limit = Math.max(1, Number(req.query.limit) || 10);
|
||||
const type: MonitorType[] = req.query.type as MonitorType[];
|
||||
|
||||
monitors = await this.monitorService.getAllEmbedChecks(page, limit, type);
|
||||
} else {
|
||||
monitors = await this.monitorService.getAll();
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: "Monitors retrieved successfully",
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getChecks = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const page = Number(req.query.page);
|
||||
const rowsPerPage = Number(req.query.rowsPerPage);
|
||||
|
||||
if (isNaN(page)) throw new ApiError("Page query parameter must be a number", 400);
|
||||
if (isNaN(rowsPerPage)) throw new ApiError("rowsPerPage query parameter must be a number", 400);
|
||||
|
||||
if (page < 0) throw new ApiError("Page must be greater than 0", 400);
|
||||
if (rowsPerPage < 0) throw new ApiError("rowsPerPage must be greater than 0", 400);
|
||||
|
||||
const { count, checks } = await this.checkService.getChecks(id, page, rowsPerPage);
|
||||
res.status(200).json({
|
||||
message: "Checks retrieved successfully",
|
||||
data: { count, checks },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
toggleActive = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const monitor = await this.monitorService.toggleActive(id, tokenizedUser);
|
||||
res.status(200).json({
|
||||
message: "Monitor paused/unpaused successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
get = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
@@ -62,55 +145,6 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
getAll = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
let monitors;
|
||||
if (req.query.embedChecks === "true") {
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const limit = Math.max(1, Number(req.query.limit) || 10);
|
||||
const type: MonitorType[] = req.query.type as MonitorType[];
|
||||
|
||||
monitors = await this.monitorService.getAllEmbedChecks(page, limit, type);
|
||||
} else {
|
||||
monitors = await this.monitorService.getAll();
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: "Monitors retrieved successfully",
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
toggleActive = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const monitor = await this.monitorService.toggleActive(id, tokenizedUser);
|
||||
res.status(200).json({
|
||||
message: "Monitor paused/unpaused successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
update = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
|
||||
@@ -17,12 +17,14 @@ class MonitorRoutes {
|
||||
|
||||
this.router.get("/", verifyToken, verifyPermission(["monitors.view"]), this.controller.getAll);
|
||||
|
||||
this.router.get("/:id/checks", verifyToken, verifyPermission(["monitors.view"]), this.controller.getChecks);
|
||||
|
||||
this.router.patch("/:id/active", verifyToken, verifyPermission(["monitors.update"]), this.controller.toggleActive);
|
||||
|
||||
this.router.patch("/:id", verifyToken, verifyPermission(["monitors.update"]), this.controller.update);
|
||||
|
||||
this.router.get("/:id", verifyToken, verifyPermission(["monitors.view"]), this.controller.get);
|
||||
|
||||
this.router.patch("/:id", verifyToken, verifyPermission(["monitors.update"]), this.controller.update);
|
||||
|
||||
this.router.delete("/:id", verifyToken, verifyPermission(["monitors.delete"]), this.controller.delete);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MonitorType } from "../../../db/v2/models/monitors/Monitor.js";
|
||||
import { StatusResponse } from "../infrastructure/NetworkService.js";
|
||||
import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js";
|
||||
import mongoose from "mongoose";
|
||||
import { stat } from "fs";
|
||||
|
||||
const SERVICE_NAME = "CheckServiceV2";
|
||||
export interface ICheckService {
|
||||
@@ -60,6 +61,7 @@ class CheckService implements ICheckService {
|
||||
monitorId: monitorId,
|
||||
type: statusResponse?.type,
|
||||
status: statusResponse?.status,
|
||||
httpStatusCode: statusResponse?.code,
|
||||
message: statusResponse?.message,
|
||||
responseTime: statusResponse?.responseTime,
|
||||
timings: statusResponse?.timings,
|
||||
@@ -130,6 +132,16 @@ class CheckService implements ICheckService {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
getChecks = async (monitorId: string, page: number, rowsPerPage: number) => {
|
||||
const count = await Check.countDocuments({ monitorId: new mongoose.Types.ObjectId(monitorId) });
|
||||
const checks = await Check.find({ monitorId: new mongoose.Types.ObjectId(monitorId) })
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(page * rowsPerPage)
|
||||
.limit(rowsPerPage)
|
||||
.exec();
|
||||
return { checks, count };
|
||||
};
|
||||
}
|
||||
|
||||
export default CheckService;
|
||||
|
||||
@@ -63,8 +63,8 @@ class MonitorService implements IMonitorService {
|
||||
private getStartDate(range: string): Date {
|
||||
const now = new Date();
|
||||
switch (range) {
|
||||
case "30m":
|
||||
return new Date(now.getTime() - 30 * 60 * 1000);
|
||||
case "2h":
|
||||
return new Date(now.getTime() - 2 * 60 * 60 * 1000);
|
||||
case "24h":
|
||||
return new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
case "7d":
|
||||
@@ -78,7 +78,7 @@ class MonitorService implements IMonitorService {
|
||||
|
||||
private getDateFormat(range: string): string {
|
||||
switch (range) {
|
||||
case "30m":
|
||||
case "2h":
|
||||
return "%Y-%m-%dT%H:%M:00Z";
|
||||
case "24h":
|
||||
case "7d":
|
||||
|
||||
Reference in New Issue
Block a user