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..432f710eb 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", @@ -3858,9 +3858,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 +4713,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" @@ -16305,12 +16305,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 +16320,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/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 ( - + {authState.user?.firstName} {authState.user?.lastName} @@ -706,12 +715,21 @@ function Sidebar() { }} > {collapsed && ( - - + + {authState.user?.firstName} {authState.user?.lastName} diff --git a/src/Components/Table/index.jsx b/src/Components/Table/index.jsx index b716038e9..93772a712 100644 --- a/src/Components/Table/index.jsx +++ b/src/Components/Table/index.jsx @@ -15,6 +15,8 @@ import { useTheme } from "@emotion/react"; * @typedef {Object} Header * @property {number|string} id - The unique identifier for the header. * @property {React.ReactNode} content - The content to display in the header cell. + * @property {Function} onClick - A function to be called when this cell is clicked, receiving the event and row data as an argument. + * @property {Object} getCellSx - Function that takes a row and returns a style object for the table cell. * @property {Function} render - A function to render the cell content for a given row. */ @@ -107,6 +109,8 @@ const DataTable = ({ header.onClick(e, row) : null} + sx={header.getCellSx ? header.getCellSx(row) : {}} > {header.render(row)} 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/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx b/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx index 7c60b6ca2..7c498d753 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); @@ -25,8 +25,8 @@ const useSubscribeToMonitors = () => { teamId: user.teamId, limit: 25, types: ["distributed_http"], - page: 0, - rowsPerPage: 10, + page, + rowsPerPage, filter: null, field: null, order: null, @@ -56,7 +56,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 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/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/Components/StatusPagesTable/index.jsx b/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx index 746d10f42..7c951d420 100644 --- a/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ b/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -17,16 +17,40 @@ const StatusPagesTable = ({ data }) => { }, { id: "url", - content: "URL", + content: "Public URL", + onClick: (e, row) => { + if (row.isPublished) { + e.stopPropagation(); + const url = + row.type === "distributed" + ? `/status/distributed/public/${row.url}` + : `/status/uptime/public/${row.url}`; + navigate(url); + } + }, render: (row) => { + const content = row.isPublished ? `/${row.url}` : "Unpublished"; return ( - {`/${row.url}`} - + {content} + {row.isPublished && } ); }, 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/Components/ChartBoxes/index.jsx b/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx index 99d17b6f8..1875ef760 100644 --- a/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -35,6 +35,7 @@ const ChartBoxes = ({ const denominator = totalUpChecks + totalDownChecks > 0 ? totalUpChecks + totalDownChecks : 1; const groupedUptimePercentage = (totalUpChecks / denominator) * 100; + const noIncidentsMessage = "Great. No Incidents, yet!"; return ( } header="Uptime" + isEmpty={monitor?.uptimePercentage === 0 && !monitor?.upChecks?.length} > } header="Incidents" + noDataMessage={noIncidentsMessage} + isEmpty={monitor?.groupedDownChecks?.length === 0} > diff --git a/src/Utils/Theme/constants.js b/src/Utils/Theme/constants.js index 36ab0a0f4..cd2b2e316 100644 --- a/src/Utils/Theme/constants.js +++ b/src/Utils/Theme/constants.js @@ -282,7 +282,7 @@ const newSemanticColors = { }, chatbot: { background: { - light: "#112B2B", + light: newColors.gray200, dark: "#112B2B", }, textAccent: { diff --git a/src/Validation/validation.js b/src/Validation/validation.js index 68c0b5f12..a243b37e8 100644 --- a/src/Validation/validation.js +++ b/src/Validation/validation.js @@ -7,11 +7,18 @@ const nameSchema = joi .string() .max(50) .trim() - .pattern(/^[A-Za-z]+$/) + .pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s']+$/u, { + name: "name.containsLetterOrSymbol", + }) + .pattern(/^[\p{L}\p{Sc}\s']+$/u, { + name: "name.validCharacters", + }) .messages({ "string.empty": "Name is required", - "string.max": "Name must be less than 50 characters long", - "string.pattern.base": "Name must contain only letters", + "string.max": "Name must be less than 50 characters", + "string.pattern.name": "Name must contain at least 1 letter or currency symbol", + "string.pattern.base.validCharacters": + "Can only contain letters, spaces, apostrophes, and currency symbols", }); const passwordSchema = joi diff --git a/src/locales/gb.json b/src/locales/gb.json index 2de3fa6f8..e12309469 100644 --- a/src/locales/gb.json +++ b/src/locales/gb.json @@ -63,8 +63,7 @@ "webhookUnsupportedPlatform": "Unsupported platform: {platform}", "distributedStatusHeaderText": "Real-time, real-device coverage", "distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city", - "distributedRightCatagoryTitle": "Monitor", - "distributedRightCatagoryDescription": "Mainnet Beta Cluster", + "distributedRightCategoryTitle": "Monitor", "distributedStatusServerMonitors": "Server Monitors", "distributedStatusServerMonitorsDescription": "Monitor status of related servers" } diff --git a/src/locales/tr.json b/src/locales/tr.json index 7632c31fd..5ff3448b7 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -58,6 +58,5 @@ "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", - "distributedRightCatagoryTitle": "Monitör", - "distributedRightCatagoryDescription": "Mainnet Beta Kümesi" + "distributedRightCategoryTitle": "Monitör" }