mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-26 11:54:11 -06:00
iniital uptime
This commit is contained in:
101
client/src/Components/v2/DesignElements/StatusBox.tsx
Normal file
101
client/src/Components/v2/DesignElements/StatusBox.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
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"
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="5%"
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBox = ({
|
||||
label,
|
||||
n,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
n: number;
|
||||
color: string | undefined;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BGBox>
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Typography
|
||||
variant={"h2"}
|
||||
textTransform="uppercase"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h1"
|
||||
color={color}
|
||||
>
|
||||
{n}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</BGBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const UpStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("monitorStatus.up")}
|
||||
n={n}
|
||||
color={theme.palette.success.lowContrast}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DownStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("monitorStatus.down")}
|
||||
n={n}
|
||||
color={theme.palette.error.lowContrast}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PausedStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("monitorStatus.paused")}
|
||||
n={n}
|
||||
color={theme.palette.warning.lowContrast}
|
||||
/>
|
||||
);
|
||||
};
|
||||
69
client/src/Components/v2/DesignElements/Table.tsx
Normal file
69
client/src/Components/v2/DesignElements/Table.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
|
||||
export type Header<T> = {
|
||||
id: number | string;
|
||||
content: React.ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
|
||||
render: (row: T) => React.ReactNode;
|
||||
};
|
||||
|
||||
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
|
||||
headers: Header<T>[];
|
||||
data: T[];
|
||||
};
|
||||
|
||||
export function DataTable<T extends { id?: string | number; _id?: string | number }>({
|
||||
headers,
|
||||
data,
|
||||
}: DataTableProps<T>) {
|
||||
if (data.length === 0 || headers.length === 0) return <div>No data</div>;
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headers.map((header, idx) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={idx === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
>
|
||||
{header.content}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row) => {
|
||||
const key = row.id || row._id || Math.random();
|
||||
|
||||
return (
|
||||
<TableRow key={key}>
|
||||
{headers.map((header, index) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={index === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
onClick={
|
||||
header.onClick ? (e) => header.onClick!(e, row) : undefined
|
||||
}
|
||||
>
|
||||
{header.render(row)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
|
||||
export { BasePage } from "./BasePage";
|
||||
export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
|
||||
export { DataTable as Table } from "./Table";
|
||||
|
||||
12
client/src/Components/v2/Inputs/Button.tsx
Normal file
12
client/src/Components/v2/Inputs/Button.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import type { ButtonProps } from "@mui/material/Button";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
export const ButtonInput: React.FC<ButtonProps> = ({ ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
sx={{ textTransform: "none", height: 34, fontWeight: 400, borderRadius: 2 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
1
client/src/Components/v2/Inputs/index.tsx
Normal file
1
client/src/Components/v2/Inputs/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { ButtonInput as Button } from "./Button";
|
||||
@@ -6,6 +6,7 @@ const RootLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
overflow={"hidden"}
|
||||
direction="row"
|
||||
minHeight="100vh"
|
||||
>
|
||||
|
||||
35
client/src/Components/v2/Monitors/HeaderCreate.tsx
Normal file
35
client/src/Components/v2/Monitors/HeaderCreate.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Button } from "@/Components/v2/Inputs";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router";
|
||||
export const HeaderCreate = ({
|
||||
label,
|
||||
isLoading,
|
||||
path,
|
||||
}: {
|
||||
label?: string;
|
||||
isLoading: boolean;
|
||||
path: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="end"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => navigate(path)}
|
||||
>
|
||||
{label || t("createNew")}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
91
client/src/Components/v2/Monitors/HistogramResponseTime.tsx
Normal file
91
client/src/Components/v2/Monitors/HistogramResponseTime.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { Check } from "@/Types/Check";
|
||||
|
||||
export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => {
|
||||
let data = Array<any>();
|
||||
|
||||
data = checks.map((c) => Math.max(c.responseTime, 1));
|
||||
|
||||
const logResponses = data.map((r) => Math.log10(r));
|
||||
const logMin = Math.min(...logResponses);
|
||||
const logMax = Math.max(...logResponses);
|
||||
|
||||
if (!checks) {
|
||||
return null;
|
||||
}
|
||||
if (checks.length !== 25) {
|
||||
const placeholders = Array(25 - checks.length).fill("placeholder");
|
||||
data = [...checks, ...placeholders];
|
||||
} else {
|
||||
data = checks;
|
||||
}
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="nowrap"
|
||||
gap={theme.spacing(1.5)}
|
||||
height="50px"
|
||||
width="fit-content"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
sx={{
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{data.map((check, index) => {
|
||||
const safeResponse = Math.max(check.responseTime, 1);
|
||||
const logValue = Math.log10(safeResponse);
|
||||
const barHeight =
|
||||
logMax === logMin ? 100 : ((logValue - logMin) / (logMax - logMin)) * 100;
|
||||
|
||||
if (check === "placeholder") {
|
||||
return (
|
||||
<Box
|
||||
key={`${check}-${index}`}
|
||||
position="relative"
|
||||
width={theme.spacing(4.5)}
|
||||
height="100%"
|
||||
bgcolor={theme.palette.primary.lowContrast}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box
|
||||
key={`${check}-${index}`}
|
||||
position="relative"
|
||||
width="9px"
|
||||
height="100%"
|
||||
bgcolor={theme.palette.primary.lowContrast}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
width="100%"
|
||||
height={`${barHeight}%`}
|
||||
bgcolor={
|
||||
check.status
|
||||
? theme.palette.success.lowContrast
|
||||
: theme.palette.error.lowContrast
|
||||
}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
1
client/src/Components/v2/Monitors/index.tsx
Normal file
1
client/src/Components/v2/Monitors/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { HeaderCreate } from "./HeaderCreate";
|
||||
63
client/src/Pages/v2/Uptime/MonitorTable.tsx
Normal file
63
client/src/Pages/v2/Uptime/MonitorTable.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
const getHeaders = (t: Function) => {
|
||||
const headers: Header<IMonitor>[] = [
|
||||
{
|
||||
id: "name",
|
||||
content: t("host"),
|
||||
render: (row) => {
|
||||
return row.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
render: (row) => {
|
||||
return row.status;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "histogram",
|
||||
content: t("responseTime"),
|
||||
render: (row) => {
|
||||
return (
|
||||
<Stack alignItems={"center"}>
|
||||
<HistogramResponseTime checks={row.latestChecks} />
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: t("type"),
|
||||
render: (row) => {
|
||||
return row.type;
|
||||
},
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const MonitorTable = ({ monitors }: { monitors: IMonitor[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
let headers = getHeaders(t);
|
||||
|
||||
if (isSmall) {
|
||||
headers = headers.filter((h) => h.id !== "histogram");
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
headers={headers}
|
||||
data={monitors}
|
||||
/>
|
||||
);
|
||||
};
|
||||
49
client/src/Pages/v2/Uptime/UptimeMonitors.tsx
Normal file
49
client/src/Pages/v2/Uptime/UptimeMonitors.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
BasePage,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
} from "@/Components/v2/DesignElements";
|
||||
import { HeaderCreate } from "@/Components/v2/Monitors";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { MonitorTable } from "@/Pages/v2/Uptime/MonitorTable";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useGet } from "@/Hooks/v2/UseApi";
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
import type { IMonitor } from "@/Types/Monitor";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
|
||||
const UptimeMonitors = () => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const { response, error, loading, refetch } = useGet<ApiResponse>(
|
||||
"/monitors?embedChecks=true"
|
||||
);
|
||||
const monitors = response?.data ?? ([] as IMonitor[]);
|
||||
|
||||
if (monitors.length === 0 && !loading) {
|
||||
return "No monitors found";
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<HeaderCreate
|
||||
isLoading={loading}
|
||||
path="/v2/uptime/create"
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={1} />
|
||||
<DownStatusBox n={1} />
|
||||
<PausedStatusBox n={1} />
|
||||
</Stack>
|
||||
<MonitorTable monitors={monitors} />
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeMonitors;
|
||||
@@ -4,6 +4,7 @@ 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 RootLayout from "@/Components/v2/Layouts/RootLayout";
|
||||
|
||||
@@ -27,11 +28,11 @@ const V2Routes = ({ mode = "light" }) => {
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={<h1>Uptime</h1>}
|
||||
element={<UptimeMonitorsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="uptime"
|
||||
element={<h1>Test Page</h1>}
|
||||
element={<UptimeMonitorsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="uptime/create"
|
||||
|
||||
12
client/src/Types/Check.ts
Normal file
12
client/src/Types/Check.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface Check {
|
||||
_id: string;
|
||||
status: string;
|
||||
responseTime: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface GroupedCheck {
|
||||
_id: string;
|
||||
avgResponseTime: number;
|
||||
count: number;
|
||||
}
|
||||
19
client/src/Types/Monitor.ts
Normal file
19
client/src/Types/Monitor.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Check } from "@/Types/Check";
|
||||
|
||||
export interface IMonitor {
|
||||
checks: Check[];
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
interval: number;
|
||||
isActive: boolean;
|
||||
latestChecks: Check[];
|
||||
n: number;
|
||||
name: string;
|
||||
status: string;
|
||||
type: string;
|
||||
updatedAt: string;
|
||||
updatedBy: string;
|
||||
url: string;
|
||||
__v: number;
|
||||
_id: string;
|
||||
}
|
||||
@@ -58,8 +58,8 @@ const baseTheme = (palette) => ({
|
||||
variants: [
|
||||
{
|
||||
props: (props) => props.variant === "contained" && props.color === "accent",
|
||||
backgroundColor: theme.palette.accent.main,
|
||||
style: {
|
||||
backgroundColor: theme.palette.accent.main,
|
||||
color: theme.palette.primary.contrastTextSecondaryDarkBg,
|
||||
letterSpacing: "0.5px",
|
||||
textShadow: "0 0 1px rgba(0, 0, 0, 0.15)",
|
||||
|
||||
@@ -66,6 +66,21 @@ export const lightPalette = {
|
||||
main: colors.gray100,
|
||||
contrastText: colors.blueGray800,
|
||||
},
|
||||
success: {
|
||||
main: colors.green700,
|
||||
contrastText: colors.offWhite,
|
||||
lowContrast: colors.green400,
|
||||
},
|
||||
warning: {
|
||||
main: colors.orange700,
|
||||
contrastText: colors.offWhite,
|
||||
lowContrast: colors.orange100,
|
||||
},
|
||||
error: {
|
||||
main: colors.red700,
|
||||
contrastText: colors.offWhite,
|
||||
lowContrast: colors.red400,
|
||||
},
|
||||
};
|
||||
|
||||
export const darkPalette = {
|
||||
@@ -91,4 +106,19 @@ export const darkPalette = {
|
||||
main: colors.blueGray800,
|
||||
contrastText: colors.gray100,
|
||||
},
|
||||
success: {
|
||||
main: colors.green100,
|
||||
contrastText: colors.offBlack,
|
||||
lowContrast: colors.green200,
|
||||
},
|
||||
warning: {
|
||||
main: colors.orange200,
|
||||
contrastText: colors.offBlack,
|
||||
lowContrast: colors.orange600,
|
||||
},
|
||||
error: {
|
||||
main: colors.red100,
|
||||
contrastText: colors.offBlack,
|
||||
lowContrast: colors.red600,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user