Remove business V2 logic
This commit is contained in:
1 parent
1e3ee23120
commit
72bcc0d020
117 files changed
+1
-8794
No files matched your search
@@ -1,53 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Settings from "@/assets/icons/settings-bold.svg?react";
|
||||
|
||||
export type ActionMenuItem = {
|
||||
id: number | string;
|
||||
label: React.ReactNode;
|
||||
action: Function;
|
||||
closeMenu?: boolean;
|
||||
};
|
||||
|
||||
export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | any>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<any>) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(e.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = (e: React.MouseEvent<any>) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton onClick={handleClick}>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item.id}
|
||||
onClick={(e: React.MouseEvent<HTMLLIElement>) => {
|
||||
e.stopPropagation();
|
||||
if (item.closeMenu) handleClose(e);
|
||||
item.action();
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import LeftArrow from "@/assets/icons/left-arrow.svg?react";
|
||||
import LeftArrowDouble from "@/assets/icons/left-arrow-double.svg?react";
|
||||
import LeftArrowLong from "@/assets/icons/left-arrow-long.svg?react";
|
||||
|
||||
export const ArrowLeft = ({
|
||||
type,
|
||||
color = "#667085",
|
||||
...props
|
||||
}: {
|
||||
type?: string;
|
||||
color?: string | undefined;
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<LeftArrowDouble
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (type === "long") {
|
||||
return (
|
||||
<LeftArrowLong
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LeftArrow
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import RightArrow from "@/assets/icons/right-arrow.svg?react";
|
||||
import RightArrowDouble from "@/assets/icons/right-arrow-double.svg?react";
|
||||
|
||||
export const ArrowRight = ({
|
||||
type,
|
||||
color = "#667085",
|
||||
...props
|
||||
}: {
|
||||
type?: string;
|
||||
color?: string | undefined;
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<RightArrowDouble
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RightArrow
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { HeaderAuth } from "@/Components/v2/Auth";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
interface AuthBasePageProps extends StackProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthBasePage: React.FC<AuthBasePageProps> = ({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
...props
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
minHeight="100vh"
|
||||
{...props}
|
||||
>
|
||||
<HeaderAuth />
|
||||
<Stack
|
||||
alignItems="center"
|
||||
margin="auto"
|
||||
width="100%"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Box
|
||||
width={{ xs: 60, sm: 70, md: 80 }}
|
||||
mb={theme.spacing(10)}
|
||||
>
|
||||
<Logo style={{ width: "100%", height: "100%" }} />
|
||||
</Box>
|
||||
<Typography variant="h1">{title}</Typography>
|
||||
<Typography variant="h1">{subtitle}</Typography>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LanguageSelector, ThemeSwitch } from "@/Components/v2/Inputs";
|
||||
|
||||
export const HeaderAuth = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack
|
||||
width={"100%"}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<LanguageSelector />
|
||||
<ThemeSwitch color="red" />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export { HeaderAuth } from "./HeaderAuth";
|
||||
export { AuthBasePage } from "./AuthBasePage";
|
||||
@@ -1,23 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { ErrorFallback, EmptyFallback } from "./Fallback";
|
||||
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
interface BasePageProps extends StackProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BasePage: React.FC<BasePageProps> = ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
spacing={theme.spacing(10)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface BasePageWithStatesProps extends StackProps {
|
||||
loading: boolean;
|
||||
error: any;
|
||||
items: any[];
|
||||
page: string;
|
||||
actionLink?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const isEmpty = (items: any[]) => {
|
||||
if (!items) return true;
|
||||
if (Array.isArray(items) && items.length === 0) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const BasePageWithStates: React.FC<BasePageWithStatesProps> = ({
|
||||
loading,
|
||||
error,
|
||||
items,
|
||||
page,
|
||||
actionLink,
|
||||
children,
|
||||
...props
|
||||
}: BasePageWithStatesProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorFallback
|
||||
title="Something went wrong..."
|
||||
subtitle="Please try again later"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEmpty(items)) {
|
||||
return (
|
||||
<EmptyFallback
|
||||
page={page}
|
||||
title={t(`${page}Monitor.fallback.title`)}
|
||||
bullets={t(`${page}Monitor.fallback.checks`, { returnObjects: true })}
|
||||
actionButtonText={t(`${page}Monitor.fallback.actionButton`)}
|
||||
actionLink={actionLink || ""}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <BasePage {...props}>{children}</BasePage>;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import CheckOutlined from "@/assets/icons/check-outlined.svg?react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const BulletPointCheck = ({
|
||||
text,
|
||||
variant = "info",
|
||||
}: {
|
||||
text: string;
|
||||
noHighlightText?: string;
|
||||
variant?: "success" | "error" | "info";
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const colors: Record<string, string | undefined> = {
|
||||
success: theme.palette.success.main,
|
||||
error: theme.palette.error.main,
|
||||
info: theme.palette.primary.contrastTextSecondary,
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
className="check"
|
||||
gap={theme.spacing(6)}
|
||||
alignItems="center"
|
||||
>
|
||||
<CheckOutlined />
|
||||
<Typography
|
||||
component="span"
|
||||
color={
|
||||
variant === "info"
|
||||
? theme.palette.primary.contrastTextSecondary
|
||||
: colors[variant]
|
||||
}
|
||||
fontWeight={450}
|
||||
sx={{
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import OutputAnimation from "@/assets/Animations/output.gif";
|
||||
import DarkmodeOutput from "@/assets/Animations/darkmodeOutput.gif";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BulletPointCheck } from "@/Components/v2/DesignElements";
|
||||
import { Button } from "@/Components/v2/Inputs";
|
||||
|
||||
import { useNavigate } from "react-router";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import type { BoxProps } from "@mui/material";
|
||||
|
||||
interface BaseFallbackProps extends BoxProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BaseFallback: React.FC<BaseFallbackProps> = ({ children, ...props }) => {
|
||||
const theme = useTheme();
|
||||
const mode = useSelector((state: any) => state.ui.mode);
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
return (
|
||||
<Box
|
||||
margin={isSmall ? "inherit" : "auto"}
|
||||
marginTop={isSmall ? "33%" : "auto"}
|
||||
width={{
|
||||
sm: "90%",
|
||||
md: "70%",
|
||||
lg: "50%",
|
||||
xl: "40%",
|
||||
}}
|
||||
padding={theme.spacing(16)}
|
||||
bgcolor={theme.palette.primary.main}
|
||||
position="relative"
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
borderStyle: "dashed",
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
sx={{
|
||||
width: "fit-content",
|
||||
margin: "auto",
|
||||
marginTop: "100px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
||||
bgcolor="transparent"
|
||||
alt="Loading animation"
|
||||
width="100%"
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
border: "none",
|
||||
borderRadius: theme.spacing(8),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="center"
|
||||
maxWidth={"300px"}
|
||||
zIndex={1}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorFallback = ({
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography>{subtitle}</Typography>
|
||||
</BaseFallback>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyFallback = ({
|
||||
page,
|
||||
title,
|
||||
bullets,
|
||||
actionButtonText,
|
||||
actionLink,
|
||||
}: {
|
||||
page: string;
|
||||
title: string;
|
||||
bullets: any;
|
||||
actionButtonText: string;
|
||||
actionLink: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<BaseFallback>
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
zIndex={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
gap: theme.spacing(2),
|
||||
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
|
||||
}}
|
||||
>
|
||||
{bullets?.map((bullet: string, index: number) => (
|
||||
<BulletPointCheck
|
||||
text={bullet}
|
||||
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => navigate(actionLink)}
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BaseFallback>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMediaQuery } from "@mui/material";
|
||||
export const SplitBox = ({
|
||||
left,
|
||||
right,
|
||||
}: {
|
||||
left: React.ReactNode;
|
||||
right: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
return (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
bgcolor={theme.palette.primary.main}
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.spacing(2)}
|
||||
>
|
||||
<Box
|
||||
padding={theme.spacing(15)}
|
||||
borderRight={isSmall ? 0 : 1}
|
||||
borderBottom={isSmall ? 1 : 0}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
flex={0.7}
|
||||
>
|
||||
{left}
|
||||
</Box>
|
||||
<Box
|
||||
flex={1}
|
||||
padding={theme.spacing(15)}
|
||||
>
|
||||
{right}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigBox = ({
|
||||
title,
|
||||
subtitle,
|
||||
rightContent,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
rightContent: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<SplitBox
|
||||
left={
|
||||
<Fragment>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography component="p">{subtitle}</Typography>
|
||||
</Fragment>
|
||||
}
|
||||
right={rightContent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
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,100 +0,0 @@
|
||||
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";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
type StatusBoxProps = React.PropsWithChildren<{}>;
|
||||
|
||||
export const BGBox: React.FC<StatusBoxProps> = ({ children }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
padding: theme.spacing(8),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-10%"
|
||||
left="5%"
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
{children}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { BaseBox } from "@/Components/v2/DesignElements";
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
import { getStatusPalette } from "@/Utils/v2/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const StatusLabel = ({
|
||||
status,
|
||||
isActive,
|
||||
}: {
|
||||
status: MonitorStatus;
|
||||
isActive?: boolean;
|
||||
}) => {
|
||||
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"
|
||||
/>
|
||||
{isActive === false ? "Paused" : transformedText}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
@@ -1,215 +0,0 @@
|
||||
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";
|
||||
|
||||
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;
|
||||
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[];
|
||||
onRowClick?: (row: T) => void;
|
||||
};
|
||||
|
||||
export function DataTable<
|
||||
T extends {
|
||||
id?: string | number;
|
||||
_id?: string | number;
|
||||
onRowClick?: (row: T) => void;
|
||||
},
|
||||
>({ headers, data, onRowClick }: DataTableProps<T>) {
|
||||
const theme = useTheme();
|
||||
if (data.length === 0 || headers.length === 0) return <div>No data</div>;
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table
|
||||
stickyHeader
|
||||
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,
|
||||
},
|
||||
"& :is(td)": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
},
|
||||
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
sx={{ cursor: onRowClick ? "pointer" : "default" }}
|
||||
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
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,9 +0,0 @@
|
||||
export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
|
||||
export { BasePage, BasePageWithStates } from "./BasePage";
|
||||
export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
|
||||
export { DataTable as Table, Pagination } from "./Table";
|
||||
export { GradientBox, StatBox } from "./StatBox";
|
||||
export { BaseBox } from "./BaseBox";
|
||||
export { StatusLabel } from "./StatusLabel";
|
||||
export { BaseFallback, ErrorFallback, EmptyFallback } from "./Fallback";
|
||||
export { BulletPointCheck } from "./BulletPointCheck";
|
||||
@@ -1,48 +0,0 @@
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import type { AutocompleteProps } from "@mui/material/Autocomplete";
|
||||
import { TextInput } from "@/Components/v2/Inputs/TextInput";
|
||||
import { CheckboxInput } from "@/Components/v2/Inputs/Checkbox";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
type AutoCompleteInputProps = Omit<
|
||||
AutocompleteProps<any, boolean, boolean, boolean>,
|
||||
"renderInput"
|
||||
> & {
|
||||
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
|
||||
};
|
||||
|
||||
export const AutoCompleteInput: React.FC<AutoCompleteInputProps> = ({ ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Autocomplete
|
||||
{...props}
|
||||
disableCloseOnSelect
|
||||
renderInput={(params) => (
|
||||
<TextInput
|
||||
{...params}
|
||||
placeholder="Type to search"
|
||||
/>
|
||||
)}
|
||||
getOptionKey={(option) => option._id}
|
||||
renderTags={() => null}
|
||||
renderOption={(props, option, { selected }) => {
|
||||
const { key, ...optionProps } = props;
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
{...optionProps}
|
||||
>
|
||||
<CheckboxInput checked={selected} />
|
||||
{option.name}
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
||||
padding: `0 ${theme.spacing(5)}`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import type { ButtonProps } from "@mui/material/Button";
|
||||
|
||||
export const ButtonInput: React.FC<ButtonProps> = ({ sx, ...props }) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
sx={{ textTransform: "none", height: 34, fontWeight: 400, borderRadius: 2, ...sx }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
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,23 +0,0 @@
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import type { CheckboxProps } from "@mui/material/Checkbox";
|
||||
import CheckboxOutline from "@/assets/icons/checkbox-outline.svg?react";
|
||||
import CheckboxFilled from "@/assets/icons/checkbox-filled.svg?react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
type CheckboxInputProps = CheckboxProps & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const CheckboxInput: React.FC<CheckboxInputProps> = ({ label, ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Checkbox
|
||||
{...props}
|
||||
icon={<CheckboxOutline />}
|
||||
checkedIcon={<CheckboxFilled />}
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"& svg": { width: theme.spacing(8), height: theme.spacing(8) },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
import { Select } from "@/Components/v2/Inputs";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { setLanguage } from "@/Features/UI/uiSlice";
|
||||
import type { SelectChangeEvent } from "@mui/material/Select";
|
||||
|
||||
export const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const language = useSelector((state: any) => state.ui.language);
|
||||
const languages = Object.keys(i18n.options.resources || {});
|
||||
const languageMap: Record<string, string> = {
|
||||
cs: "cz",
|
||||
ja: "jp",
|
||||
uk: "ua",
|
||||
vi: "vn",
|
||||
};
|
||||
|
||||
const handleChange = (event: SelectChangeEvent<unknown>) => {
|
||||
const newLang = event.target.value;
|
||||
dispatch(setLanguage(newLang));
|
||||
};
|
||||
|
||||
const languagesForDisplay = languages.map((l) => {
|
||||
let formattedLanguage = l === "en" ? "gb" : l;
|
||||
formattedLanguage = formattedLanguage.includes("-")
|
||||
? formattedLanguage.split("-")[1].toLowerCase()
|
||||
: formattedLanguage;
|
||||
formattedLanguage = languageMap[formattedLanguage] || formattedLanguage;
|
||||
const flag = formattedLanguage ? `fi fi-${formattedLanguage}` : null;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={l}
|
||||
value={l}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
{flag && <span className={flag} />}
|
||||
<Typography textTransform={"uppercase"}>{l}</Typography>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={language}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{languagesForDisplay}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import Radio from "@mui/material/Radio";
|
||||
import type { RadioProps } from "@mui/material/Radio";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import RadioChecked from "@/assets/icons/radio-checked.svg?react";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
interface RadioInputProps extends RadioProps {}
|
||||
|
||||
export const RadioInput: React.FC<RadioInputProps> = ({ ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Radio
|
||||
{...props}
|
||||
checkedIcon={<RadioChecked />}
|
||||
sx={{
|
||||
color: "transparent",
|
||||
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
|
||||
"&:not(.Mui-checked)": {
|
||||
boxShadow: `inset 0 0 0 1px ${theme.palette.primary.contrastText}70`, // Use theme text color for the outline
|
||||
},
|
||||
mt: theme.spacing(0.5),
|
||||
padding: 0,
|
||||
"& .MuiSvgIcon-root": {
|
||||
fontSize: 16,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RadioWithDescription: React.FC<
|
||||
RadioInputProps & { label: string; description: string }
|
||||
> = ({ label, description, ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={<RadioInput {...props} />}
|
||||
label={
|
||||
<>
|
||||
<Typography component="p">{label}</Typography>
|
||||
<Typography
|
||||
component="h6"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
sx={{
|
||||
alignItems: "flex-start",
|
||||
p: theme.spacing(2.5),
|
||||
m: theme.spacing(-2.5),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
},
|
||||
"& .MuiButtonBase-root": {
|
||||
p: 0,
|
||||
mr: theme.spacing(6),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Typography, Select } from "@mui/material";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import type { SelectProps } from "@mui/material/Select";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
export const SelectInput: React.FC<SelectProps> = ({ ...props }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
sx={{
|
||||
height: "34px",
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ItemTypes = string | number;
|
||||
interface SelectItem {
|
||||
_id: ItemTypes;
|
||||
name: string;
|
||||
}
|
||||
export type CustomSelectProps = SelectProps & {
|
||||
items: SelectItem[];
|
||||
placeholder?: string;
|
||||
isHidden?: boolean;
|
||||
hasError?: boolean;
|
||||
};
|
||||
|
||||
export const SelectFromItems: React.FC<CustomSelectProps> = ({
|
||||
items,
|
||||
placeholder,
|
||||
isHidden = false,
|
||||
hasError = false,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<SelectInput
|
||||
error={hasError}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
displayEmpty
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
renderValue={(selected) => {
|
||||
if (!selected) {
|
||||
return (
|
||||
<Typography
|
||||
noWrap
|
||||
color="text.secondary"
|
||||
>
|
||||
{placeholder ?? ""}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
const selectedItem = items.find((item) => item._id === selected);
|
||||
const displayName = selectedItem ? selectedItem.name : placeholder;
|
||||
return (
|
||||
<Typography
|
||||
noWrap
|
||||
title={displayName}
|
||||
>
|
||||
{displayName}
|
||||
</Typography>
|
||||
);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item._id}
|
||||
value={item._id}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SelectInput>
|
||||
);
|
||||
};
|
||||
|
||||
SelectInput.displayName = "SelectInput";
|
||||
SelectFromItems.displayName = "SelectFromItems";
|
||||
@@ -1,22 +0,0 @@
|
||||
import { forwardRef } from "react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type { TextFieldProps } from "@mui/material";
|
||||
import { typographyLevels } from "@/Utils/Theme/v2/palette";
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
function TextInput(props, ref) {
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
inputRef={ref}
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
height: 34,
|
||||
fontSize: typographyLevels.base,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
TextInput.displayName = "TextInput";
|
||||
@@ -1,37 +0,0 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Link from "@mui/material/Link";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const TextLink = ({
|
||||
text,
|
||||
linkText,
|
||||
href,
|
||||
target = "_self",
|
||||
}: {
|
||||
text: string;
|
||||
linkText: string;
|
||||
href: string;
|
||||
target?: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Typography>{text}</Typography>
|
||||
<Link
|
||||
color="accent"
|
||||
to={href}
|
||||
component={RouterLink}
|
||||
target={target}
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setMode } from "@/Features/UI/uiSlice.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
|
||||
const SunAndMoonIcon = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<svg
|
||||
className="sun-and-moon"
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<mask
|
||||
className="moon"
|
||||
id="moon-mask"
|
||||
>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
cx="24"
|
||||
cy="10"
|
||||
r="6"
|
||||
fill="#000"
|
||||
/>
|
||||
</mask>
|
||||
<circle
|
||||
className="sun"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="6"
|
||||
fill={theme.palette.primary.contrastTextSecondary}
|
||||
mask="url(#moon-mask)"
|
||||
/>
|
||||
<g
|
||||
className="sun-beams"
|
||||
stroke={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
<line
|
||||
x1="12"
|
||||
y1="1"
|
||||
x2="12"
|
||||
y2="3"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
y1="21"
|
||||
x2="12"
|
||||
y2="23"
|
||||
/>
|
||||
<line
|
||||
x1="4.22"
|
||||
y1="4.22"
|
||||
x2="5.64"
|
||||
y2="5.64"
|
||||
/>
|
||||
<line
|
||||
x1="18.36"
|
||||
y1="18.36"
|
||||
x2="19.78"
|
||||
y2="19.78"
|
||||
/>
|
||||
<line
|
||||
x1="1"
|
||||
y1="12"
|
||||
x2="3"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="21"
|
||||
y1="12"
|
||||
x2="23"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="4.22"
|
||||
y1="19.78"
|
||||
x2="5.64"
|
||||
y2="18.36"
|
||||
/>
|
||||
<line
|
||||
x1="18.36"
|
||||
y1="5.64"
|
||||
x2="19.78"
|
||||
y2="4.22"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThemeSwitch = ({
|
||||
width = 48,
|
||||
height = 48,
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}) => {
|
||||
const mode = useSelector((state: any) => state.ui.mode);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = () => {
|
||||
dispatch(setMode(mode === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
id="theme-toggle"
|
||||
title={t("common.buttons.toggleTheme")}
|
||||
className={`theme-${mode}`}
|
||||
aria-label="auto"
|
||||
aria-live="polite"
|
||||
onClick={handleChange}
|
||||
sx={{
|
||||
width,
|
||||
height,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<SunAndMoonIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export { ButtonInput as Button } from "./Button";
|
||||
export { ButtonGroupInput as ButtonGroup } from "./ButtonGroup";
|
||||
export { TextInput } from "./TextInput";
|
||||
export { SelectInput as Select } from "./Select";
|
||||
export { LanguageSelector } from "./LanguageSelector";
|
||||
export { ThemeSwitch } from "./ThemeSwitch";
|
||||
export { TextLink } from "./TextLink";
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Outlet } from "react-router";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { SideBar } from "@/Components/v2/Layouts/Sidebar";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
const RootLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
minHeight="100vh"
|
||||
>
|
||||
<SideBar />
|
||||
<Stack
|
||||
flex={1}
|
||||
padding={theme.spacing(12)}
|
||||
>
|
||||
<Outlet />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
import Stack from "@mui/material/Stack";
|
||||
|
||||
export const BottomControls = ({}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
height={50}
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(8)}
|
||||
gap={theme.spacing(2)}
|
||||
></Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { ArrowRight } from "@/Components/v2/Arrows/ArrowRight";
|
||||
import { ArrowLeft } from "@/Components/v2/Arrows/ArrowLeft";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { toggleSidebar } from "../../../../Features/UI/uiSlice.js";
|
||||
|
||||
export const CollapseButton = ({ collapsed }: { collapsed: boolean }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const arrowIcon = collapsed ? (
|
||||
<ArrowRight
|
||||
height={theme.spacing(8)}
|
||||
width={theme.spacing(8)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
/>
|
||||
) : (
|
||||
<ArrowLeft
|
||||
height={theme.spacing(8)}
|
||||
width={theme.spacing(8)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
/* TODO 60 is a magic number. if logo chnges size this might break */
|
||||
top: 60,
|
||||
right: 0,
|
||||
transform: `translate(50%, 0)`,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
p: theme.spacing(2.5),
|
||||
|
||||
"&:focus": { outline: "none" },
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(toggleSidebar());
|
||||
}}
|
||||
>
|
||||
{arrowIcon}
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const Logo = ({ collapsed }: { collapsed: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
pt={theme.spacing(6)}
|
||||
pb={theme.spacing(12)}
|
||||
pl={theme.spacing(8)}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={() => navigate("/")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Typography
|
||||
pl={theme.spacing("1px")}
|
||||
minWidth={theme.spacing(16)}
|
||||
minHeight={theme.spacing(16)}
|
||||
display={"flex"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
bgcolor={theme.palette.accent.main}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
color={theme.palette.accent.contrastText}
|
||||
fontSize={18}
|
||||
>
|
||||
C
|
||||
</Typography>
|
||||
<Box
|
||||
overflow={"hidden"}
|
||||
sx={{
|
||||
transition: "opacity 900ms ease, width 900ms ease",
|
||||
opacity: collapsed ? 0 : 1,
|
||||
whiteSpace: "nowrap",
|
||||
width: collapsed ? 0 : "100%",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<Typography
|
||||
lineHeight={1}
|
||||
mt={theme.spacing(2)}
|
||||
color={theme.palette.primary.contrastText}
|
||||
variant="h2"
|
||||
>
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
// import Notifications from "@/assets/icons/notifications.svg?react";
|
||||
import Monitors from "@/assets/icons/monitors.svg?react";
|
||||
// import PageSpeed from "@/assets/icons/page-speed.svg?react";
|
||||
// import Integrations from "@/assets/icons/integrations.svg?react";
|
||||
// import Incidents from "@/assets/icons/incidents.svg?react";
|
||||
// import StatusPages from "@/assets/icons/status-pages.svg?react";
|
||||
// import Maintenance from "@/assets/icons/maintenance.svg?react";
|
||||
// import Logs from "@/assets/icons/logs.svg?react";
|
||||
// import Settings from "@/assets/icons/settings.svg?react";
|
||||
import Support from "@/assets/icons/support.svg?react";
|
||||
import Discussions from "@/assets/icons/discussions.svg?react";
|
||||
import Docs from "@/assets/icons/docs.svg?react";
|
||||
import ChangeLog from "@/assets/icons/changeLog.svg?react";
|
||||
|
||||
export const getMenu = (t: Function) => [
|
||||
{ name: t("menu.uptime"), path: "v2/uptime", icon: <Monitors /> },
|
||||
// { name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
|
||||
|
||||
// { name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
|
||||
// {
|
||||
// name: t("menu.notifications"),
|
||||
// path: "notifications",
|
||||
// icon: <Notifications />,
|
||||
// },
|
||||
// { name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
||||
|
||||
// { name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
||||
// { name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
|
||||
// { name: t("menu.logs"), path: "logs", icon: <Logs /> },
|
||||
|
||||
// {
|
||||
// name: t("menu.settings"),
|
||||
// icon: <Settings />,
|
||||
// path: "settings",
|
||||
// },
|
||||
];
|
||||
|
||||
export const getBottomMenu = (t: Function) => [
|
||||
{ name: t("menu.support"), path: "support", icon: <Support />, url: "invite" },
|
||||
{
|
||||
name: t("menu.discussions"),
|
||||
path: "discussions",
|
||||
icon: <Discussions />,
|
||||
url: "https://github.com/bluewave-labs/checkmate/discussions",
|
||||
},
|
||||
{
|
||||
name: t("menu.docs"),
|
||||
path: "docs",
|
||||
icon: <Docs />,
|
||||
url: "https://bluewavelabs.gitbook.io/checkmate",
|
||||
},
|
||||
{
|
||||
name: t("menu.changelog"),
|
||||
path: "changelog",
|
||||
icon: <ChangeLog />,
|
||||
url: "https://github.com/bluewave-labs/checkmate/releases",
|
||||
},
|
||||
];
|
||||
@@ -1,103 +0,0 @@
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export interface NavData {
|
||||
name: string;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
|
||||
export const NavItem = ({
|
||||
item,
|
||||
collapsed,
|
||||
selected,
|
||||
onClick,
|
||||
}: {
|
||||
item: NavData;
|
||||
collapsed: boolean;
|
||||
selected: boolean;
|
||||
onClick: (event: React.MouseEvent) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const iconStroke = selected
|
||||
? theme.palette.primary.contrastText
|
||||
: theme.palette.primary.contrastTextTertiary;
|
||||
|
||||
const buttonBgColor = selected ? theme.palette.secondary.main : "transparent";
|
||||
const buttonBgHoverColor = selected
|
||||
? theme.palette.secondary.main
|
||||
: theme.palette.tertiary.main;
|
||||
const fontWeight = selected ? 600 : 400;
|
||||
return (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
backgroundColor: buttonBgColor,
|
||||
"&:hover": {
|
||||
backgroundColor: buttonBgHoverColor,
|
||||
},
|
||||
height: 37,
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
pl: theme.spacing(5),
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
"& svg": {
|
||||
height: 20,
|
||||
width: 20,
|
||||
opacity: 0.81,
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: iconStroke,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
transition: "opacity 900ms ease",
|
||||
opacity: collapsed ? 0 : 1,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
sx={{
|
||||
fontWeight: fontWeight,
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { setCollapsed } from "@/Features/UI/uiSlice";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
import { CollapseButton } from "@/Components/v2/Layouts/Sidebar/CollapseButton";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import List from "@mui/material/List";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import { Logo } from "@/Components/v2/Layouts/Sidebar/Logo";
|
||||
import { getMenu, getBottomMenu } from "@/Components/v2/Layouts/Sidebar/Menu";
|
||||
import { NavItem } from "@/Components/v2/Layouts/Sidebar/NavItem";
|
||||
import { BottomControls } from "@/Components/v2/Layouts/Sidebar/BottomControls";
|
||||
|
||||
export const COLLAPSED_WIDTH = 64;
|
||||
export const EXPANDED_WIDTH = 250;
|
||||
|
||||
export const SideBar = () => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const dispatch = useDispatch();
|
||||
const collapsed = useSelector((state: any) => state.ui.sidebar.collapsed);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const menu = getMenu(t);
|
||||
const bottomMenu = getBottomMenu(t);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setCollapsed({ collapsed: isSmall }));
|
||||
}, [isSmall]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
component="aside"
|
||||
position="sticky"
|
||||
top={0}
|
||||
minHeight={"100vh"}
|
||||
maxHeight={"100vh"}
|
||||
paddingTop={theme.spacing(6)}
|
||||
paddingBottom={theme.spacing(6)}
|
||||
gap={theme.spacing(6)}
|
||||
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
width={collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH}
|
||||
sx={{
|
||||
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
|
||||
}}
|
||||
>
|
||||
<CollapseButton collapsed={collapsed} />
|
||||
<Logo collapsed={collapsed} />
|
||||
<List
|
||||
component="nav"
|
||||
disablePadding
|
||||
sx={{
|
||||
px: theme.spacing(6),
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{menu.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
selected={selected}
|
||||
onClick={() => navigate(`/${item.path}`)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<List
|
||||
component="nav"
|
||||
disablePadding
|
||||
sx={{
|
||||
px: theme.spacing(6),
|
||||
}}
|
||||
>
|
||||
{bottomMenu.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
selected={selected}
|
||||
onClick={() => {
|
||||
if (item.url) {
|
||||
window.open(item.url, "_blank", "noreferrer");
|
||||
} else {
|
||||
navigate(`/${item.path}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
|
||||
<BottomControls />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
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/v2/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>
|
||||
);
|
||||
};
|
||||
@@ -1,163 +0,0 @@
|
||||
import { BaseChart } from "./HistogramStatus";
|
||||
import { BaseBox } from "../DesignElements";
|
||||
import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react";
|
||||
import { normalizeResponseTimes } from "@/Utils/v2/DataUtils";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
} from "recharts";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import {
|
||||
formatDateWithTz,
|
||||
tickDateFormatLookup,
|
||||
tooltipDateFormatLookup,
|
||||
} from "@/Utils/v2/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]?.payload?.avgResponseTime || 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);
|
||||
const normalized = normalizeResponseTimes<GroupedCheck, "avgResponseTime">(
|
||||
checks,
|
||||
"avgResponseTime"
|
||||
);
|
||||
return (
|
||||
<BaseChart
|
||||
icon={<ResponseTimeIcon />}
|
||||
title="Response times"
|
||||
>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={300}
|
||||
>
|
||||
<AreaChart data={normalized?.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="normalResponseTime"
|
||||
stroke={theme.palette.accent.main}
|
||||
fill="url(#colorUv)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
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(`/monitors/${monitor._id}/active`);
|
||||
refetch();
|
||||
}}
|
||||
startIcon={
|
||||
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||
}
|
||||
>
|
||||
{monitor?.isActive ? t("pause") : t("resume")}
|
||||
</Button>
|
||||
<Button startIcon={<SettingsOutlinedIcon />}>{t("configure")}</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { Check } from "@/Types/Check";
|
||||
import { HistogramResponseTimeTooltip } from "@/Components/v2/Monitors/HistogramResponseTimeTooltip";
|
||||
import { normalizeResponseTimes } from "@/Utils/v2/DataUtils";
|
||||
|
||||
export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => {
|
||||
const normalChecks = normalizeResponseTimes(checks, "responseTime");
|
||||
let data = Array<any>();
|
||||
|
||||
if (!normalChecks || normalChecks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (normalChecks.length !== 25) {
|
||||
const placeholders = Array(25 - normalChecks.length).fill("placeholder");
|
||||
data = [...normalChecks, ...placeholders];
|
||||
} else {
|
||||
data = normalChecks;
|
||||
}
|
||||
|
||||
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) => {
|
||||
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 (
|
||||
<HistogramResponseTimeTooltip
|
||||
key={`${check}-${index}`}
|
||||
check={check}
|
||||
>
|
||||
<Box
|
||||
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={`${check.normalResponseTime}%`}
|
||||
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>
|
||||
</HistogramResponseTimeTooltip>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { formatDateWithTz } from "@/Utils/v2/TimeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { LatestCheck } from "@/Types/Check";
|
||||
|
||||
export const HistogramResponseTimeTooltip: React.FC<{
|
||||
children: React.ReactElement;
|
||||
check: LatestCheck;
|
||||
}> = ({ children, check }) => {
|
||||
const uiTimezone = useSelector((state: any) => state.ui.timezone);
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<Stack>
|
||||
<Typography>
|
||||
{formatDateWithTz(check?.checkedAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
{check?.responseTime && (
|
||||
<Typography>Response Time: {check.responseTime} ms</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,218 +0,0 @@
|
||||
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 { normalizeResponseTimes } from "@/Utils/v2/DataUtils";
|
||||
import { useState } from "react";
|
||||
import { formatDateWithTz } from "@/Utils/v2/TimeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { getResponseTimeColor } from "@/Utils/v2/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";
|
||||
const normalChecks = normalizeResponseTimes(checks, "avgResponseTime");
|
||||
|
||||
if (normalChecks.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 = normalChecks.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">{normalChecks[idx].count}</Typography>
|
||||
<Typography
|
||||
position={"absolute"}
|
||||
top={"100%"}
|
||||
>
|
||||
{formatDateWithTz(normalChecks[idx]._id, dateFormat, uiTimezone)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="h2">{totalChecks}</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={155}
|
||||
>
|
||||
<BarChart data={normalChecks}>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<XLabel
|
||||
p1={normalChecks[0]}
|
||||
p2={normalChecks[normalChecks.length - 1]}
|
||||
range={range}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="normalResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{normalChecks?.map((groupedCheck, idx) => {
|
||||
const fillColor = getResponseTimeColor(groupedCheck.normalResponseTime);
|
||||
return (
|
||||
<Cell
|
||||
onMouseEnter={() => setIdx(idx)}
|
||||
onMouseLeave={() => setIdx(null)}
|
||||
key={groupedCheck._id}
|
||||
fill={theme.palette[fillColor].main}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Stack>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
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/v2/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>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { HeaderCreate } from "./HeaderCreate";
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
isAuthenticated: false,
|
||||
};
|
||||
|
||||
const v2AuthSlice = createSlice({
|
||||
name: "v2Auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
setIsAuthenticated: (state, action) => {
|
||||
const { authenticated } = action.payload;
|
||||
state.isAuthenticated = authenticated;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default v2AuthSlice.reducer;
|
||||
export const { setIsAuthenticated } = v2AuthSlice.actions;
|
||||
@@ -1,90 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import type { SWRConfiguration } from "swr";
|
||||
import type { AxiosRequestConfig } from "axios";
|
||||
import { get, post, patch } from "@/Utils/v2/ApiClient"; // your axios wrapper
|
||||
|
||||
export type ApiResponse = {
|
||||
message: string;
|
||||
data: any;
|
||||
};
|
||||
|
||||
// Generic fetcher for GET requests
|
||||
const fetcher = async <T,>(url: string, config?: AxiosRequestConfig) => {
|
||||
const res = await get<T>(url, config);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const useGet = <T,>(
|
||||
url: string,
|
||||
axiosConfig?: AxiosRequestConfig,
|
||||
swrConfig?: SWRConfiguration<T, Error>
|
||||
) => {
|
||||
const { data, error, isLoading, isValidating, mutate } = useSWR<T>(
|
||||
url,
|
||||
(url) => fetcher<T>(url, axiosConfig),
|
||||
swrConfig
|
||||
);
|
||||
|
||||
return {
|
||||
response: data ?? null,
|
||||
loading: isLoading,
|
||||
isValidating,
|
||||
error: error?.message ?? null,
|
||||
refetch: mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const usePost = <B = any, R = any>() => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const postFn = async (
|
||||
endpoint: string,
|
||||
body: B,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<R | null> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await post<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 { post: postFn, loading, error };
|
||||
};
|
||||
|
||||
export const usePatch = <B = any, R = any>() => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const patchFn = async (
|
||||
endpoint: string,
|
||||
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 };
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { monitorSchema } from "@/Validation/v2/zod";
|
||||
import { z } from "zod";
|
||||
export const useInitForm = ({
|
||||
initialData,
|
||||
}: {
|
||||
initialData: Partial<z.infer<typeof monitorSchema>> | undefined;
|
||||
}) => {
|
||||
const defaults: z.infer<typeof monitorSchema> = {
|
||||
type: initialData?.type || "http",
|
||||
url: initialData?.url || "",
|
||||
n: initialData?.n || 3,
|
||||
notificationChannels: initialData?.notificationChannels || [],
|
||||
name: initialData?.name || "",
|
||||
interval: initialData?.interval || "1 minute",
|
||||
};
|
||||
return { defaults };
|
||||
};
|
||||
@@ -1,136 +0,0 @@
|
||||
import { AuthBasePage } from "@/Components/v2/Auth";
|
||||
import { Button } from "@/Components/v2/Inputs";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { TextInput, TextLink } from "@/Components/v2/Inputs";
|
||||
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { usePost } from "@/Hooks/v2/UseApi";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setIsAuthenticated } from "@/Features/Auth/v2AuthSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email("Invalid email address"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters"),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
const Login = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const { post, loading } = usePost<FormData, ApiResponse>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
const result = await post("/auth/login", data);
|
||||
if (result) {
|
||||
dispatch(setIsAuthenticated({ authenticated: true }));
|
||||
navigate("/v2/uptime");
|
||||
} else {
|
||||
dispatch(setIsAuthenticated({ authenticated: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthBasePage
|
||||
title={t("auth.login.welcome")}
|
||||
subtitle={t("auth.login.heading")}
|
||||
>
|
||||
<Stack
|
||||
width={"100%"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
padding={theme.spacing(8)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
maxWidth={400}
|
||||
sx={{
|
||||
width: {
|
||||
sm: "80%",
|
||||
md: "70%",
|
||||
lg: "65%",
|
||||
xl: "65%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
fullWidth
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
error={!!errors.email}
|
||||
helperText={errors.email ? errors.email.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
fullWidth
|
||||
placeholder="••••••••••"
|
||||
error={!!errors.password}
|
||||
helperText={errors.password ? errors.password.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
loading={loading}
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
<TextLink
|
||||
text={t("auth.login.links.forgotPassword")}
|
||||
linkText={t("auth.login.links.forgotPasswordLink")}
|
||||
href="/forgot-password"
|
||||
/>
|
||||
<TextLink
|
||||
text={t("auth.login.links.register")}
|
||||
linkText={t("auth.login.links.registerLink")}
|
||||
href="/register"
|
||||
/>
|
||||
</Stack>
|
||||
</AuthBasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,177 +0,0 @@
|
||||
import { AuthBasePage } from "@/Components/v2/Auth";
|
||||
import { TextInput } from "@/Components/v2/Inputs";
|
||||
import { Button } from "@/Components/v2/Inputs";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Stack from "@mui/material/Stack";
|
||||
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { usePost } from "@/Hooks/v2/UseApi";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
email: z.email({ message: "Invalid email address" }),
|
||||
firstName: z.string().min(1, { message: "First Name is required" }),
|
||||
lastName: z.string().min(1, { message: "Last Name is required" }),
|
||||
password: z.string().min(6, { message: "Password must be at least 6 characters" }),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(6, { message: "Confirm Password must be at least 6 characters" }),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
path: ["confirmPassword"],
|
||||
message: "Passwords must match",
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
const Register = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { post, loading, error } = usePost<FormData, ApiResponse>();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
const result = await post("/auth/register", data);
|
||||
if (result) {
|
||||
navigate("/v2/uptime");
|
||||
} else {
|
||||
console.error("Login failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthBasePage
|
||||
title={t("auth.registration.welcome")}
|
||||
subtitle={t("auth.registration.heading.user")}
|
||||
>
|
||||
<Stack
|
||||
alignItems={"center"}
|
||||
width={"100%"}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
padding={theme.spacing(8)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
maxWidth={400}
|
||||
sx={{
|
||||
width: {
|
||||
sm: "80%",
|
||||
md: "70%",
|
||||
lg: "65%",
|
||||
xl: "65%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
fullWidth
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
error={!!errors.email}
|
||||
helperText={errors.email ? errors.email.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("auth.common.inputs.firstName.label")}
|
||||
fullWidth
|
||||
placeholder={t("auth.common.inputs.firstName.placeholder")}
|
||||
error={!!errors.firstName}
|
||||
helperText={errors.firstName ? errors.firstName.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="lastName"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={t("auth.common.inputs.lastName.label")}
|
||||
fullWidth
|
||||
placeholder={t("auth.common.inputs.lastName.placeholder")}
|
||||
error={!!errors.lastName}
|
||||
helperText={errors.lastName ? errors.lastName.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
fullWidth
|
||||
placeholder="••••••••••"
|
||||
error={!!errors.password}
|
||||
helperText={errors.password ? errors.password.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="confirmPassword"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="password"
|
||||
label={t("auth.common.inputs.passwordConfirm.label")}
|
||||
fullWidth
|
||||
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
|
||||
error={!!errors.confirmPassword}
|
||||
helperText={errors.confirmPassword ? errors.confirmPassword.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
loading={loading}
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
{error && <Typography color="error">{error}</Typography>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</AuthBasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
@@ -1,93 +0,0 @@
|
||||
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/v2/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>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { monitorSchema } from "@/Validation/v2/zod";
|
||||
import { useGet, usePost } from "@/Hooks/v2/UseApi";
|
||||
import { UptimeForm } from "@/Pages/v2/Uptime/UptimeForm";
|
||||
|
||||
import { useParams } from "react-router";
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
import humanInterval from "human-interval";
|
||||
import { z } from "zod";
|
||||
|
||||
export const UptimeConfigurePage = () => {
|
||||
type FormValues = z.infer<typeof monitorSchema>;
|
||||
type SubmitValues = Omit<FormValues, "interval"> & { interval: number | undefined };
|
||||
|
||||
const { id } = useParams();
|
||||
const { response } = useGet<ApiResponse>("/notification-channels");
|
||||
const { response: monitorResponse } = useGet<ApiResponse>(`/monitors/${id}`);
|
||||
const monitor = monitorResponse?.data || null;
|
||||
const notificationOptions = response?.data ?? [];
|
||||
|
||||
const { post, loading, error } = usePost<SubmitValues, ApiResponse>();
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
let interval = humanInterval(data.interval);
|
||||
if (!interval) interval = 60000;
|
||||
const submitData = { ...data, interval };
|
||||
const result = await post("/monitors", submitData);
|
||||
if (result) {
|
||||
console.log(result);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<UptimeForm
|
||||
initialData={{
|
||||
...monitor,
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
notificationOptions={notificationOptions}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,262 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { TextInput } from "@/Components/v2/Inputs/TextInput";
|
||||
import { AutoCompleteInput } from "@/Components/v2/Inputs/AutoComplete";
|
||||
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 "@/Components/v2/Inputs";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import { Typography } from "@mui/material";
|
||||
import humanInterval from "human-interval";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, Controller, useWatch } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { monitorSchema } from "@/Validation/v2/zod";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useGet, usePost } from "@/Hooks/v2/UseApi";
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
|
||||
const UptimeCreatePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
type FormValues = z.infer<typeof monitorSchema>;
|
||||
type SubmitValues = Omit<FormValues, "interval"> & { interval: number | undefined };
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(monitorSchema) as any,
|
||||
defaultValues: {
|
||||
type: "http",
|
||||
url: "",
|
||||
n: 3,
|
||||
notificationChannels: [],
|
||||
name: "",
|
||||
interval: "1 minute",
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const { response } = useGet<ApiResponse>("/notification-channels");
|
||||
const { post, loading, error } = usePost<SubmitValues>();
|
||||
const selectedType = useWatch({
|
||||
control,
|
||||
name: "type",
|
||||
});
|
||||
const notificationChannels = useWatch({
|
||||
control,
|
||||
name: "notificationChannels",
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
let interval = humanInterval(data.interval);
|
||||
if (!interval) interval = 60000;
|
||||
const submitData = { ...data, interval };
|
||||
const result = await post("/monitors", submitData);
|
||||
if (result) {
|
||||
console.log(result);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const notificationOptions = response?.data ?? [];
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<BasePage>
|
||||
<ConfigBox
|
||||
title={t("distributedUptimeCreateChecks")}
|
||||
subtitle={t("distributedUptimeCreateChecksDescription")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl error={!!errors.type}>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
sx={{ gap: theme.spacing(6) }}
|
||||
>
|
||||
<RadioWithDescription
|
||||
value="http"
|
||||
label={"HTTP"}
|
||||
description={"Use HTTP to monitor your website or API endpoint."}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="https"
|
||||
label="HTTPS"
|
||||
description="Use HTTPS to monitor your website or API endpoint.
|
||||
"
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="ping"
|
||||
label={t("pingMonitoring")}
|
||||
description={t("pingMonitoringDescription")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("settingsGeneralSettings")}
|
||||
subtitle={t(`uptimeGeneralInstructions.${selectedType}`)}
|
||||
rightContent={
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("url")}
|
||||
fullWidth
|
||||
error={!!errors.url}
|
||||
helperText={errors.url ? errors.url.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("displayName")}
|
||||
fullWidth
|
||||
error={!!errors.name}
|
||||
helperText={errors.name ? errors.name.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("createMonitorPage.incidentConfigTitle")}
|
||||
subtitle={t("createMonitorPage.incidentConfigDescriptionV2")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="n"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
label={t("createMonitorPage.incidentConfigStatusCheckNumber")}
|
||||
fullWidth
|
||||
error={!!errors.n}
|
||||
helperText={errors.n ? errors.n.message : ""}
|
||||
onChange={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
field.onChange(target.valueAsNumber);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("notificationConfig.title")}
|
||||
subtitle={t("notificationConfig.description")}
|
||||
rightContent={
|
||||
<Stack>
|
||||
<Controller
|
||||
name="notificationChannels"
|
||||
control={control}
|
||||
defaultValue={[]} // important!
|
||||
render={({ field }) => (
|
||||
<AutoCompleteInput
|
||||
multiple
|
||||
options={notificationOptions}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={notificationOptions.filter((o: any) =>
|
||||
(field.value || []).includes(o._id)
|
||||
)}
|
||||
onChange={(_, newValue) => {
|
||||
field.onChange(newValue.map((o: any) => o._id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
mt={theme.spacing(2)}
|
||||
>
|
||||
{notificationChannels.map((notificationId) => {
|
||||
const option = notificationOptions.find(
|
||||
(o: any) => o._id === notificationId
|
||||
);
|
||||
if (!option) return null;
|
||||
return (
|
||||
<Stack
|
||||
width={"100%"}
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
key={notificationId}
|
||||
>
|
||||
<Typography>{option.name}</Typography>
|
||||
<DeleteOutlineRoundedIcon
|
||||
onClick={() => {
|
||||
const updated = notificationChannels.filter(
|
||||
(id) => id !== notificationId
|
||||
);
|
||||
setValue("notificationChannels", updated);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("createMonitorPage.intervalTitle")}
|
||||
subtitle="How often to check the URL"
|
||||
rightContent={
|
||||
<Controller
|
||||
name="interval"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("createMonitorPage.intervalDescription")}
|
||||
fullWidth
|
||||
error={!!errors.interval}
|
||||
helperText={errors.interval ? errors.interval.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
>
|
||||
{t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</BasePage>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeCreatePage;
|
||||
@@ -1,157 +0,0 @@
|
||||
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/v2/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, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const {
|
||||
response: upResponse,
|
||||
isValidating: upIsValidating,
|
||||
error: upError,
|
||||
} = useGet<ApiResponse>(
|
||||
`/monitors/${id}?embedChecks=true&range=${range}&status=up`,
|
||||
{},
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
const {
|
||||
response: downResponse,
|
||||
error: downError,
|
||||
isValidating: downIsValidating,
|
||||
} = useGet<ApiResponse>(
|
||||
`/monitors/${id}?embedChecks=true&range=${range}&status=down`,
|
||||
{},
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { patch, loading: isPatching, error: postError } = usePatch<ApiResponse>();
|
||||
|
||||
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,158 +0,0 @@
|
||||
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 { 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 { useNavigate } from "react-router-dom";
|
||||
import { usePatch } from "@/Hooks/v2/UseApi";
|
||||
import type { ApiResponse } from "@/Hooks/v2/UseApi";
|
||||
|
||||
import type { IMonitor } from "@/Types/Monitor";
|
||||
import type { ActionMenuItem } from "@/Components/v2/ActionsMenu";
|
||||
|
||||
export const MonitorTable = ({
|
||||
monitors,
|
||||
refetch,
|
||||
}: {
|
||||
monitors: IMonitor[];
|
||||
refetch: Function;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
patch,
|
||||
// loading: isPatching,
|
||||
// error: postError,
|
||||
} = usePatch<ApiResponse>();
|
||||
|
||||
const getActions = (monitor: IMonitor): ActionMenuItem[] => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
label: "Open site",
|
||||
action: () => {
|
||||
window.open(monitor.url, "_blank", "noreferrer");
|
||||
},
|
||||
closeMenu: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: "Details",
|
||||
action: () => {
|
||||
navigate(`${monitor._id}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "Incidents",
|
||||
action: () => {
|
||||
navigate(`/v2/incidents/${monitor._id}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: "Configure",
|
||||
action: () => {
|
||||
console.log("Open configure");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: "Clone",
|
||||
action: () => {
|
||||
console.log("Open clone");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: monitor.isActive ? "Pause" : "Resume",
|
||||
action: async () => {
|
||||
await patch(`/monitors/${monitor._id}/active`);
|
||||
refetch();
|
||||
},
|
||||
closeMenu: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: <Typography color={theme.palette.error.main}>Remove</Typography>,
|
||||
action: () => {
|
||||
console.log("Open delete");
|
||||
},
|
||||
closeMenu: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getHeaders = () => {
|
||||
const headers: Header<IMonitor>[] = [
|
||||
{
|
||||
id: "name",
|
||||
content: t("host"),
|
||||
render: (row) => {
|
||||
return row.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
render: (row) => {
|
||||
return (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
isActive={row.isActive}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: t("actions"),
|
||||
render: (row) => {
|
||||
return <ActionsMenu items={getActions(row)} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
};
|
||||
|
||||
let headers = getHeaders();
|
||||
|
||||
if (isSmall) {
|
||||
headers = headers.filter((h) => h.id !== "histogram");
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
headers={headers}
|
||||
data={monitors}
|
||||
onRowClick={(row) => {
|
||||
navigate(row._id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,247 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { TextInput } from "@/Components/v2/Inputs/TextInput";
|
||||
import { AutoCompleteInput } from "@/Components/v2/Inputs/AutoComplete";
|
||||
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 "@/Components/v2/Inputs";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { monitorSchema } from "@/Validation/v2/zod";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm, Controller, useWatch, type SubmitHandler } from "react-hook-form";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useInitForm } from "@/Hooks/v2/useInitMonitorForm";
|
||||
|
||||
type FormValues = z.infer<typeof monitorSchema>;
|
||||
|
||||
export const UptimeForm = ({
|
||||
initialData,
|
||||
onSubmit,
|
||||
notificationOptions,
|
||||
loading,
|
||||
}: {
|
||||
initialData?: Partial<FormValues>;
|
||||
onSubmit: SubmitHandler<FormValues>;
|
||||
notificationOptions: any[];
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { defaults } = useInitForm({ initialData: initialData });
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(monitorSchema) as any,
|
||||
defaultValues: defaults,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const selectedType = useWatch({
|
||||
control,
|
||||
name: "type",
|
||||
});
|
||||
const notificationChannels = useWatch({
|
||||
control,
|
||||
name: "notificationChannels",
|
||||
});
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
component={"form"}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<ConfigBox
|
||||
title={t("distributedUptimeCreateChecks")}
|
||||
subtitle={t("distributedUptimeCreateChecksDescription")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl error={!!errors.type}>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
sx={{ gap: theme.spacing(6) }}
|
||||
>
|
||||
<RadioWithDescription
|
||||
value="http"
|
||||
label={"HTTP"}
|
||||
description={"Use HTTP to monitor your website or API endpoint."}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="https"
|
||||
label="HTTPS"
|
||||
description="Use HTTPS to monitor your website or API endpoint.
|
||||
"
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="ping"
|
||||
label={t("pingMonitoring")}
|
||||
description={t("pingMonitoringDescription")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("settingsGeneralSettings")}
|
||||
subtitle={t(`uptimeGeneralInstructions.${selectedType}`)}
|
||||
rightContent={
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("url")}
|
||||
fullWidth
|
||||
error={!!errors.url}
|
||||
helperText={errors.url ? errors.url.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("displayName")}
|
||||
fullWidth
|
||||
error={!!errors.name}
|
||||
helperText={errors.name ? errors.name.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("createMonitorPage.incidentConfigTitle")}
|
||||
subtitle={t("createMonitorPage.incidentConfigDescriptionV2")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="n"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
label={t("createMonitorPage.incidentConfigStatusCheckNumber")}
|
||||
fullWidth
|
||||
error={!!errors.n}
|
||||
helperText={errors.n ? errors.n.message : ""}
|
||||
onChange={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
field.onChange(target.valueAsNumber);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("notificationConfig.title")}
|
||||
subtitle={t("notificationConfig.description")}
|
||||
rightContent={
|
||||
<Stack>
|
||||
<Controller
|
||||
name="notificationChannels"
|
||||
control={control}
|
||||
defaultValue={[]} // important!
|
||||
render={({ field }) => (
|
||||
<AutoCompleteInput
|
||||
multiple
|
||||
options={notificationOptions}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={notificationOptions.filter((o: any) =>
|
||||
(field.value || []).includes(o._id)
|
||||
)}
|
||||
onChange={(_, newValue) => {
|
||||
field.onChange(newValue.map((o: any) => o._id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
mt={theme.spacing(2)}
|
||||
>
|
||||
{notificationChannels.map((notificationId) => {
|
||||
const option = notificationOptions.find(
|
||||
(o: any) => o._id === notificationId
|
||||
);
|
||||
if (!option) return null;
|
||||
return (
|
||||
<Stack
|
||||
width={"100%"}
|
||||
justifyContent={"space-between"}
|
||||
direction="row"
|
||||
key={notificationId}
|
||||
>
|
||||
<Typography>{option.name}</Typography>
|
||||
<DeleteOutlineRoundedIcon
|
||||
onClick={() => {
|
||||
const updated = notificationChannels.filter(
|
||||
(id) => id !== notificationId
|
||||
);
|
||||
setValue("notificationChannels", updated);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("createMonitorPage.intervalTitle")}
|
||||
subtitle="How often to check the URL"
|
||||
rightContent={
|
||||
<Controller
|
||||
name="interval"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="text"
|
||||
label={t("createMonitorPage.intervalDescription")}
|
||||
fullWidth
|
||||
error={!!errors.interval}
|
||||
helperText={errors.interval ? errors.interval.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
>
|
||||
{t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import {
|
||||
BasePageWithStates,
|
||||
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, isValidating, error, refetch } = useGet<ApiResponse>(
|
||||
"/monitors?embedChecks=true",
|
||||
{},
|
||||
{ refreshInterval: 30000, keepPreviousData: true }
|
||||
);
|
||||
const monitors: IMonitor[] = response?.data ?? ([] as IMonitor[]);
|
||||
|
||||
const monitorStatuses = monitors.reduce(
|
||||
(acc, monitor) => {
|
||||
if (monitor.status === "up") {
|
||||
acc.up += 1;
|
||||
} else if (monitor.status === "down") {
|
||||
acc.down += 1;
|
||||
} else if (monitor.isActive === false) {
|
||||
acc.paused += 1;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
up: 0,
|
||||
down: 0,
|
||||
paused: 0,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<BasePageWithStates
|
||||
loading={isValidating}
|
||||
error={error}
|
||||
items={monitors}
|
||||
page="uptime"
|
||||
actionLink="create"
|
||||
>
|
||||
<HeaderCreate
|
||||
isLoading={isValidating}
|
||||
path="/v2/uptime/create"
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={monitorStatuses.up} />
|
||||
<DownStatusBox n={monitorStatuses.down} />
|
||||
<PausedStatusBox n={monitorStatuses.paused} />
|
||||
</Stack>
|
||||
<MonitorTable
|
||||
monitors={monitors}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</BasePageWithStates>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeMonitors;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme";
|
||||
import { Navigate, Route, Routes as LibRoutes } from "react-router";
|
||||
import HomeLayout from "@/Components/v1/Layouts/HomeLayout";
|
||||
import NotFound from "../Pages/v1/NotFound";
|
||||
@@ -54,18 +53,11 @@ import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
|
||||
import BulkImport from "../Pages/v1/Uptime/BulkImport";
|
||||
import Logs from "../Pages/v1/Logs";
|
||||
|
||||
import V2Routes from "@/Routes/v2router";
|
||||
|
||||
const Routes = () => {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const AdminCheckedRegister = withAdminCheck(AuthRegister);
|
||||
return (
|
||||
<LibRoutes>
|
||||
<Route
|
||||
path="/v2/*"
|
||||
element={<V2Routes mode={mode} />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Routes, Route } from "react-router";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
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 UptimeCreatePage from "@/Pages/v2/Uptime/Create";
|
||||
import UptimeDetailsPage from "@/Pages/v2/Uptime/Details";
|
||||
import RootLayout from "@/Components/v2/Layouts/RootLayout";
|
||||
|
||||
const V2Routes = ({ mode = "light" }) => {
|
||||
const v2Theme = mode === "light" ? lightTheme : darkTheme;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={v2Theme}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="login"
|
||||
element={<AuthLoginV2 />}
|
||||
/>
|
||||
<Route
|
||||
path="register"
|
||||
element={<AuthRegisterV2 />}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={<RootLayout />}
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={<UptimeMonitorsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="uptime"
|
||||
element={<UptimeMonitorsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="uptime/:id"
|
||||
element={<UptimeDetailsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="uptime/create"
|
||||
element={<UptimeCreatePage />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default V2Routes;
|
||||
@@ -1,124 +0,0 @@
|
||||
import { lighten, darken } from "@mui/material/styles";
|
||||
|
||||
const typographyBase = 13;
|
||||
|
||||
export const typographyLevels = {
|
||||
base: typographyBase,
|
||||
xs: `${(typographyBase - 4) / 16}rem`,
|
||||
s: `${(typographyBase - 2) / 16}rem`,
|
||||
m: `${typographyBase / 16}rem`,
|
||||
l: `${(typographyBase + 2) / 16}rem`,
|
||||
xl: `${(typographyBase + 10) / 16}rem`,
|
||||
};
|
||||
|
||||
const colors = {
|
||||
offWhite: "#FEFEFE",
|
||||
offBlack: "#131315",
|
||||
gray0: "#FDFDFD",
|
||||
gray10: "#F4F4FF",
|
||||
gray50: "#F9F9F9",
|
||||
gray100: "#F3F3F3",
|
||||
gray200: "#EFEFEF",
|
||||
gray250: "#DADADA",
|
||||
gray500: "#A2A3A3",
|
||||
gray900: "#1c1c1c",
|
||||
blueGray50: "#E8F0FE",
|
||||
blueGray500: "#475467",
|
||||
blueGray600: "#344054",
|
||||
blueGray800: "#1C2130",
|
||||
blueGray900: "#515151",
|
||||
blueBlueWave: "#1570EF",
|
||||
lightBlueWave: "#CDE2FF",
|
||||
green100: "#67cd78",
|
||||
green200: "#4B9B77",
|
||||
green400: "#079455",
|
||||
green700: "#026513",
|
||||
orange100: "#FD8F22",
|
||||
orange200: "#D69A5D",
|
||||
orange600: "#9B734B",
|
||||
orange700: "#884605",
|
||||
red100: "#F27C7C",
|
||||
red400: "#D92020",
|
||||
red600: "#9B4B4B",
|
||||
red700: "#980303",
|
||||
};
|
||||
|
||||
export const lightPalette = {
|
||||
accent: {
|
||||
main: colors.blueBlueWave,
|
||||
light: lighten(colors.blueBlueWave, 0.2),
|
||||
dark: darken(colors.blueBlueWave, 0.2),
|
||||
contrastText: colors.offWhite,
|
||||
},
|
||||
primary: {
|
||||
main: colors.offWhite,
|
||||
contrastText: colors.blueGray800,
|
||||
contrastTextSecondary: colors.blueGray600,
|
||||
contrastTextTertiary: colors.blueGray500,
|
||||
lowContrast: colors.gray250,
|
||||
},
|
||||
secondary: {
|
||||
main: colors.gray200,
|
||||
light: colors.lightBlueWave,
|
||||
contrastText: colors.blueGray600,
|
||||
},
|
||||
tertiary: {
|
||||
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 = {
|
||||
accent: {
|
||||
main: colors.blueBlueWave,
|
||||
light: lighten(colors.blueBlueWave, 0.2),
|
||||
dark: darken(colors.blueBlueWave, 0.2),
|
||||
contrastText: colors.offWhite,
|
||||
},
|
||||
primary: {
|
||||
main: colors.offBlack,
|
||||
contrastText: colors.blueGray50,
|
||||
contrastTextSecondary: colors.gray200,
|
||||
contrastTextTertiary: colors.gray500,
|
||||
lowContrast: colors.blueGray600,
|
||||
},
|
||||
secondary: {
|
||||
main: "#313131",
|
||||
light: colors.lightBlueWave,
|
||||
contrastText: colors.gray200,
|
||||
},
|
||||
tertiary: {
|
||||
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,
|
||||
},
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
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)";
|
||||
|
||||
export const theme = (mode: string, palette: any) =>
|
||||
createTheme({
|
||||
spacing: 2,
|
||||
palette: {
|
||||
mode: mode,
|
||||
...palette,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: fontFamilyPrimary,
|
||||
fontSize: typographyLevels.base,
|
||||
h1: {
|
||||
fontSize: typographyLevels.xl,
|
||||
color: palette.primary.contrastText,
|
||||
fontWeight: 500,
|
||||
},
|
||||
h2: {
|
||||
fontSize: typographyLevels.l,
|
||||
color: palette.primary.contrastTextSecondary,
|
||||
fontWeight: 400,
|
||||
},
|
||||
|
||||
body1: {
|
||||
fontSize: typographyLevels.m,
|
||||
color: palette.primary.contrastTextTertiary,
|
||||
fontWeight: 400,
|
||||
},
|
||||
body2: {
|
||||
fontSize: typographyLevels.s,
|
||||
color: palette.primary.contrastTextTertiary,
|
||||
fontWeight: 400,
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
MuiFormLabel: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
fontSize: typographyLevels.base,
|
||||
"&.Mui-focused": {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiInputLabel: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
top: `-${theme.spacing(4)}`,
|
||||
"&.MuiInputLabel-shrink": {
|
||||
top: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: palette.accent.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => {
|
||||
return {
|
||||
marginTop: 4,
|
||||
padding: 0,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: 4,
|
||||
boxShadow: shadow,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
backgroundImage: "none",
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const lightTheme = createTheme(theme("light", lightPalette));
|
||||
export const darkTheme = createTheme(theme("dark", darkPalette));
|
||||
@@ -1,26 +0,0 @@
|
||||
import axios from "axios";
|
||||
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
const BASE_URL = import.meta.env.VITE_APP_API_V2_BASE_URL;
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL || "http://localhost:55555/api/v2",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
export const get = <T>(
|
||||
url: string,
|
||||
config: AxiosRequestConfig = {}
|
||||
): Promise<AxiosResponse<T>> => api.get<T>(url, config);
|
||||
|
||||
export const post = <T>(
|
||||
url: string,
|
||||
data: any,
|
||||
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;
|
||||
@@ -1,37 +0,0 @@
|
||||
const MIN_OUT = 10;
|
||||
const MAX_OUT = 100;
|
||||
|
||||
export const normalizeResponseTimes = <T extends Record<K, number>, K extends keyof T>(
|
||||
checks: T[],
|
||||
key: K
|
||||
): (T & { normalResponseTime: number })[] => {
|
||||
if (!Array.isArray(checks) || checks.length === 0)
|
||||
return checks as (T & {
|
||||
normalResponseTime: number;
|
||||
})[];
|
||||
|
||||
if (checks.length === 1) {
|
||||
return [
|
||||
{
|
||||
...checks[0],
|
||||
normalResponseTime: 50,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const { min, max } = checks.reduce(
|
||||
(acc, check) => {
|
||||
if (check[key] > acc.max) acc.max = check[key];
|
||||
if (check[key] < acc.min) acc.min = check[key];
|
||||
return acc;
|
||||
},
|
||||
{ max: -Infinity, min: Infinity }
|
||||
);
|
||||
|
||||
const range = max - min || 1;
|
||||
|
||||
return checks.map((check) => ({
|
||||
...check,
|
||||
normalResponseTime: MIN_OUT + ((check[key] - min) * (MAX_OUT - MIN_OUT)) / range,
|
||||
}));
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { MonitorStatus } from "@/Types/Monitor";
|
||||
import type { PaletteKey } from "@/Utils/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,53 +0,0 @@
|
||||
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";
|
||||
|
||||
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;
|
||||
export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
||||
export const MS_PER_DAY = 24 * MS_PER_HOUR;
|
||||
export const MS_PER_WEEK = MS_PER_DAY * 7;
|
||||
|
||||
export const formatDateWithTz = (timestamp: string, format: string, timezone: string) => {
|
||||
if (!timestamp) {
|
||||
return "Unknown time";
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import humanInterval from "human-interval";
|
||||
const urlRegex =
|
||||
/^(?:https?:\/\/)?([a-zA-Z0-9.-]+|\d{1,3}(\.\d{1,3}){3}|\[[0-9a-fA-F:]+\])(:\d{1,5})?$/;
|
||||
|
||||
const durationSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val || val.trim() === "") return;
|
||||
const ms = humanInterval(val);
|
||||
|
||||
if (!ms || isNaN(ms)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Invalid duration format",
|
||||
});
|
||||
} else if (ms < 10000) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Minimum duration is 10 seconds",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const monitorSchema = z.object({
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"),
|
||||
n: z.coerce
|
||||
.number({ message: "Number required" })
|
||||
.min(1, "Minimum value is 1")
|
||||
.max(25, "Maximum value is 25"),
|
||||
notificationChannels: z.array(z.string()).optional().default([]),
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
interval: durationSchema,
|
||||
});
|
||||
+1
-3
@@ -1,7 +1,6 @@
|
||||
import { configureStore, combineReducers } from "@reduxjs/toolkit";
|
||||
|
||||
import authReducer from "./Features/Auth/authSlice";
|
||||
import v2AuthReducer from "./Features/Auth/v2AuthSlice";
|
||||
import uiReducer from "./Features/UI/uiSlice";
|
||||
import storage from "redux-persist/lib/storage";
|
||||
import { persistReducer, persistStore, createTransform } from "redux-persist";
|
||||
@@ -20,13 +19,12 @@ const authTransform = createTransform(
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
whitelist: ["auth", "v2Auth", "ui"],
|
||||
whitelist: ["auth", "ui"],
|
||||
transforms: [authTransform],
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
v2Auth: v2AuthReducer,
|
||||
ui: uiReducer,
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user