mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-31 06:10:07 -06:00
Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-integrations
This commit is contained in:
118
Client/package-lock.json
generated
118
Client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
138
Client/src/Components/LanguageSelector.jsx
Normal file
138
Client/src/Components/LanguageSelector.jsx
Normal 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;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
217
Client/src/Pages/DistributedUptime/CreateStatus/index.jsx
Normal file
217
Client/src/Pages/DistributedUptime/CreateStatus/index.jsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
export const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -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 };
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -79,7 +79,7 @@ const MonitorTable = ({ isLoading, monitors }) => {
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/distributed-uptime/${row.id}`);
|
||||
navigate(`/distributed-uptime/${row._id}`);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
export const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
export const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -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 };
|
||||
194
Client/src/Pages/DistributedUptime/Status/index.jsx
Normal file
194
Client/src/Pages/DistributedUptime/Status/index.jsx
Normal 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;
|
||||
@@ -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),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -191,7 +191,7 @@ const UptimeDataTable = ({
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/uptime/${row.id}`);
|
||||
navigate(`/uptime/${row._id}`);
|
||||
},
|
||||
emptyView: "No monitors found",
|
||||
}}
|
||||
|
||||
@@ -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="*"
|
||||
|
||||
@@ -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}`,
|
||||
},
|
||||
|
||||
@@ -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
34
Client/src/Utils/i18n.js
Normal 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;
|
||||
@@ -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")
|
||||
|
||||
5
Client/src/assets/Images/logo_placeholder.svg
Normal file
5
Client/src/assets/Images/logo_placeholder.svg
Normal 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 |
59
Client/src/locales/gb.json
Normal file
59
Client/src/locales/gb.json
Normal 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"
|
||||
}
|
||||
59
Client/src/locales/tr.json
Normal file
59
Client/src/locales/tr.json
Normal 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:"
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
35
Server/package-lock.json
generated
35
Server/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user