Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-integrations

This commit is contained in:
Skorpios
2025-02-09 12:15:19 -08:00
55 changed files with 1367 additions and 291 deletions

118
Client/package-lock.json generated
View File

@@ -16,12 +16,13 @@
"@mui/lab": "6.0.0-beta.26",
"@mui/material": "6.4.3",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.25.0",
"@mui/x-date-pickers": "7.25.0",
"@mui/x-data-grid": "7.26.0",
"@mui/x-date-pickers": "7.26.0",
"@reduxjs/toolkit": "2.5.1",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
"i18next": "^24.2.2",
"immutability-helper": "^3.1.1",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
@@ -31,6 +32,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-i18next": "^15.4.0",
"react-redux": "9.2.0",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
@@ -1500,15 +1502,15 @@
"license": "MIT"
},
"node_modules/@mui/x-charts": {
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.25.0.tgz",
"integrity": "sha512-+DhnojHrVTt8RsTgq8AztzdFpW1kzOgiBdo0Pkl0DyxVdaKELC5QaetFwim9nIxT2zmU/RmiBcOU+qbqmQpFNA==",
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.26.0.tgz",
"integrity": "sha512-fla1pbMuyHhbWeDo6j5Vz8FHULdPnqACqQrXeLXo9p5kuJpcT9m28DQ1E3YmQ6xGD9NuaxiZdOaITT9PA2zMFQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-charts-vendor": "7.20.0",
"@mui/x-internals": "7.25.0",
"@mui/x-internals": "7.26.0",
"@react-spring/rafz": "^9.7.5",
"@react-spring/web": "^9.7.5",
"clsx": "^2.1.1",
@@ -1558,17 +1560,18 @@
}
},
"node_modules/@mui/x-data-grid": {
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.25.0.tgz",
"integrity": "sha512-e9ZLbCgnDiADFiDyXo91ucZFHEMkKBNpwpkaTq5KohzefJfMpMQjTEbJeueSfBG2G1Q1Am2TPeBqrNeReIA7RQ==",
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.26.0.tgz",
"integrity": "sha512-9RNQeT2OL6jBOCE0MSUH11ol3fV5Zs9MkGxUIAGXcy/Fui0rZRNFO1yLmWDZU5yvskiNmUZJHWV/qXh++ZFarA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.25.0",
"@mui/x-internals": "7.26.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1"
"reselect": "^5.1.1",
"use-sync-external-store": "^1.0.0"
},
"engines": {
"node": ">=14.0.0"
@@ -1595,14 +1598,14 @@
}
},
"node_modules/@mui/x-date-pickers": {
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.25.0.tgz",
"integrity": "sha512-t62OSFAKwj7KYQ8KcwTuKj6OgDuLQPSe4QUJcKDzD9rEhRIJVRUw2x27gBSdcls4l0PTrba19TghvDxCZprriw==",
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.26.0.tgz",
"integrity": "sha512-bhSDce1b5MBYYlCdHQJBThe10LGTE3D/u53TDQ41+IRj+iiNCun8jivw3DxKhmoBxlB+hVdkcltpTtIGlPjQZQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.25.0",
"@mui/x-internals": "7.26.0",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1661,9 +1664,9 @@
}
},
"node_modules/@mui/x-internals": {
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.25.0.tgz",
"integrity": "sha512-tBUN54YznAkmtCIRAOl35Kgl0MjFDIjUbzIrbWRgVSIR3QJ8bXnVSkiRBi+P91SZEl9+ZW0rDj+osq7xFJV0kg==",
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz",
"integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -3823,9 +3826,9 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
"version": "0.4.18",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.18.tgz",
"integrity": "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw==",
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz",
"integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -4493,6 +4496,44 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "24.2.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz",
"integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.23.2"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -5740,9 +5781,9 @@
}
},
"node_modules/prettier": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -5888,6 +5929,27 @@
"react": "^18.3.1"
}
},
"node_modules/react-i18next": {
"version": "15.4.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz",
"integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==",
"dependencies": {
"@babel/runtime": "^7.25.0",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -6936,6 +6998,14 @@
"vite": ">=2.6.0"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",

View File

@@ -19,12 +19,13 @@
"@mui/lab": "6.0.0-beta.26",
"@mui/material": "6.4.3",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.25.0",
"@mui/x-date-pickers": "7.25.0",
"@mui/x-data-grid": "7.26.0",
"@mui/x-date-pickers": "7.26.0",
"@reduxjs/toolkit": "2.5.1",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
"i18next": "^24.2.2",
"immutability-helper": "^3.1.1",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
@@ -34,6 +35,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-i18next": "^15.4.0",
"react-redux": "9.2.0",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",

View File

@@ -73,9 +73,12 @@ const CustomToolTip = ({ active, payload, label, dateRange }) => {
component="span"
sx={{ opacity: 0.8 }}
>
Response Time
</Typography>{" "}
<Typography component="span">
Response time:
</Typography>
<Typography
ml={theme.spacing(4)}
component="span"
>
{Math.floor(responseTime)}
<Typography
component="span"

View File

@@ -11,9 +11,12 @@ const Image = ({
alt,
width = "auto",
height = "auto",
minWidth = "auto",
minHeight = "auto",
maxWidth = "auto",
maxHeight = "auto",
base64,
placeholder,
sx,
}) => {
if (shouldRender === false) {
@@ -28,11 +31,21 @@ const Image = ({
src = `data:image/png;base64,${base64}`;
}
if (
typeof src === "undefined" &&
typeof base64 === "undefined" &&
typeof placeholder !== "undefined"
) {
src = placeholder;
}
return (
<Box
component="img"
src={src}
alt={alt}
minWidth={minWidth}
minHeight={minHeight}
maxWidth={maxWidth}
maxHeight={maxHeight}
width={width}
@@ -48,6 +61,8 @@ Image.propTypes = {
alt: PropTypes.string.isRequired,
width: PropTypes.string,
height: PropTypes.string,
minWidth: PropTypes.string,
minHeight: PropTypes.string,
maxWidth: PropTypes.string,
maxHeight: PropTypes.string,
base64: PropTypes.string,

View File

@@ -106,7 +106,7 @@ const ImageField = ({ id, src, loading, onChange, error, isRound = true, maxSize
<Typography
component="span"
fontSize="inherit"
color={theme.palette.primary.main}
color="info"
fontWeight={500}
>
Click to upload

View File

@@ -0,0 +1,138 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Box, MenuItem, Select, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
import "flag-icons/css/flag-icons.min.css";
const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
const [language, setLanguage] = useState(i18n.language || "gb");
const handleChange = (event) => {
const newLang = event.target.value;
setLanguage(newLang);
i18n.changeLanguage(newLang);
};
// i18n instance'ından mevcut dilleri al
const languages = Object.keys(i18n.options.resources || {});
return (
<Select
value={language}
onChange={handleChange}
size="small"
sx={{
height: 28,
width: 64,
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
borderRadius: theme.shape.borderRadius,
fontSize: 10,
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.lowContrast,
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.lowContrast,
},
"& .MuiSvgIcon-root": {
color: theme.palette.primary.contrastText,
width: 16,
height: 16,
right: 4,
top: "calc(50% - 8px)",
},
"& .MuiSelect-select": {
padding: "2px 20px 2px 8px",
display: "flex",
alignItems: "center",
fontSize: 10,
},
}}
MenuProps={{
PaperProps: {
sx: {
backgroundColor: theme.palette.primary.main,
borderRadius: theme.shape.borderRadius,
marginTop: 1,
width: 64,
"& .MuiMenuItem-root": {
padding: "2px 8px",
minHeight: 28,
fontSize: 10,
},
},
},
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
}}
>
{languages.map((lang) => {
const flag = lang ? `fi fi-${lang}` : null;
return (
<MenuItem
key={lang}
value={lang}
sx={{
color: theme.palette.primary.contrastText,
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
},
"&.Mui-selected": {
backgroundColor: theme.palette.primary.lowContrast,
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
},
},
}}
>
<Stack
direction="row"
spacing={2}
alignItems="center"
ml={0.5}
>
<Box
component="span"
sx={{
width: 16,
height: 12,
display: "flex",
alignItems: "center",
"& img": {
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: 0.5,
},
}}
>
{flag && <span className={flag} />}
</Box>
<Box
component="span"
sx={{ textTransform: "uppercase", fontSize: 10 }}
>
{lang}
</Box>
</Stack>
</MenuItem>
);
})}
</Select>
);
};
export default LanguageSelector;

View File

@@ -3,7 +3,12 @@ import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import SkeletonLayout from "./skeleton";
const CreateMonitorHeader = ({ isAdmin, shouldRender = true, path }) => {
const CreateMonitorHeader = ({
isAdmin,
label = "Create new",
shouldRender = true,
path,
}) => {
const navigate = useNavigate();
if (!isAdmin) return null;
if (!shouldRender) return <SkeletonLayout />;
@@ -18,7 +23,7 @@ const CreateMonitorHeader = ({ isAdmin, shouldRender = true, path }) => {
color="accent"
onClick={() => navigate(path)}
>
Create new
{label}
</Button>
</Stack>
);
@@ -30,4 +35,5 @@ CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
shouldRender: PropTypes.bool,
path: PropTypes.string.isRequired,
label: PropTypes.string,
};

View File

@@ -22,10 +22,7 @@ const useMonitorUtils = () => {
}
return {
id: monitor._id,
name: monitor.name,
url: monitor.url,
title: monitor.name,
...monitor,
percentage: uptimePercentage,
percentageColor,
monitor: monitor,

View File

@@ -3,6 +3,7 @@ import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import TextInput from "../../../../Components/Inputs/TextInput";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
/**
* Renders the email step of the login process which includes an email field.
@@ -18,6 +19,7 @@ import PropTypes from "prop-types";
const EmailStep = ({ form, errors, onSubmit, onChange }) => {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
if (inputRef.current) {
@@ -33,8 +35,8 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
position="relative"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your email address</Typography>
<Typography component="h1">{t("authLoginTitle")}</Typography>
<Typography>{t("authLoginEnterEmail")}</Typography>
</Box>
<Box
textAlign="left"
@@ -48,7 +50,7 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
<TextInput
type="email"
id="login-email-input"
label="Email"
label={t("email")}
isRequired={true}
placeholder="jordan.ellis@domain.com"
autoComplete="email"
@@ -77,7 +79,7 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
},
}}
>
Continue
{t("continue")}
</Button>
</Stack>
</Box>

View File

@@ -1,10 +1,12 @@
import { Box, Typography, useTheme } from "@mui/material";
import PropTypes from "prop-types";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
const ForgotPasswordLabel = ({ email, errorEmail }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
const handleNavigate = () => {
if (email !== "" && !errorEmail) {
@@ -20,7 +22,7 @@ const ForgotPasswordLabel = ({ email, errorEmail }) => {
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
{t("authForgotPasswordTitle")}
</Typography>
<Typography
component="span"
@@ -29,15 +31,15 @@ const ForgotPasswordLabel = ({ email, errorEmail }) => {
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
{t("authForgotPasswordResetPassword")}
</Typography>
</Box>
);
};
ForgotPasswordLabel.proptype = {
ForgotPasswordLabel.propTypes = {
email: PropTypes.string.isRequired,
emailError: PropTypes.string.isRequired,
errorEmail: PropTypes.string.isRequired,
};
export default ForgotPasswordLabel;

View File

@@ -7,7 +7,7 @@ import TextInput from "../../../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
/**
* Renders the password step of the login process, including a password input field.
*
@@ -23,6 +23,7 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
const theme = useTheme();
const inputRef = useRef(null);
const authState = useSelector((state) => state.auth);
const { t } = useTranslation();
useEffect(() => {
if (inputRef.current) {
@@ -38,8 +39,8 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your password</Typography>
<Typography component="h1">{t("authLoginTitle")}</Typography>
<Typography>{t("authLoginEnterPassword")}</Typography>
</Box>
<Box
component="form"
@@ -56,7 +57,7 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
<TextInput
type="password"
id="login-password-input"
label="Password"
label={t("password")}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
@@ -87,7 +88,7 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
}}
>
<ArrowBackRoundedIcon />
Back
{t("commonBack")}{" "}
</Button>
<LoadingButton
variant="contained"
@@ -104,7 +105,7 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
},
}}
>
Continue
{t("continue")}
</LoadingButton>
</Stack>
</Box>

View File

@@ -15,6 +15,7 @@ import EmailStep from "./Components/EmailStep";
import PasswordStep from "./Components/PasswordStep";
import ThemeSwitch from "../../../Components/ThemeSwitch";
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
import LanguageSelector from "../../../Components/LanguageSelector";
const DEMO = import.meta.env.VITE_APP_DEMO;
@@ -163,11 +164,26 @@ const Login = () => {
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<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%"
@@ -213,9 +229,6 @@ const Login = () => {
email={form.email}
errorEmail={errors.email}
/>
<Box marginX={"auto"}>
<ThemeSwitch />
</Box>
</Stack>
</Stack>
);

View File

@@ -15,7 +15,7 @@ import Background from "../../../assets/Images/background-grid.svg?react";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import Mail from "../../../assets/icons/mail.svg?react";
import "../index.css";
import { useTranslation } from "react-i18next";
/**
* Displays the initial landing page.
*
@@ -26,7 +26,7 @@ import "../index.css";
*/
const LandingPage = ({ isSuperAdmin, onSignup }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<>
<Stack
@@ -37,7 +37,9 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
<Box>
<Typography component="h1">Sign Up</Typography>
<Typography>
Create your {isSuperAdmin ? "Super admin " : ""}account to get started.
{isSuperAdmin
? t("authRegisterCreateSuperAdminAccount")
: t("authRegisterCreateAccount")}
</Typography>
</Box>
<Box width="100%">
@@ -60,12 +62,12 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
}}
>
<Mail />
Sign up with Email
{t("authRegisterSignUpWithEmail")}
</Button>
</Box>
<Box maxWidth={400}>
<Typography className="tos-p">
By signing up, you agree to our{" "}
{t("authRegisterBySigningUp")}
<Typography
component="span"
onClick={() => {
@@ -118,6 +120,7 @@ const Register = ({ isSuperAdmin }) => {
const navigate = useNavigate();
const { token } = useParams();
const theme = useTheme();
const { t } = useTranslation();
// TODO If possible, change the IDs of these fields to match the backend
const idMap = {
"register-firstname-input": "firstName",
@@ -307,7 +310,7 @@ const Register = ({ isSuperAdmin }) => {
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<Typography sx={{ userSelect: "none" }}>{t("commonAppName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -367,7 +370,9 @@ const Register = ({ isSuperAdmin }) => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">Already have an account? </Typography>
<Typography display="inline-block">
{t("authRegisterAlreadyHaveAccount")} -
</Typography>
<Typography
component="span"
ml={theme.spacing(2)}
@@ -376,7 +381,7 @@ const Register = ({ isSuperAdmin }) => {
}}
sx={{ userSelect: "none", color: theme.palette.primary.main }}
>
Log In
{t("authLoginTitle")}
</Typography>
</Box>
</Stack>

View File

@@ -6,7 +6,7 @@ import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import TextInput from "../../../../Components/Inputs/TextInput";
import Check from "../../../../Components/Check/Check";
import { useValidatePassword } from "../../hooks/useValidatePassword";
import { useTranslation } from "react-i18next";
StepThree.propTypes = {
onSubmit: PropTypes.func,
onBack: PropTypes.func,
@@ -23,6 +23,7 @@ StepThree.propTypes = {
function StepThree({ onSubmit, onBack }) {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
if (inputRef.current) {
@@ -38,8 +39,8 @@ function StepThree({ onSubmit, onBack }) {
textAlign="center"
>
<Box>
<Typography component="h1">Sign Up</Typography>
<Typography>Create your password</Typography>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>{t("createPassword")}</Typography>
</Box>
<Box
component="form"
@@ -63,9 +64,9 @@ function StepThree({ onSubmit, onBack }) {
type="password"
id="register-password-input"
name="password"
label="Password"
label={t("commonPassword")}
isRequired={true}
placeholder="Create a password"
placeholder={t("createAPassword")}
autoComplete="current-password"
value={form.password}
onChange={handleChange}
@@ -76,9 +77,9 @@ function StepThree({ onSubmit, onBack }) {
type="password"
id="register-confirm-input"
name="confirm"
label="Confirm password"
label={t("authSetNewPasswordConfirmPassword")}
isRequired={true}
placeholder="Confirm your password"
placeholder={t("confirmPassword")}
autoComplete="current-password"
value={form.confirm}
onChange={handleChange}
@@ -90,33 +91,33 @@ function StepThree({ onSubmit, onBack }) {
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
noHighlightText={"Must be at least"}
text={"8 characters long"}
noHighlightText={t("authPasswordMustBeAtLeast")}
text={t("authPasswordCharactersLong")}
variant={feedbacks.length}
/>
<Check
noHighlightText={"Must contain at least"}
text={"one special character"}
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordSpecialCharacter")}
variant={feedbacks.special}
/>
<Check
noHighlightText={"Must contain at least"}
text={"one number"}
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordOneNumber")}
variant={feedbacks.number}
/>
<Check
noHighlightText={"Must contain at least"}
text={"one upper character"}
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordUpperCharacter")}
variant={feedbacks.uppercase}
/>
<Check
noHighlightText={"Must contain at least"}
text={"one lower character"}
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordLowerCharacter")}
variant={feedbacks.lowercase}
/>
<Check
noHighlightText={"Confirm password and password"}
text={"must match"}
noHighlightText={t("authPasswordConfirmAndPassword")}
text={t("authPasswordMustMatch")}
variant={feedbacks.confirm}
/>
</Stack>
@@ -140,7 +141,7 @@ function StepThree({ onSubmit, onBack }) {
}}
>
<ArrowBackRoundedIcon />
Back
{t("commonBack")}
</Button>
<Button
type="submit"
@@ -160,7 +161,7 @@ function StepThree({ onSubmit, onBack }) {
},
}}
>
Continue
{t("continue")}
</Button>
</Stack>
</Box>

View File

@@ -4,6 +4,7 @@ import { useTheme } from "@emotion/react";
import { Box, Button, Stack, Typography } from "@mui/material";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import TextInput from "../../../../Components/Inputs/TextInput";
import { useTranslation } from "react-i18next";
StepTwo.propTypes = {
form: PropTypes.object,
@@ -27,6 +28,7 @@ StepTwo.propTypes = {
function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
if (inputRef.current) {
@@ -41,8 +43,8 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
textAlign="center"
>
<Box>
<Typography component="h1">Sign Up</Typography>
<Typography>Enter your email address</Typography>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>{t("enterEmail")}</Typography>
</Box>
<Box
@@ -58,7 +60,7 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
<TextInput
type="email"
id="register-email-input"
label="Email"
label={t("commonEmail")}
isRequired={true}
placeholder="jordan.ellis@domain.com"
autoComplete="email"
@@ -89,7 +91,7 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
}}
>
<ArrowBackRoundedIcon />
Back
{t("commonBack")}
</Button>
<Button
variant="contained"
@@ -105,7 +107,7 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
},
}}
>
Continue
{t("continue")}
</Button>
</Stack>
</Box>

View File

@@ -17,11 +17,13 @@ import Logo from "../../assets/icons/checkmate-icon.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import "./index.css";
import { useValidatePassword } from "./hooks/useValidatePassword";
import { useTranslation } from "react-i18next";
const SetNewPassword = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const theme = useTheme();
const { t } = useTranslation();
const passwordId = useId();
const confirmPasswordId = useId();
@@ -97,7 +99,7 @@ const SetNewPassword = () => {
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<Typography sx={{ userSelect: "none" }}>{t("commonAppName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -142,10 +144,8 @@ const SetNewPassword = () => {
<LockIcon alt="lock icon" />
</IconBox>
</Stack>
<Typography component="h1">Set new password</Typography>
<Typography>
Your new password must be different to previously used passwords.
</Typography>
<Typography component="h1">{t("authSetNewPasswordTitle")}</Typography>
<Typography>{t("authSetNewPasswordDescription")}</Typography>
</Box>
<Box
width="100%"
@@ -166,7 +166,7 @@ const SetNewPassword = () => {
id={passwordId}
type="password"
name="password"
label="Password"
label={t("commonPassword")}
isRequired={true}
placeholder="••••••••"
value={form.password}
@@ -186,7 +186,7 @@ const SetNewPassword = () => {
id={confirmPasswordId}
type="password"
name="confirm"
label="Confirm password"
label={t("authSetNewPasswordConfirmPassword")}
isRequired={true}
placeholder="••••••••"
value={form.confirm}
@@ -201,33 +201,33 @@ const SetNewPassword = () => {
mb={theme.spacing(12)}
>
<Check
noHighlightText={"Must be at least"}
text={"8 characters long"}
noHighlightText={t("authPasswordMustBeAtLeast")}
text={t("authPasswordCharactersLong")}
variant={feedbacks.length}
/>
<Check
noHighlightText={"Must contain at least"}
text={"one special character"}
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordSpecialCharacter")}
variant={feedbacks.special}
/>
<Check
noHighlightText={"Must contain at least"}
text={"one number"}
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordOneNumber")}
variant={feedbacks.number}
/>
<Check
noHighlightText={"Must contain at least"}
text={"one upper character"}
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordUpperCharacter")}
variant={feedbacks.uppercase}
/>
<Check
noHighlightText={"Must contain at least"}
text={"one lower character"}
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordLowerCharacter")}
variant={feedbacks.lowercase}
/>
<Check
noHighlightText={"Confirm password and password"}
text={"must match"}
noHighlightText={t("authPasswordConfirmAndPassword")}
text={t("authPasswordMustMatch")}
variant={feedbacks.confirm}
/>
</Stack>
@@ -244,7 +244,7 @@ const SetNewPassword = () => {
}
sx={{ width: "100%", maxWidth: 400 }}
>
Reset password
{t("authSetNewPasswordResetPassword")}
</LoadingButton>
</Stack>
</Stack>
@@ -252,7 +252,7 @@ const SetNewPassword = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">Go back to </Typography>
<Typography display="inline-block">{t("goBackTo")} </Typography>
<Typography
component="span"
color={theme.palette.primary.main}
@@ -260,7 +260,7 @@ const SetNewPassword = () => {
onClick={() => navigate("/login")}
sx={{ userSelect: "none" }}
>
Log In
{t("authLoginTitle")}
</Typography>
</Box>
</Stack>

View File

@@ -0,0 +1,22 @@
const VisuallyHiddenInput = ({ onChange }) => {
return (
<input
type="file"
accept="image/*"
onChange={onChange}
style={{
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
}}
/>
);
};
export default VisuallyHiddenInput;

View File

@@ -0,0 +1,217 @@
// Components
import { Stack, Typography, Button, Box } from "@mui/material";
import ConfigBox from "../../../Components/ConfigBox";
import Checkbox from "../../../Components/Inputs/Checkbox";
import TextInput from "../../../Components/Inputs/TextInput";
import VisuallyHiddenInput from "./Components/VisuallyHiddenInput";
import Image from "../../../Components/Image";
import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg";
import Breadcrumbs from "../../../Components/Breadcrumbs";
// Utils
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage";
import { useLocation } from "react-router-dom";
import { statusPageValidation } from "../../../Validation/validation";
import { buildErrors } from "../../../Validation/error";
import { createToast } from "../../../Utils/toastUtils";
import { useNavigate } from "react-router-dom";
const CreateStatus = () => {
const theme = useTheme();
const { monitorId } = useParams();
const navigate = useNavigate();
const location = useLocation();
const isCreate = location.pathname.startsWith("/distributed-uptime/status/create");
const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate);
const BREADCRUMBS = [
{ name: "distributed uptime", path: "/distributed-uptime" },
{ name: "details", path: `/distributed-uptime/${monitorId}` },
{ name: "create status page", path: `` },
];
// Local state
const [form, setForm] = useState({
isPublished: false,
url: Math.floor(Math.random() * 1000000).toFixed(0),
logo: undefined,
companyName: "",
monitors: [monitorId],
});
const [errors, setErrors] = useState({});
const handleFormChange = (e) => {
const { name, value, checked, type } = e.target;
// Check for errors
const { error } = statusPageValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => buildErrors(prev, name, error));
if (type === "checkbox") {
setForm({ ...form, [name]: checked });
return;
}
setForm({ ...form, [name]: value });
};
const handleImageUpload = (e) => {
const img = e.target?.files?.[0];
setForm((prev) => ({
...prev,
logo: img,
}));
};
const handleSubmit = async () => {
let logoToSubmit = undefined;
// Handle image
if (typeof form.logo !== "undefined") {
logoToSubmit = {
src: URL.createObjectURL(form.logo),
name: form.logo.name,
type: form.logo.type,
size: form.logo.size,
};
}
const formToSubmit = { ...form };
if (typeof logoToSubmit !== "undefined") {
formToSubmit.logo = logoToSubmit;
}
// Validate
const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false });
if (typeof error === "undefined") {
const success = await createStatusPage({ form: formToSubmit });
if (success) {
createToast({ body: "Status page created successfully" });
navigate(`/distributed-uptime/status/${form.url}`);
}
return;
}
const newErrors = {};
error?.details?.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors((prev) => ({ ...prev, ...newErrors }));
};
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h1">
<Typography
component="span"
fontSize="inherit"
>
Create your{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
status page
</Typography>
</Typography>
<ConfigBox>
<Stack>
<Typography component="h2">Access</Typography>
<Typography component="p">
If your status page is ready, you can mark it as published.
</Typography>
</Stack>
<Stack gap={theme.spacing(18)}>
<Checkbox
id="publish"
name="isPublished"
label={`Published and visible to the public`}
isChecked={form.isPublished}
onChange={handleFormChange}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">Basic Information</Typography>
<Typography component="p">
Define company name and the subdomain that your status page points to.
</Typography>
</Stack>
<Stack gap={theme.spacing(18)}>
<TextInput
id="companyName"
name="companyName"
type="text"
label="Company name"
placeholder="Company name"
value={form.companyName}
onChange={handleFormChange}
helperText={errors["companyName"]}
error={errors["companyName"] ? true : false}
/>
<TextInput
id="url"
name="url"
type="url"
label="Your status page address"
value={form.url}
onChange={handleFormChange}
helperText={errors["url"]}
error={errors["url"] ? true : false}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">Logo</Typography>
<Typography component="p">Upload a logo for your status page </Typography>
</Stack>
<Stack
gap={theme.spacing(18)}
alignItems="center"
>
<Image
src={form.logo ? URL.createObjectURL(form.logo) : undefined}
alt="Logo"
minWidth={"300px"}
minHeight={"100px"}
maxWidth={"300px"}
maxHeight={"300px"}
placeholder={LogoPlaceholder}
/>
<Box>
<Button
component="label"
role={undefined}
variant="contained"
color="accent"
tabIndex={-1}
>
Upload logo
<VisuallyHiddenInput onChange={handleImageUpload} />
</Button>
</Box>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={handleSubmit}
>
Save
</Button>
</Stack>
</Stack>
);
};
export default CreateStatus;

View File

@@ -1,7 +1,7 @@
import { Stack, Typography, List, ListItem } from "@mui/material";
import { useTheme } from "@emotion/react";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import "/node_modules/flag-icons/css/flag-icons.min.css";
import "flag-icons/css/flag-icons.min.css";
const BASE_BOX_PADDING_VERTICAL = 16;
const BASE_BOX_PADDING_HORIZONTAL = 8;

View File

@@ -6,7 +6,7 @@ import maplibregl from "maplibre-gl";
import { useSelector } from "react-redux";
import buildStyle from "./buildStyle";
const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => {
const DistributedUptimeMap = ({ width = "100%", checks }) => {
const mapContainer = useRef(null);
const map = useRef(null);
const theme = useTheme();
@@ -87,7 +87,6 @@ const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => {
ref={mapContainer}
style={{
width: width,
height: height,
}}
/>
);

View File

@@ -24,73 +24,70 @@ const CustomToolTip = ({ active, payload, label }) => {
? payload[0]?.payload?.originalAvgResponseTime
: (payload[0]?.payload?.avgResponseTime ?? 0);
return (
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
sx={{ padding: 0 }}
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Box
className="area-tooltip"
<Typography
sx={{
backgroundColor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
<Typography
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
</Typography>
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={theme.palette.primary.main}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
</Typography>
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={theme.palette.primary.main}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response time
</Typography>
<Typography
ml={theme.spacing(4)}
component="span"
>
{Math.floor(responseTime)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response Time
</Typography>{" "}
<Typography component="span">
{Math.floor(responseTime)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
{" "}
ms
</Typography>
</Stack>
</Box>
{/* Display original value */}
</Typography>
</Stack>
</Box>
</ChartBox>
{/* Display original value */}
</Box>
);
}
return null;
@@ -143,75 +140,81 @@ const DistributedUptimeResponseChart = ({ checks }) => {
if (checks.length === 0) return null;
return (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
sx={{ padding: 0 }}
>
<AreaChart
<ResponsiveContainer
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
minWidth={25}
height={220}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.accent}
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
/>
</AreaChart>
</ResponsiveContainer>
<AreaChart
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.accent}
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
/>
</AreaChart>
</ResponsiveContainer>
</ChartBox>
);
};

View File

@@ -0,0 +1,14 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -45,7 +45,7 @@ const getRandomDevice = () => {
model: randomModel,
};
};
const useSubscribeToDetails = ({ monitorId, dateRange }) => {
const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) => {
const [isLoading, setIsLoading] = useState(true);
const [connectionStatus, setConnectionStatus] = useState(undefined);
const [retryCount, setRetryCount] = useState(0);
@@ -58,6 +58,14 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
const prevDateRangeRef = useRef(dateRange);
useEffect(() => {
if (typeof monitorId === "undefined") {
return;
}
// If this page is public and not published, don't subscribe to details
if (isPublic && isPublished === false) {
return;
}
try {
const cleanup = networkService.subscribeToDistributedUptimeDetails({
authToken,
@@ -65,6 +73,9 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
dateRange: dateRange,
normalize: true,
onUpdate: (data) => {
if (isLoading === true) {
setIsLoading(false);
}
if (networkError === true) {
setNetworkError(false);
}
@@ -84,6 +95,7 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
setRetryCount(0); // Reset retry count on successful connection
},
onError: () => {
setIsLoading(false);
setNetworkError(true);
setConnectionStatus("down");
},
@@ -91,8 +103,6 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
return cleanup;
} catch (error) {
setNetworkError(true);
} finally {
setIsLoading(false);
}
}, [
authToken,
@@ -102,6 +112,7 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
setConnectionStatus,
networkError,
devices,
isLoading,
]);
useEffect(() => {
@@ -116,4 +127,4 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger];
};
export default useSubscribeToDetails;
export { useSubscribeToDetails };

View File

@@ -10,21 +10,23 @@ import StatBoxes from "./Components/StatBoxes";
import MonitorHeader from "./Components/MonitorHeader";
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
import GenericFallback from "../../../Components/GenericFallback";
import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
import SkeletonLayout from "./Components/Skeleton";
//Utils
import { useTheme } from "@mui/material/styles";
import { useState } from "react";
import { useParams } from "react-router-dom";
import useSubscribeToDetails from "./Hooks/useSubscribeToDetails";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useSubscribeToDetails } from "./Hooks/useSubscribeToDetails";
const DistributedUptimeDetails = () => {
const { monitorId } = useParams();
// Local State
const [dateRange, setDateRange] = useState("day");
// Utils
const theme = useTheme();
const isAdmin = useIsAdmin();
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
useSubscribeToDetails({ monitorId, dateRange });
// Constants
@@ -33,6 +35,10 @@ const DistributedUptimeDetails = () => {
{ name: "Details", path: `/distributed-uptime/${monitorId}` },
];
if (isLoading) {
return <SkeletonLayout />;
}
if (networkError) {
return (
<GenericFallback>
@@ -65,6 +71,11 @@ const DistributedUptimeDetails = () => {
gap={theme.spacing(10)}
>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorCreateHeader
label="Create status page"
isAdmin={isAdmin}
path={`/distributed-uptime/status/create/${monitorId}`}
/>
<MonitorHeader monitor={monitor} />
<StatBoxes
monitor={monitor}

View File

@@ -79,7 +79,7 @@ const MonitorTable = ({ isLoading, monitors }) => {
},
},
onRowClick: (row) => {
navigate(`/distributed-uptime/${row.id}`);
navigate(`/distributed-uptime/${row._id}`);
},
}}
/>

View File

@@ -0,0 +1,14 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -32,6 +32,10 @@ const useSubscribeToMonitors = () => {
field: null,
order: null,
onUpdate: (data) => {
if (isLoading === true) {
setIsLoading(false);
}
const res = data.monitors;
const { monitors, filteredMonitors, summary } = res;
const mappedMonitors = filteredMonitors.map((monitor) =>
@@ -41,6 +45,9 @@ const useSubscribeToMonitors = () => {
setMonitorsSummary(summary);
setFilteredMonitors(mappedMonitors);
},
onError: () => {
setIsLoading(false);
},
});
return cleanup;
@@ -49,8 +56,6 @@ const useSubscribeToMonitors = () => {
body: error.message,
});
setNetworkError(true);
} finally {
setIsLoading(false);
}
}, [authToken, user, getMonitorWithPercentage, theme]);
return [isLoading, networkError, monitors, monitorsSummary, filteredMonitors];

View File

@@ -10,6 +10,7 @@ import GenericFallback from "../../../Components/GenericFallback";
import { useTheme } from "@mui/material/styles";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useSubscribeToMonitors } from "./Hooks/useSubscribeToMonitors";
import SkeletonLayout from "./Components/Skeleton";
// Constants
const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" }];
@@ -21,6 +22,10 @@ const DistributedUptimeMonitors = () => {
const [isLoading, networkError, monitors, monitorsSummary, filteredMonitors] =
useSubscribeToMonitors();
if (isLoading) {
return <SkeletonLayout />;
}
if (networkError) {
return (
<GenericFallback>

View File

@@ -0,0 +1,14 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -0,0 +1,37 @@
import { useState, useEffect } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
import { useSelector } from "react-redux";
const useStatusPageFetchByUrl = ({ url }) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [statusPage, setStatusPage] = useState(undefined);
const [monitorId, setMonitorId] = useState(undefined);
const [isPublished, setIsPublished] = useState(false);
const { authToken } = useSelector((state) => state.auth);
useEffect(() => {
const fetchStatusPageByUrl = async () => {
try {
const response = await networkService.getStatusPageByUrl({ authToken, url });
if (!response?.data?.data) return;
const statusPage = response.data.data;
setStatusPage(statusPage);
setMonitorId(statusPage?.monitors[0]);
setIsPublished(statusPage?.isPublished);
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchStatusPageByUrl();
}, [authToken, url]);
return [isLoading, networkError, statusPage, monitorId, isPublished];
};
export { useStatusPageFetchByUrl };

View File

@@ -0,0 +1,194 @@
//Components
import DistributedUptimeMap from "../Details/Components/DistributedUptimeMap";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Typography } from "@mui/material";
import DeviceTicker from "../Details/Components/DeviceTicker";
import DistributedUptimeResponseChart from "../Details/Components/DistributedUptimeResponseChart";
import NextExpectedCheck from "../Details/Components/NextExpectedCheck";
import Footer from "../Details/Components/Footer";
import StatBoxes from "../Details/Components/StatBoxes";
import ControlsHeader from "../../StatusPage/Status/Components/ControlsHeader";
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
import GenericFallback from "../../../Components/GenericFallback";
import Dialog from "../../../Components/Dialog";
import SkeletonLayout from "./Components/Skeleton";
//Utils
import { useTheme } from "@mui/material/styles";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useSubscribeToDetails } from "../Details/Hooks/useSubscribeToDetails";
import { useStatusPageFetchByUrl } from "./Hooks/useStatusPageFetchByUrl";
import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete";
import { useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
const DistributedUptimeStatus = () => {
const { url } = useParams();
const location = useLocation();
const isPublic = location.pathname.startsWith("/distributed-uptime/status/public");
// Local State
const [dateRange, setDateRange] = useState("day");
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
// Utils
const theme = useTheme();
const navigate = useNavigate();
const [
statusPageIsLoading,
statusPageNetworkError,
statusPage,
monitorId,
isPublished,
] = useStatusPageFetchByUrl({
url,
});
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
useSubscribeToDetails({ monitorId, dateRange, isPublic, isPublished });
const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => {
navigate("/distributed-uptime");
}, url);
// Constants
const BREADCRUMBS = [
{ name: "Distributed Uptime", path: "/distributed-uptime" },
{ name: "details", path: `/distributed-uptime/${monitorId}` },
{ name: "status", path: `` },
];
let sx = {};
if (isPublic) {
sx = {
paddingTop: "10vh",
paddingRight: "10vw",
paddingBottom: "10vh",
paddingLeft: "10vw",
};
}
// Done loading, a status page doesn't exist
if (!statusPageIsLoading && typeof statusPage === "undefined") {
return (
<Stack sx={sx}>
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
A public status page is not set up.
</Typography>
<Typography>Please contact to your administrator</Typography>
</GenericFallback>
</Stack>
);
}
// Done loading, a status page exists but is not public
if (!statusPageIsLoading && statusPage.isPublished === false) {
return (
<Stack sx={sx}>
<GenericFallback>
<Typography>This status page is not public.</Typography>
</GenericFallback>
</Stack>
);
}
if (isLoading || statusPageIsLoading) {
return <SkeletonLayout />;
}
if (networkError || statusPageNetworkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
</Typography>
<Typography>Please check your connection</Typography>
</GenericFallback>
);
}
if (
typeof statusPage === "undefined" ||
typeof monitor === "undefined" ||
monitor.totalChecks === 0
) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<GenericFallback>
<Typography>There is no check history for this monitor yet.</Typography>
</GenericFallback>
</Stack>
);
}
return (
<Stack
direction="column"
gap={theme.spacing(10)}
sx={sx}
>
{!isPublic && <Breadcrumbs list={BREADCRUMBS} />}
<ControlsHeader
shouldShow={!isPublic}
statusPage={statusPage}
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
/>
<StatBoxes
monitor={monitor}
lastUpdateTrigger={lastUpdateTrigger}
/>
<NextExpectedCheck
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
interval={monitor?.interval ?? 0}
trigger={lastUpdateTrigger}
/>
<MonitorTimeFrameHeader
dateRange={dateRange}
setDateRange={setDateRange}
/>
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
<Stack
direction="row"
gap={theme.spacing(8)}
>
<DistributedUptimeMap
checks={monitor?.groupedMapChecks ?? []}
height={"100%"}
width={"100%"}
/>
<DeviceTicker
width={"25vw"}
data={monitor?.latestChecks ?? []}
connectionStatus={connectionStatus}
/>
</Stack>
<Footer />
<Dialog
// open={isOpen.deleteStats}
title="Do you want to delete this status page?"
onConfirm={() => {
deleteStatusPage();
setIsDeleteOpen(false);
}}
onCancel={() => {
setIsDeleteOpen(false);
}}
open={isDeleteOpen}
confirmationButtonLabel="Yes, delete status page"
description="Once deleted, your status page cannot be retrieved."
isLoading={isDeleting || isLoading}
/>
</Stack>
);
};
export default DistributedUptimeStatus;

View File

@@ -126,7 +126,7 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
transition: "background-color .3s ease",
},
},
onRowClick: (row) => openDetails(row.id),
onRowClick: (row) => openDetails(row._id),
}}
/>
);

View File

@@ -14,9 +14,15 @@ const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting }) => {
const location = useLocation();
const currentPath = location.pathname;
const navigate = useNavigate();
if (currentPath === "/status/public") {
return null;
}
if (currentPath.startsWith("/distributed-uptime/status/public")) {
return null;
}
return (
<Stack
direction="row"
@@ -80,7 +86,7 @@ const ControlsHeader = ({ statusPage, isDeleting, isDeleteOpen, setIsDeleteOpen
<Image
shouldRender={statusPage?.logo?.data ? true : false}
alt={"Company logo"}
maxWidth={"100px"}
maxWidth={"300px"}
base64={statusPage?.logo?.data}
/>
<Typography variant="h2">{statusPage?.companyName}</Typography>

View File

@@ -34,7 +34,7 @@ const MonitorsList = ({ monitors = [] }) => {
gap={theme.spacing(20)}
>
<Box flex={9}>
<StatusPageBarChart checks={monitor.checks.slice().reverse()} />
<StatusPageBarChart checks={monitor?.checks?.slice().reverse()} />
</Box>
<Box flex={1}>
<StatusLabel

View File

@@ -4,15 +4,14 @@ import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
import { useNavigate } from "react-router-dom";
const useStatusPageDelete = (fetchStatusPage) => {
const useStatusPageDelete = (fetchStatusPage, url = "/status/public") => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const { authToken } = useSelector((state) => state.auth);
const deleteStatusPage = async () => {
try {
setIsLoading(true);
await networkService.deleteStatusPage({ authToken });
await networkService.deleteStatusPage({ authToken, url });
fetchStatusPage?.();
return true;
} catch (error) {

View File

@@ -18,11 +18,13 @@ const useStatusPageFetch = (isCreate = false) => {
const response = await networkService.getStatusPage({ authToken });
if (!response?.data?.data) return;
const { statusPage, monitors } = response.data.data;
setStatusPage(statusPage);
const monitorsWithPercentage = monitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(monitorsWithPercentage);
} catch (error) {
// If there is a 404, status page is not found
@@ -35,7 +37,7 @@ const useStatusPageFetch = (isCreate = false) => {
} finally {
setIsLoading(false);
}
}, [authToken, theme]);
}, [authToken, theme, getMonitorWithPercentage]);
useEffect(() => {
if (isCreate === true) {

View File

@@ -32,6 +32,7 @@ const PublicStatus = () => {
const currentPath = location.pathname;
let sx = { paddingLeft: theme.spacing(20), paddingRight: theme.spacing(20) };
let link = undefined;
// Public status page
if (currentPath === "/status/public") {
sx = {
@@ -139,7 +140,6 @@ const PublicStatus = () => {
<MonitorsList monitors={monitors} />
{link}
<Dialog
// open={isOpen.deleteStats}
title="Do you want to delete this status page?"
onConfirm={() => {
deleteStatusPage();

View File

@@ -191,7 +191,7 @@ const UptimeDataTable = ({
},
},
onRowClick: (row) => {
navigate(`/uptime/${row.id}`);
navigate(`/uptime/${row._id}`);
},
emptyView: "No monitors found",
}}

View File

@@ -31,6 +31,8 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details";
import DistributedUptimeMonitors from "../Pages/DistributedUptime/Monitors";
import CreateDistributedUptime from "../Pages/DistributedUptime/Create";
import DistributedUptimeDetails from "../Pages/DistributedUptime/Details";
import DistributedUptimeStatus from "../Pages/DistributedUptime/Status";
import CreateDistributedUptimeStatus from "../Pages/DistributedUptime/CreateStatus";
// Incidents
import Incidents from "../Pages/Incidents";
@@ -110,6 +112,23 @@ const Routes = () => {
</ProtectedDistributedUptimeRoute>
}
/>
<Route
path="/distributed-uptime/status/create/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
<Route
path="/distributed-uptime/status/:url"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
<Route
path="pagespeed"
element={<PageSpeed />}
@@ -223,6 +242,10 @@ const Routes = () => {
path="/status/public"
element={<Status />}
/>
<Route
path="/distributed-uptime/status/public/:url"
element={<DistributedUptimeStatus />}
/>
<Route
path="*"

View File

@@ -1,4 +1,5 @@
import axios from "axios";
import i18next from 'i18next';
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
import { clearAuthState } from "../Features/Auth/authSlice";
@@ -23,6 +24,21 @@ class NetworkService {
}
this.setBaseUrl(baseURL);
});
this.axiosInstance.interceptors.request.use(
(config) => {
const currentLanguage = i18next.language || 'en';
config.headers = {
...config.headers,
"Accept-Language": currentLanguage,
};
return config;
},
(error) => {
return Promise.reject(error);
}
);
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
@@ -974,19 +990,31 @@ class NetworkService {
});
}
async getStatusPageByUrl(config) {
const { authToken, url } = config;
return this.axiosInstance.get(`/status-page/${url}`, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
}
async createStatusPage(config) {
const { authToken, user, form, isCreate } = config;
const fd = new FormData();
fd.append("isPublished", form.isPublished);
fd.append("companyName", form.companyName);
fd.append("url", form.url);
fd.append("timezone", form.timezone);
fd.append("color", form.color);
fd.append("showCharts", form.showCharts);
fd.append("showUptimePercentage", form.showUptimePercentage);
form.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId);
});
form.isPublished && fd.append("isPublished", form.isPublished);
form.companyName && fd.append("companyName", form.companyName);
form.url && fd.append("url", form.url);
form.timezone && fd.append("timezone", form.timezone);
form.color && fd.append("color", form.color);
form.showCharts && fd.append("showCharts", form.showCharts);
form.showUptimePercentage &&
fd.append("showUptimePercentage", form.showUptimePercentage);
form.monitors &&
form.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId);
});
if (form?.logo?.src && form?.logo?.src !== "") {
const imageResult = await axios.get(form.logo.src, {
responseType: "blob",
@@ -1013,8 +1041,10 @@ class NetworkService {
}
async deleteStatusPage(config) {
const { authToken } = config;
return this.axiosInstance.delete(`/status-page`, {
const { authToken, url } = config;
const encodedUrl = encodeURIComponent(url);
return this.axiosInstance.delete(`/status-page/${encodedUrl}`, {
headers: {
Authorization: `Bearer ${authToken}`,
},

View File

@@ -128,6 +128,10 @@ const baseTheme = (palette) => ({
"&:hover": {
boxShadow: "none",
},
"&.Mui-disabled": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.primary.contrastText,
},
"&.MuiLoadingButton-root": {
"&:disabled": {
backgroundColor: theme.palette.secondary.main,

34
Client/src/Utils/i18n.js Normal file
View File

@@ -0,0 +1,34 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
const primaryLanguage = "gb";
const translations = import.meta.glob("../locales/*.json", { eager: true });
const resources = {};
Object.keys(translations).forEach((path) => {
const langCode = path.match(/\/([^/]+)\.json$/)[1];
resources[langCode] = {
translation: translations[path].default || translations[path],
};
});
const savedLanguage = localStorage.getItem("language") || primaryLanguage;
i18n.use(initReactI18next).init({
resources,
lng: savedLanguage,
fallbackLng: primaryLanguage,
debug: import.meta.env.MODE === "development",
ns: ["translation"],
defaultNS: "translation",
interpolation: {
escapeValue: false,
},
});
i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng);
});
export default i18n;

View File

@@ -180,6 +180,8 @@ const imageValidation = joi.object({
const logoImageValidation = joi
.object({
src: joi.string(),
name: joi.string(),
type: joi
.string()
.valid("image/jpeg", "image/png")

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100" viewBox="0 0 300 100">
<rect width="100%" height="100%" fill="#DDDDDD" />
<path fill="#999999"
d="m76.795 37.635-7.38 12.4v7.84h-3.75v-7.84l-7.38-12.4h3.32q.49 0 .78.23.29.24.48.61l3.7 6.76q.32.6.57 1.13.24.53.44 1.07.18-.54.42-1.07.23-.53.54-1.13l3.68-6.76q.16-.31.47-.58.3-.26.78-.26zm7.88 5.65q1.6 0 2.91.52t2.24 1.47 1.43 2.32q.51 1.38.51 3.07 0 1.71-.51 3.08-.5 1.37-1.43 2.34-.93.96-2.24 1.48t-2.91.52q-1.61 0-2.92-.52-1.32-.52-2.25-1.48-.93-.97-1.44-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.44-2.32t2.25-1.47q1.31-.52 2.92-.52m0 12.14q1.8 0 2.66-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.66-1.22-1.82 0-2.69 1.22-.88 1.23-.88 3.54t.88 3.52q.87 1.21 2.69 1.21m18.65-11.91h3.46v14.36h-2.11q-.69 0-.87-.63l-.24-1.15q-.88.9-1.95 1.45-1.06.55-2.5.55-1.18 0-2.08-.4-.9-.39-1.52-1.12t-.93-1.73q-.32-1-.32-2.21v-9.12h3.46v9.12q0 1.32.61 2.04t1.83.72q.89 0 1.68-.4.78-.4 1.48-1.1zm9.93.86.21 1.63q.67-1.29 1.59-2.02.93-.74 2.19-.74.99 0 1.59.43l-.22 2.59q-.07.26-.2.36-.14.11-.36.11-.21 0-.62-.07-.42-.07-.81-.07-.57 0-1.02.16-.45.17-.8.49-.36.31-.63.76-.28.45-.52 1.02v8.85h-3.45v-14.36h2.03q.53 0 .74.18.21.19.28.68m14.24-7.3h3.45v20.8h-3.45zm13.27 6.21q1.59 0 2.9.52t2.24 1.47 1.44 2.32q.5 1.38.5 3.07 0 1.71-.5 3.08-.51 1.37-1.44 2.34-.93.96-2.24 1.48t-2.9.52q-1.61 0-2.93-.52-1.31-.52-2.24-1.48-.94-.97-1.45-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.45-2.32.93-.95 2.24-1.47 1.32-.52 2.93-.52m0 12.14q1.79 0 2.65-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.65-1.22-1.82 0-2.7 1.22-.87 1.23-.87 3.54t.87 3.52q.88 1.21 2.7 1.21m14.98-5.08q.64 0 1.12-.18.47-.17.79-.48.31-.31.48-.74.16-.44.16-.95 0-1.07-.64-1.69t-1.91-.62q-1.28 0-1.91.62-.64.62-.64 1.69 0 .5.16.93.16.44.48.75.31.32.8.49.48.18 1.11.18m3.9 8.17q0-.42-.25-.68-.25-.27-.68-.42-.44-.14-1.02-.21t-1.23-.11l-1.34-.06q-.7-.03-1.36-.11-.57.32-.93.75-.35.44-.35 1.01 0 .38.19.71.18.33.6.57.41.23 1.07.37.66.13 1.61.13.96 0 1.66-.15.7-.14 1.16-.4.45-.26.66-.62t.21-.78m-.68-14.51h4.13v1.28q0 .62-.74.76l-1.29.24q.29.74.29 1.62 0 1.07-.42 1.93-.43.86-1.19 1.46-.75.6-1.78.93t-2.22.33q-.42 0-.81-.04-.4-.04-.77-.11-.68.4-.68.91 0 .43.4.63.4.21 1.06.29t1.5.1q.84.03 1.72.1t1.72.24q.84.18 1.5.55.66.38 1.06 1.03t.4 1.68q0 .95-.47 1.84-.47.9-1.36 1.6t-2.18 1.13q-1.3.42-2.95.42-1.63 0-2.83-.31-1.2-.32-2-.84-.8-.53-1.19-1.21-.39-.69-.39-1.43 0-1.01.61-1.69.6-.68 1.67-1.08-.58-.3-.91-.79-.34-.49-.34-1.28 0-.33.12-.67t.35-.68q.23-.33.58-.64.35-.3.83-.53-1.09-.59-1.72-1.57-.62-.98-.62-2.3 0-1.06.43-1.92.42-.86 1.19-1.47.76-.61 1.8-.93 1.05-.33 2.28-.33.92 0 1.73.19.82.19 1.49.56m12.67-.72q1.59 0 2.9.52t2.24 1.47 1.44 2.32q.5 1.38.5 3.07 0 1.71-.5 3.08-.51 1.37-1.44 2.34-.93.96-2.24 1.48t-2.9.52q-1.61 0-2.93-.52-1.31-.52-2.24-1.48-.94-.97-1.45-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.45-2.32.93-.95 2.24-1.47 1.32-.52 2.93-.52m0 12.14q1.79 0 2.65-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.65-1.22-1.82 0-2.7 1.22-.87 1.23-.87 3.54t.87 3.52q.88 1.21 2.7 1.21m19.99-18.35v7.98q.84-.79 1.85-1.28t2.36-.49q1.18 0 2.09.4t1.52 1.12.92 1.72q.32 1 .32 2.21v9.14h-3.46v-9.14q0-1.32-.6-2.04t-1.84-.72q-.89 0-1.68.41-.78.4-1.48 1.1v10.39h-3.46v-20.8zm15.06 11.97h6.53q0-.68-.19-1.27-.19-.6-.57-1.04-.38-.45-.96-.71t-1.35-.26q-1.5 0-2.36.85-.86.86-1.1 2.43m8.82 2.08h-8.89q.09 1.11.4 1.91.3.81.81 1.33.5.53 1.2.79.69.26 1.53.26t1.45-.2 1.06-.43q.46-.24.8-.44.34-.19.66-.19.44 0 .65.32l.99 1.26q-.57.67-1.29 1.12-.71.46-1.49.73-.77.28-1.58.39-.8.11-1.56.11-1.5 0-2.79-.5-1.28-.49-2.24-1.47-.95-.97-1.49-2.41-.55-1.43-.55-3.32 0-1.47.48-2.77.47-1.29 1.36-2.25t2.17-1.52 2.89-.56q1.36 0 2.51.44 1.15.43 1.97 1.26.83.84 1.3 2.05t.47 2.76q0 .79-.17 1.06t-.65.27m6.46-6.75.21 1.63q.67-1.29 1.59-2.02.93-.74 2.19-.74.99 0 1.59.43l-.22 2.59q-.07.26-.2.36-.14.11-.36.11-.21 0-.62-.07-.42-.07-.81-.07-.57 0-1.02.16-.45.17-.8.49-.36.31-.63.76-.28.45-.52 1.02v8.85h-3.45v-14.36h2.03q.53 0 .74.18.21.19.28.68m10.12 4.67h6.52q0-.68-.18-1.27-.19-.6-.57-1.04-.38-.45-.96-.71t-1.35-.26q-1.5 0-2.36.85-.86.86-1.1 2.43m8.82 2.08h-8.89q.08 1.11.39 1.91.31.81.81 1.33.51.53 1.2.79t1.53.26 1.45-.2 1.07-.43q.45-.24.8-.44.34-.19.66-.19.43 0 .64.32l1 1.26q-.58.67-1.29 1.12-.71.46-1.49.73-.78.28-1.58.39-.81.11-1.56.11-1.5 0-2.79-.5-1.29-.49-2.24-1.47-.95-.97-1.5-2.41-.54-1.43-.54-3.32 0-1.47.47-2.77.48-1.29 1.37-2.25t2.17-1.52 2.89-.56q1.36 0 2.5.44 1.15.43 1.98 1.26.82.84 1.29 2.05t.47 2.76q0 .79-.17 1.06-.16.27-.64.27" />
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,59 @@
{
"dontHaveAccount": "Don't have account",
"email": "E-mail",
"forgotPassword": "Forgot Password",
"password": "password",
"signUp": "Sign up",
"submit": "Submit",
"title": "Title",
"continue": "Continue",
"enterEmail": "Enter your email",
"authLoginTitle": "Log In",
"authLoginEnterPassword": "Enter your password",
"commonPassword": "Password",
"createPassword": "Create your password",
"createAPassword": "Create a password",
"commonBack": "Back",
"authForgotPasswordTitle": "Forgot password?",
"authForgotPasswordResetPassword": "Reset password",
"authRegisterAlreadyHaveAccount": "Already have an account?",
"commonAppName": "Checkmate",
"authLoginEnterEmail": "Enter your email",
"authRegisterTitle": "Create an account",
"authRegisterStepOneTitle": "Create your account",
"authRegisterStepOneDescription": "Enter your details to get started",
"authRegisterStepTwoTitle": "Set up your profile",
"authRegisterStepTwoDescription": "Tell us more about yourself",
"authRegisterStepThreeTitle": "Almost done!",
"authRegisterStepThreeDescription": "Review your information",
"authForgotPasswordDescription": "No worries, we'll send you reset instructions.",
"authForgotPasswordSendInstructions": "Send instructions",
"authForgotPasswordBackTo": "Back to",
"authCheckEmailTitle": "Check your email",
"authCheckEmailDescription": "We sent a password reset link to {{email}}",
"authCheckEmailResendEmail": "Resend email",
"authCheckEmailBackTo": "Back to",
"goBackTo": "Go back to",
"authCheckEmailDidntReceiveEmail": "Didn't receive the email?",
"authCheckEmailClickToResend": "Click to resend",
"authSetNewPasswordTitle": "Set new password",
"authSetNewPasswordDescription": "Your new password must be different from previously used passwords.",
"authSetNewPasswordNewPassword": "New password",
"authSetNewPasswordConfirmPassword": "Confirm password",
"confirmPassword": "Confirm your password",
"authSetNewPasswordResetPassword": "Reset password",
"authSetNewPasswordBackTo": "Back to",
"authPasswordMustBeAtLeast": "Must be at least",
"authPasswordCharactersLong": "8 characters long",
"authPasswordMustContainAtLeast": "Must contain at least",
"authPasswordSpecialCharacter": "one special character",
"authPasswordOneNumber": "one number",
"authPasswordUpperCharacter": "one upper character",
"authPasswordLowerCharacter": "one lower character",
"authPasswordConfirmAndPassword": "Confirm password and password",
"authPasswordMustMatch": "must match",
"authRegisterCreateAccount": "Create your account to get started",
"authRegisterCreateSuperAdminAccount": "Create your Super admin account to get started",
"authRegisterSignUpWithEmail": "Sign up with Email",
"authRegisterBySigningUp": "By signing up, you agree to our"
}

View File

@@ -0,0 +1,59 @@
{
"dontHaveAccount": "Hesabınız yok mu",
"email": "E-posta",
"forgotPassword": "Parolamı unuttum",
"password": "Parola",
"signUp": "Kayıt Ol",
"submit": "Gönder",
"title": "Başlık",
"continue": "Devam Et",
"enterEmail": "E-posta adresinizi girin",
"authLoginTitle": "Giriş Yap",
"authLoginEnterPassword": "Parolanızı girin",
"commonPassword": "Parola",
"createPassword": "Parolanızı oluşturun",
"createAPassword": "Bir parola oluşturun",
"commonBack": "Geri",
"authForgotPasswordTitle": "Parolanızı mı unuttunuz?",
"authForgotPasswordResetPassword": "Parola sıfırla",
"authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
"commonAppName": "Checkmate",
"authLoginEnterEmail": "E-posta adresinizi girin",
"authRegisterTitle": "Hesap oluştur",
"authRegisterStepOneTitle": "Hesabınızı oluşturun",
"authRegisterStepOneDescription": "Başlamak için bilgilerinizi girin",
"authRegisterStepTwoTitle": "Profilinizi ayarlayın",
"authRegisterStepTwoDescription": "Kendiniz hakkında daha fazla bilgi verin",
"authRegisterStepThreeTitle": "Neredeyse bitti!",
"authRegisterStepThreeDescription": "Bilgilerinizi gözden geçirin",
"authForgotPasswordDescription": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.",
"authForgotPasswordSendInstructions": "Talimatları gönder",
"authForgotPasswordBackTo": "Geri dön",
"authCheckEmailTitle": "E-postanızı kontrol edin",
"authCheckEmailDescription": "{{email}} adresine şifre sıfırlama bağlantısı gönderdik",
"authCheckEmailResendEmail": "E-postayı yeniden gönder",
"authCheckEmailBackTo": "Geri dön",
"goBackTo": "Geri dön",
"authCheckEmailDidntReceiveEmail": "E-posta almadınız mı?",
"authCheckEmailClickToResend": "Yeniden göndermek için tıklayın",
"authSetNewPasswordTitle": "Yeni şifre belirle",
"authSetNewPasswordDescription": "Yeni şifreniz daha önce kullanılan şifrelerden farklı olmalıdır.",
"authSetNewPasswordNewPassword": "Yeni şifre",
"authSetNewPasswordConfirmPassword": "Parolayı onayla",
"confirmPassword": "Parolanızı onaylayın",
"authSetNewPasswordResetPassword": "Parola sıfırla",
"authSetNewPasswordBackTo": "Geri dön",
"authPasswordMustBeAtLeast": "En az",
"authPasswordCharactersLong": "8 karakter uzunluğunda olmalı",
"authPasswordMustContainAtLeast": "En az içermeli",
"authPasswordSpecialCharacter": "bir özel karakter",
"authPasswordOneNumber": "bir rakam",
"authPasswordUpperCharacter": "bir büyük harf",
"authPasswordLowerCharacter": "bir küçük harf",
"authPasswordConfirmAndPassword": "Onay şifresi ve şifre",
"authPasswordMustMatch": "eşleşmelidir",
"authRegisterCreateAccount": "Hesap oluşturmak için devam et",
"authRegisterCreateSuperAdminAccount": "Super admin hesabınızı oluşturmak için devam edin",
"authRegisterSignUpWithEmail": "E-posta ile kayıt ol",
"authRegisterBySigningUp": "Kayıt olarak, aşağıdaki şartları kabul ediyorsunuz:"
}

View File

@@ -1,6 +1,7 @@
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import "./Utils/i18n";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import { persistor, store } from "./store";
@@ -17,7 +18,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
>
<Router>
<NetworkServiceProvider>
<App />
<App />
</NetworkServiceProvider>
</Router>
</PersistGate>

View File

@@ -1,7 +1,6 @@
import { handleError } from "./controllerUtils.js";
import Monitor from "../db/models/Monitor.js";
import DistributedUptimeCheck from "../db/models/DistributedUptimeCheck.js";
const SERVICE_NAME = "DistributedUptimeQueueController";
class DistributedUptimeController {

View File

@@ -62,7 +62,7 @@ class StatusPageController {
try {
const statusPage = await this.db.getStatusPage();
return res.success({
msg: successMessages.STATUS_PAGE_BY_URL,
msg: successMessages.STATUS_PAGE,
data: statusPage,
});
} catch (error) {
@@ -70,9 +70,28 @@ class StatusPageController {
}
};
getStatusPageByUrl = async (req, res, next) => {
try {
await getStatusPageParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const statusPage = await this.db.getStatusPageByUrl(req.params.url);
return res.success({
msg: successMessages.STATUS_PAGE_BY_URL,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getStatusPageByUrl"));
}
};
deleteStatusPage = async (req, res, next) => {
try {
await this.db.deleteStatusPage();
await this.db.deleteStatusPage(req.params.url);
return res.success({
msg: successMessages.STATUS_PAGE_DELETE,
});

View File

@@ -15,12 +15,11 @@ const StatusPageSchema = mongoose.Schema(
},
timezone: {
type: String,
required: true,
default: "America/Toronto",
required: false,
},
color: {
type: String,
required: true,
required: false,
default: "#4169E1",
},
monitors: [

View File

@@ -47,10 +47,21 @@ const updateStatusPage = async (statusPageData, image) => {
}
};
const getStatusPageByUrl = async (url) => {
try {
const statusPage = await StatusPage.aggregate([{ $match: { url } }]);
return statusPage[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
const getStatusPage = async () => {
try {
const statusPageQuery = await StatusPage.aggregate([
{ $limit: 1 },
{ $match: { url: "/status/public" } },
{
$set: {
originalMonitors: "$monitors",
@@ -143,14 +154,20 @@ const getStatusPage = async () => {
}
};
const deleteStatusPage = async () => {
const deleteStatusPage = async (url) => {
try {
await StatusPage.deleteOne({});
await StatusPage.deleteOne({ url });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createStatusPage";
error.method = "deleteStatusPage";
throw error;
}
};
export { createStatusPage, updateStatusPage, getStatusPage, deleteStatusPage };
export {
createStatusPage,
updateStatusPage,
getStatusPage,
getStatusPageByUrl,
deleteStatusPage,
};

View File

@@ -324,9 +324,9 @@
"dev": true
},
"node_modules/@eslint/js": {
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
"version": "9.20.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
"integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3071,18 +3071,18 @@
}
},
"node_modules/eslint": {
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
"version": "9.20.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz",
"integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.10.0",
"@eslint/core": "^0.11.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.19.0",
"@eslint/js": "9.20.0",
"@eslint/plugin-kit": "^0.2.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -3217,6 +3217,19 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@eslint/core": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
"integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -6869,9 +6882,9 @@
}
},
"node_modules/prettier": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"

View File

@@ -12,6 +12,7 @@ class StatusPageRoutes {
initRoutes() {
this.router.get("/", this.statusPageController.getStatusPage);
this.router.get("/:url", this.statusPageController.getStatusPageByUrl);
this.router.post(
"/",
upload.single("logo"),
@@ -24,7 +25,7 @@ class StatusPageRoutes {
verifyJWT,
this.statusPageController.updateStatusPage
);
this.router.delete("/", verifyJWT, this.statusPageController.deleteStatusPage);
this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage);
}
getRouter() {

View File

@@ -135,6 +135,7 @@ const successMessages = {
// Status Page
STATUS_PAGE_BY_URL: "Got status page by url successfully",
STATUS_PAGE: "Got status page successfully",
STATUS_PAGE_CREATE: "Status page created successfully",
STATUS_PAGE_DELETE: "Status page deleted successfully",
STATUS_PAGE_UPDATE: "Status page updated successfully",

View File

@@ -421,8 +421,8 @@ const getStatusPageParamValidation = joi.object({
const createStatusPageBodyValidation = joi.object({
companyName: joi.string().required(),
url: joi.string().required(),
timezone: joi.string().required(),
color: joi.string().required(),
timezone: joi.string().optional(),
color: joi.string().optional(),
monitors: joi
.array()
.items(joi.string().pattern(/^[0-9a-fA-F]{24}$/))
@@ -434,7 +434,7 @@ const createStatusPageBodyValidation = joi.object({
"any.required": "Monitors are required",
}),
isPublished: joi.boolean(),
showCharts: joi.boolean(),
showCharts: joi.boolean().optional(),
showUptimePercentage: joi.boolean(),
});