format all files

This commit is contained in:
Alex Holliday
2025-05-27 09:10:32 -07:00
parent bfa6197ee4
commit 1b81251c93
81 changed files with 2628 additions and 2330 deletions

View File

@@ -1,2 +1 @@
This directory contains the client side (frontend) of Checkmate.

View File

@@ -1,17 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
href="./checkmate_favicon.svg"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./checkmate_favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checkmate</title>
</head>
<title>Checkmate</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.jsx"
></script>
</body>
</html>

View File

@@ -8,7 +8,9 @@
"build": "vite build",
"build-dev": "vite build --mode development",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"preview": "vite preview",
"format": "prettier --write .",
"format-check": "prettier --check ."
},
"dependencies": {
"@emotion/react": "^11.13.3",

View File

@@ -1,7 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"labels": ["dependencies"]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"labels": ["dependencies"]
}

View File

@@ -3,28 +3,28 @@ import Button from "@mui/material/Button";
import { styled } from "@mui/material/styles";
const RoundGradientButton = styled(Button)(({ theme }) => ({
position: "relative",
border: "5px solid transparent",
backgroundClip: "padding-box",
borderRadius: 30,
fontSize: "1.2rem",
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.background.main,
position: "relative",
border: "5px solid transparent",
backgroundClip: "padding-box",
borderRadius: 30,
fontSize: "1.2rem",
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.background.main,
"&:after": {
position: "absolute",
top: -3,
left: -3,
right: -3,
bottom: -3,
background:
theme.palette.mode === "dark"
? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)"
: "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)",
content: '""',
zIndex: -1,
borderRadius: 30,
},
"&:after": {
position: "absolute",
top: -3,
left: -3,
right: -3,
bottom: -3,
background:
theme.palette.mode === "dark"
? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)"
: "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)",
content: '""',
zIndex: -1,
borderRadius: 30,
},
}));
export default RoundGradientButton;

View File

@@ -31,78 +31,76 @@ import IconBox from "../../IconBox";
*/
const EmptyView = ({
icon,
header,
message = "No Data",
headingLevel = "h2",
justifyContent = "flex-start",
height = "100%"
icon,
header,
message = "No Data",
headingLevel = "h2",
justifyContent = "flex-start",
height = "100%",
}) => {
const theme = useTheme();
return (
<Stack
flex={1}
direction="row"
sx={{
backgroundColor: theme.palette.primary.main,
const theme = useTheme();
return (
<Stack
flex={1}
direction="row"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
}}
>
<Stack
flex={1}
alignItems="center"
sx={{
padding: theme.spacing(8),
justifyContent,
gap: theme.spacing(8),
height,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
}}
>
<Stack
flex={1}
alignItems="center"
sx={{
padding: theme.spacing(8),
justifyContent,
gap: theme.spacing(8),
height,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Stack
alignSelf="flex-start"
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
{icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>}
</Stack>
<Stack
flex={1}
justifyContent="center"
alignItems="center"
>
<Typography component={headingLevel}>
{message}
</Typography>
</Stack>
</Stack>
</Stack>
);
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Stack
alignSelf="flex-start"
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
{icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>}
</Stack>
<Stack
flex={1}
justifyContent="center"
alignItems="center"
>
<Typography component={headingLevel}>{message}</Typography>
</Stack>
</Stack>
</Stack>
);
};
EmptyView.propTypes = {
message: PropTypes.string,
icon: PropTypes.node,
header: PropTypes.string,
headingLevel: PropTypes.oneOf(['h1', 'h2', 'h3']),
justifyContent: PropTypes.string,
height: PropTypes.string
message: PropTypes.string,
icon: PropTypes.node,
header: PropTypes.string,
headingLevel: PropTypes.oneOf(["h1", "h2", "h3"]),
justifyContent: PropTypes.string,
height: PropTypes.string,
};
export default EmptyView;

View File

@@ -18,7 +18,13 @@ const ChartBox = ({
}) => {
const theme = useTheme();
if (isEmpty) {
return <EmptyView icon={icon} header={header} message={noDataMessage} />;
return (
<EmptyView
icon={icon}
header={header}
message={noDataMessage}
/>
);
}
return (
<Stack
@@ -95,5 +101,5 @@ ChartBox.propTypes = {
header: PropTypes.string,
height: PropTypes.string,
noDataMessage: PropTypes.string,
isEmpty: PropTypes.bool
isEmpty: PropTypes.bool,
};

View File

@@ -1,214 +1,255 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
import IconButton from '@mui/material/IconButton';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from 'react-icons/fa';
import * as React from "react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import IconButton from "@mui/material/IconButton";
import Link from "@mui/material/Link";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from "react-icons/fa";
function Copyright() {
return (
<Typography sx={{ color: 'text.secondary', mt: 1 }}>
{'Copyright © '}
<Link color="text.secondary" href="https://prism.uprock.com/">
UpRock
</Link>
&nbsp;
{new Date().getFullYear()}
</Typography>
);
return (
<Typography sx={{ color: "text.secondary", mt: 1 }}>
{"Copyright © "}
<Link
color="text.secondary"
href="https://prism.uprock.com/"
>
UpRock
</Link>
&nbsp;
{new Date().getFullYear()}
</Typography>
);
}
export default function Footer() {
return (
<Container
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: { xs: 4, sm: 8 },
py: { xs: 24, sm: 24 },
px: { xs: 12, sm: 12 },
textAlign: { sm: 'center', md: 'left' },
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
pt: { xs: 4, sm: 8 },
width: '100%',
borderColor: 'divider',
}}
>
<div>
<Link color="text.secondary" href="https://uprock.com/privacy-policy">
Privacy Policy
</Link>
<Typography sx={{ display: 'inline', mx: 0.5, opacity: 0.5 }}>
&nbsp;&nbsp;
</Typography>
<Link color="text.secondary" href="https://uprock.com/terms-of-use">
Terms of Service
</Link>
<Copyright />
</div>
<Stack
direction="row"
spacing={1}
useFlexGap
sx={{ justifyContent: 'left', color: 'text.secondary' }}
>
<IconButton
color="inherit"
size="small"
href="mailto:prism@uprock.com?subject=Interested%20in%20UpRock%20Prism"
aria-label="Contact Us"
sx={{ alignSelf: 'center' }}
>
<FaEnvelope />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://facebook.com/uprockcom"
aria-label="Facebook"
sx={{ alignSelf: 'center' }}
>
<FaFacebook />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://x.com/uprockcom"
aria-label="X"
sx={{ alignSelf: 'center' }}
>
<FaTwitter />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://www.linkedin.com/company/uprock/"
aria-label="LinkedIn"
sx={{ alignSelf: 'center' }}
>
<FaLinkedin />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://github.com/uprockcom"
aria-label="GitHub"
sx={{ alignSelf: 'center' }}
>
<FaGithub />
</IconButton>
return (
<Container
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: { xs: 4, sm: 8 },
py: { xs: 24, sm: 24 },
px: { xs: 12, sm: 12 },
textAlign: { sm: "center", md: "left" },
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
pt: { xs: 4, sm: 8 },
width: "100%",
borderColor: "divider",
}}
>
<div>
<Link
color="text.secondary"
href="https://uprock.com/privacy-policy"
>
Privacy Policy
</Link>
<Typography sx={{ display: "inline", mx: 0.5, opacity: 0.5 }}>
&nbsp;&nbsp;
</Typography>
<Link
color="text.secondary"
href="https://uprock.com/terms-of-use"
>
Terms of Service
</Link>
<Copyright />
</div>
<Stack
direction="row"
spacing={1}
useFlexGap
sx={{ justifyContent: "left", color: "text.secondary" }}
>
<IconButton
color="inherit"
size="small"
href="mailto:prism@uprock.com?subject=Interested%20in%20UpRock%20Prism"
aria-label="Contact Us"
sx={{ alignSelf: "center" }}
>
<FaEnvelope />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://facebook.com/uprockcom"
aria-label="Facebook"
sx={{ alignSelf: "center" }}
>
<FaFacebook />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://x.com/uprockcom"
aria-label="X"
sx={{ alignSelf: "center" }}
>
<FaTwitter />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://www.linkedin.com/company/uprock/"
aria-label="LinkedIn"
sx={{ alignSelf: "center" }}
>
<FaLinkedin />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://github.com/uprockcom"
aria-label="GitHub"
sx={{ alignSelf: "center" }}
>
<FaGithub />
</IconButton>
</Stack>
</Box>
</Stack>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
mt: 4,
}}
>
<Typography variant="h2" sx={{ color: 'text.secondary' }}>
Made with by&nbsp;
<Link href="https://uprock.com" color="inherit" sx={{ mx: 0.5 }}>
UpRock&nbsp;
</Link>
&&nbsp;
<Link href="https://bluewavelabs.ca" color="inherit" sx={{ mx: 0.5 }}>
Bluewave Labs
</Link>
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
mt: 2,
}}
>
<Typography variant="h2" sx={{ color: 'text.secondary', mr: 1 }}>
Built on&nbsp;
</Typography>
<svg
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 397.7 311.7"
xmlSpace="preserve"
width="15"
height="15"
>
<style type="text/css">
{`.st0{fill:url(#SVGID_1_);}
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
mt: 4,
}}
>
<Typography
variant="h2"
sx={{ color: "text.secondary" }}
>
Made with by&nbsp;
<Link
href="https://uprock.com"
color="inherit"
sx={{ mx: 0.5 }}
>
UpRock&nbsp;
</Link>
&&nbsp;
<Link
href="https://bluewavelabs.ca"
color="inherit"
sx={{ mx: 0.5 }}
>
Bluewave Labs
</Link>
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
mt: 2,
}}
>
<Typography
variant="h2"
sx={{ color: "text.secondary", mr: 1 }}
>
Built on&nbsp;
</Typography>
<svg
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 397.7 311.7"
xmlSpace="preserve"
width="15"
height="15"
>
<style type="text/css">
{`.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}`}
</style>
<linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="360.8791"
y1="351.4553"
x2="141.213"
y2="-69.2936"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} />
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} />
</linearGradient>
<path
className="st0"
d="M64.6,237.9c2.4-2.4,5.7-3.8,9.2-3.8h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,237.9z"
/>
<linearGradient
id="SVGID_2_"
gradientUnits="userSpaceOnUse"
x1="264.8291"
y1="401.6014"
x2="45.163"
y2="-19.1475"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} />
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} />
</linearGradient>
<path
className="st1"
d="M64.6,3.8C67.1,1.4,70.4,0,73.8,0h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,3.8z"
/>
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="312.5484"
y1="376.688"
x2="92.8822"
y2="-44.061"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} />
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} />
</linearGradient>
<path
className="st2"
d="M333.1,120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8,0-8.7,7-4.6,11.1l62.7,62.7c2.4,2.4,5.7,3.8,9.2,3.8h317.4 c5.8,0,8.7-7,4.6-11.1L333.1,120.1z"
/>
</svg>
<Typography variant="h2" sx={{ color: 'text.secondary', ml: 1 }}>
&nbsp;Solana
</Typography>
</Box>
</Box>
</Container>
);
</style>
<linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="360.8791"
y1="351.4553"
x2="141.213"
y2="-69.2936"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
offset="0"
style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st0"
d="M64.6,237.9c2.4-2.4,5.7-3.8,9.2-3.8h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,237.9z"
/>
<linearGradient
id="SVGID_2_"
gradientUnits="userSpaceOnUse"
x1="264.8291"
y1="401.6014"
x2="45.163"
y2="-19.1475"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
offset="0"
style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st1"
d="M64.6,3.8C67.1,1.4,70.4,0,73.8,0h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,3.8z"
/>
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="312.5484"
y1="376.688"
x2="92.8822"
y2="-44.061"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
offset="0"
style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st2"
d="M333.1,120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8,0-8.7,7-4.6,11.1l62.7,62.7c2.4,2.4,5.7,3.8,9.2,3.8h317.4 c5.8,0,8.7-7,4.6-11.1L333.1,120.1z"
/>
</svg>
<Typography
variant="h2"
sx={{ color: "text.secondary", ml: 1 }}
>
&nbsp;Solana
</Typography>
</Box>
</Box>
</Container>
);
}

View File

@@ -29,7 +29,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
},
"& h1, & h2": {
color: theme.palette.primary.contrastTextSecondary,
}
},
}));
export default ConfigBox;

View File

@@ -7,7 +7,7 @@ import ConfigBox from "../ConfigBox";
* ConfigRow is a styled container used to layout content in a row format with specific padding, border, and spacing.
* It serves as the wrapper for ConfigBox, with the left section displaying the title and description,
* and the right section displaying the children.
*
*
* @component
* @example
* return (
@@ -19,31 +19,40 @@ import ConfigBox from "../ConfigBox";
*/
const ConfigRow = ({ title, description, children }) => {
const theme = useTheme();
const theme = useTheme();
return (
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">
{title}
</Typography>
{description && (
<Typography variant="body2" mt={theme.spacing(2)}>
{description}
</Typography>
)}
</Box>
<Stack gap={theme.spacing(15)} mt={theme.spacing(4)}>
{children}
</Stack>
</ConfigBox>
);
return (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{title}
</Typography>
{description && (
<Typography
variant="body2"
mt={theme.spacing(2)}
>
{description}
</Typography>
)}
</Box>
<Stack
gap={theme.spacing(15)}
mt={theme.spacing(4)}
>
{children}
</Stack>
</ConfigBox>
);
};
ConfigRow.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
children: PropTypes.node,
title: PropTypes.string.isRequired,
description: PropTypes.string,
children: PropTypes.node,
};
export default ConfigRow;

View File

@@ -42,10 +42,14 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
return header;
}
return header + " | " + selected
.map((value) => options.find((option) => option.value === value)?.label)
.filter(Boolean)
.join(", ");
return (
header +
" | " +
selected
.map((value) => options.find((option) => option.value === value)?.label)
.filter(Boolean)
.join(", ")
);
}}
MenuProps={{
anchorOrigin: {

View File

@@ -36,199 +36,198 @@ const ImageUpload = ({
error,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const [uploadComplete, setUploadComplete] = useState(false);
const [completedFile, setCompletedFile] = useState(null);
const [file, setFile] = useState(null);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
const intervalRef = useRef(null);
const [localError, setLocalError] = useState(null);
const [isDragging, setIsDragging] = useState(false);
const { t } = useTranslation();
const [uploadComplete, setUploadComplete] = useState(false);
const [completedFile, setCompletedFile] = useState(null);
const [file, setFile] = useState(null);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
const intervalRef = useRef(null);
const [localError, setLocalError] = useState(null);
const [isDragging, setIsDragging] = useState(false);
const roundStyle = previewIsRound ? { borderRadius: "50%" } : {};
const handleImageChange = useCallback(
(file) => {
if (!file) return;
(file) => {
if (!file) return;
const isValidType = accept.some((type) =>
file.type.includes(type)
);
const isValidSize = file.size <= maxSize;
if (!isValidType) {
setLocalError(t('invalidFileFormat'));
return;
}
if (!isValidSize) {
setLocalError(t('invalidFileSize'));
return;
}
setLocalError(null);
const previewFile = {
src: URL.createObjectURL(file),
name: file.name,
file,
};
setFile(previewFile);
setProgress({ value: 0, isLoading: true });
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const buffer = 12;
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
setUploadComplete(true);
setCompletedFile(previewFile);
return { value: 100, isLoading: false };
}
return { value: prev.value + buffer, isLoading: true };
});
}, 120);
},
[maxSize, accept]
);
const isValidType = accept.some((type) => file.type.includes(type));
const isValidSize = file.size <= maxSize;
useEffect(() => {
if (uploadComplete && completedFile) {
onChange?.(completedFile);
setUploadComplete(false);
setCompletedFile(null);
}
}, [uploadComplete, completedFile, onChange]);
if (!isValidType) {
setLocalError(t("invalidFileFormat"));
return;
}
if (!isValidSize) {
setLocalError(t("invalidFileSize"));
return;
}
setLocalError(null);
const previewFile = {
src: URL.createObjectURL(file),
name: file.name,
file,
};
setFile(previewFile);
setProgress({ value: 0, isLoading: true });
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const buffer = 12;
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
setUploadComplete(true);
setCompletedFile(previewFile);
return { value: 100, isLoading: false };
}
return { value: prev.value + buffer, isLoading: true };
});
}, 120);
},
[maxSize, accept]
);
useEffect(() => {
if (uploadComplete && completedFile) {
onChange?.(completedFile);
setUploadComplete(false);
setCompletedFile(null);
}
}, [uploadComplete, completedFile, onChange]);
return (
<>
{src ? (
<Stack direction="row" justifyContent="center">
<Image
alt="Uploaded preview"
src={src}
width="250px"
height="250px"
sx={{ ...roundStyle }}
/>
</Stack>
) : (
<>
<Box
className="image-field-wrapper"
mt={theme.spacing(8)}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
onDrop={() => setIsDragging(false)}
sx={{
position: "relative",
height: "fit-content",
border: "dashed",
borderRadius: theme.shape.borderRadius,
borderColor: isDragging
? theme.palette.primary.main
: theme.palette.primary.lowContrast,
backgroundColor: isDragging
? "hsl(215, 87%, 51%, 0.05)"
: "transparent",
borderWidth: "2px",
transition: "0.2s",
"&:hover": {
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
}}
>
<TextField
type="file"
onChange={(e) => handleImageChange(e?.target?.files?.[0])}
sx={{
width: "100%",
"& .MuiInputBase-input[type='file']": {
opacity: 0,
cursor: "pointer",
maxWidth: "500px",
minHeight: "175px",
zIndex: 1,
},
"& fieldset": {
padding: 0,
border: "none",
},
}}
/>
<Stack
alignItems="center"
gap="4px"
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 0,
width: "100%",
}}
>
<IconButton
sx={{
pointerEvents: "none",
borderRadius: theme.shape.borderRadius,
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
boxShadow: theme.shape.boxShadow,
}}
>
<CloudUploadIcon />
</IconButton>
<Typography
component="h2"
color={theme.palette.primary.contrastTextTertiary}
>
<Typography
component="span"
fontSize="inherit"
color="info"
fontWeight={500}
>
{t('ClickUpload')}
</Typography>{" "}
or {t('DragandDrop')}
</Typography>
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>({t('MaxSize')}: {Math.round(maxSize / 1024 / 1024)}MB)
</Typography>
</Stack>
</Box>
{(localError || progress.isLoading || progress.value !== 0) && (
<ProgressUpload
icon={<ImageIcon />}
label={file?.name || "Upload failed"}
size={file?.size}
progress={progress.value}
onClick={() => {
clearInterval(intervalRef.current);
setFile(null);
setProgress({ value: 0, isLoading: false });
setLocalError(null);
onChange(undefined);
}}
error={localError || error}
/>
)}
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>
{t('SupportedFormats')}: {accept.join(", ").toUpperCase()}
</Typography>
</>
)}
</>
);
<>
{src ? (
<Stack
direction="row"
justifyContent="center"
>
<Image
alt="Uploaded preview"
src={src}
width="250px"
height="250px"
sx={{ ...roundStyle }}
/>
</Stack>
) : (
<>
<Box
className="image-field-wrapper"
mt={theme.spacing(8)}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
onDrop={() => setIsDragging(false)}
sx={{
position: "relative",
height: "fit-content",
border: "dashed",
borderRadius: theme.shape.borderRadius,
borderColor: isDragging
? theme.palette.primary.main
: theme.palette.primary.lowContrast,
backgroundColor: isDragging ? "hsl(215, 87%, 51%, 0.05)" : "transparent",
borderWidth: "2px",
transition: "0.2s",
"&:hover": {
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
}}
>
<TextField
type="file"
onChange={(e) => handleImageChange(e?.target?.files?.[0])}
sx={{
width: "100%",
"& .MuiInputBase-input[type='file']": {
opacity: 0,
cursor: "pointer",
maxWidth: "500px",
minHeight: "175px",
zIndex: 1,
},
"& fieldset": {
padding: 0,
border: "none",
},
}}
/>
<Stack
alignItems="center"
gap="4px"
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 0,
width: "100%",
}}
>
<IconButton
sx={{
pointerEvents: "none",
borderRadius: theme.shape.borderRadius,
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
boxShadow: theme.shape.boxShadow,
}}
>
<CloudUploadIcon />
</IconButton>
<Typography
component="h2"
color={theme.palette.primary.contrastTextTertiary}
>
<Typography
component="span"
fontSize="inherit"
color="info"
fontWeight={500}
>
{t("ClickUpload")}
</Typography>{" "}
or {t("DragandDrop")}
</Typography>
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>
({t("MaxSize")}: {Math.round(maxSize / 1024 / 1024)}MB)
</Typography>
</Stack>
</Box>
{(localError || progress.isLoading || progress.value !== 0) && (
<ProgressUpload
icon={<ImageIcon />}
label={file?.name || "Upload failed"}
size={file?.size}
progress={progress.value}
onClick={() => {
clearInterval(intervalRef.current);
setFile(null);
setProgress({ value: 0, isLoading: false });
setLocalError(null);
onChange(undefined);
}}
error={localError || error}
/>
)}
<Typography
component="p"
color={theme.palette.primary.contrastTextTertiary}
sx={{ opacity: 0.6 }}
>
{t("SupportedFormats")}: {accept.join(", ").toUpperCase()}
</Typography>
</>
)}
</>
);
};
ImageUpload.propTypes = {
@@ -240,4 +239,4 @@ ImageUpload.propTypes = {
error: PropTypes.string,
};
export default ImageUpload;
export default ImageUpload;

View File

@@ -158,7 +158,15 @@ const StatusLabel = ({ status, text, customStyles }) => {
};
StatusLabel.propTypes = {
status: PropTypes.oneOf(["up", "down", "paused", "pending", "cannot resolve", "published", "unpublished"]),
status: PropTypes.oneOf([
"up",
"down",
"paused",
"pending",
"cannot resolve",
"published",
"unpublished",
]),
text: PropTypes.string,
customStyles: PropTypes.object,
};

View File

@@ -30,4 +30,4 @@
max-width: 1400px;
margin: 0 auto;
flex: 1;
}
}

View File

@@ -4,17 +4,11 @@ import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
const CreateMonitorHeader = ({
isAdmin,
label,
isLoading = true,
path,
bulkPath,
}) => {
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
// Use the provided label or fall back to the translated default
if (!isAdmin) return null;

View File

@@ -28,8 +28,9 @@ const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography variant="h2"
style={{fontFamily : "monospace" , fontWeight : 'bolder'}}
<Typography
variant="h2"
style={{ fontFamily: "monospace", fontWeight: "bolder" }}
>
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>

View File

@@ -1,109 +1,115 @@
import React from "react";
import {
Typography,
Box,
Button,
CircularProgress
} from "@mui/material";
import { Typography, Box, Button, CircularProgress } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import TextInput from "../../../Components/Inputs/TextInput";
import Checkbox from "../../../Components/Inputs/Checkbox";
const TabComponent = ({
type,
integrations,
handleIntegrationChange,
handleInputChange,
handleTestNotification,
isLoading
const TabComponent = ({
type,
integrations,
handleIntegrationChange,
handleInputChange,
handleTestNotification,
isLoading,
}) => {
const theme = useTheme();
const { t } = useTranslation();
// Helper to get the field state key (e.g., slackWebhook, telegramToken)
const getFieldKey = (typeId, fieldId) => {
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
};
// Check if all fields have values to enable test button
const areAllFieldsFilled = () => {
return type.fields.every(field => {
const fieldKey = getFieldKey(type.id, field.id);
return integrations[fieldKey];
});
};
return (
<>
<Typography variant="subtitle1" component="h4" sx={{
fontWeight: 'bold',
color: theme.palette.primary.contrastTextSecondary
}}>
{type.label}
</Typography>
<Typography sx={{
mt: theme.spacing(0.5),
mb: theme.spacing(1.5),
color: theme.palette.primary.contrastTextTertiary
}}>
{type.description}
</Typography>
<Box sx={{ pl: theme.spacing(1.5) }}>
<Checkbox
id={`enable-${type.id}`}
label={t('notifications.enableNotifications', { platform: type.label })}
isChecked={integrations[type.id]}
onChange={(e) => handleIntegrationChange(type.id, e.target.checked)}
disabled={isLoading}
/>
</Box>
{type.fields.map(field => {
const fieldKey = getFieldKey(type.id, field.id);
return (
<Box key={field.id} sx={{ mt: theme.spacing(1) }}>
<Typography sx={{
mb: theme.spacing(2),
fontWeight: 'bold',
color: theme.palette.primary.contrastTextSecondary
}}>
{field.label}
</Typography>
<TextInput
id={`${type.id}-${field.id}`}
type={field.type}
placeholder={field.placeholder}
value={integrations[fieldKey]}
onChange={(e) => handleInputChange(fieldKey, e.target.value)}
disabled={!integrations[type.id] || isLoading}
/>
</Box>
);
})}
<Box sx={{ mt: theme.spacing(1) }}>
<Button
variant="text"
color="info"
onClick={() => handleTestNotification(type.id)}
disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading}
>
{isLoading ? (
<CircularProgress
size={theme.spacing(8)}
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main}}
/>
) : null}
{t('notifications.testNotification')}
</Button>
</Box>
</>
);
const theme = useTheme();
const { t } = useTranslation();
// Helper to get the field state key (e.g., slackWebhook, telegramToken)
const getFieldKey = (typeId, fieldId) => {
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
};
// Check if all fields have values to enable test button
const areAllFieldsFilled = () => {
return type.fields.every((field) => {
const fieldKey = getFieldKey(type.id, field.id);
return integrations[fieldKey];
});
};
return (
<>
<Typography
variant="subtitle1"
component="h4"
sx={{
fontWeight: "bold",
color: theme.palette.primary.contrastTextSecondary,
}}
>
{type.label}
</Typography>
<Typography
sx={{
mt: theme.spacing(0.5),
mb: theme.spacing(1.5),
color: theme.palette.primary.contrastTextTertiary,
}}
>
{type.description}
</Typography>
<Box sx={{ pl: theme.spacing(1.5) }}>
<Checkbox
id={`enable-${type.id}`}
label={t("notifications.enableNotifications", { platform: type.label })}
isChecked={integrations[type.id]}
onChange={(e) => handleIntegrationChange(type.id, e.target.checked)}
disabled={isLoading}
/>
</Box>
{type.fields.map((field) => {
const fieldKey = getFieldKey(type.id, field.id);
return (
<Box
key={field.id}
sx={{ mt: theme.spacing(1) }}
>
<Typography
sx={{
mb: theme.spacing(2),
fontWeight: "bold",
color: theme.palette.primary.contrastTextSecondary,
}}
>
{field.label}
</Typography>
<TextInput
id={`${type.id}-${field.id}`}
type={field.type}
placeholder={field.placeholder}
value={integrations[fieldKey]}
onChange={(e) => handleInputChange(fieldKey, e.target.value)}
disabled={!integrations[type.id] || isLoading}
/>
</Box>
);
})}
<Box sx={{ mt: theme.spacing(1) }}>
<Button
variant="text"
color="info"
onClick={() => handleTestNotification(type.id)}
disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading}
>
{isLoading ? (
<CircularProgress
size={theme.spacing(8)}
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main }}
/>
) : null}
{t("notifications.testNotification")}
</Button>
</Box>
</>
);
};
export default TabComponent;
export default TabComponent;

View File

@@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react";
/**
* TabPanel component that displays content for the selected tab.
*
*
* @component
* @param {Object} props - The component props.
* @param {React.ReactNode} props.children - The content to be displayed when this tab panel is selected.
@@ -15,32 +15,27 @@ import { useTheme } from "@emotion/react";
* @returns {React.ReactElement|null} The rendered tab panel or null if not selected.
*/
function TabPanel({ children, value, index, ...other }) {
const theme = useTheme();
return (
<div
role="tabpanel"
hidden={value !== index}
id={`notification-tabpanel-${index}`}
aria-labelledby={`notification-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ pt: theme.spacing(3) }}>
{children}
</Box>
)}
</div>
);
const theme = useTheme();
return (
<div
role="tabpanel"
hidden={value !== index}
id={`notification-tabpanel-${index}`}
aria-labelledby={`notification-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ pt: theme.spacing(3) }}>{children}</Box>}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};
export default TabPanel;
export default TabPanel;

View File

@@ -1,128 +1,129 @@
import { useState } from 'react';
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { networkService } from '../../../Utils/NetworkService';
import { createToast } from '../../../Utils/toastUtils';
import { networkService } from "../../../Utils/NetworkService";
import { createToast } from "../../../Utils/toastUtils";
// Define constants for notification types to avoid magic values
const NOTIFICATION_TYPES = {
SLACK: 'slack',
DISCORD: 'discord',
TELEGRAM: 'telegram',
WEBHOOK: 'webhook'
SLACK: "slack",
DISCORD: "discord",
TELEGRAM: "telegram",
WEBHOOK: "webhook",
};
// Define constants for field IDs
const FIELD_IDS = {
WEBHOOK: 'webhook',
TOKEN: 'token',
CHAT_ID: 'chatId',
URL: 'url'
WEBHOOK: "webhook",
TOKEN: "token",
CHAT_ID: "chatId",
URL: "url",
};
/**
* Custom hook for notification-related operations
*/
const useNotifications = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined);
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined);
const { t } = useTranslation();
/**
* Send a test notification
* @param {string} type - The notification type (slack, discord, telegram, webhook)
* @param {object} config - Configuration object with necessary params
*/
const sendTestNotification = async (type, config) => {
setLoading(true);
setError(undefined);
// Validation based on notification type
let payload = { platform: type };
let isValid = true;
let errorMessage = '';
switch(type) {
case NOTIFICATION_TYPES.SLACK:
payload.webhookUrl = config.webhook;
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
isValid = false;
errorMessage = t('notifications.slack.webhookRequired');
}
break;
case NOTIFICATION_TYPES.DISCORD:
payload.webhookUrl = config.webhook;
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
isValid = false;
errorMessage = t('notifications.discord.webhookRequired');
}
break;
case NOTIFICATION_TYPES.TELEGRAM:
payload.botToken = config.token;
payload.chatId = config.chatId;
if (typeof payload.botToken === 'undefined' || payload.botToken === '' ||
typeof payload.chatId === 'undefined' || payload.chatId === '') {
isValid = false;
errorMessage = t('notifications.telegram.fieldsRequired');
}
break;
case NOTIFICATION_TYPES.WEBHOOK:
payload.webhookUrl = config.url;
payload.platform = NOTIFICATION_TYPES.SLACK;
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
isValid = false;
errorMessage = t('notifications.webhook.urlRequired');
}
break;
default:
isValid = false;
errorMessage = t('notifications.unsupportedType');
}
/**
* Send a test notification
* @param {string} type - The notification type (slack, discord, telegram, webhook)
* @param {object} config - Configuration object with necessary params
*/
const sendTestNotification = async (type, config) => {
setLoading(true);
setError(undefined);
// If validation fails, show error and return
if (isValid === false) {
createToast({
body: errorMessage,
variant: "error"
});
setLoading(false);
return;
}
// Validation based on notification type
let payload = { platform: type };
let isValid = true;
let errorMessage = "";
try {
const response = await networkService.testNotification({
platform: type,
payload: payload
});
if (response.data.success === true) {
createToast({
body: t('notifications.testSuccess'),
variant: "info"
});
} else {
throw new Error(response.data.msg || t('notifications.testFailed'));
}
} catch (error) {
const errorMsg = error.response?.data?.msg || error.message || t('notifications.networkError');
createToast({
body: `${t('notifications.testFailed')}: ${errorMsg}`,
variant: "error"
});
setError(errorMsg);
} finally {
setLoading(false);
}
};
switch (type) {
case NOTIFICATION_TYPES.SLACK:
payload.webhookUrl = config.webhook;
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
isValid = false;
errorMessage = t("notifications.slack.webhookRequired");
}
break;
return [
loading,
error,
sendTestNotification
];
case NOTIFICATION_TYPES.DISCORD:
payload.webhookUrl = config.webhook;
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
isValid = false;
errorMessage = t("notifications.discord.webhookRequired");
}
break;
case NOTIFICATION_TYPES.TELEGRAM:
payload.botToken = config.token;
payload.chatId = config.chatId;
if (
typeof payload.botToken === "undefined" ||
payload.botToken === "" ||
typeof payload.chatId === "undefined" ||
payload.chatId === ""
) {
isValid = false;
errorMessage = t("notifications.telegram.fieldsRequired");
}
break;
case NOTIFICATION_TYPES.WEBHOOK:
payload.webhookUrl = config.url;
payload.platform = NOTIFICATION_TYPES.SLACK;
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
isValid = false;
errorMessage = t("notifications.webhook.urlRequired");
}
break;
default:
isValid = false;
errorMessage = t("notifications.unsupportedType");
}
// If validation fails, show error and return
if (isValid === false) {
createToast({
body: errorMessage,
variant: "error",
});
setLoading(false);
return;
}
try {
const response = await networkService.testNotification({
platform: type,
payload: payload,
});
if (response.data.success === true) {
createToast({
body: t("notifications.testSuccess"),
variant: "info",
});
} else {
throw new Error(response.data.msg || t("notifications.testFailed"));
}
} catch (error) {
const errorMsg =
error.response?.data?.msg || error.message || t("notifications.networkError");
createToast({
body: `${t("notifications.testFailed")}: ${errorMsg}`,
variant: "error",
});
setError(errorMsg);
} finally {
setLoading(false);
}
};
return [loading, error, sendTestNotification];
};
export default useNotifications;
export default useNotifications;

View File

@@ -93,7 +93,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
<Typography
component="h2"
mb={theme.spacing(1.5)}
sx={{ wordBreak: 'break-all' }}
sx={{ wordBreak: "break-all" }}
>
{error ? error : label}
</Typography>

View File

@@ -121,7 +121,7 @@ function Sidebar() {
const dispatch = useDispatch();
const { t } = useTranslation();
const authState = useSelector((state) => state.auth);
const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t);
const accountMenuItems = getAccountMenuItems(t);
@@ -147,7 +147,7 @@ function Sidebar() {
} else {
setSidebarReady(false);
}
}, [collapsed]);
}, [collapsed]);
const renderAccountMenuItems = () => {
let filteredAccountMenuItems = [...accountMenuItems];
@@ -170,8 +170,8 @@ function Sidebar() {
<MenuItem
key={item.name}
onClick={() => {
closePopup()
navigate(item.path)
closePopup();
navigate(item.path);
}}
sx={{
gap: theme.spacing(2),
@@ -231,7 +231,7 @@ function Sidebar() {
*/
sx={{
position: "relative",
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
borderColor: theme.palette.primary.lowContrast,
borderRadius: 0,
backgroundColor: theme.palette.primary.main,
@@ -732,11 +732,15 @@ function Sidebar() {
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography sx={{ textTransform: "capitalize" }}>
{authState.user?.role?.includes("superadmin") ? t("roles.superAdmin") :
authState.user?.role?.includes("admin") ? t("roles.admin") :
authState.user?.role?.includes("user") ? t("roles.teamMember") :
authState.user?.role?.includes("demo") ? t("roles.demoUser") :
authState.user?.role}
{authState.user?.role?.includes("superadmin")
? t("roles.superAdmin")
: authState.user?.role?.includes("admin")
? t("roles.admin")
: authState.user?.role?.includes("user")
? t("roles.teamMember")
: authState.user?.role?.includes("demo")
? t("roles.demoUser")
: authState.user?.role}
</Typography>
</Box>
<Stack

View File

@@ -1,101 +1,108 @@
import React from 'react';
import { Typography, IconButton, Stack, Box } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { useTheme } from '@emotion/react';
import { useSelector, useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { setStarPromptOpen } from '../../Features/UI/uiSlice';
import React from "react";
import { Typography, IconButton, Stack, Box } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { useTheme } from "@emotion/react";
import { useSelector, useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { setStarPromptOpen } from "../../Features/UI/uiSlice";
const StarPrompt = ({
repoUrl = 'https://github.com/bluewave-labs/checkmate'
}) => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true);
const mode = useSelector((state) => state.ui.mode);
const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true);
const mode = useSelector((state) => state.ui.mode);
const handleClose = () => {
dispatch(setStarPromptOpen(false));
};
const handleClose = () => {
dispatch(setStarPromptOpen(false));
};
const handleStarClick = () => {
window.open(repoUrl, '_blank');
};
const handleStarClick = () => {
window.open(repoUrl, "_blank");
};
if (!isOpen) return null;
if (!isOpen) return null;
return (
<Stack
direction="column"
sx={{
width: '100%',
padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: 0,
gap: theme.spacing(1.5),
backgroundColor: theme.palette.primary.main,
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="center" width="100%" pl={theme.spacing(4)}>
<Typography
variant="subtitle2"
sx={{
color: mode === 'dark' ? theme.palette.primary.contrastText : theme.palette.text.primary,
mt: theme.spacing(3)
}}
>
{t('starPromptTitle')}
</Typography>
<IconButton
onClick={handleClose}
size="small"
sx={{
color: theme.palette.text.primary,
padding: 0,
marginTop: theme.spacing(-5),
'&:hover': {
backgroundColor: 'transparent',
opacity: 0.8
},
}}
>
<CloseIcon sx={{ fontSize: '1.25rem' }} />
</IconButton>
</Stack>
return (
<Stack
direction="column"
sx={{
width: "100%",
padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: 0,
gap: theme.spacing(1.5),
backgroundColor: theme.palette.primary.main,
}}
>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
width="100%"
pl={theme.spacing(4)}
>
<Typography
variant="subtitle2"
sx={{
color:
mode === "dark"
? theme.palette.primary.contrastText
: theme.palette.text.primary,
mt: theme.spacing(3),
}}
>
{t("starPromptTitle")}
</Typography>
<IconButton
onClick={handleClose}
size="small"
sx={{
color: theme.palette.text.primary,
padding: 0,
marginTop: theme.spacing(-5),
"&:hover": {
backgroundColor: "transparent",
opacity: 0.8,
},
}}
>
<CloseIcon sx={{ fontSize: "1.25rem" }} />
</IconButton>
</Stack>
<Typography
variant="body1"
sx={{
color: theme.palette.text.secondary,
fontSize: '0.938rem',
lineHeight: 1.5,
mb: 1,
px: theme.spacing(4)
}}
>
{t('starPromptDescription')}
</Typography>
<Typography
variant="body1"
sx={{
color: theme.palette.text.secondary,
fontSize: "0.938rem",
lineHeight: 1.5,
mb: 1,
px: theme.spacing(4),
}}
>
{t("starPromptDescription")}
</Typography>
<Box
component="img"
src={`https://img.shields.io/github/stars/bluewave-labs/checkmate?label=checkmate&style=social${mode === 'dark' ? '&color=white' : ''}`}
alt="GitHub stars"
onClick={handleStarClick}
sx={{
cursor: 'pointer',
transform: 'scale(0.65)',
transformOrigin: 'left center',
'&:hover': {
opacity: 0.8
},
pl: theme.spacing(4),
filter: mode === 'dark' ? 'invert(1)' : 'none'
}}
/>
</Stack>
);
<Box
component="img"
src={`https://img.shields.io/github/stars/bluewave-labs/checkmate?label=checkmate&style=social${mode === "dark" ? "&color=white" : ""}`}
alt="GitHub stars"
onClick={handleStarClick}
sx={{
cursor: "pointer",
transform: "scale(0.65)",
transformOrigin: "left center",
"&:hover": {
opacity: 0.8,
},
pl: theme.spacing(4),
filter: mode === "dark" ? "invert(1)" : "none",
}}
/>
</Stack>
);
};
export default StarPrompt;

View File

@@ -80,7 +80,7 @@ const StatBox = ({
};
const responsiveWidths = {
default: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
default: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
md: `calc(50% - (1 * ${theme.spacing(8)} / 2))`,
};

View File

@@ -20,7 +20,11 @@ const CustomTabList = ({ value, onChange, children, ...props }) => {
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
}}
>
<TabList value={value} onChange={onChange} {...props}>
<TabList
value={value}
onChange={onChange}
{...props}
>
{children}
</TabList>
</Box>

View File

@@ -94,7 +94,10 @@ const PasswordPanel = () => {
const action = await dispatch(update({ localData }));
if (action.payload.success) {
createToast({
body: t("passwordPanel.passwordChangedSuccess", "Your password was changed successfully."),
body: t(
"passwordPanel.passwordChangedSuccess",
"Your password was changed successfully."
),
});
setLocalData({
password: "",
@@ -104,7 +107,10 @@ const PasswordPanel = () => {
} else {
// TODO: Check for other errors?
createToast({
body: t("passwordPanel.passwordInputIncorrect", "Your password input was incorrect."),
body: t(
"passwordPanel.passwordInputIncorrect",
"Your password input was incorrect."
),
});
setErrors({ password: "*" + action.payload.msg + "." });
}
@@ -153,7 +159,10 @@ const PasswordPanel = () => {
<TextInput
type="password"
id="edit-current-password"
placeholder={t("passwordPanel.enterCurrentPassword", "Enter your current password")}
placeholder={t(
"passwordPanel.enterCurrentPassword",
"Enter your current password"
)}
autoComplete="current-password"
value={localData.password}
onChange={handleChange}
@@ -219,7 +228,10 @@ const PasswordPanel = () => {
<Box sx={{ maxWidth: "70ch" }}>
<Alert
variant="warning"
body={t("passwordPanel.passwordRequirements", "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.")}
body={t(
"passwordPanel.passwordRequirements",
"New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character."
)}
/>
</Box>
)}

View File

@@ -85,13 +85,13 @@ const ProfilePanel = () => {
// Resets picture-related states and clears interval
const removePicture = () => {
errors["picture"] && clearError("picture");
setFile(undefined);
setFile(undefined);
setLocalData((prev) => ({
...prev,
file: undefined,
deleteProfileImage: true,
deleteProfileImage: true,
}));
};
};
// Opens the picture update modal
const openPictureModal = () => {
@@ -102,9 +102,9 @@ const ProfilePanel = () => {
// Closes the picture update modal and resets related states
const closePictureModal = () => {
if (errors["picture"]) clearError("picture");
setFile(undefined);
setIsOpen("");
};
setFile(undefined);
setIsOpen("");
};
// Updates profile image displayed on UI
const handleUpdatePicture = () => {
@@ -116,24 +116,23 @@ const ProfilePanel = () => {
setIsOpen("");
if (errors["unchanged"]) clearError("unchanged");
};
// Handles form submission to update user profile
const handleSaveProfile = async (event) => {
event.preventDefault();
const nameChanged =
localData.firstName !== user.firstName ||
localData.lastName !== user.lastName;
localData.firstName !== user.firstName || localData.lastName !== user.lastName;
const avatarChanged =
localData.deleteProfileImage === true ||
(localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`);
localData.deleteProfileImage === true ||
(localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`);
if (!nameChanged && !avatarChanged) {
createToast({
body: "Unable to update profile — no changes detected.",
});
setErrors({ unchanged: "unable to update profile" });
return;
createToast({
body: "Unable to update profile — no changes detected.",
});
setErrors({ unchanged: "unable to update profile" });
return;
}
const action = await dispatch(update({ localData }));
@@ -205,7 +204,7 @@ const ProfilePanel = () => {
>
{/* This 0.9 is a bit magic numbering, refactor */}
<Box flex={0.9}>
<Typography component="h1">{t('FirstName')}</Typography>
<Typography component="h1">{t("FirstName")}</Typography>
</Box>
<TextInput
id="edit-first-name"
@@ -223,7 +222,7 @@ const ProfilePanel = () => {
gap={SPACING_GAP}
>
<Box flex={0.9}>
<Typography component="h1">{t('LastName')}</Typography>
<Typography component="h1">{t("LastName")}</Typography>
</Box>
<TextInput
id="edit-last-name"
@@ -241,12 +240,12 @@ const ProfilePanel = () => {
gap={SPACING_GAP}
>
<Stack flex={0.9}>
<Typography component="h1">{t('email')}</Typography>
<Typography component="h1">{t("email")}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
>
{t('EmailDescriptionText')}
{t("EmailDescriptionText")}
</Typography>
</Stack>
<TextInput
@@ -264,12 +263,12 @@ const ProfilePanel = () => {
gap={SPACING_GAP}
>
<Stack flex={0.9}>
<Typography component="h1">{t('YourPhoto')}</Typography>
<Typography component="h1">{t("YourPhoto")}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
>
{t('PhotoDescriptionText')}
{t("PhotoDescriptionText")}
</Typography>
</Stack>
<Stack
@@ -293,14 +292,14 @@ const ProfilePanel = () => {
color="error"
onClick={handleDeletePicture}
>
{t('delete')}
{t("delete")}
</Button>
<Button
variant="contained" // CAIO_REVIEW
color="accent"
onClick={openPictureModal}
>
{t('update')}
{t("update")}
</Button>
</Stack>
</Stack>
@@ -325,7 +324,7 @@ const ProfilePanel = () => {
disabled={Object.keys(errors).length !== 0 && !errors?.picture && true}
sx={{ px: theme.spacing(12) }}
>
{t('save')}
{t("save")}
</Button>
</Box>
</Stack>
@@ -344,12 +343,12 @@ const ProfilePanel = () => {
spellCheck="false"
>
<Box mb={theme.spacing(6)}>
<Typography component="h1">{t('DeleteAccountTitle')}</Typography>
<Typography component="h1">{t("DeleteAccountTitle")}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
>
{t('DeleteDescriptionText')}
{t("DeleteDescriptionText")}
</Typography>
</Box>
<Button
@@ -357,17 +356,17 @@ const ProfilePanel = () => {
color="error"
onClick={() => setIsOpen("delete")}
>
{t('DeleteAccountButton')}
{t("DeleteAccountButton")}
</Button>
</Box>
)}
<Dialog
open={isModalOpen("delete")}
theme={theme}
title={t('DeleteWarningTitle')}
description={t('DeleteAccountWarning')}
title={t("DeleteWarningTitle")}
description={t("DeleteAccountWarning")}
onCancel={() => setIsOpen("")}
confirmationButtonLabel={t('DeleteAccountButton')}
confirmationButtonLabel={t("DeleteAccountButton")}
onConfirm={handleDeleteAccount}
isLoading={isLoading}
/>
@@ -389,13 +388,13 @@ const ProfilePanel = () => {
: user?.avatarImage
? `data:image/png;base64,${user.avatarImage}`
: ""
}
}
onChange={(newFile) => {
if (newFile) {
setFile(newFile);
clearError("unchanged");
}
}}
}}
previewIsRound
maxSize={3 * 1024 * 1024}
/>
@@ -410,7 +409,7 @@ const ProfilePanel = () => {
color="info"
onClick={removePicture}
>
{t('remove')}
{t("remove")}
</Button>
<Button
variant="contained"
@@ -418,7 +417,7 @@ const ProfilePanel = () => {
onClick={handleUpdatePicture}
disabled={!!errors.picture || !file?.src}
>
{t('update')}
{t("update")}
</Button>
</Stack>
</GenericDialog>

View File

@@ -48,7 +48,8 @@ const TeamPanel = () => {
{row.firstName + " " + row.lastName}
</Typography>
<Typography>
{t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()}
{t("teamPanel.table.created")}{" "}
{new Date(row.createdAt).toLocaleDateString()}
</Typography>
</Stack>
);

View File

@@ -1,10 +1,10 @@
.wallet-adapter-dropdown {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: var(--env-var-spacing-1);
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: var(--env-var-spacing-1);
}
.wallet-adapter-button {
@@ -15,37 +15,39 @@
height: var(--env-var-height-2) !important;
font-size: var(--env-var-font-size-medium-plus) !important;
font-weight: 500 !important;
margin: 0;
padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2) var(--env-var-spacing-1);
margin: 0;
padding: calc(
(var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2
)
var(--env-var-spacing-1);
white-space: nowrap;
}
.wallet-adapter-modal-title {
font-size: var(--font-size-h5) !important;
font-size: var(--font-size-h5) !important;
}
/* Separator styling */
.wallet-adapter-modal-divider {
background-color: var(--border-color) !important;
margin: var(--spacing-md) 0 !important;
background-color: var(--border-color) !important;
margin: var(--spacing-md) 0 !important;
}
/* Responsive fixes */
@media (max-width: 1200px) {
@media (max-width: 1200px) {
.wallet-adapter-button {
font-size: var(--env-var-font-size-medium) !important;
padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium) * 1.2) / 2)
var(--env-var-spacing-1-minus) !important;
}
}
@media (max-width: 900px) {
.wallet-adapter-modal-wrapper {
flex-direction: column !important;
}
.wallet-adapter-modal-divider {
margin: var(--spacing-sm) 0 !important;
}
}
@media (max-width: 900px) {
.wallet-adapter-modal-wrapper {
flex-direction: column !important;
}
.wallet-adapter-modal-divider {
margin: var(--spacing-sm) 0 !important;
}
}

View File

@@ -2,26 +2,26 @@ import { useState } from "react";
import { networkService } from "../main";
export const useBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => {
setIsLoading(true);
const createBulkMonitors = async (file, user) => {
setIsLoading(true);
const formData = new FormData();
formData.append("csvFile", file);
formData.append("userId", user._id);
formData.append("teamId", user.teamId);
const formData = new FormData();
formData.append("csvFile", file);
formData.append("userId", user._id);
formData.append("teamId", user.teamId);
try {
const response = await networkService.createBulkMonitors(formData);
return [true, response.data, null]; // [success, data, error]
} catch (err) {
const errorMessage = err?.response?.data?.msg || err.message;
return [false, null, errorMessage];
} finally {
setIsLoading(false);
}
};
try {
const response = await networkService.createBulkMonitors(formData);
return [true, response.data, null]; // [success, data, error]
} catch (err) {
const errorMessage = err?.response?.data?.msg || err.message;
return [false, null, errorMessage];
} finally {
setIsLoading(false);
}
};
return [createBulkMonitors, isLoading];
return [createBulkMonitors, isLoading];
};

View File

@@ -2,7 +2,6 @@ import { useCallback } from "react";
import { useTheme } from "@mui/material";
const useMonitorUtils = () => {
const getMonitorWithPercentage = useCallback((monitor, theme) => {
let uptimePercentage = "";
let percentageColor = "";

View File

@@ -20,18 +20,20 @@ const useSendTestEmail = () => {
setError(null);
// Send the test email with or without configuration
const response = await networkService.sendTestEmail({
const response = await networkService.sendTestEmail({
to: user.email,
emailConfig
emailConfig,
});
if (typeof response?.data?.data?.messageId !== "undefined") {
createToast({
body: t("settingsTestEmailSuccess", "Test email sent successfully"),
});
} else {
throw new Error(response?.data?.error || t("settingsTestEmailFailed", "Failed to send test email"));
throw new Error(
response?.data?.error ||
t("settingsTestEmailFailed", "Failed to send test email")
);
}
} catch (error) {
createToast({
@@ -39,10 +41,14 @@ const useSendTestEmail = () => {
});
setError(error);
createToast({
body: t("settingsTestEmailFailedWithReason", "Failed to send test email: {{reason}}", {
reason: error.message || t("settingsTestEmailUnknownError", "Unknown error")
}),
variant: "error"
body: t(
"settingsTestEmailFailedWithReason",
"Failed to send test email: {{reason}}",
{
reason: error.message || t("settingsTestEmailUnknownError", "Unknown error"),
}
),
variant: "error",
});
} finally {
setIsSending(false);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import AppBar from '../../Components/Common/AppBar';
import Footer from '../../Components/Common/Footer';
import React from "react";
import AppBar from "../../Components/Common/AppBar";
import Footer from "../../Components/Common/Footer";
import { useTranslation } from "react-i18next";
const AboutUs = () => {

View File

@@ -31,21 +31,19 @@ const Account = ({ open = "profile" }) => {
let tabList = [
{ name: t("menu.profile"), value: "profile" },
{ name: t("menu.password"), value: "password" },
{ name: t("menu.team"), value: "team" }
{ name: t("menu.team"), value: "team" },
];
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
if (hideTeams) {
tabList = [
{ name: t("menu.profile"), value: "profile" },
{ name: t("menu.password"), value: "password" }
{ name: t("menu.password"), value: "password" },
];
}
// Remove password for demo
if (user.role.includes("demo")) {
tabList = [
{ name: t("menu.profile"), value: "profile" }
];
tabList = [{ name: t("menu.profile"), value: "profile" }];
}
const handleKeyDown = (event) => {
@@ -70,7 +68,11 @@ const Account = ({ open = "profile" }) => {
py={theme.spacing(12)}
>
<TabContext value={tab}>
<CustomTabList value={tab} onChange={handleTabChange} aria-label="account tabs">
<CustomTabList
value={tab}
onChange={handleTabChange}
aria-label="account tabs"
>
{tabList.map((item, index) => (
<Tab
label={item.name}

View File

@@ -75,11 +75,11 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
width: "30%",
px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
'&.MuiButton-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,

View File

@@ -79,11 +79,11 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
sx={{
px: theme.spacing(5),
borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
'&.MuiButton-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
@@ -108,11 +108,11 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
width: "30%",
px: theme.spacing(4),
borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
'&.MuiButton-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,

View File

@@ -43,8 +43,6 @@ const Login = () => {
const [errors, setErrors] = useState({});
const [step, setStep] = useState(0);
useEffect(() => {
if (authToken) {
navigate("/uptime");
@@ -84,7 +82,9 @@ const Login = () => {
);
if (error) {
const errorMessage = error.details[0].message;
const translatedMessage = errorMessage.startsWith('auth') ? t(errorMessage) : errorMessage;
const translatedMessage = errorMessage.startsWith("auth")
? t(errorMessage)
: errorMessage;
setErrors((prev) => ({ ...prev, email: translatedMessage }));
createToast({ body: translatedMessage });
} else {
@@ -103,7 +103,9 @@ const Login = () => {
createToast({
body:
error.details && error.details.length > 0
? (error.details[0].message.startsWith('auth') ? t(error.details[0].message) : error.details[0].message)
? error.details[0].message.startsWith("auth")
? t(error.details[0].message)
: error.details[0].message
: t("Error validating data."),
});
} else {
@@ -227,9 +229,9 @@ const Login = () => {
email={form.email}
errorEmail={errors.email}
/>
{/* Registration link */}
<Box textAlign="center" >
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
@@ -241,11 +243,11 @@ const Login = () => {
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
sx={{
cursor: 'pointer',
'&:hover': {
color: theme.palette.accent.darker
}
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.accent.darker,
},
}}
onClick={() => navigate("/register")}
>

View File

@@ -100,9 +100,7 @@ const NewPasswordConfirmed = () => {
</IconBox>
</Stack>
<Typography component="h1">{t("passwordreset")}</Typography>
<Typography mb={theme.spacing(2)}>
{t("authNewPasswordConfirmed")}
</Typography>
<Typography mb={theme.spacing(2)}>{t("authNewPasswordConfirmed")}</Typography>
</Box>
<Button
variant="contained"

View File

@@ -105,7 +105,7 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
);
}}
/>
)
),
}}
/>
</Typography>

View File

@@ -68,11 +68,14 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email && (
errors.email.includes("required") ? t("authRegisterEmailRequired") :
errors.email.includes("valid email") ? t("authRegisterEmailInvalid") :
t(errors.email)
)}
helperText={
errors.email &&
(errors.email.includes("required")
? t("authRegisterEmailRequired")
: errors.email.includes("valid email")
? t("authRegisterEmailInvalid")
: t(errors.email))
}
ref={inputRef}
/>
<Stack

View File

@@ -199,9 +199,7 @@ const CreateDistributedUptime = () => {
<ConfigBox>
<Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
<Typography component="p">
{t("distributedUptimeCreateSelectURL")}
</Typography>
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
@@ -293,7 +291,9 @@ const CreateDistributedUptime = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
<Typography component="h2">
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
@@ -312,7 +312,9 @@ const CreateDistributedUptime = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
<Typography component="h2">
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Select

View File

@@ -17,7 +17,7 @@ const MESSAGES = [
const ChatBot = ({ sx }) => {
const theme = useTheme();
const { t } = useTranslation();
const { t } = useTranslation();
return (
<ColContainer
backgroundColor={theme.palette.chatbot.background}

View File

@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, monitorId }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
const { t } = useTranslation();
return (
<Stack

View File

@@ -76,7 +76,9 @@ const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
<Typography>{city}</Typography>
</td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography>{Math.floor(dataPoint.responseTime)} {t("ms")}</Typography>
<Typography>
{Math.floor(dataPoint.responseTime)} {t("ms")}
</Typography>
</td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography color={theme.palette.warning.main}>

View File

@@ -10,9 +10,7 @@ const MonitorHeader = ({ monitor }) => {
<Stack direction="row">
<Stack gap={theme.spacing(2)}>
<Typography variant="h1">{monitor.name}</Typography>
<Typography variant="h2">
{t("distributedUptimeDetailsMonitorHeader")}
</Typography>
<Typography variant="h2">{t("distributedUptimeDetailsMonitorHeader")}</Typography>
</Stack>
</Stack>
);

View File

@@ -2,7 +2,14 @@ import { useState, useEffect } from "react";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
const useChecksFetch = ({ selectedMonitor, selectedMonitorType, filter, dateRange, page, rowsPerPage }) => {
const useChecksFetch = ({
selectedMonitor,
selectedMonitorType,
filter,
dateRange,
page,
rowsPerPage,
}) => {
//Redux
const { user } = useSelector((state) => state.auth);

View File

@@ -30,7 +30,7 @@ const useMonitorsFetch = ({ teamId }) => {
acc[monitor._id] = {
_id: monitor._id,
name: monitor.name,
type: monitor.type
type: monitor.type,
};
return acc;
}, {});

View File

@@ -18,8 +18,10 @@ const Incidents = () => {
// Redux state
const { user } = useSelector((state) => state.auth);
const { t } = useTranslation();
const BREADCRUMBS = [{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }];
const BREADCRUMBS = [
{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" },
];
// Local state
const [selectedMonitor, setSelectedMonitor] = useState("0");

View File

@@ -6,7 +6,10 @@ import { useSelector, useDispatch } from "react-redux";
// Utility and Network
import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { createInfrastructureMonitor, updateInfrastructureMonitor } from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import {
createInfrastructureMonitor,
updateInfrastructureMonitor,
} from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { useHardwareMonitorsFetch } from "../Details/Hooks/useHardwareMonitorsFetch";
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
import { useTranslation } from "react-i18next";
@@ -55,7 +58,7 @@ const CreateInfrastructureMonitor = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { monitorId } = useParams();
const { t } = useTranslation();
const { t } = useTranslation();
// Determine if we are creating or editing
const isCreate = typeof monitorId === "undefined";
@@ -90,20 +93,26 @@ const CreateInfrastructureMonitor = () => {
setInfrastructureMonitor({
url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "",
notifications: monitor.notifications?.filter(n => typeof n === "object") || [],
notifications: monitor.notifications?.filter((n) => typeof n === "object") || [],
notify_email: (monitor.notifications?.length ?? 0) > 0,
interval: monitor.interval / MS_PER_MINUTE,
cpu: monitor.thresholds?.usage_cpu !== undefined,
usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "",
memory: monitor.thresholds?.usage_memory !== undefined,
usage_memory: monitor.thresholds?.usage_memory ? monitor.thresholds.usage_memory * 100 : "",
usage_memory: monitor.thresholds?.usage_memory
? monitor.thresholds.usage_memory * 100
: "",
disk: monitor.thresholds?.usage_disk !== undefined,
usage_disk: monitor.thresholds?.usage_disk ? monitor.thresholds.usage_disk * 100 : "",
usage_disk: monitor.thresholds?.usage_disk
? monitor.thresholds.usage_disk * 100
: "",
temperature: monitor.thresholds?.usage_temperature !== undefined,
usage_temperature: monitor.thresholds?.usage_temperature ? monitor.thresholds.usage_temperature * 100 : "",
usage_temperature: monitor.thresholds?.usage_temperature
? monitor.thresholds.usage_temperature * 100
: "",
secret: monitor.secret || "",
});
setHttps(monitor.url.startsWith("https"));
@@ -199,7 +208,9 @@ const CreateInfrastructureMonitor = () => {
: await dispatch(updateInfrastructureMonitor({ monitorId, monitor: form }));
if (action.meta.requestStatus === "fulfilled") {
createToast({
body: isCreate ? t("infrastructureMonitorCreated") : t("infrastructureMonitorUpdated"),
body: isCreate
? t("infrastructureMonitorCreated")
: t("infrastructureMonitorUpdated"),
});
navigate("/infrastructure");
} else {
@@ -236,22 +247,23 @@ const CreateInfrastructureMonitor = () => {
const handleNotifications = (event, type) => {
const { value, checked } = event.target;
let notifications = [...infrastructureMonitor.notifications];
if (checked) {
if (!notifications.some((n) => n.type === type && n.address === value)) {
notifications.push({ type, address: value });
}
} else {
notifications = notifications.filter((n) => !(n.type === type && n.address === value));
notifications = notifications.filter(
(n) => !(n.type === type && n.address === value)
);
}
setInfrastructureMonitor((prev) => ({
...prev,
notifications,
...(type === "email" ? { notify_email: checked } : {})
...(type === "email" ? { notify_email: checked } : {}),
}));
};
return (
<Box className="create-infrastructure-monitor">
@@ -260,8 +272,10 @@ const CreateInfrastructureMonitor = () => {
{ name: "Infrastructure monitors", path: "/infrastructure" },
...(isCreate
? [{ name: "Create", path: "/infrastructure/create" }]
: [{ name: "Details", path: `/infrastructure/${monitorId}` }, { name: "Configure", path: `/infrastructure/configure/${monitorId}` }]
),
: [
{ name: "Details", path: `/infrastructure/${monitorId}` },
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
]),
]}
/>
<Stack
@@ -281,7 +295,7 @@ const CreateInfrastructureMonitor = () => {
component="span"
fontSize="inherit"
>
{t(isCreate? "infrastructureCreateYour": "infrastructureEditYour")}{" "}
{t(isCreate ? "infrastructureCreateYour" : "infrastructureEditYour")}{" "}
</Typography>
<Typography
component="span"
@@ -294,7 +308,12 @@ const CreateInfrastructureMonitor = () => {
</Typography>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">
{t("infrastructureCreateGeneralSettingsDescription")}
</Typography>
@@ -323,25 +342,25 @@ const CreateInfrastructureMonitor = () => {
disabled={!isCreate}
/>
{isCreate && (
<Box>
<Typography component="p">{t("infrastructureProtocol")}</Typography>
<ButtonGroup>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
</Box>
<Box>
<Typography component="p">{t("infrastructureProtocol")}</Typography>
<ButtonGroup>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
</Box>
)}
<TextInput
type="text"
@@ -368,7 +387,12 @@ const CreateInfrastructureMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
@@ -385,7 +409,12 @@ const CreateInfrastructureMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("infrastructureCustomizeAlerts")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("infrastructureCustomizeAlerts")}
</Typography>
<Typography component="p">
{t("infrastructureAlertNotificationDescription")}
</Typography>
@@ -432,7 +461,12 @@ const CreateInfrastructureMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Select
@@ -455,7 +489,7 @@ const CreateInfrastructureMonitor = () => {
onClick={handleCreateInfrastructureMonitor}
loading={monitorState?.isLoading}
>
{t(isCreate? "infrastructureCreateMonitor": "infrastructureEditMonitor")}
{t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")}
</Button>
</Stack>
</Stack>

View File

@@ -16,8 +16,8 @@ const Gauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) =>
mb: theme.spacing(2),
mt: theme.spacing(2),
pr: theme.spacing(2),
textAlign: 'right'
}
textAlign: "right",
};
return (
<BaseContainer>
@@ -32,14 +32,19 @@ const Gauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) =>
flexDirection: "column",
alignItems: "center",
width: "100%",
backgroundColor: theme.palette.gradient.color1
backgroundColor: theme.palette.gradient.color1,
}}
>
<CustomGauge
progress={value}
radius={100}
/>
<Typography component="h2" sx={{ fontWeight: 600 }}>{heading}</Typography>
<Typography
component="h2"
sx={{ fontWeight: 600 }}
>
{heading}
</Typography>
</Box>
<Box
sx={{

View File

@@ -2,11 +2,10 @@ import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => {
// Abort early if creating monitor
if (!monitorId) {
return { monitor: undefined, isLoading: false, networkError: undefined };
}
if (!monitorId) {
return { monitor: undefined, isLoading: false, networkError: undefined };
}
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);

View File

@@ -53,10 +53,10 @@ const Filter = ({
return (
<Box
sx={{
m: theme.spacing(2),
ml: theme.spacing(4),
}}
sx={{
m: theme.spacing(2),
ml: theme.spacing(4),
}}
>
<FilterHeader
header={t("status")}
@@ -80,10 +80,10 @@ const Filter = ({
};
Filter.propTypes = {
selectedStatus: PropTypes.arrayOf(PropTypes.string),
setSelectedStatus: PropTypes.func.isRequired,
setToFilterStatus: PropTypes.func.isRequired,
handleReset: PropTypes.func.isRequired,
selectedStatus: PropTypes.arrayOf(PropTypes.string),
setSelectedStatus: PropTypes.func.isRequired,
setToFilterStatus: PropTypes.func.isRequired,
handleReset: PropTypes.func.isRequired,
};
export default Filter;

View File

@@ -73,7 +73,7 @@ const InfrastructureMonitors = () => {
);
}
if (!isLoading && typeof summary?.totalMonitors === "undefined" ) {
if (!isLoading && typeof summary?.totalMonitors === "undefined") {
return (
<Fallback
vowelStart={true}

View File

@@ -134,9 +134,7 @@ const Integrations = () => {
}}
>
<Typography component="h1">{t("integrations")}</Typography>
<Typography mb={theme.spacing(12)}>
{t("integrationsPrism")}
</Typography>
<Typography mb={theme.spacing(12)}>{t("integrationsPrism")}</Typography>
<Grid
container
spacing={theme.spacing(12)}

View File

@@ -50,7 +50,7 @@ const MaintenanceTable = ({
);
setPage(0);
};
const { t } = useTranslation();
const headers = [

View File

@@ -310,7 +310,12 @@ const PageSpeedConfigure = () => {
</Stack>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">
{t("pageSpeedConfigureSettingsDescription")}
</Typography>
@@ -349,7 +354,12 @@ const PageSpeedConfigure = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
@@ -394,9 +404,7 @@ const PageSpeedConfigure = () => {
value=""
onChange={() => logger.warn("disabled")}
/>
<Typography mt={theme.spacing(4)}>
{t("seperateEmails")}
</Typography>
<Typography mt={theme.spacing(4)}>{t("seperateEmails")}</Typography>
</Box>
) : (
""
@@ -405,7 +413,12 @@ const PageSpeedConfigure = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select

View File

@@ -216,10 +216,13 @@ const CreatePageSpeed = () => {
</Typography>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
<Typography component="p">
{t("distributedUptimeCreateSelectURL")}
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
@@ -251,7 +254,12 @@ const CreatePageSpeed = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("distributedUptimeCreateChecks")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateChecks")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateChecksDescription")}
</Typography>
@@ -300,7 +308,12 @@ const CreatePageSpeed = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
@@ -320,7 +333,12 @@ const CreatePageSpeed = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Select

View File

@@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
const PerformanceReport = ({ shouldRender, audits }) => {
const theme = useTheme();
const { t } = useTranslation();
if (!shouldRender) {
return <SkeletonLayout />;
}

View File

@@ -12,150 +12,158 @@ import ThemeSwitch from "../Components/ThemeSwitch";
import LanguageSelector from "../Components/LanguageSelector";
const ServerUnreachable = () => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
// State for tracking connection check status
const [isCheckingConnection, setIsCheckingConnection] = useState(false);
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
const handleRetry = React.useCallback(async () => {
setIsCheckingConnection(true);
try {
// Try to connect to the backend with a simple API call
// We'll use any lightweight endpoint that doesn't require authentication
await networkService.axiosInstance.get('/health', { timeout: 5000 });
// If successful, show toast and navigate to login page
createToast({
body: t("backendReconnected", "Connection to server restored"),
});
navigate("/login");
} catch (error) {
// If still unreachable, stay on this page and show toast
createToast({
body: t("backendStillUnreachable", "Server is still unreachable"),
});
} finally {
setIsCheckingConnection(false);
}
}, [navigate, t]);
// State for tracking connection check status
const [isCheckingConnection, setIsCheckingConnection] = useState(false);
return (
<Stack
className="login-page auth"
overflow="hidden"
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
{/* Header with logo */}
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
alignItems="center"
>
<LanguageSelector />
<ThemeSwitch />
</Stack>
</Stack>
<Stack
width="100%"
maxWidth={600}
flex={1}
justifyContent="center"
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
rowGap={theme.spacing(8)}
sx={{
"& > .MuiStack-root": {
border: 1,
borderRadius: theme.spacing(5),
borderColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: {
xs: theme.spacing(12),
sm: theme.spacing(20),
},
},
}}
>
<Stack spacing={theme.spacing(6)} alignItems="center">
<Box sx={{
width: theme.spacing(220),
mx: 'auto',
'& .alert.row-stack': {
width: '100%',
alignItems: 'center',
gap: theme.spacing(3)
}
}}>
<Alert
variant="error"
body={t("backendUnreachable", "Server Unreachable")}
hasIcon={true}
/>
</Box>
<Box mt={theme.spacing(2)}>
<Typography
variant="body1"
align="center"
color={theme.palette.primary.contrastTextSecondary}
>
{t("backendUnreachableMessage", "The Checkmate server is not responding. Please check your deployment configuration or try again later.")}
</Typography>
</Box>
<Box sx={{ mt: theme.spacing(4) }}>
<Button
variant="contained"
color="accent"
onClick={handleRetry}
disabled={isCheckingConnection}
className="dashboard-style-button"
sx={{
px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
},
'&.MuiButton-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
}
}}
>
{isCheckingConnection ?
t("retryingConnection", "Retrying Connection...") :
t("retryConnection", "Retry Connection")}
</Button>
</Box>
</Stack>
</Stack>
</Stack>
);
const handleRetry = React.useCallback(async () => {
setIsCheckingConnection(true);
try {
// Try to connect to the backend with a simple API call
// We'll use any lightweight endpoint that doesn't require authentication
await networkService.axiosInstance.get("/health", { timeout: 5000 });
// If successful, show toast and navigate to login page
createToast({
body: t("backendReconnected", "Connection to server restored"),
});
navigate("/login");
} catch (error) {
// If still unreachable, stay on this page and show toast
createToast({
body: t("backendStillUnreachable", "Server is still unreachable"),
});
} finally {
setIsCheckingConnection(false);
}
}, [navigate, t]);
return (
<Stack
className="login-page auth"
overflow="hidden"
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
{/* Header with logo */}
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
alignItems="center"
>
<LanguageSelector />
<ThemeSwitch />
</Stack>
</Stack>
<Stack
width="100%"
maxWidth={600}
flex={1}
justifyContent="center"
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
rowGap={theme.spacing(8)}
sx={{
"& > .MuiStack-root": {
border: 1,
borderRadius: theme.spacing(5),
borderColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: {
xs: theme.spacing(12),
sm: theme.spacing(20),
},
},
}}
>
<Stack
spacing={theme.spacing(6)}
alignItems="center"
>
<Box
sx={{
width: theme.spacing(220),
mx: "auto",
"& .alert.row-stack": {
width: "100%",
alignItems: "center",
gap: theme.spacing(3),
},
}}
>
<Alert
variant="error"
body={t("backendUnreachable", "Server Unreachable")}
hasIcon={true}
/>
</Box>
<Box mt={theme.spacing(2)}>
<Typography
variant="body1"
align="center"
color={theme.palette.primary.contrastTextSecondary}
>
{t(
"backendUnreachableMessage",
"The Checkmate server is not responding. Please check your deployment configuration or try again later."
)}
</Typography>
</Box>
<Box sx={{ mt: theme.spacing(4) }}>
<Button
variant="contained"
color="accent"
onClick={handleRetry}
disabled={isCheckingConnection}
className="dashboard-style-button"
sx={{
px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`,
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
}}
>
{isCheckingConnection
? t("retryingConnection", "Retrying Connection...")
: t("retryConnection", "Retry Connection")}
</Button>
</Box>
</Stack>
</Stack>
</Stack>
);
};
export default ServerUnreachable;

View File

@@ -12,7 +12,12 @@ const SettingsAbout = () => {
return (
<ConfigBox>
<Box>
<Typography component="h1" variant="h2">{t("settingsAbout")}</Typography>
<Typography
component="h1"
variant="h2"
>
{t("settingsAbout")}
</Typography>
</Box>
<Box>
<Typography component="h2">Checkmate {2.1}</Typography>

View File

@@ -23,7 +23,12 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
<>
<ConfigBox>
<Box>
<Typography component="h1" variant="h2">{t("settingsDemoMonitors")}</Typography>
<Typography
component="h1"
variant="h2"
>
{t("settingsDemoMonitors")}
</Typography>
<Typography sx={HEADER_SX}>{t("settingsDemoMonitorsDescription")}</Typography>
</Box>
<Box>
@@ -48,7 +53,12 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1" variant="h2">{t("settingsSystemReset")}</Typography>
<Typography
component="h1"
variant="h2"
>
{t("settingsSystemReset")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>

View File

@@ -52,18 +52,18 @@ const SettingsEmail = ({
systemEmailUser: settingsData?.settings?.systemEmailUser,
systemEmailAddress: settingsData?.settings?.systemEmailAddress,
systemEmailPassword: password || settingsData?.settings?.systemEmailPassword,
systemEmailConnectionHost: settingsData?.settings?.systemEmailConnectionHost
systemEmailConnectionHost: settingsData?.settings?.systemEmailConnectionHost,
};
// Basic validation
if (!emailConfig.systemEmailHost || !emailConfig.systemEmailPort) {
createToast({
body: t("settingsEmailRequiredFields", "Email host and port are required"),
variant: "error"
variant: "error",
});
return;
}
// Send test email with current form values
sendTestEmail(emailConfig);
};
@@ -75,7 +75,12 @@ const SettingsEmail = ({
return (
<ConfigBox>
<Box>
<Typography component="h1" variant="h2">{t("settingsEmail")}</Typography>
<Typography
component="h1"
variant="h2"
>
{t("settingsEmail")}
</Typography>
<Typography sx={HEADER_SX}>{t("settingsEmailDescription")}</Typography>
</Box>
<Box>

View File

@@ -41,7 +41,12 @@ const SettingsPagespeed = ({
return (
<ConfigBox>
<Box>
<Typography component="h1" variant="h2">{t("pageSpeedApiKeyFieldTitle")}</Typography>
<Typography
component="h1"
variant="h2"
>
{t("pageSpeedApiKeyFieldTitle")}
</Typography>
<Typography sx={HEADING_SX}>{t("pageSpeedApiKeyFieldDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>

View File

@@ -15,7 +15,12 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
return (
<ConfigBox>
<Box>
<Typography component="h1" variant="h2">{t("settingsGeneralSettings")}</Typography>
<Typography
component="h1"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography sx={HEADING_SX}>
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
{t("settingsDisplayTimezoneDescription")}

View File

@@ -16,7 +16,12 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => {
return (
<ConfigBox>
<Box>
<Typography component="h1" variant="h2">{t("settingsAppearance")}</Typography>
<Typography
component="h1"
variant="h2"
>
{t("settingsAppearance")}
</Typography>
<Typography sx={HEADING_SX}>{t("settingsAppearanceDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>

View File

@@ -9,7 +9,12 @@ const ConfigStack = ({ title, description, children }) => {
return (
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2" variant="h2">{title}</Typography>
<Typography
component="h2"
variant="h2"
>
{title}
</Typography>
<Typography component="p">{description}</Typography>
</Stack>
{children}

View File

@@ -34,10 +34,13 @@ const TabSettings = ({
<Stack gap={theme.spacing(10)}>
<ConfigBox>
<Stack>
<Typography component="h2" variant="h2">{t("access")}</Typography>
<Typography component="p">
{t("statusPageCreateSettings")}
<Typography
component="h2"
variant="h2"
>
{t("access")}
</Typography>
<Typography component="p">{t("statusPageCreateSettings")}</Typography>
</Stack>
<Stack gap={theme.spacing(18)}>
<Checkbox
@@ -51,7 +54,12 @@ const TabSettings = ({
</ConfigBox>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2" variant="h2">{t("basicInformation")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("basicInformation")}
</Typography>
<Typography component="p">
{t("statusPageCreateBasicInfoDescription")}
</Typography>
@@ -82,7 +90,12 @@ const TabSettings = ({
</ConfigBox>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2" variant="h2">{t("timezone")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("timezone")}
</Typography>
<Typography component="p">
{t("statusPageCreateSelectTimeZoneDescription")}
</Typography>
@@ -100,7 +113,12 @@ const TabSettings = ({
</ConfigBox>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2" variant="h2">{t("settingsAppearance")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("settingsAppearance")}
</Typography>
<Typography component="p">
{t("statusPageCreateAppearanceDescription")}
</Typography>

View File

@@ -23,7 +23,6 @@ const Tabs = ({
setTab,
TAB_LIST,
}) => {
const theme = useTheme();
return (
<TabContext value={TAB_LIST[tab]}>
@@ -63,7 +62,6 @@ const Tabs = ({
setSelectedMonitors={setSelectedMonitors}
/>
)}
</TabContext>
);
};

View File

@@ -194,7 +194,7 @@ const CreateStatusPage = () => {
color: statusPage?.color,
logo: newLogo,
showCharts: statusPage?.showCharts ?? true,
showUptimePercentage: statusPage?.showUptimePercentage ?? true
showUptimePercentage: statusPage?.showUptimePercentage ?? true,
};
});
setSelectedMonitors(statusPageMonitors);

View File

@@ -34,7 +34,7 @@ const PublicStatus = () => {
const [statusPage, monitors, isLoading, networkError, fetchStatusPage] =
useStatusPageFetch(false, url);
const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
// Breadcrumbs
const crumbs = [
{ name: t("statusBreadCrumbsStatusPages"), path: "/status" },

View File

@@ -4,7 +4,8 @@ import { Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
const UploadFile = ({ onFileSelect }) => {
// Changed prop to onFileSelect
const theme = useTheme();
const [file, setFile] = useState();
const [error, setError] = useState("");
@@ -18,17 +19,17 @@ const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
const handleFileChange = (e) => {
setError("");
const selectedFile = e.target.files[0];
// Basic file validation
if (!selectedFile) return;
if (!selectedFile.name.endsWith('.csv')) {
if (!selectedFile.name.endsWith(".csv")) {
setError(t("bulkImport.invalidFileType"));
return;
}
setFile(selectedFile);
onFileSelect(selectedFile); // Pass the file directly to parent
onFileSelect(selectedFile); // Pass the file directly to parent
};
return (
@@ -63,11 +64,10 @@ const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
</Button>
</div>
);
}
};
UploadFile.prototype = {
onFileSelect: PropTypes.func.isRequired,
};
export default UploadFile;
export default UploadFile;

View File

@@ -61,7 +61,12 @@ const BulkImport = () => {
</Typography>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("bulkImport.selectFileTips")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("bulkImport.selectFileTips")}
</Typography>
<Typography component="p">
<Trans
i18nKey="bulkImport.selectFileDescription"

View File

@@ -343,7 +343,12 @@ const Configure = () => {
</Stack>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateSelectURL")}
</Typography>
@@ -389,7 +394,10 @@ const Configure = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
@@ -457,7 +465,12 @@ const Configure = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("ignoreTLSError")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("ignoreTLSError")}
</Typography>
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
</Box>
<Stack>
@@ -477,7 +490,10 @@ const Configure = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>

View File

@@ -261,7 +261,12 @@ const CreateMonitor = () => {
</Typography>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("distributedUptimeCreateChecks")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateChecks")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateChecksDescription")}
</Typography>
@@ -342,7 +347,12 @@ const CreateMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">{t("uptimeCreateSelectURL")}</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
@@ -386,7 +396,10 @@ const CreateMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
@@ -417,7 +430,12 @@ const CreateMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">{t("ignoreTLSError")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("ignoreTLSError")}
</Typography>
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
</Box>
<Stack>
@@ -437,7 +455,10 @@ const CreateMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2" variant="h2">
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>

View File

@@ -150,7 +150,10 @@ const UptimeDataTable = ({
id: "responseTime",
content: t("responseTime"),
render: (row) => (
<Box display="flex" justifyContent="center">
<Box
display="flex"
justifyContent="center"
>
<BarChart checks={row.monitor.checks.slice().reverse()} />
</Box>
),

View File

@@ -1162,21 +1162,25 @@ class NetworkService {
async sendTestEmail(config) {
// Extract recipient and email configuration
const { to, emailConfig } = config;
// If emailConfig is provided, use the new endpoint with direct parameters
if (emailConfig) {
return this.axiosInstance.post(`/settings/test-email`, {
return this.axiosInstance.post(`/settings/test-email`, {
to,
systemEmailHost: emailConfig.systemEmailHost,
systemEmailPort: emailConfig.systemEmailPort,
systemEmailAddress: emailConfig.systemEmailAddress,
systemEmailPassword: emailConfig.systemEmailPassword,
// Only include these if they are present
...(emailConfig.systemEmailConnectionHost && { systemEmailConnectionHost: emailConfig.systemEmailConnectionHost }),
...(emailConfig.systemEmailUser && { systemEmailUser: emailConfig.systemEmailUser })
...(emailConfig.systemEmailConnectionHost && {
systemEmailConnectionHost: emailConfig.systemEmailConnectionHost,
}),
...(emailConfig.systemEmailUser && {
systemEmailUser: emailConfig.systemEmailUser,
}),
});
}
// Fallback to original behavior for backward compatibility
return this.axiosInstance.post(`/settings/test-email`, { to });
}

View File

@@ -167,7 +167,7 @@ const Greeting = ({ type = "" }) => {
fontSize="inherit"
color={theme.palette.primary.contrastTextTertiary}
>
{t("greeting.prepend", { defaultValue: prepend })}, {" "}
{t("greeting.prepend", { defaultValue: prepend })},{" "}
</Typography>
<Typography
component="span"
@@ -183,7 +183,8 @@ const Greeting = ({ type = "" }) => {
lineHeight={1}
color={theme.palette.primary.contrastTextTertiary}
>
{t("greeting.append", { defaultValue: append })} {t("greeting.overview", { type: t(`menu.${type}`) })}
{t("greeting.append", { defaultValue: append })} {" "}
{t("greeting.overview", { type: t(`menu.${type}`) })}
</Typography>
</Box>
);

View File

@@ -1,13 +1,12 @@
/**
* Update errors if passed id matches the error.details[0].path, otherwise remove
* the error for the id
* @param {*} prev Previous errors *
* @param {*} prev Previous errors *
* @param {*} id ID of the field whose error is to be either updated or removed
* @param {*} error the error object
* @param {*} error the error object
* @returns the Update Errors with the specific field with id being either removed or updated
*/
const buildErrors = (prev, id, error) => {
const updatedErrors = { ...prev };
if (error && id == error.details[0].path) {
@@ -43,7 +42,7 @@ const getTouchedFieldErrors = (validation, touchedErrors) => {
return newErrors;
};
/**
*
*
* @param {*} form The form object of the submitted form data
* @param {*} validation The Joi validation rules
* @param {*} setErrors The function used to set the local errors
@@ -69,7 +68,7 @@ const hasValidationErrors = (form, validation, setErrors) => {
"_id",
"__v",
"createdAt",
"updatedAt"
"updatedAt",
].includes(err.path[0])
) {
newErrors[err.path[0]] = err.message ?? "Validation error";

View File

@@ -52,8 +52,8 @@ html {
--env-var-default-1: 24px;
--env-var-default-2: 40px;
--env-var-shadow-1: 0px 4px 24px -4px rgba(16, 24, 40, 0.08),
0px 3px 3px -3px rgba(16, 24, 40, 0.03);
--env-var-shadow-1:
0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03);
}
.MuiInputBase-root.Mui-disabled input {

View File

@@ -549,7 +549,7 @@
"sendTestEmail": "Send test email",
"emailSent": "Email sent successfully",
"failedToSendEmail": "Failed to send email",
"statusMsg" : {
"statusMsg": {
"paused": "Monitoring is paused.",
"up": "Your site is up.",
"down": "Your site is down.",

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff