diff --git a/Client/package-lock.json b/Client/package-lock.json
index 51a0c8964..556caf1bc 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -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",
diff --git a/Client/package.json b/Client/package.json
index 6b006e24e..d4c26e1c5 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -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",
diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
index 0b959a392..a1ead3cf9 100644
--- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
+++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
@@ -73,9 +73,12 @@ const CustomToolTip = ({ active, payload, label, dateRange }) => {
component="span"
sx={{ opacity: 0.8 }}
>
- Response Time
- {" "}
-
+ Response time:
+
+
{Math.floor(responseTime)}
{
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 (
Click to upload
diff --git a/Client/src/Components/LanguageSelector.jsx b/Client/src/Components/LanguageSelector.jsx
new file mode 100644
index 000000000..7a809065f
--- /dev/null
+++ b/Client/src/Components/LanguageSelector.jsx
@@ -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 (
+
+ );
+};
+
+export default LanguageSelector;
diff --git a/Client/src/Components/MonitorCreateHeader/index.jsx b/Client/src/Components/MonitorCreateHeader/index.jsx
index 01a102b7d..565dca032 100644
--- a/Client/src/Components/MonitorCreateHeader/index.jsx
+++ b/Client/src/Components/MonitorCreateHeader/index.jsx
@@ -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 ;
@@ -18,7 +23,7 @@ const CreateMonitorHeader = ({ isAdmin, shouldRender = true, path }) => {
color="accent"
onClick={() => navigate(path)}
>
- Create new
+ {label}
);
@@ -30,4 +35,5 @@ CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
shouldRender: PropTypes.bool,
path: PropTypes.string.isRequired,
+ label: PropTypes.string,
};
diff --git a/Client/src/Hooks/useMonitorUtils.js b/Client/src/Hooks/useMonitorUtils.js
index 1e235af68..fc1966b69 100644
--- a/Client/src/Hooks/useMonitorUtils.js
+++ b/Client/src/Hooks/useMonitorUtils.js
@@ -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,
diff --git a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx
index b54f46d12..ec5a4b2db 100644
--- a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx
+++ b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx
@@ -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"
>
- Log In
- Enter your email address
+ {t("authLoginTitle")}
+ {t("authLoginEnterEmail")}
{
{
},
}}
>
- Continue
+ {t("continue")}
diff --git a/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
index d7a80e5f5..a9cb17d7c 100644
--- a/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
+++ b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
@@ -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")}
{
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
- Reset password
+ {t("authForgotPasswordResetPassword")}
);
};
-ForgotPasswordLabel.proptype = {
+ForgotPasswordLabel.propTypes = {
email: PropTypes.string.isRequired,
- emailError: PropTypes.string.isRequired,
+ errorEmail: PropTypes.string.isRequired,
};
export default ForgotPasswordLabel;
diff --git a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx
index 10ded02f5..298a1d105 100644
--- a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx
+++ b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx
@@ -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"
>
- Log In
- Enter your password
+ {t("authLoginTitle")}
+ {t("authLoginEnterPassword")}
{
{
}}
>
- Back
+ {t("commonBack")}{" "}
{
},
}}
>
- Continue
+ {t("continue")}
diff --git a/Client/src/Pages/Auth/Login/Login.jsx b/Client/src/Pages/Auth/Login/Login.jsx
index 30743c60c..60aaed6ba 100644
--- a/Client/src/Pages/Auth/Login/Login.jsx
+++ b/Client/src/Pages/Auth/Login/Login.jsx
@@ -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 = () => {
-
- Checkmate
+
+
+ Checkmate
+
+
+
+
+
{
email={form.email}
errorEmail={errors.email}
/>
-
-
-
);
diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx
index 9e8248cf8..9f93cc821 100644
--- a/Client/src/Pages/Auth/Register/Register.jsx
+++ b/Client/src/Pages/Auth/Register/Register.jsx
@@ -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 (
<>
{
Sign Up
- Create your {isSuperAdmin ? "Super admin " : ""}account to get started.
+ {isSuperAdmin
+ ? t("authRegisterCreateSuperAdminAccount")
+ : t("authRegisterCreateAccount")}
@@ -60,12 +62,12 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
}}
>
- Sign up with Email
+ {t("authRegisterSignUpWithEmail")}
- By signing up, you agree to our{" "}
+ {t("authRegisterBySigningUp")}
{
@@ -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)}
>
- Checkmate
+ {t("commonAppName")}
{
textAlign="center"
p={theme.spacing(12)}
>
- Already have an account? —
+
+ {t("authRegisterAlreadyHaveAccount")} -
+
{
}}
sx={{ userSelect: "none", color: theme.palette.primary.main }}
>
- Log In
+ {t("authLoginTitle")}
diff --git a/Client/src/Pages/Auth/Register/StepThree/index.jsx b/Client/src/Pages/Auth/Register/StepThree/index.jsx
index 6221a8ef1..eb05f843a 100644
--- a/Client/src/Pages/Auth/Register/StepThree/index.jsx
+++ b/Client/src/Pages/Auth/Register/StepThree/index.jsx
@@ -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"
>
- Sign Up
- Create your password
+ {t("signUp")}
+ {t("createPassword")}
@@ -140,7 +141,7 @@ function StepThree({ onSubmit, onBack }) {
}}
>
- Back
+ {t("commonBack")}
diff --git a/Client/src/Pages/Auth/Register/StepTwo/index.jsx b/Client/src/Pages/Auth/Register/StepTwo/index.jsx
index da247cdf9..a16d5344b 100644
--- a/Client/src/Pages/Auth/Register/StepTwo/index.jsx
+++ b/Client/src/Pages/Auth/Register/StepTwo/index.jsx
@@ -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"
>
- Sign Up
- Enter your email address
+ {t("signUp")}
+ {t("enterEmail")}
- Back
+ {t("commonBack")}
diff --git a/Client/src/Pages/Auth/SetNewPassword.jsx b/Client/src/Pages/Auth/SetNewPassword.jsx
index a4c3b0a5a..630ca23f0 100644
--- a/Client/src/Pages/Auth/SetNewPassword.jsx
+++ b/Client/src/Pages/Auth/SetNewPassword.jsx
@@ -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)}
>
- Checkmate
+ {t("commonAppName")}
{
- Set new password
-
- Your new password must be different to previously used passwords.
-
+ {t("authSetNewPasswordTitle")}
+ {t("authSetNewPasswordDescription")}
{
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)}
>
@@ -244,7 +244,7 @@ const SetNewPassword = () => {
}
sx={{ width: "100%", maxWidth: 400 }}
>
- Reset password
+ {t("authSetNewPasswordResetPassword")}
@@ -252,7 +252,7 @@ const SetNewPassword = () => {
textAlign="center"
p={theme.spacing(12)}
>
- Go back to —
+ {t("goBackTo")} —
{
onClick={() => navigate("/login")}
sx={{ userSelect: "none" }}
>
- Log In
+ {t("authLoginTitle")}
diff --git a/Client/src/Pages/DistributedUptime/CreateStatus/Components/VisuallyHiddenInput/index.jsx b/Client/src/Pages/DistributedUptime/CreateStatus/Components/VisuallyHiddenInput/index.jsx
new file mode 100644
index 000000000..fd002d2a4
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/CreateStatus/Components/VisuallyHiddenInput/index.jsx
@@ -0,0 +1,22 @@
+const VisuallyHiddenInput = ({ onChange }) => {
+ return (
+
+ );
+};
+
+export default VisuallyHiddenInput;
diff --git a/Client/src/Pages/DistributedUptime/CreateStatus/index.jsx b/Client/src/Pages/DistributedUptime/CreateStatus/index.jsx
new file mode 100644
index 000000000..d244bfe38
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/CreateStatus/index.jsx
@@ -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 (
+
+
+
+
+ Create your{" "}
+
+
+ status page
+
+
+
+
+ Access
+
+ If your status page is ready, you can mark it as published.
+
+
+
+
+
+
+
+
+ Basic Information
+
+ Define company name and the subdomain that your status page points to.
+
+
+
+
+
+
+
+
+
+ Logo
+ Upload a logo for your status page
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CreateStatus;
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
index 22ef5596d..dc5604a47 100644
--- a/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
+++ b/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
@@ -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;
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
index 99e591750..f4e6a2b19 100644
--- a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
+++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
@@ -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,
}}
/>
);
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx
index 7a4c4b210..b57d0f9dc 100644
--- a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx
+++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx
@@ -24,73 +24,70 @@ const CustomToolTip = ({ active, payload, label }) => {
? payload[0]?.payload?.originalAvgResponseTime
: (payload[0]?.payload?.avgResponseTime ?? 0);
return (
- }
- header="Response Times"
- sx={{ padding: 0 }}
+
-
-
+
+
+
- {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
-
-
-
-
+ Response time
+
+
+ {Math.floor(responseTime)}
- Response Time
- {" "}
-
- {Math.floor(responseTime)}
-
- {" "}
- ms
-
+ {" "}
+ ms
-
-
- {/* Display original value */}
+
+
-
+ {/* Display original value */}
+
);
}
return null;
@@ -143,75 +140,81 @@ const DistributedUptimeResponseChart = ({ checks }) => {
if (checks.length === 0) return null;
return (
- }
+ header="Response Times"
+ sx={{ padding: 0 }}
>
- setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
+ minWidth={25}
+ height={220}
>
-
-
-
-
-
-
-
- }
- minTickGap={0}
- axisLine={false}
- tickLine={false}
- height={20}
- />
- }
- wrapperStyle={{ pointerEvents: "none" }}
- />
-
-
-
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
+
+
+
+
+ }
+ minTickGap={0}
+ axisLine={false}
+ tickLine={false}
+ height={20}
+ />
+ }
+ wrapperStyle={{ pointerEvents: "none" }}
+ />
+
+
+
+
);
};
diff --git a/Client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx
new file mode 100644
index 000000000..233fc3c06
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx
@@ -0,0 +1,14 @@
+import { Stack, Skeleton } from "@mui/material";
+
+export const SkeletonLayout = () => {
+ return (
+
+
+
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx b/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx
index 409e9cf89..9ee7fa873 100644
--- a/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx
+++ b/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx
@@ -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 };
diff --git a/Client/src/Pages/DistributedUptime/Details/index.jsx b/Client/src/Pages/DistributedUptime/Details/index.jsx
index ab39ec2ec..27a596af4 100644
--- a/Client/src/Pages/DistributedUptime/Details/index.jsx
+++ b/Client/src/Pages/DistributedUptime/Details/index.jsx
@@ -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 ;
+ }
+
if (networkError) {
return (
@@ -65,6 +71,11 @@ const DistributedUptimeDetails = () => {
gap={theme.spacing(10)}
>
+
{
},
},
onRowClick: (row) => {
- navigate(`/distributed-uptime/${row.id}`);
+ navigate(`/distributed-uptime/${row._id}`);
},
}}
/>
diff --git a/Client/src/Pages/DistributedUptime/Monitors/Components/Skeleton/index.jsx b/Client/src/Pages/DistributedUptime/Monitors/Components/Skeleton/index.jsx
new file mode 100644
index 000000000..233fc3c06
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Monitors/Components/Skeleton/index.jsx
@@ -0,0 +1,14 @@
+import { Stack, Skeleton } from "@mui/material";
+
+export const SkeletonLayout = () => {
+ return (
+
+
+
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx b/Client/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx
index 1a1db1453..428791a3e 100644
--- a/Client/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx
+++ b/Client/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx
@@ -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];
diff --git a/Client/src/Pages/DistributedUptime/Monitors/index.jsx b/Client/src/Pages/DistributedUptime/Monitors/index.jsx
index a3466b845..159d265a0 100644
--- a/Client/src/Pages/DistributedUptime/Monitors/index.jsx
+++ b/Client/src/Pages/DistributedUptime/Monitors/index.jsx
@@ -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 ;
+ }
+
if (networkError) {
return (
diff --git a/Client/src/Pages/DistributedUptime/Status/Components/Skeleton/index.jsx b/Client/src/Pages/DistributedUptime/Status/Components/Skeleton/index.jsx
new file mode 100644
index 000000000..233fc3c06
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Status/Components/Skeleton/index.jsx
@@ -0,0 +1,14 @@
+import { Stack, Skeleton } from "@mui/material";
+
+export const SkeletonLayout = () => {
+ return (
+
+
+
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/DistributedUptime/Status/Hooks/useStatusPageFetchByUrl.jsx b/Client/src/Pages/DistributedUptime/Status/Hooks/useStatusPageFetchByUrl.jsx
new file mode 100644
index 000000000..583fed69d
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Status/Hooks/useStatusPageFetchByUrl.jsx
@@ -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 };
diff --git a/Client/src/Pages/DistributedUptime/Status/index.jsx b/Client/src/Pages/DistributedUptime/Status/index.jsx
new file mode 100644
index 000000000..d91761e06
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Status/index.jsx
@@ -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 (
+
+
+
+ A public status page is not set up.
+
+ Please contact to your administrator
+
+
+ );
+ }
+
+ // Done loading, a status page exists but is not public
+ if (!statusPageIsLoading && statusPage.isPublished === false) {
+ return (
+
+
+ This status page is not public.
+
+
+ );
+ }
+
+ if (isLoading || statusPageIsLoading) {
+ return ;
+ }
+
+ if (networkError || statusPageNetworkError) {
+ return (
+
+
+ Network error
+
+ Please check your connection
+
+ );
+ }
+
+ if (
+ typeof statusPage === "undefined" ||
+ typeof monitor === "undefined" ||
+ monitor.totalChecks === 0
+ ) {
+ return (
+
+
+
+ There is no check history for this monitor yet.
+
+
+ );
+ }
+
+ return (
+
+ {!isPublic && }
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DistributedUptimeStatus;
diff --git a/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
index 3f1832889..6f030b2e5 100644
--- a/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
+++ b/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
@@ -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),
}}
/>
);
diff --git a/Client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx b/Client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx
index 11733fc61..113f0bafc 100644
--- a/Client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx
+++ b/Client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx
@@ -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 (
{statusPage?.companyName}
diff --git a/Client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/Client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
index ff7a18f97..7f4af1e0c 100644
--- a/Client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
+++ b/Client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
@@ -34,7 +34,7 @@ const MonitorsList = ({ monitors = [] }) => {
gap={theme.spacing(20)}
>
-
+
{
+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) {
diff --git a/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx
index d2b2eae2a..013e32dd9 100644
--- a/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx
+++ b/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx
@@ -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) {
diff --git a/Client/src/Pages/StatusPage/Status/index.jsx b/Client/src/Pages/StatusPage/Status/index.jsx
index e27bc8184..25a794b28 100644
--- a/Client/src/Pages/StatusPage/Status/index.jsx
+++ b/Client/src/Pages/StatusPage/Status/index.jsx
@@ -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 = () => {
{link}