diff --git a/README.md b/README.md
index 68216c880..5505f72fe 100644
--- a/README.md
+++ b/README.md
@@ -107,7 +107,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
## 🤝 Contributing
-We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 3700+ stars and attracted 45+ contributors from around the globe.
+We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 4.4K+ stars and attracted 50+ contributors from around the globe.
Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us!
diff --git a/package-lock.json b/package-lock.json
index 1a7fcdabc..29e06bd5d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
"@mui/lab": "6.0.0-beta.29",
"@mui/material": "6.4.6",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.27.1",
+ "@mui/x-data-grid": "7.27.2",
"@mui/x-date-pickers": "7.27.1",
"@reduxjs/toolkit": "2.6.0",
"@solana/wallet-adapter-base": "0.9.23",
@@ -2688,10 +2688,13 @@
"license": "MIT"
},
"node_modules/@fontsource/roboto": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.1.tgz",
- "integrity": "sha512-XwVVXtERDQIM7HPUIbyDe0FP4SRovpjF7zMI8M7pbqFp3ahLJsJTd18h+E6pkar6UbV3btbwkKjYARr5M+SQow==",
- "license": "Apache-2.0"
+ "version": "5.2.5",
+ "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.5.tgz",
+ "integrity": "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
},
"node_modules/@fractalwagmi/popup-connection": {
"version": "1.1.1",
@@ -3858,9 +3861,9 @@
}
},
"node_modules/@mui/x-data-grid": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.27.1.tgz",
- "integrity": "sha512-dDgrCIiIcb3XPxUVxXQsk9yjXFSAXBunqW1m1jjcr0D3jj3qHibBtGslSKnBmmcoT6XLAXdbZOu1fFC+xYhyGA==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.27.2.tgz",
+ "integrity": "sha512-Nj6PWp6WN0gkps676j1Jai3mP1fQotJbqMTvllU7LwQp09S/RE/uJicYdqfeWQLxtQipWm5dnAY63gzsTiAHGA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -4713,9 +4716,9 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.22.0",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz",
- "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==",
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -11412,9 +11415,9 @@
}
},
"node_modules/eslint-plugin-react-hooks": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz",
- "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -16305,12 +16308,12 @@
}
},
"node_modules/react-router": {
- "version": "6.29.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz",
- "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==",
+ "version": "6.30.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
+ "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==",
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.22.0"
+ "@remix-run/router": "1.23.0"
},
"engines": {
"node": ">=14.0.0"
@@ -16320,13 +16323,13 @@
}
},
"node_modules/react-router-dom": {
- "version": "6.29.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz",
- "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==",
+ "version": "6.30.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz",
+ "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==",
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.22.0",
- "react-router": "6.29.0"
+ "@remix-run/router": "1.23.0",
+ "react-router": "6.30.0"
},
"engines": {
"node": ">=14.0.0"
diff --git a/package.json b/package.json
index fe51f7003..9684ff111 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"@fontsource/roboto": "^5.0.13",
"@hello-pangea/dnd": "^18.0.0",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.27.1",
+ "@mui/x-data-grid": "7.27.2",
"@mui/x-date-pickers": "7.27.1",
"@mui/icons-material": "6.4.6",
"@mui/lab": "6.0.0-beta.29",
diff --git a/src/App.jsx b/src/App.jsx
index 04dfeeb4f..feddfb60f 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -12,17 +12,28 @@ import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import WalletProvider from "./Components/WalletProvider";
+import { useTranslation } from "react-i18next";
+import { setLanguage } from "./Features/UI/uiSlice";
function App() {
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
+ const { i18n } = useTranslation();
useEffect(() => {
if (authToken) {
- dispatch(getAppSettings({ authToken }));
+ dispatch(getAppSettings({ authToken })).then((action) => {
+ if (action.payload && action.payload.success) {
+ const { language } = action.payload.data;
+ if (language) {
+ dispatch(setLanguage(language));
+ i18n.changeLanguage(language);
+ }
+ }
+ });
}
- }, [dispatch, authToken]);
+ }, [dispatch, authToken, i18n]);
// Cleanup
useEffect(() => {
diff --git a/src/Components/Charts/ChartBox/EmptyView.jsx b/src/Components/Charts/ChartBox/EmptyView.jsx
new file mode 100644
index 000000000..94b1af7b0
--- /dev/null
+++ b/src/Components/Charts/ChartBox/EmptyView.jsx
@@ -0,0 +1,108 @@
+// Components
+import { Typography, Stack } from "@mui/material";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import IconBox from "../../IconBox";
+
+/**
+ * `EmptyView` is a functional React component that displays an empty state view with an optional icon, header, and message.
+ *
+ * @component
+ * @param {Object} props - The properties that define the `EmptyView` component.
+ * @param {React.ReactNode} [props.icon] - An optional icon to display at the top of the empty view.
+ * @param {string} [props.header] - An optional header text displayed next to the icon.
+ * @param {string} [props.message="No Data"] - The message to be displayed in the empty view.
+ * @param {'h1' | 'h2' | 'h3'} [props.headingLevel="h2"] - The heading level for the message text.
+ * @param {string} [props.justifyContent="flex-start"] - The CSS `justify-content` value to align elements vertically.
+ * @param {string} [props.height="100%"] - The height of the empty view container.
+ *
+ * @example
+ * // Example usage of EmptyView component:
+ * }
+ * header="Average Response Time"
+ * message="No Response Time Available"
+ * headingLevel="h2"
+ * justifyContent="center"
+ * height="50%"
+ * />
+ *
+ * @returns {React.Element} The `EmptyView` component with customizable icon, header, and message.
+ */
+
+const EmptyView = ({
+ icon,
+ header,
+ message = "No Data",
+ headingLevel = "h2",
+ justifyContent = "flex-start",
+ height = "100%"
+}) => {
+ const theme = useTheme();
+ return (
+
+
+
+ {icon && {icon}}
+ {header && {header}}
+
+
+
+ {message}
+
+
+
+
+ );
+};
+
+EmptyView.propTypes = {
+ message: PropTypes.string,
+ icon: PropTypes.node,
+ header: PropTypes.string,
+ headingLevel: PropTypes.oneOf(['h1', 'h2', 'h3']),
+ justifyContent: PropTypes.string,
+ height: PropTypes.string
+};
+
+export default EmptyView;
diff --git a/src/Components/Charts/ChartBox/index.jsx b/src/Components/Charts/ChartBox/index.jsx
index bd27d4775..4b72fe16a 100644
--- a/src/Components/Charts/ChartBox/index.jsx
+++ b/src/Components/Charts/ChartBox/index.jsx
@@ -1,6 +1,7 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox";
+import EmptyView from "./EmptyView";
import PropTypes from "prop-types";
const ChartBox = ({
@@ -12,8 +13,13 @@ const ChartBox = ({
Legend,
borderRadiusRight = 4,
sx,
+ noDataMessage,
+ isEmpty = false,
}) => {
const theme = useTheme();
+ if (isEmpty) {
+ return ;
+ }
return (
{
i18n.changeLanguage(newLang);
};
- // i18n instance'ından mevcut dilleri al
const languages = Object.keys(i18n.options.resources || {});
return (
diff --git a/src/Components/Sidebar/index.jsx b/src/Components/Sidebar/index.jsx
index 2d5d7636f..ee0071d4f 100644
--- a/src/Components/Sidebar/index.jsx
+++ b/src/Components/Sidebar/index.jsx
@@ -628,10 +628,19 @@ function Sidebar() {
) : (
<>
-
+
{authState.user?.firstName} {authState.user?.lastName}
@@ -706,12 +715,21 @@ function Sidebar() {
}}
>
{collapsed && (
-
);
};
diff --git a/src/Pages/DistributedUptimeStatus/Status/index.jsx b/src/Pages/DistributedUptimeStatus/Status/index.jsx
index 4a8610036..033174b0c 100644
--- a/src/Pages/DistributedUptimeStatus/Status/index.jsx
+++ b/src/Pages/DistributedUptimeStatus/Status/index.jsx
@@ -18,10 +18,11 @@ import InfoBox from "../../../Components/InfoBox";
import StatusHeader from "../../DistributedUptime/Details/Components/StatusHeader";
import MonitorsList from "./Components/MonitorsList";
import { RowContainer } from "../../../Components/StandardContainer";
+import ThemeSwitch from "../../../Components/ThemeSwitch";
//Utils
import { useTheme } from "@mui/material/styles";
-import { useState, useRef } from "react";
+import { useState, useEffect, useRef } from "react";
import { useParams } from "react-router-dom";
import { useSubscribeToDetails } from "../../DistributedUptime/Details/Hooks/useSubscribeToDetails";
import { useDUStatusPageFetchByUrl } from "./Hooks/useDUStatusPageFetchByUrl";
@@ -31,6 +32,8 @@ import SubHeader from "../../../Components/Subheader";
import { safelyParseFloat } from "../../../Utils/utils";
import { useNavigate, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
+import { useSelector, useDispatch } from "react-redux";
+import { setMode } from "../../../Features/UI/uiSlice";
// Responsive
import { useMediaQuery } from "@mui/material";
@@ -40,8 +43,12 @@ const DistributedUptimeStatus = () => {
const location = useLocation();
const { t } = useTranslation();
const isPublic = location.pathname.startsWith("/status/distributed/public");
-
const elementToCapture = useRef(null);
+
+ // Redux state
+ const mode = useSelector((state) => state.ui.mode);
+ const originalModeRef = useRef(null);
+
// Local State
const [dateRange, setDateRange] = useState("day");
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
@@ -50,6 +57,7 @@ const DistributedUptimeStatus = () => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm"));
const navigate = useNavigate();
+ const dispatch = useDispatch();
const [
statusPageIsLoading,
@@ -85,6 +93,28 @@ const DistributedUptimeStatus = () => {
};
}
+ // Default to dark mode
+ useEffect(() => {
+ const cleanup = () => {
+ if (originalModeRef.current === null) {
+ originalModeRef.current = mode;
+ }
+
+ if (isPublic) {
+ dispatch(setMode(originalModeRef.current));
+ }
+ };
+
+ if (isPublic) {
+ dispatch(setMode("dark"));
+ }
+
+ window.addEventListener("beforeunload", cleanup);
+ return () => {
+ window.removeEventListener("beforeunload", cleanup);
+ };
+ }, [dispatch, isPublic]);
+
// Done loading, a status page doesn't exist
if (!statusPageIsLoading && typeof statusPage === "undefined") {
return (
@@ -158,9 +188,10 @@ const DistributedUptimeStatus = () => {
}}
>
{!isPublic && }
+
{
- {t("distributedRightCatagoryTitle")}
-
-
- {t("distributedRightCatagoryDescription")}
+ {t("distributedRightCategoryTitle")}
+ {statusPage.companyName}
diff --git a/src/Pages/Settings/index.jsx b/src/Pages/Settings/index.jsx
index 04b8658dc..c33042772 100644
--- a/src/Pages/Settings/index.jsx
+++ b/src/Pages/Settings/index.jsx
@@ -27,19 +27,24 @@ import {
setTimezone,
setMode,
setDistributedUptimeEnabled,
+ setLanguage,
} from "../../Features/UI/uiSlice";
import timezones from "../../Utils/timezones.json";
import { useState, useEffect } from "react";
import { networkService } from "../../main";
import { settingsValidation } from "../../Validation/validation";
+import { updateAppSettings } from "../../Features/Settings/settingsSlice";
+import { useTranslation } from "react-i18next";
// Constants
const SECONDS_PER_DAY = 86400;
const Settings = () => {
const theme = useTheme();
+ const { t, i18n } = useTranslation();
const isAdmin = useIsAdmin();
const { user } = useSelector((state) => state.auth);
+ const { language } = useSelector((state) => state.ui);
const { checkTTL } = user;
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
@@ -119,10 +124,13 @@ const Settings = () => {
});
const updatedUser = { ...user, checkTTL: form.ttl };
const action = await dispatch(update({ localData: updatedUser }));
+ const settingsAction = await dispatch(
+ updateAppSettings({ settings: { language: language } })
+ );
- if (action.payload.success) {
+ if (action.payload.success && settingsAction.payload.success) {
createToast({
- body: "Settings saved successfully",
+ body: t("settingsSuccessSaved"),
});
} else {
if (action.payload) {
@@ -139,7 +147,7 @@ const Settings = () => {
}
} catch (error) {
console.log(error);
- createToast({ body: "Failed to save settings" });
+ createToast({ body: t("settingsFailedToSave") });
} finally {
setChecksIsLoading(false);
}
@@ -147,18 +155,16 @@ const Settings = () => {
const handleClearStats = async () => {
try {
- const action = await dispatch(
- deleteMonitorChecksByTeamId({ teamId: user.teamId })
- );
+ const action = await dispatch(deleteMonitorChecksByTeamId({ teamId: user.teamId }));
if (deleteMonitorChecksByTeamId.fulfilled.match(action)) {
- createToast({ body: "Stats cleared successfully" });
+ createToast({ body: t("settingsStatsCleared") });
} else {
- createToast({ body: "Failed to clear stats" });
+ createToast({ body: t("settingsFailedToClearStats") });
}
} catch (error) {
logger.error(error);
- createToast({ body: "Failed to clear stats" });
+ createToast({ body: t("settingsFailedToClearStats") });
} finally {
setIsOpen(deleteStatsMonitorsInitState);
}
@@ -168,13 +174,13 @@ const Settings = () => {
try {
const action = await dispatch(addDemoMonitors());
if (addDemoMonitors.fulfilled.match(action)) {
- createToast({ body: "Successfully added demo monitors" });
+ createToast({ body: t("settingsDemoMonitorsAdded") });
} else {
- createToast({ body: "Failed to add demo monitors" });
+ createToast({ body: t("settingsFailedToAddDemoMonitors") });
}
} catch (error) {
logger.error(error);
- createToast({ Body: "Failed to add demo monitors" });
+ createToast({ Body: t("settingsFailedToAddDemoMonitors") });
}
};
@@ -182,18 +188,20 @@ const Settings = () => {
try {
const action = await dispatch(deleteAllMonitors());
if (deleteAllMonitors.fulfilled.match(action)) {
- createToast({ body: "Successfully deleted all monitors" });
+ createToast({ body: t("settingsMonitorsDeleted") });
} else {
- createToast({ body: "Failed to add demo monitors" });
+ createToast({ body: t("settingsFailedToDeleteMonitors") });
}
} catch (error) {
logger.error(error);
- createToast({ Body: "Failed to delete all monitors" });
+ createToast({ Body: t("settingsFailedToDeleteMonitors") });
} finally {
setIsOpen(deleteStatsMonitorsInitState);
}
};
+ const languages = Object.keys(i18n.options.resources || {});
+
return (
{
>
- General settings
+ {t("settingsGeneralSettings")}
- Display timezone- The timezone of
- the dashboard you publicly display.
+ {t("settingsDisplayTimezone")}- {t("settingsDisplayTimezoneDescription")}
- Appearance
+ {t("settingsAppearance")}
- Switch between light and dark mode.
+ {t("settingsAppearanceDescription")}
+
{isAdmin && (
- Distributed uptime
+ {t("settingsDistributedUptime")}
- Enable/disable distributed uptime monitoring.
+ {t("settingsDistributedUptimeDescription")}
@@ -266,31 +283,30 @@ const Settings = () => {
dispatch(setDistributedUptimeEnabled(e.target.checked));
}}
/>
- {distributedUptimeEnabled === true ? "Enabled" : "Disabled"}
+ {distributedUptimeEnabled === true ? t("settingsEnabled") : t("settingsDisabled")}
)}
{isAdmin && (
- History and monitoring
+ {t("settingsHistoryAndMonitoring")}
- Define here for how long you want to keep the data. You can also remove
- all past data.
+ {t("settingsHistoryAndMonitoringDescription")}
- Clear all stats. This is irreversible.
+ {t("settingsClearAllStats")}