diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 402f5b15e..22252c301 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,7 +16,7 @@ Mention the issue number(s) this PR addresses (e.g., #123). - [ ] I have labelled the PR correctly. - [ ] The issue I am working on is assigned to me. - [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). -- [ ] I made sure font sizes, color choices etc are all referenced from the theme. +- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I have no hardcoded dimensions. - [ ] My PR is granular and targeted to one specific feature. - [ ] I took a screenshot or a video and attached to this PR if there is a UI change. 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 432f710eb..89b17b968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,12 @@ "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", "@hello-pangea/dnd": "^18.0.0", - "@mui/icons-material": "6.4.6", - "@mui/lab": "6.0.0-beta.29", - "@mui/material": "6.4.6", + "@mui/icons-material": "6.4.7", + "@mui/lab": "6.0.0-beta.30", + "@mui/material": "6.4.7", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.27.2", - "@mui/x-date-pickers": "7.27.1", + "@mui/x-data-grid": "7.27.3", + "@mui/x-date-pickers": "7.27.3", "@reduxjs/toolkit": "2.6.0", "@solana/wallet-adapter-base": "0.9.23", "@solana/wallet-adapter-material-ui": "0.16.34", @@ -33,8 +33,8 @@ "immutability-helper": "^3.1.1", "joi": "17.13.3", "jwt-decode": "^4.0.0", - "maplibre-gl": "5.1.1", - "mui-color-input": "^5.0.1", + "maplibre-gl": "5.2.0", + "mui-color-input": "^6.0.0", "react": "18.3.1", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -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", @@ -3513,9 +3516,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.6.tgz", - "integrity": "sha512-rho5Q4IscbrVmK9rCrLTJmjLjfH6m/NcqKr/mchvck0EIXlyYUB9+Z0oVmkt/+Mben43LMRYBH8q/Uzxj/c4Vw==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.7.tgz", + "integrity": "sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==", "license": "MIT", "funding": { "type": "opencollective", @@ -3523,9 +3526,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.6.tgz", - "integrity": "sha512-rGJBvIQQbQAlyKYljHQ8wAQS/K2/uYwvemcpygnAmCizmCI4zSF9HQPuiG8Ql4YLZ6V/uKjA3WHIYmF/8sV+pQ==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.7.tgz", + "integrity": "sha512-Rk8cs9ufQoLBw582Rdqq7fnSXXZTqhYRbpe1Y5SAz9lJKZP3CIdrj0PfG8HJLGw1hrsHFN/rkkm70IDzhJsG1g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -3538,7 +3541,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.4.6", + "@mui/material": "^6.4.7", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -3549,14 +3552,14 @@ } }, "node_modules/@mui/lab": { - "version": "6.0.0-beta.29", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.29.tgz", - "integrity": "sha512-C5nTm9xfzVi4Zz4DLrZq1Kn+cQ2fGZmSJxyoUbOu+jDPlLZrUpSsgxEkkbjJB5dasiv6fgGKhO3EaKKyfobLAQ==", + "version": "6.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.30.tgz", + "integrity": "sha512-ayDYkzTlkm5cnDGa10bvuFygX+2b9Hm1T4QZYMqV8+nSx3frKE0TLAbE7/qQ4vInOO5E4aOkHVBwZjyO+UbMTA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@mui/base": "5.0.0-beta.69", - "@mui/system": "^6.4.6", + "@mui/system": "^6.4.7", "@mui/types": "^7.2.21", "@mui/utils": "^6.4.6", "clsx": "^2.1.1", @@ -3572,8 +3575,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material": "^6.4.6", - "@mui/material-pigment-css": "^6.4.6", + "@mui/material": "^6.4.7", + "@mui/material-pigment-css": "^6.4.7", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3594,14 +3597,14 @@ } }, "node_modules/@mui/material": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.6.tgz", - "integrity": "sha512-6UyAju+DBOdMogfYmLiT3Nu7RgliorimNBny1pN/acOjc+THNFVE7hlxLyn3RDONoZJNDi/8vO4AQQr6dLAXqA==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.7.tgz", + "integrity": "sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.4.6", - "@mui/system": "^6.4.6", + "@mui/core-downloads-tracker": "^6.4.7", + "@mui/system": "^6.4.7", "@mui/types": "^7.2.21", "@mui/utils": "^6.4.6", "@popperjs/core": "^2.11.8", @@ -3622,7 +3625,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.4.6", + "@mui/material-pigment-css": "^6.4.7", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3710,9 +3713,9 @@ } }, "node_modules/@mui/system": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.6.tgz", - "integrity": "sha512-FQjWwPec7pMTtB/jw5f9eyLynKFZ6/Ej9vhm5kGdtmts1z5b7Vyn3Rz6kasfYm1j2TfrfGnSXRvvtwVWxjpz6g==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.7.tgz", + "integrity": "sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -3858,9 +3861,9 @@ } }, "node_modules/@mui/x-data-grid": { - "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==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.27.3.tgz", + "integrity": "sha512-7zbDbFrhV6ODjyn3ImOZG34nbMbCvmHgqYTYP273TNAj8hMy4BiLyiKFFZTzVddIj3KQ6qLzBpByhqifGgEDOg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -3896,9 +3899,9 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.27.1.tgz", - "integrity": "sha512-2YPhTM9TM39dmIkEQdSB6P6NASePB9LuhXXKQqq0PX4FXGymYEPz/acQXkk617zwfxJJaDhJZ6g8SAv5pklTJQ==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.27.3.tgz", + "integrity": "sha512-igfKTPC4ZVCmS5j/NXcXBtj/hHseQHzRpCpIB1PMnJGhMdRYXnz8qZz5XhlNBKlzJVXkGu6Uil+obZpCLNj1xg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -9186,9 +9189,9 @@ } }, "node_modules/axios": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", - "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -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": { @@ -14110,9 +14113,9 @@ } }, "node_modules/maplibre-gl": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.1.1.tgz", - "integrity": "sha512-0Z6ODzyFu/grwT6K1eIBpv6MZE4xnJD1AV+Yq1hPzOh/YCY36r9BlSaU7d7n2/HJOaoKOy0b2YF8cS4dD+iEVQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.2.0.tgz", + "integrity": "sha512-9zZKD0M80qtDsqBet+EDuAhoCeA/cnAuZAA0p3hcGKGbyjM/SH+R6wQvnBEgvJz9UhDynnkoKdUwhI+fUkHoXQ==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -14845,19 +14848,20 @@ "license": "MIT" }, "node_modules/mui-color-input": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-5.0.1.tgz", - "integrity": "sha512-50Ws4vhg4UPQSZEZDCNc7vyUBSb9x1bK+bO1o0wxJvQYgeSyg2r7mYDlavpCh+ZvisgBL/98y0GVN6M9901JWg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-6.0.0.tgz", + "integrity": "sha512-F9n/RqPLq7fnPWeF53/qssbIHvart7clpOfE+f+dsM+3ddfawwg98+PEhzv3cdAYlJLE9zfVViVTsp/ckUUd3g==", + "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.1.0" }, "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", + "@emotion/react": "^11.13.0", + "@emotion/styled": "^11.13.0", "@mui/material": "^6.0.0", - "@types/react": "^18.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -15639,9 +15643,9 @@ } }, "node_modules/prettier": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", - "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 9684ff111..a8fa1128e 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,11 @@ "@fontsource/roboto": "^5.0.13", "@hello-pangea/dnd": "^18.0.0", "@mui/x-charts": "^7.5.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", - "@mui/material": "6.4.6", + "@mui/x-data-grid": "7.27.3", + "@mui/x-date-pickers": "7.27.3", + "@mui/icons-material": "6.4.7", + "@mui/lab": "6.0.0-beta.30", + "@mui/material": "6.4.7", "@reduxjs/toolkit": "2.6.0", "@solana/wallet-adapter-base": "0.9.23", "@solana/wallet-adapter-material-ui": "0.16.34", @@ -36,8 +36,8 @@ "i18next": "^24.2.2", "immutability-helper": "^3.1.1", "joi": "17.13.3", - "maplibre-gl": "5.1.1", - "mui-color-input": "^5.0.1", + "maplibre-gl": "5.2.0", + "mui-color-input": "^6.0.0", "react": "18.3.1", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", 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/Alert/index.jsx b/src/Components/Alert/index.jsx index a52761ca4..0b32fe2b1 100644 --- a/src/Components/Alert/index.jsx +++ b/src/Components/Alert/index.jsx @@ -36,8 +36,8 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { */ const text = theme.palette.secondary.contrastText; - const bg = theme.palette.secondary.main; - const border = theme.palette.secondary.contrastText; + const border = theme.palette.alert.contrastText; + const bg = theme.palette.alert.main; const icon = icons[variant]; return ( diff --git a/src/Components/Charts/CustomGauge/index.jsx b/src/Components/Charts/CustomGauge/index.jsx index d29663004..3c7e13610 100644 --- a/src/Components/Charts/CustomGauge/index.jsx +++ b/src/Components/Charts/CustomGauge/index.jsx @@ -62,6 +62,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = className="radial-chart" width={radius} height={radius} + sx={{ backgroundColor: theme.palette.primary.main, borderRadius: "50%" }} > {`${progressWithinRange.toFixed(1)}%`} diff --git a/src/Components/Charts/StatusPageBarChart/index.jsx b/src/Components/Charts/StatusPageBarChart/index.jsx index 5acf4c3d6..e912934a0 100644 --- a/src/Components/Charts/StatusPageBarChart/index.jsx +++ b/src/Components/Charts/StatusPageBarChart/index.jsx @@ -3,6 +3,45 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material"; import { formatDateWithTz } from "../../../Utils/timeUtils"; import { useEffect, useState } from "react"; import { useSelector } from "react-redux"; +import PropTypes from "prop-types"; + +/** + * A customizable Bar component that renders a colored bar with optional children. + * @component + * + * @param {string} width The width of the bar (e.g., "100px"). + * @param {string} height The height of the bar (e.g., "50px"). + * @param {string} backgroundColor The background color of the bar (e.g., "#FF5733"). + * @param {string} [borderRadius] Optional border radius for the bar (e.g., "8px"). + * @param {node} children The content to be rendered inside the bar. + * @returns {JSX.Element} The Bar component. + */ + +const Bar = ({ width, height, backgroundColor, borderRadius, children }) => { + const theme = useTheme(); + + return ( + + {children} + + ); +}; + +Bar.propTypes = { + width: PropTypes.string.isRequired, + height: PropTypes.string.isRequired, + backgroundColor: PropTypes.string.isRequired, + borderRadius: PropTypes.string, + children: PropTypes.node, +}; /* TODO add prop validation and jsdocs */ const StatusPageBarChart = ({ checks = [] }) => { @@ -10,6 +49,11 @@ const StatusPageBarChart = ({ checks = [] }) => { const [animate, setAnimate] = useState(false); const uiTimezone = useSelector((state) => state.ui.timezone); + const barWidth = { + xs: "calc(60% / 25)", + xl: "calc(40% / 25)", + }; + useEffect(() => { setAnimate(true); }, []); @@ -41,15 +85,11 @@ const StatusPageBarChart = ({ checks = [] }) => { /* TODO what is the purpose of this box? */ // CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars // even if there are less than 25 checks - ) : ( { }, }} > - { transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)", }} /> - + ) )} diff --git a/src/Components/Charts/Utils/chartUtilFunctions.js b/src/Components/Charts/Utils/chartUtilFunctions.js index 70f5ac9c3..d8ec6cdb6 100644 --- a/src/Components/Charts/Utils/chartUtilFunctions.js +++ b/src/Components/Charts/Utils/chartUtilFunctions.js @@ -13,6 +13,7 @@ export const tooltipDateFormatLookup = (dateRange) => { export const tickDateFormatLookup = (dateRange) => { const tickFormatLookup = { + recent: "h:mm A", day: "h:mm A", week: "MM/D, h:mm A", month: "ddd. M/D", diff --git a/src/Components/Inputs/Select/index.jsx b/src/Components/Inputs/Select/index.jsx index f0a1d11c0..2aa979434 100644 --- a/src/Components/Inputs/Select/index.jsx +++ b/src/Components/Inputs/Select/index.jsx @@ -18,6 +18,7 @@ import "./index.css"; * @param {string} props.items.name - The display name of each item. * @param {function} props.onChange - The function to handle onChange event. * @param {object} props.sx - The custom styles object for MUI Select component. + * @param {number} props.maxWidth - Maximum width in pixels for the select component. Enables responsive text truncation. * @returns {JSX.Element} * * @example @@ -50,6 +51,7 @@ const Select = ({ sx, name = "", labelControlSpacing = 2, + maxWidth, }) => { const theme = useTheme(); const itemStyles = { @@ -59,6 +61,13 @@ const Select = ({ margin: theme.spacing(2), }; + const responsiveMaxWidth = { + xs: `${maxWidth * 0.5}px`, + sm: `${maxWidth * 0.75}px`, + md: `${maxWidth * 0.9}px`, + lg: `${maxWidth}px`, + }; + return ( { + const selectedItem = items.find((item) => item._id === selected); + const displayName = selectedItem ? selectedItem.name : placeholder; + return ( + + {displayName} + + ); + }} > {placeholder && ( { i18n.changeLanguage(newLang); }; - // i18n instance'ından mevcut dilleri al const languages = Object.keys(i18n.options.resources || {}); return ( diff --git a/src/Components/MonitorTimeFrameHeader/index.jsx b/src/Components/MonitorTimeFrameHeader/index.jsx index e43d63494..9ac8b8b6a 100644 --- a/src/Components/MonitorTimeFrameHeader/index.jsx +++ b/src/Components/MonitorTimeFrameHeader/index.jsx @@ -20,6 +20,13 @@ const MonitorTimeFrameHeader = ({ if (hasDateRange) { timeFramePicker = ( + diff --git a/src/Components/Sidebar/index.jsx b/src/Components/Sidebar/index.jsx index e1623e425..4fbc12538 100644 --- a/src/Components/Sidebar/index.jsx +++ b/src/Components/Sidebar/index.jsx @@ -65,34 +65,28 @@ const menu = [ { name: "Status pages", path: "status", icon: }, { name: "Maintenance", path: "maintenance", icon: }, // { name: "Integrations", path: "integrations", icon: }, - { - name: "Account", - icon: , - nested: [ - { name: "Profile", path: "account/profile", icon: }, - { name: "Password", path: "account/password", icon: }, - { name: "Team", path: "account/team", icon: }, - ], - }, { name: "Settings", icon: , path: "settings", }, +]; + +const otherMenuItems = [ + { name: "Support", path: "support", icon: }, { - name: "Other", - icon: , - nested: [ - { name: "Support", path: "support", icon: }, - { - name: "Discussions", - path: "discussions", - icon: , - }, - { name: "Docs", path: "docs", icon: }, - { name: "Changelog", path: "changelog", icon: }, - ], + name: "Discussions", + path: "discussions", + icon: , }, + { name: "Docs", path: "docs", icon: }, + { name: "Changelog", path: "changelog", icon: }, +]; + +const accountMenuItems = [ + { name: "Profile", path: "account/profile", icon: }, + { name: "Password", path: "account/password", icon: }, + { name: "Team", path: "account/team", icon: }, ]; /* TODO this could be a key in nested Path would be the link */ @@ -134,12 +128,38 @@ function Sidebar() { (state) => state.ui.distributedUptimeEnabled ); - const accountMenuItem = menu.find((item) => item.name === "Account"); - if (user.role?.includes("demo") && accountMenuItem) { - accountMenuItem.nested = accountMenuItem.nested.filter((item) => { - return item.name !== "Password"; - }); - } + const renderAccountMenuItems = () => { + let filteredAccountMenuItems = [...accountMenuItems]; + + // If the user is in demo mode, remove the "Password" option + if (user.role?.includes("demo")) { + filteredAccountMenuItems = filteredAccountMenuItems.filter( + (item) => item.name !== "Password" + ); + } + + // If the user is NOT a superadmin, remove the "Team" option + if (user.role && !user.role.includes("superadmin")) { + filteredAccountMenuItems = filteredAccountMenuItems.filter( + (item) => item.name !== "Team" + ); + } + + return filteredAccountMenuItems.map((item) => ( + navigate(item.path)} + sx={{ + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + }} + > + {item.icon} + {item.name} + + )); + }; const openPopup = (event, id) => { setAnchorEl(event.currentTarget); @@ -261,7 +281,6 @@ function Sidebar() { > {collapsed ? : } - {/* TODO Alignment done using padding. Use single source of truth to that*/} - {!collapsed && } - + + + {otherMenuItems.map((item) => { + return item.path ? ( + + { + const url = URL_MAP[item.path]; + if (url) { + window.open(url, "_blank", "noreferrer"); + } else { + navigate(`/${item.path}`); + } + }} + sx={{ + height: "37px", + gap: theme.spacing(4), + borderRadius: theme.shape.borderRadius, + px: theme.spacing(4), + }} + > + {item.icon} + {item.name} {" "} + + + ) : null; + })} + + {collapsed && ( {authState.user?.firstName} {authState.user?.lastName} - + {authState.user?.role} @@ -744,7 +828,8 @@ function Sidebar() { )} {/* TODO Do we need two dividers? */} {collapsed && } - + {/* */} + {renderAccountMenuItems()} {heading} - {subHeading} + {subHeading} ); diff --git a/src/Features/Settings/settingsSlice.js b/src/Features/Settings/settingsSlice.js index a3d96ea0e..345a8a264 100644 --- a/src/Features/Settings/settingsSlice.js +++ b/src/Features/Settings/settingsSlice.js @@ -5,6 +5,7 @@ const initialState = { isLoading: false, apiBaseUrl: "", logLevel: "debug", + language: "", }; export const getAppSettings = createAsyncThunk( @@ -34,6 +35,7 @@ export const updateAppSettings = createAsyncThunk( const parsedSettings = { apiBaseUrl: settings.apiBaseUrl, logLevel: settings.logLevel, + language: settings.language, clientHost: settings.clientHost, jwtSecret: settings.jwtSecret, dbType: settings.dbType, @@ -68,6 +70,7 @@ const handleGetSettingsFulfilled = (state, action) => { state.msg = action.payload.msg; state.apiBaseUrl = action.payload.data.apiBaseUrl; state.logLevel = action.payload.data.logLevel; + state.language = action.payload.data.language; }; const handleGetSettingsRejected = (state, action) => { state.isLoading = false; diff --git a/src/Features/UI/uiSlice.js b/src/Features/UI/uiSlice.js index 5731307ec..ebafdbbf6 100644 --- a/src/Features/UI/uiSlice.js +++ b/src/Features/UI/uiSlice.js @@ -53,8 +53,8 @@ const uiSlice = createSlice({ state.timezone = action.payload.timezone; }, setLanguage(state, action) { - state.language = action.payload; - }, + state.language = action.payload; + }, }, }); diff --git a/src/Pages/Account/index.jsx b/src/Pages/Account/index.jsx index 00d805f34..48b0984f5 100644 --- a/src/Pages/Account/index.jsx +++ b/src/Pages/Account/index.jsx @@ -57,10 +57,6 @@ const Account = ({ open = "profile" }) => { className="account" px={theme.spacing(20)} py={theme.spacing(12)} - backgroundColor={theme.palette.primary.main} - border={1} - borderColor={theme.palette.primary.lowContrast} - borderRadius={theme.shape.borderRadius} > { const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); const statusColor = { up: theme.palette.success.main, down: theme.palette.error.main, + undefined: theme.palette.warning.main, }; return ( @@ -30,7 +28,7 @@ const DeviceTicker = ({ data, width = "100%", connectionStatus }) => { mb={theme.spacing(8)} sx={{ alignSelf: "center" }} > - {connectionStatus === "up" ? "Connected" : "No connection"} + {connectionStatus === "up" ? "Connected" : "Connecting..."}
{ } minTickGap={0} axisLine={false} diff --git a/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Bar/index.jsx b/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Bar/index.jsx index 6b013c29f..a6d3d455b 100644 --- a/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Bar/index.jsx +++ b/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Bar/index.jsx @@ -67,7 +67,7 @@ const DistributedUptimeResponseBarChart = ({ checks }) => { /> } minTickGap={0} axisLine={false} diff --git a/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/ToolTip.jsx b/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/ToolTip.jsx index db8cb3890..064678897 100644 --- a/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/ToolTip.jsx +++ b/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/ToolTip.jsx @@ -75,7 +75,7 @@ CustomToolTip.propTypes = { PropTypes.shape({ value: PropTypes.number, payload: PropTypes.shape({ - _id: PropTypes.string, + _id: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), avgResponseTime: PropTypes.number, originalAvgResponseTime: PropTypes.number, }), diff --git a/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx b/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx index ffc8dbac8..0cdd3f01d 100644 --- a/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx +++ b/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx @@ -9,6 +9,11 @@ import PropTypes from "prop-types"; const StatBoxes = ({ monitor, lastUpdateTrigger }) => { const theme = useTheme(); + let timeSinceLastCheck = 0; + + if (monitor?.lastCheckTimestamp) { + timeSinceLastCheck = Date.now() - monitor?.lastCheckTimestamp; + } return ( { heading={"Last check"} subHeading={ } diff --git a/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx b/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx index ad36c91f9..e8a49e886 100644 --- a/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx +++ b/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx @@ -1,50 +1,7 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import { networkService } from "../../../../main"; -import { useSelector } from "react-redux"; +import { createToast } from "../../../../Utils/toastUtils"; -const getRandomDevice = () => { - const manufacturers = { - Apple: ["iPhone 15 Pro Max", "iPhone 15", "iPhone 14 Pro", "iPhone 14", "iPhone 13"], - Samsung: [ - "Galaxy S23 Ultra", - "Galaxy S23+", - "Galaxy S23", - "Galaxy Z Fold5", - "Galaxy Z Flip5", - ], - Google: ["Pixel 8 Pro", "Pixel 8", "Pixel 7a", "Pixel 7", "Pixel 6a"], - OnePlus: [ - "OnePlus 11", - "OnePlus 10T", - "OnePlus Nord 3", - "OnePlus 10 Pro", - "OnePlus Nord 2T", - ], - Xiaomi: ["13 Pro", "13", "Redmi Note 12", "POCO F5", "Redmi 12"], - Huawei: ["P60 Pro", "Mate X3", "Nova 11", "P50 Pro", "Mate 50"], - Sony: ["Xperia 1 V", "Xperia 5 V", "Xperia 10 V", "Xperia Pro-I", "Xperia 1 IV"], - Motorola: ["Edge 40 Pro", "Edge 40", "G84", "G54", "Razr 40 Ultra"], - ASUS: [ - "ROG Phone 7", - "Zenfone 10", - "ROG Phone 6", - "Zenfone 9", - "ROG Phone 7 Ultimate", - ], - }; - - const manufacturerNames = Object.keys(manufacturers); - const randomManufacturer = - manufacturerNames[Math.floor(Math.random() * manufacturerNames.length)]; - - const models = manufacturers[randomManufacturer]; - const randomModel = models[Math.floor(Math.random() * models.length)]; - - return { - manufacturer: randomManufacturer, - model: randomModel, - }; -}; const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) => { const [isLoading, setIsLoading] = useState(true); const [connectionStatus, setConnectionStatus] = useState(undefined); @@ -52,9 +9,6 @@ const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) const [networkError, setNetworkError] = useState(false); const [monitor, setMonitor] = useState(undefined); const [lastUpdateTrigger, setLastUpdateTrigger] = useState(0); - const [devices, setDevices] = useState(Array.from({ length: 5 }, getRandomDevice)); - - const prevDateRangeRef = useRef(dateRange); useEffect(() => { if (typeof monitorId === "undefined") { @@ -65,6 +19,36 @@ const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) return; } + // Get initial data + + const fetchInitialData = async () => { + try { + const res = await networkService.getDistributedUptimeDetails({ + monitorId, + dateRange: dateRange, + normalize: true, + }); + const responseData = res?.data?.data; + + if (typeof responseData === "undefined") { + throw new Error("No data"); + } + setConnectionStatus("up"); + setLastUpdateTrigger(Date.now()); + setMonitor(responseData); + } catch (error) { + setNetworkError(true); + setConnectionStatus("down"); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + + fetchInitialData(); + try { const cleanup = networkService.subscribeToDistributedUptimeDetails({ monitorId, @@ -78,15 +62,7 @@ const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) setNetworkError(false); } setLastUpdateTrigger(Date.now()); - const latestChecksWithDevice = data?.monitor?.latestChecks.map((check, idx) => { - check.device = devices[idx]; - return check; - }); - const monitorWithDevice = { - ...data.monitor, - latestChecks: latestChecksWithDevice, - }; - setMonitor(monitorWithDevice); + setMonitor(data.monitor); }, onOpen: () => { setConnectionStatus("up"); @@ -108,21 +84,11 @@ const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) retryCount, setConnectionStatus, networkError, - devices, isLoading, isPublic, isPublished, ]); - useEffect(() => { - const hasDateRangeChanged = prevDateRangeRef.current !== dateRange; - prevDateRangeRef.current = dateRange; // Update the ref to the current dateRange - - if (!hasDateRangeChanged) { - setDevices(Array.from({ length: 5 }, getRandomDevice)); - } - }, [dateRange]); - return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger]; }; diff --git a/src/Pages/DistributedUptime/Details/index.jsx b/src/Pages/DistributedUptime/Details/index.jsx index 18aea07b5..6e84240aa 100644 --- a/src/Pages/DistributedUptime/Details/index.jsx +++ b/src/Pages/DistributedUptime/Details/index.jsx @@ -26,7 +26,7 @@ import { useNavigate } from "react-router-dom"; const DistributedUptimeDetails = () => { const { monitorId } = useParams(); // Local State - const [dateRange, setDateRange] = useState("day"); + const [dateRange, setDateRange] = useState("recent"); const [isDeleteOpen, setIsDeleteOpen] = useState(false); // Utils @@ -75,7 +75,6 @@ const DistributedUptimeDetails = () => { ); } - return ( { const theme = useTheme(); const navigate = useNavigate(); const { determineState } = useMonitorUtils(); - const headers = [ { id: "name", diff --git a/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx b/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx index 7c60b6ca2..3b6c262ba 100644 --- a/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx +++ b/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx @@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react"; import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils"; import { createToast } from "../../../../Utils/toastUtils"; -const useSubscribeToMonitors = () => { +const useSubscribeToMonitors = (page, rowsPerPage) => { // Redux const { user } = useSelector((state) => state.auth); @@ -20,13 +20,50 @@ const useSubscribeToMonitors = () => { const { getMonitorWithPercentage } = useMonitorUtils(); useEffect(() => { + const fetchInitialData = async () => { + try { + const initialDataRes = await networkService.getDistributedUptimeMonitors({ + teamId: user.teamId, + limit: 25, + types: ["distributed_http"], + page, + rowsPerPage, + }); + const responseData = initialDataRes?.data?.data; + if (typeof responseData === "undefined") throw new Error("No data"); + + const { monitors, filteredMonitors, summary } = responseData; + + const mappedMonitors = filteredMonitors?.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + + setMonitors(monitors); + setMonitorsSummary(summary); + setFilteredMonitors(mappedMonitors); + } catch (error) { + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + + fetchInitialData(); + try { const cleanup = networkService.subscribeToDistributedUptimeMonitors({ teamId: user.teamId, limit: 25, - types: ["distributed_http"], - page: 0, - rowsPerPage: 10, + types: [ + typeof import.meta.env.VITE_DEPIN_TESTING === "undefined" + ? "distributed_http" + : "distributed_test", + ], + page, + rowsPerPage, filter: null, field: null, order: null, @@ -56,7 +93,7 @@ const useSubscribeToMonitors = () => { }); setNetworkError(true); } - }, [user, getMonitorWithPercentage, theme, isLoading]); + }, [user, getMonitorWithPercentage, theme, isLoading, page, rowsPerPage]); return [isLoading, networkError, monitors, monitorsSummary, filteredMonitors]; }; export { useSubscribeToMonitors }; diff --git a/src/Pages/DistributedUptime/Monitors/index.jsx b/src/Pages/DistributedUptime/Monitors/index.jsx index 2111ddca3..41e21130d 100644 --- a/src/Pages/DistributedUptime/Monitors/index.jsx +++ b/src/Pages/DistributedUptime/Monitors/index.jsx @@ -3,10 +3,12 @@ import { Stack, Typography } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; import MonitorTable from "./Components/MonitorTable"; +import Pagination from "../../../Components/Table/TablePagination"; import Fallback from "../../../Components/Fallback"; import GenericFallback from "../../../Components/GenericFallback"; // Utils +import { useState } from "react"; import { useTheme } from "@mui/material/styles"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useSubscribeToMonitors } from "./Hooks/useSubscribeToMonitors"; @@ -16,11 +18,23 @@ const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" } const DistributedUptimeMonitors = () => { // Local state + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + // Utils const theme = useTheme(); const isAdmin = useIsAdmin(); const [isLoading, networkError, monitors, monitorsSummary, filteredMonitors] = - useSubscribeToMonitors(); + useSubscribeToMonitors(page, rowsPerPage); + // Handlers + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); + setPage(0); + }; if (isLoading) { return ; @@ -69,6 +83,14 @@ const DistributedUptimeMonitors = () => { isLoading={isLoading} monitors={filteredMonitors} /> + ); }; diff --git a/src/Pages/DistributedUptimeStatus/Status/index.jsx b/src/Pages/DistributedUptimeStatus/Status/index.jsx index fa5069eae..515bd9be5 100644 --- a/src/Pages/DistributedUptimeStatus/Status/index.jsx +++ b/src/Pages/DistributedUptimeStatus/Status/index.jsx @@ -21,7 +21,7 @@ import { RowContainer } from "../../../Components/StandardContainer"; //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 +31,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,16 +42,21 @@ 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 [dateRange, setDateRange] = useState("recent"); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [timeFrame, setTimeFrame] = useState(30); // Utils const theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); const navigate = useNavigate(); + const dispatch = useDispatch(); const [ statusPageIsLoading, @@ -85,6 +92,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 +187,10 @@ const DistributedUptimeStatus = () => { }} > {!isPublic && } + { /> ; return ( @@ -27,17 +34,13 @@ const OptionsHeader = ({ direction="row" justifyContent="space-between" > - + - Incidents for + Incidents for: { dispatch(setTimezone({ timezone: e.target.value })); @@ -229,15 +237,15 @@ const Settings = () => { - Appearance + {t("settingsAppearance")} - Switch between light and dark mode. + {t("settingsAppearanceDescription")} + {isAdmin && ( - Distributed uptime + {t("settingsDistributedUptime")} - Enable/disable distributed uptime monitoring. + {t("settingsDistributedUptimeDescription")} @@ -266,109 +284,18 @@ const Settings = () => { dispatch(setDistributedUptimeEnabled(e.target.checked)); }} /> - {distributedUptimeEnabled === true ? "Enabled" : "Disabled"} + {distributedUptimeEnabled === true + ? t("settingsEnabled") + : t("settingsDisabled")} )} {isAdmin && ( - History and monitoring + {t("settingsWallet")} - Define here for how long you want to keep the data. You can also remove - all past data. - - - - - - Clear all stats. This is irreversible. - - - - setIsOpen(deleteStatsMonitorsInitState)} - confirmationButtonLabel="Yes, clear all stats" - onConfirm={handleClearStats} - isLoading={isLoading || authIsLoading || checksIsLoading} - /> - - )} - {isAdmin && ( - - - Demo monitors - - Here you can add and remove demo monitors. - - - - - Add demo monitors - - - - Remove all monitors - - - - setIsOpen(deleteStatsMonitorsInitState)} - confirmationButtonLabel="Yes, clear all monitors" - onConfirm={handleDeleteAllMonitors} - isLoading={isLoading || authIsLoading || checksIsLoading} - /> - - )} - {isAdmin && ( - - - Wallet - - Connect your wallet here. This is required for the Distributed Uptime - monitor to connect to multiple nodes globally. + {t("settingsWalletDescription")} @@ -382,15 +309,106 @@ const Settings = () => { )} + {isAdmin && ( + + + {t("settingsHistoryAndMonitoring")} + + {t("settingsHistoryAndMonitoringDescription")} + + + + + + {t("settingsClearAllStats")} + + + + setIsOpen(deleteStatsMonitorsInitState)} + confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")} + onConfirm={handleClearStats} + isLoading={isLoading || authIsLoading || checksIsLoading} + /> + + )} + {isAdmin && ( + + + {t("settingsDemoMonitors")} + + {t("settingsDemoMonitorsDescription")} + + + + + {t("settingsAddDemoMonitors")} + + + + {t("settingsRemoveAllMonitors")} + + + + setIsOpen(deleteStatsMonitorsInitState)} + confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")} + onConfirm={handleDeleteAllMonitors} + isLoading={isLoading || authIsLoading || checksIsLoading} + /> + + )} - About + {t("settingsAbout")} Checkmate {version} - Developed by Bluewave Labs. + {t("settingsDevelopedBy")} { sx={{ px: theme.spacing(12), mt: theme.spacing(20) }} onClick={handleSave} > - Save + {t("settingsSave")} diff --git a/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx new file mode 100644 index 000000000..6eb64a519 --- /dev/null +++ b/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx @@ -0,0 +1,26 @@ +import { Stack, Typography } from "@mui/material"; +import ConfigBox from "../../../../../Components/ConfigBox"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; + +// This can be used to add any extra/additional section/stacks on top of existing sections on the tab +const ConfigStack = ({ title, description, children }) => { + const theme = useTheme(); + return ( + + + {title} + {description} + + {children} + + ); +}; + +ConfigStack.propTypes = { + title: PropTypes.string.isRequired, // Title must be a string and is required + description: PropTypes.string.isRequired, // Description must be a string and is required + children: PropTypes.node.isRequired, +}; + +export default ConfigStack; diff --git a/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx index 8eda4fe0a..f6945ab5b 100644 --- a/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx +++ b/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx @@ -1,13 +1,13 @@ // Components -import { Stack, Typography, Button } from "@mui/material"; +import { Stack, Typography } from "@mui/material"; import { TabPanel } from "@mui/lab"; -import ConfigBox from "../../../../../Components/ConfigBox"; import MonitorList from "../MonitorList"; import Search from "../../../../../Components/Inputs/Search"; import Checkbox from "../../../../../Components/Inputs/Checkbox"; // Utils import { useState } from "react"; import { useTheme } from "@emotion/react"; +import ConfigStack from "./ConfigStack"; const Content = ({ tabValue, form, @@ -34,14 +34,10 @@ const Content = ({ return ( - - - Status page servers - - You can add any number of servers that you monitor to your status page. You - can also reorder them for the best viewing experience. - - + - {" "} - - - Features - Show more details on the status page - - + + + - + - ); }; diff --git a/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx index 14f7ac20c..0fdf1a9b8 100644 --- a/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -73,6 +73,8 @@ const TabSettings = ({ label="Your status page address" value={form.url} onChange={handleFormChange} + helperText={errors["url"]} + error={errors["url"] ? true : false} /> diff --git a/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx b/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx index 9eacf9b36..f534e73b3 100644 --- a/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx +++ b/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx @@ -2,7 +2,7 @@ import { Box, Stack, Typography, Button } from "@mui/material"; import Image from "../../../../../Components/Image"; import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react"; - +import ThemeSwitch from "../../../../../Components/ThemeSwitch"; //Utils import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; @@ -76,6 +76,7 @@ Controls.propTypes = { const ControlsHeader = ({ statusPage, + isPublic, isDeleting, isDeleteOpen, setIsDeleteOpen, @@ -105,7 +106,16 @@ const ControlsHeader = ({ maxHeight={"50px"} base64={statusPage?.logo?.data} /> - {statusPage?.companyName} + + {statusPage?.companyName} + + {isPublic && } ); }; @@ -121,6 +132,7 @@ const ControlsHeader = ({ ControlsHeader.propTypes = { url: PropTypes.string, statusPage: PropTypes.object, + isPublic: PropTypes.bool, isDeleting: PropTypes.bool, isDeleteOpen: PropTypes.bool.isRequired, setIsDeleteOpen: PropTypes.func.isRequired, diff --git a/src/Pages/StatusPage/StatusPages/index.jsx b/src/Pages/StatusPage/StatusPages/index.jsx index 9027467a4..54a41f850 100644 --- a/src/Pages/StatusPage/StatusPages/index.jsx +++ b/src/Pages/StatusPage/StatusPages/index.jsx @@ -51,6 +51,7 @@ const StatusPages = () => { /> ); } + return ( diff --git a/src/Pages/Uptime/Details/index.jsx b/src/Pages/Uptime/Details/index.jsx index 24b5793f2..d170f7b18 100644 --- a/src/Pages/Uptime/Details/index.jsx +++ b/src/Pages/Uptime/Details/index.jsx @@ -34,14 +34,15 @@ const UptimeDetails = () => { const uiTimezone = useSelector((state) => state.ui.timezone); // Local state - const [dateRange, setDateRange] = useState("day"); + const [dateRange, setDateRange] = useState("recent"); const [hoveredUptimeData, setHoveredUptimeData] = useState(null); const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(5); // Utils - const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; + const dateFormat = + dateRange === "day" || dateRange === "recent" ? "MMM D, h A" : "MMM D"; const { monitorId } = useParams(); const theme = useTheme(); const isAdmin = useIsAdmin(); diff --git a/src/Utils/NetworkService.js b/src/Utils/NetworkService.js index 73a54136d..6b9aebd60 100644 --- a/src/Utils/NetworkService.js +++ b/src/Utils/NetworkService.js @@ -567,6 +567,7 @@ class NetworkService { async getChecksByMonitor(config) { const params = new URLSearchParams(); + if (config.type) params.append("type", config.type); if (config.sortOrder) params.append("sortOrder", config.sortOrder); if (config.limit) params.append("limit", config.limit); if (config.dateRange) params.append("dateRange", config.dateRange); @@ -807,6 +808,31 @@ class NetworkService { ); } + getDistributedUptimeMonitors(config) { + const { teamId, limit, types, page, rowsPerPage, filter, field, order } = config; + + const params = new URLSearchParams(); + + if (limit) params.append("limit", limit); + if (types) { + types.forEach((type) => { + params.append("type", type); + }); + } + if (page) params.append("page", page); + if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); + if (filter) params.append("filter", filter); + if (field) params.append("field", field); + if (order) params.append("order", order); + + if (this.eventSource) { + this.eventSource.close(); + } + + const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}/initial?${params.toString()}`; + return this.axiosInstance.get(url); + } + subscribeToDistributedUptimeMonitors(config) { const { teamId, @@ -872,6 +898,15 @@ class NetworkService { }; } + getDistributedUptimeDetails(config) { + const params = new URLSearchParams(); + const { monitorId, onUpdate, onOpen, onError, dateRange, normalize } = config; + if (dateRange) params.append("dateRange", dateRange); + if (normalize) params.append("normalize", normalize); + const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}/initial?${params.toString()}`; + return this.axiosInstance.get(url); + } + subscribeToDistributedUptimeDetails(config) { const params = new URLSearchParams(); const { monitorId, onUpdate, onOpen, onError, dateRange, normalize } = config; diff --git a/src/Utils/Theme/constants.js b/src/Utils/Theme/constants.js index 36ab0a0f4..915acedc5 100644 --- a/src/Utils/Theme/constants.js +++ b/src/Utils/Theme/constants.js @@ -40,7 +40,6 @@ const paletteColors = { blueGray150: "#667085", blueGray200: "#475467", blueGray400: "#344054", - blueGray900: "#1c2130", blueBlueWave: "#1570EF", blue700: "#4E5BA6", purple300: "#664EFF", @@ -89,11 +88,14 @@ const newColors = { gray100: "#F3F3F3", gray200: "#EFEFEF", gray500: "#A2A3A3", + gray900: "#1c1c1c", blueGray50: "#E8F0FE", blueGray500: "#475467", blueGray600: "#344054", blueGray800: "#1C2130", + blueGray900: "#515151", blueBlueWave: "#1570EF", + lightBlueWave: "#CDE2FF", /* I changed green 100 and green 700. Need to change red and warning as well, and refactor the object following the structure */ green100: "#67cd78", green200: "#4B9B77", @@ -171,6 +173,10 @@ const newSemanticColors = { light: newColors.gray200, dark: "#313131" /* newColors.blueGray600 */, }, + light: { + light: newColors.lightBlueWave, + dark: newColors.lightBlueWave, + }, contrastText: { light: newColors.blueGray600, dark: newColors.gray200, @@ -282,7 +288,7 @@ const newSemanticColors = { }, chatbot: { background: { - light: "#112B2B", + light: newColors.gray200, dark: "#112B2B", }, textAccent: { @@ -290,6 +296,16 @@ const newSemanticColors = { dark: "#C084FC", }, }, + alert: { + main: { + light: newColors.gray200, + dark: newColors.gray900, + }, + contrastText: { + light: newColors.blueGray600, + dark: newColors.blueGray900, + }, + }, }; export { typographyLevels, semanticColors as colors, newSemanticColors }; diff --git a/src/Utils/Theme/globalTheme.js b/src/Utils/Theme/globalTheme.js index 66631edec..3598af675 100644 --- a/src/Utils/Theme/globalTheme.js +++ b/src/Utils/Theme/globalTheme.js @@ -382,6 +382,18 @@ const baseTheme = (palette) => ({ }), }, }, + MuiButtonGroup: { + styleOverrides: { + root: ({ theme }) => ({ + ml: "auto", + "& .MuiButtonBase-root, & .MuiButtonBase-root:hover": { + borderColor: theme.palette.primary.lowContrast, + width: "auto", + whiteSpace: "nowrap", + }, + }), + }, + }, }, shape: { borderRadius: 2, diff --git a/src/Utils/stringUtils.js b/src/Utils/stringUtils.js index 0bf7aaf11..bb62d0169 100644 --- a/src/Utils/stringUtils.js +++ b/src/Utils/stringUtils.js @@ -3,8 +3,8 @@ * @param {string} str String whose first letter is to be capitalized * @returns A string with first letter capitalized */ -export const capitalizeFirstLetter = (str) => { - if (str === null || str === undefined) { +export const capitalizeFirstLetter = (str) => { + if (str === null || str === undefined) { return ""; } if (typeof str !== "string") { @@ -12,7 +12,7 @@ export const capitalizeFirstLetter = (str) => { } if (str.length === 0) { return ""; - } + } return str.charAt(0).toUpperCase() + str.slice(1); }; @@ -22,15 +22,25 @@ export const capitalizeFirstLetter = (str) => { * @returns A string with first letter lower cased */ -export const toLowerCaseFirstLetter = (str) => { +export const toLowerCaseFirstLetter = (str) => { if (str === null || str === undefined) { - return ""; - } - if (typeof str !== "string") { - throw new TypeError("Input must be a string"); - } - if (str.length === 0) { - return ""; - } - return str.charAt(0).toLowerCase() + str.slice(1); - }; \ No newline at end of file + return ""; + } + if (typeof str !== "string") { + throw new TypeError("Input must be a string"); + } + if (str.length === 0) { + return ""; + } + return str.charAt(0).toLowerCase() + str.slice(1); +}; + +/** + * Checks if a string is null, undefined, or empty (including strings with only whitespace). + * @param {string} str - The string to check. + * @returns {boolean} - Returns true if the string is null, undefined, or empty. + */ +export const isEmpty = (str) => { + // Check if string is null, undefined, or empty (including whitespace only) + return str === null || typeof str === "undefined" || str.trim().length === 0; +}; diff --git a/src/Validation/validation.js b/src/Validation/validation.js index a243b37e8..ed0aaf6b8 100644 --- a/src/Validation/validation.js +++ b/src/Validation/validation.js @@ -222,7 +222,14 @@ const statusPageValidation = joi.object({ .string() .trim() .messages({ "string.empty": "Company name is required." }), - url: joi.string().trim().messages({ "string.empty": "URL is required." }), + url: joi + .string() + .pattern(/^[a-zA-Z0-9_-]+$/) // Only allow alphanumeric, underscore, and hyphen + .required() + .messages({ + "string.pattern.base": + "URL can only contain letters, numbers, underscores, and hyphens", + }), timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }), color: joi.string().trim().messages({ "string.empty": "Color is required." }), theme: joi.string(), diff --git a/src/locales/gb.json b/src/locales/gb.json index e12309469..cf7711fa8 100644 --- a/src/locales/gb.json +++ b/src/locales/gb.json @@ -65,5 +65,47 @@ "distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city", "distributedRightCategoryTitle": "Monitor", "distributedStatusServerMonitors": "Server Monitors", - "distributedStatusServerMonitorsDescription": "Monitor status of related servers" + "distributedStatusServerMonitorsDescription": "Monitor status of related servers", + + "settingsGeneralSettings": "General settings", + "settingsDisplayTimezone": "Display timezone", + "settingsDisplayTimezoneDescription": "The timezone of the dashboard you publicly display.", + "settingsAppearance": "Appearance", + "settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language", + "settingsThemeMode": "Theme Mode", + "settingsLanguage": "Language", + "settingsDistributedUptime": "Distributed uptime", + "settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.", + "settingsEnabled": "Enabled", + "settingsDisabled": "Disabled", + "settingsHistoryAndMonitoring": "History and monitoring", + "settingsHistoryAndMonitoringDescription": "Define here for how long you want to keep the data. You can also remove all past data.", + "settingsTTLLabel": "The days you want to keep monitoring history.", + "settingsTTLOptionalLabel": "0 for infinite", + "settingsClearAllStats": "Clear all stats. This is irreversible.", + "settingsClearAllStatsButton": "Clear all stats", + "settingsClearAllStatsDialogTitle": "Do you want to clear all stats?", + "settingsClearAllStatsDialogDescription": "Once deleted, your monitors cannot be retrieved.", + "settingsClearAllStatsDialogConfirm": "Yes, clear all stats", + "settingsDemoMonitors": "Demo monitors", + "settingsDemoMonitorsDescription": "Here you can add and remove demo monitors.", + "settingsAddDemoMonitors": "Add demo monitors", + "settingsAddDemoMonitorsButton": "Add demo monitors", + "settingsRemoveAllMonitors": "Remove all monitors", + "settingsRemoveAllMonitorsButton": "Remove all monitors", + "settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?", + "settingsRemoveAllMonitorsDialogConfirm": "Yes, clear all monitors", + "settingsWallet": "Wallet", + "settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.", + "settingsAbout": "About", + "settingsDevelopedBy": "Developed by Bluewave Labs.", + "settingsSave": "Save", + "settingsSuccessSaved": "Settings saved successfully", + "settingsFailedToSave": "Failed to save settings", + "settingsStatsCleared": "Stats cleared successfully", + "settingsFailedToClearStats": "Failed to clear stats", + "settingsDemoMonitorsAdded": "Successfully added demo monitors", + "settingsFailedToAddDemoMonitors": "Failed to add demo monitors", + "settingsMonitorsDeleted": "Successfully deleted all monitors", + "settingsFailedToDeleteMonitors": "Failed to delete all monitors" } diff --git a/src/locales/tr.json b/src/locales/tr.json index 5ff3448b7..abd358548 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -58,5 +58,48 @@ "authRegisterBySigningUp": "Kayıt olarak, aşağıdaki şartları kabul ediyorsunuz:", "distributedStatusHeaderText": "Gerçek zamanlı, Gerçek cihazlar kapsamı", "distributedStatusSubHeaderText": "Dünya çapında milyonlarca cihaz tarafından desteklenen sistem performansını küresel bölgeye, ülkeye veya şehre göre görüntüleyin", - "distributedRightCategoryTitle": "Monitör" + "distributedRightCatagoryTitle": "Monitör", + "distributedRightCatagoryDescription": "Mainnet Beta Kümesi", + + "settingsGeneralSettings": "Genel ayarlar", + "settingsDisplayTimezone": "Görüntüleme saat dilimi", + "settingsDisplayTimezoneDescription": "Herkese açık olarak görüntülediğiniz kontrol panelinin saat dilimi.", + "settingsAppearance": "Görünüm", + "settingsAppearanceDescription": "Açık ve koyu mod arasında geçiş yapın veya kullanıcı arayüzü dilini değiştirin", + "settingsThemeMode": "Tema", + "settingsLanguage": "Dil", + "settingsDistributedUptime": "Dağıtılmış çalışma süresi", + "settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.", + "settingsEnabled": "Etkin", + "settingsDisabled": "Devre dışı", + "settingsHistoryAndMonitoring": "Geçmiş ve izleme", + "settingsHistoryAndMonitoringDescription": "Verileri ne kadar süreyle saklamak istediğinizi burada tanımlayın. Ayrıca tüm geçmiş verileri kaldırabilirsiniz.", + "settingsTTLLabel": "İzleme geçmişini saklamak istediğiniz gün sayısı.", + "settingsTTLOptionalLabel": "Sınırsız için 0", + "settingsClearAllStats": "Tüm istatistikleri temizle. Bu geri alınamaz.", + "settingsClearAllStatsButton": "Tüm istatistikleri temizle", + "settingsClearAllStatsDialogTitle": "Tüm istatistikleri temizlemek istiyor musunuz?", + "settingsClearAllStatsDialogDescription": "Silindikten sonra, monitörleriniz geri alınamaz.", + "settingsClearAllStatsDialogConfirm": "Evet, tüm istatistikleri temizle", + "settingsDemoMonitors": "Demo monitörler", + "settingsDemoMonitorsDescription": "Burada demo monitörler ekleyebilir ve kaldırabilirsiniz.", + "settingsAddDemoMonitors": "Demo monitörler ekle", + "settingsAddDemoMonitorsButton": "Demo monitörler ekle", + "settingsRemoveAllMonitors": "Tüm monitörleri kaldır", + "settingsRemoveAllMonitorsButton": "Tüm monitörleri kaldır", + "settingsRemoveAllMonitorsDialogTitle": "Tüm monitörleri kaldırmak istiyor musunuz?", + "settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri temizle", + "settingsWallet": "Cüzdan", + "settingsWalletDescription": "Cüzdanınızı buradan bağlayın. Bu, Dağıtılmış Çalışma Süresi monitörünün küresel olarak birden çok düğüme bağlanması için gereklidir.", + "settingsAbout": "Hakkında", + "settingsDevelopedBy": "Bluewave Labs tarafından geliştirilmiştir.", + "settingsSave": "Kaydet", + "settingsSuccessSaved": "Ayarlar başarıyla kaydedildi", + "settingsFailedToSave": "Ayarlar kaydedilemedi", + "settingsStatsCleared": "İstatistikler başarıyla temizlendi", + "settingsFailedToClearStats": "İstatistikler temizlenemedi", + "settingsDemoMonitorsAdded": "Demo monitörler başarıyla eklendi", + "settingsFailedToAddDemoMonitors": "Demo monitörler eklenemedi", + "settingsMonitorsDeleted": "Tüm monitörler başarıyla silindi", + "settingsFailedToDeleteMonitors": "Monitörler silinemedi" }