diff --git a/client/src/App.tsx b/client/src/App.tsx
index ef3625cad..0a6b364f8 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -8,7 +8,7 @@ import darkTheme from "./Utils/Theme/darkTheme";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { logger } from "./Utils/Logger"; // Import the logger
import { Routes } from "./Routes";
-import AppLayout from "@/Components/v1/Layouts/AppLayout";
+import AppLayout from "@/Components/v2/layout/AppLayout";
import type { RootState } from "@/Types/state";
function App() {
diff --git a/client/src/Components/v1/ArrowLeft/index.jsx b/client/src/Components/v1/ArrowLeft/index.jsx
deleted file mode 100644
index e18a7f2d5..000000000
--- a/client/src/Components/v1/ArrowLeft/index.jsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import Icon from "../Icon";
-import PropTypes from "prop-types";
-
-const ArrowLeft = ({ type, color = "#667085", ...props }) => {
- if (type === "double") {
- return (
-
- );
- } else if (type === "long") {
- return (
-
- );
- } else {
- return (
-
- );
- }
-};
-
-ArrowLeft.propTypes = {
- color: PropTypes.string,
- type: PropTypes.oneOf(["double", "long", "default"]),
-};
-export default ArrowLeft;
diff --git a/client/src/Components/v1/Avatar/index.css b/client/src/Components/v1/Avatar/index.css
deleted file mode 100644
index e69de29bb..000000000
diff --git a/client/src/Components/v1/Avatar/index.jsx b/client/src/Components/v1/Avatar/index.jsx
deleted file mode 100644
index d089080ff..000000000
--- a/client/src/Components/v1/Avatar/index.jsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Avatar as MuiAvatar } from "@mui/material";
-import PropTypes from "prop-types";
-import { useSelector } from "react-redux";
-import { useEffect, useState } from "react";
-import { useTheme } from "@emotion/react";
-/**
- * @component
- * @param {Object} props
- * @param {string} props.src - Path to image for avatar
- * @param {boolean} props.small - Specifies if avatar should be large
- * @param {Object} [props.sx] - Additional styles to apply to the button.
- * @returns {JSX.Element}
- * @example
- * // Render a red label
- *
- */
-
-const Avatar = ({ src, small, sx, onClick = () => {} }) => {
- const { user } = useSelector((state) => state.auth);
- const theme = useTheme();
-
- const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
- const border = small ? 1 : 3;
-
- const [image, setImage] = useState();
- useEffect(() => {
- if (user.avatarImage) {
- setImage(`data:image/png;base64,${user.avatarImage}`);
- }
- }, [user?.avatarImage]);
-
- return (
-
- {user.firstName?.charAt(0)}
- {user.lastName?.charAt(0) || ""}
-
- );
-};
-
-Avatar.propTypes = {
- src: PropTypes.string,
- small: PropTypes.bool,
- sx: PropTypes.object,
- onClick: PropTypes.func,
-};
-
-export default Avatar;
diff --git a/client/src/Components/v1/Label/index.css b/client/src/Components/v1/Label/index.css
deleted file mode 100644
index d3fe53da5..000000000
--- a/client/src/Components/v1/Label/index.css
+++ /dev/null
@@ -1,6 +0,0 @@
-.label {
- display: inline-flex;
- justify-content: center;
- align-items: center;
- line-height: normal;
-}
diff --git a/client/src/Components/v1/Label/index.jsx b/client/src/Components/v1/Label/index.jsx
deleted file mode 100644
index 2ed9b93bf..000000000
--- a/client/src/Components/v1/Label/index.jsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import PropTypes from "prop-types";
-import { Box } from "@mui/material";
-import { useTheme } from "@mui/material";
-import "./index.css";
-
-/**
- * @typedef {Object} Styles
- * @param {string} [color] - The text color
- * @param {string} [backgroundColor] - The background color
- * @param {string} [borderColor] - The border color
- */
-
-/**
- * @component
- * @param {Object} props
- * @param {string} props.label - The label of the label
- * @param {Styles} props.styles - CSS Styles passed from parent component
- * @param {React.ReactNode} children - Children passed from parent component
- * @returns {JSX.Element}
- */
-
-const BaseLabel = ({ label, styles, children }) => {
- const theme = useTheme();
- // Grab the default borderRadius from the theme to match button style
- const { borderRadius } = theme.shape;
- // Calculate padding for the label to mimic button. Appears to scale correctly, not 100% sure though.
- const padding = theme.spacing(3, 5);
-
- return (
-
- {children}
- {label}
-
- );
-};
-
-BaseLabel.propTypes = {
- label: PropTypes.string.isRequired,
- styles: PropTypes.shape({
- color: PropTypes.string,
- backgroundColor: PropTypes.string,
- }),
- children: PropTypes.node,
-};
-
-// Produces a lighter color based on a hex color and a percent
-// lightenColor("#067647", 20) will produce a color 20% lighter than #067647
-const lightenColor = (color, percent) => {
- let r = parseInt(color.substring(1, 3), 16);
- let g = parseInt(color.substring(3, 5), 16);
- let b = parseInt(color.substring(5, 7), 16);
-
- const amt = Math.round((255 * percent) / 100);
-
- r = r + amt <= 255 ? r + amt : 255;
- g = g + amt <= 255 ? g + amt : 255;
- b = b + amt <= 255 ? b + amt : 255;
-
- r = r.toString(16).padStart(2, "0");
- g = g.toString(16).padStart(2, "0");
- b = b.toString(16).padStart(2, "0");
-
- return `#${r}${g}${b}`;
-};
-
-/**
- * @component
- * @param {Object} props
- * @param {string} props.label - The label of the label
- * @param {string} props.color - The color of the label, specified in #RRGGBB format
- * @returns {JSX.Element}
- * @example
- * // Render a red label
- *
- */
-
-const ColoredLabel = ({ label, color }) => {
- const theme = useTheme();
- // If an invalid color is passed, default to the labelGray color
- if (typeof color !== "string" || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) {
- color = theme.palette.primary.lowContrast;
- }
-
- // Calculate lighter shades for border and bg
- const borderColor = lightenColor(color, 20);
- const bgColor = lightenColor(color, 75);
-
- return (
-
- );
-};
-
-ColoredLabel.propTypes = {
- label: PropTypes.string.isRequired,
- color: PropTypes.string.isRequired,
-};
-
-/**
- * @component
- * @param {Object} props
- * @param {'up' | 'down' | 'paused' | 'pending' | 'cannot resolve' | 'published' | 'unpublished'} props.status - The status for the label
- * @param {string} props.text - The text of the label
- * @returns {JSX.Element}
- * @example
- * // Render an active label
- *
- */
-
-const statusToTheme = {
- up: "success",
- down: "error",
- paused: "warning",
- pending: "warning",
- "cannot resolve": "error",
- published: "success",
- unpublished: "error",
-};
-
-const StatusLabel = ({ status, text, customStyles }) => {
- const theme = useTheme();
-
- const themeColor = statusToTheme[status];
-
- return (
-
-
-
- );
-};
-
-StatusLabel.propTypes = {
- status: PropTypes.oneOf([
- "up",
- "down",
- "paused",
- "pending",
- "cannot resolve",
- "published",
- "unpublished",
- ]),
- text: PropTypes.string,
- customStyles: PropTypes.object,
-};
-
-export { BaseLabel, ColoredLabel, StatusLabel };
diff --git a/client/src/Components/v1/Layouts/AppLayout/index.jsx b/client/src/Components/v1/Layouts/AppLayout/index.jsx
deleted file mode 100644
index 0dbfb1e7a..000000000
--- a/client/src/Components/v1/Layouts/AppLayout/index.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import Box from "@mui/material/Box";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import BackgroundSVG from "../../../../assets/Images/background.svg";
-import { useSelector } from "react-redux";
-
-const AppLayout = ({ children }) => {
- const theme = useTheme();
- const ui = useSelector((state) => state.ui);
- return (
-
- {children}
-
- );
-};
-
-AppLayout.propTypes = {
- children: PropTypes.node,
-};
-
-export default AppLayout;
diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.css b/client/src/Components/v1/Layouts/HomeLayout/index.css
deleted file mode 100644
index 61808aa57..000000000
--- a/client/src/Components/v1/Layouts/HomeLayout/index.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.home-layout {
- position: relative;
- min-height: 100vh;
- margin: 0 auto;
- padding: 0;
- overflow-x: hidden;
- width: 100%;
-}
-
-/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
-/* @media (max-width: 1000px) {
- .home-layout {
- flex-direction: column !important;
- }
-} */
-
-.home-layout > .home-content-wrapper {
- min-height: calc(100vh - var(--env-var-spacing-2) * 2);
- flex: 1;
-}
-
-.home-content-wrapper {
- padding: var(--env-var-spacing-2);
- max-width: 1400px;
- margin: 0 auto;
- flex: 1;
- min-width: 0;
- overflow-x: hidden;
-}
diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx
deleted file mode 100644
index b078fe048..000000000
--- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import Sidebar from "../../Sidebar/index.jsx";
-import { Outlet } from "react-router";
-import { Box, Stack } from "@mui/material";
-import { useSidebar } from "@/Hooks/useSidebar.js";
-
-import "./index.css";
-
-const HomeLayout = () => {
- const { width, transition } = useSidebar();
-
- return (
-
-
- {/* Spacer for fixed sidebar */}
-
-
-
-
-
- );
-};
-
-export default HomeLayout;
diff --git a/client/src/Components/v1/Sidebar/components/authFooter.jsx b/client/src/Components/v1/Sidebar/components/authFooter.jsx
deleted file mode 100644
index 16b60d9f2..000000000
--- a/client/src/Components/v1/Sidebar/components/authFooter.jsx
+++ /dev/null
@@ -1,276 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Box from "@mui/material/Box";
-import Typography from "@mui/material/Typography";
-import Tooltip from "@mui/material/Tooltip";
-import IconButton from "@mui/material/IconButton";
-import Avatar from "../../Avatar/index.jsx";
-import Menu from "@mui/material/Menu";
-import MenuItem from "@mui/material/MenuItem";
-import Divider from "@mui/material/Divider";
-import Icon from "../../Icon";
-
-import { useTheme } from "@emotion/react";
-import { useSelector } from "react-redux";
-import { useTranslation } from "react-i18next";
-import { useState } from "react";
-import { useNavigate } from "react-router";
-import { clearAuthState } from "../../../../Features/Auth/authSlice.js";
-import { useDispatch } from "react-redux";
-import PropTypes from "prop-types";
-
-const getFilteredAccountMenuItems = (user, items) => {
- if (!user) return [];
-
- let filtered = [...items];
-
- if (user.role?.includes("demo")) {
- filtered = filtered.filter((item) => item.name !== "Password");
- }
-
- if (!user.role?.includes("superadmin")) {
- filtered = filtered.filter((item) => item.name !== "Team");
- }
-
- return filtered;
-};
-
-const getRoleDisplayText = (user, t) => {
- if (!user?.role) return "";
-
- if (user.role.includes("superadmin")) return t("roles.superAdmin");
- if (user.role.includes("admin")) return t("roles.admin");
- if (user.role.includes("user")) return t("roles.teamMember");
- if (user.role.includes("demo")) return t("roles.demoUser");
-
- return user.role;
-};
-
-const AuthFooter = ({ collapsed, accountMenuItems }) => {
- const { t } = useTranslation();
- const theme = useTheme();
- const authState = useSelector((state) => state.auth);
- const navigate = useNavigate();
- const dispatch = useDispatch();
-
- const [anchorEl, setAnchorEl] = useState(null);
-
- const openPopup = (event) => {
- setAnchorEl(event.currentTarget);
- };
-
- const closePopup = () => {
- setAnchorEl(null);
- };
-
- const logout = async () => {
- dispatch(clearAuthState());
- navigate("/login");
- };
- const renderAccountMenuItems = (user, items) => {
- const filteredItems = getFilteredAccountMenuItems(user, items);
-
- return filteredItems.map((item) => (
-
- ));
- };
- return (
-
- collapsed && openPopup(e)}
- sx={{
- cursor: collapsed ? "pointer" : "default",
- }}
- />
-
-
-
-
- {authState.user?.firstName} {authState.user?.lastName}
-
-
- {getRoleDisplayText(authState.user, t)}
-
-
-
- openPopup(event)}
- >
-
-
-
-
-
-
- );
-};
-
-AuthFooter.propTypes = {
- collapsed: PropTypes.bool,
- accountMenuItems: PropTypes.array,
-};
-
-export default AuthFooter;
diff --git a/client/src/Components/v1/Sidebar/components/collapseButton.jsx b/client/src/Components/v1/Sidebar/components/collapseButton.jsx
deleted file mode 100644
index e3a9c91f5..000000000
--- a/client/src/Components/v1/Sidebar/components/collapseButton.jsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import IconButton from "@mui/material/IconButton";
-import ArrowRight from "../../ArrowRight/index.jsx";
-import ArrowLeft from "../../ArrowLeft/index.jsx";
-import { useTheme } from "@mui/material/styles";
-import { useDispatch } from "react-redux";
-import { toggleSidebar } from "../../../../Features/UI/uiSlice.js";
-import PropTypes from "prop-types";
-
-const CollapseButton = ({ collapsed }) => {
- const theme = useTheme();
- const dispatch = useDispatch();
- const arrowIcon = collapsed ? (
-
- ) : (
-
- );
- return (
- {
- dispatch(toggleSidebar());
- }}
- >
- {arrowIcon}
-
- );
-};
-
-CollapseButton.propTypes = {
- collapsed: PropTypes.bool.isRequired,
-};
-export default CollapseButton;
diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx
deleted file mode 100644
index 763a4123c..000000000
--- a/client/src/Components/v1/Sidebar/index.jsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import Stack from "@mui/material/Stack";
-
-import List from "@mui/material/List";
-import Logo from "./components/logo.jsx";
-import CollapseButton from "./components/collapseButton.jsx";
-import Divider from "@mui/material/Divider";
-import NavItem from "./components/navItem.jsx";
-import AuthFooter from "./components/authFooter.jsx";
-
-import StarPrompt from "../StarPrompt/index.jsx";
-import Icon from "../Icon";
-
-// Utils
-import { useTheme } from "@mui/material/styles";
-import { useTranslation } from "react-i18next";
-import { useNavigate } from "react-router";
-import { useSidebar } from "@/Hooks/useSidebar.js";
-
-const URL_MAP = {
- support: "https://discord.com/invite/NAb6H3UTjK",
- discussions: "https://github.com/bluewave-labs/checkmate/discussions",
- docs: "https://bluewavelabs.gitbook.io/checkmate",
- changelog: "https://github.com/bluewave-labs/checkmate/releases",
-};
-
-const getMenu = (t) => [
- { name: t("menu.uptime"), path: "uptime", icon: },
- { name: t("menu.pagespeed"), path: "pagespeed", icon: },
- { name: t("menu.infrastructure"), path: "infrastructure", icon: },
- {
- name: t("menu.notifications"),
- path: "notifications",
- icon: ,
- },
- { name: t("menu.checks"), path: "checks", icon: },
- { name: t("menu.incidents"), path: "incidents", icon: },
- { name: t("menu.statusPages"), path: "status", icon: },
- { name: t("menu.maintenance"), path: "maintenance", icon: },
- { name: t("menu.logs"), path: "logs", icon: },
- {
- name: t("menu.settings"),
- icon: ,
- path: "settings",
- },
-];
-
-const getOtherMenuItems = (t) => [
- { name: t("menu.support"), path: "support", icon: },
- {
- name: t("menu.discussions"),
- path: "discussions",
- icon: ,
- },
- { name: t("menu.docs"), path: "docs", icon: },
- { name: t("menu.changelog"), path: "changelog", icon: },
-];
-
-const getAccountMenuItems = (t) => [
- { name: t("menu.profile"), path: "account/profile", icon: },
- { name: t("menu.password"), path: "account/password", icon: },
- { name: t("menu.team"), path: "account/team", icon: },
-];
-
-const Sidebar = () => {
- const theme = useTheme();
- const { t } = useTranslation();
- const navigate = useNavigate();
- const { collapsed, width, transition } = useSidebar();
-
- const menu = getMenu(t);
- const otherMenuItems = getOtherMenuItems(t);
- const accountMenuItems = getAccountMenuItems(t);
-
- return (
-
-
-
-
- {menu.map((item) => {
- const selected = location.pathname.startsWith(`/${item.path}`);
- return (
- navigate(`/${item.path}`)}
- />
- );
- })}
-
- {!collapsed && }
-
- {otherMenuItems.map((item) => {
- const selected = location.pathname.startsWith(`/${item.path}`);
-
- return (
- {
- const url = URL_MAP[item.path];
- if (url) {
- window.open(url, "_blank", "noreferrer");
- } else {
- navigate(`/${item.path}`);
- }
- }}
- />
- );
- })}
-
-
-
-
- );
-};
-
-export default Sidebar;
diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx
new file mode 100644
index 000000000..74c06f8f3
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Avatar.tsx
@@ -0,0 +1,47 @@
+import { Avatar as MuiAvatar } from "@mui/material";
+import { useSelector } from "react-redux";
+import { useEffect, useState } from "react";
+import { useTheme } from "@mui/material";
+import type { RootState } from "@/Types/state";
+
+interface AvatarProps {
+ src?: string;
+ small?: boolean;
+ sx?: object;
+ onClick?: (event: React.MouseEvent) => void;
+}
+
+export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => {
+ const { user } = useSelector((state: RootState) => state.auth);
+ const theme = useTheme();
+ if (!user) return null;
+
+ const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
+
+ const [image, setImage] = useState();
+ useEffect(() => {
+ if (user.avatarImage) {
+ setImage(`data:image/png;base64,${user.avatarImage}`);
+ }
+ }, [user?.avatarImage]);
+
+ return (
+
+ {user.firstName?.charAt(0)}
+ {user.lastName?.charAt(0) || ""}
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/v2/design-elements/Icon.tsx
index b32cd84b2..22f939750 100644
--- a/client/src/Components/v2/design-elements/Icon.tsx
+++ b/client/src/Components/v2/design-elements/Icon.tsx
@@ -4,6 +4,7 @@ interface IconProps {
icon: LucideIcon;
size?: number;
strokeWidth?: number;
+ stroke?: string;
}
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => {
diff --git a/client/src/Components/v2/design-elements/OfflineBanner.tsx b/client/src/Components/v2/design-elements/OfflineBanner.tsx
new file mode 100644
index 000000000..062b1ca0d
--- /dev/null
+++ b/client/src/Components/v2/design-elements/OfflineBanner.tsx
@@ -0,0 +1,63 @@
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+import { WifiOff } from "lucide-react";
+import { useState, useEffect } from "react";
+
+interface OfflineBannerProps {
+ visible: boolean;
+}
+
+export const OfflineBanner = ({ visible }: OfflineBannerProps) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const [shouldRender, setShouldRender] = useState(visible);
+ const [isAnimating, setIsAnimating] = useState(false);
+
+ useEffect(() => {
+ if (visible) {
+ setShouldRender(true);
+ requestAnimationFrame(() => setIsAnimating(true));
+ } else {
+ setIsAnimating(false);
+ const timer = setTimeout(() => setShouldRender(false), 1000);
+ return () => clearTimeout(timer);
+ }
+ }, [visible]);
+
+ if (!shouldRender) return null;
+
+ return (
+
+
+
+
+ {t("components.offlineBanner.serverUnreachable")}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx
index b7b1d484e..ed6a885f6 100644
--- a/client/src/Components/v2/design-elements/index.tsx
+++ b/client/src/Components/v2/design-elements/index.tsx
@@ -19,3 +19,5 @@ export * from "./Gauge";
export * from "./Tabs";
export * from "./SplitBox";
export * from "./TextLink";
+export * from "./OfflineBanner";
+export * from "./Avatar";
diff --git a/client/src/Components/v2/layout/AppLayout.tsx b/client/src/Components/v2/layout/AppLayout.tsx
new file mode 100644
index 000000000..7c9c3c903
--- /dev/null
+++ b/client/src/Components/v2/layout/AppLayout.tsx
@@ -0,0 +1,69 @@
+import Box from "@mui/material/Box";
+import { useState, useEffect, useRef } from "react";
+import { ThemeProvider, useTheme } from "@mui/material/styles";
+import { useSelector } from "react-redux";
+import BackgroundSVG from "@/assets/Images/background.svg";
+import type { RootState } from "@/Types/state";
+import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
+import { OfflineBanner } from "@/Components/v2/design-elements";
+import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
+
+interface AppLayoutProps {
+ children: React.ReactNode;
+}
+
+const AppLayout = ({ children }: AppLayoutProps) => {
+ const theme = useTheme();
+ const mode = useSelector((state: RootState) => state.ui.mode);
+ const v2theme = mode === "dark" ? darkTheme : lightTheme;
+
+ const [serverUnreachable, setServerUnreachable] = useState(false);
+ const retryIntervalRef = useRef(null);
+
+ useEffect(() => {
+ setServerUnreachableCallback(setServerUnreachable);
+ }, []);
+
+ useEffect(() => {
+ if (serverUnreachable) {
+ retryIntervalRef.current = window.setInterval(async () => {
+ try {
+ await get("/health", { timeout: 5000 });
+ } catch {
+ // NO_OP
+ }
+ }, 5000);
+ } else if (retryIntervalRef.current) {
+ clearInterval(retryIntervalRef.current);
+ retryIntervalRef.current = null;
+ }
+
+ return () => {
+ if (retryIntervalRef.current) {
+ clearInterval(retryIntervalRef.current);
+ }
+ };
+ }, [serverUnreachable]);
+
+ return (
+
+
+
+
+ {children}
+
+ );
+};
+
+export default AppLayout;
diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx
new file mode 100644
index 000000000..0b6defb91
--- /dev/null
+++ b/client/src/Components/v2/layout/RootLayout.tsx
@@ -0,0 +1,51 @@
+import { Sidebar } from "@/Components/v2/sidebar";
+import { Outlet } from "react-router";
+import Stack from "@mui/material/Stack";
+import { useMediaQuery } from "@mui/material";
+import { useSidebar } from "@/Hooks/useSidebar";
+
+import { useSelector } from "react-redux";
+import type { RootState } from "@/Types/state";
+import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
+import { ThemeProvider, useTheme } from "@mui/material";
+
+const RootLayout = () => {
+ const mode = useSelector((state: RootState) => state.ui.mode);
+ const v2theme = mode === "dark" ? darkTheme : lightTheme;
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const { collapsedWidth } = useSidebar();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default RootLayout;
diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx
new file mode 100644
index 000000000..62ea5610b
--- /dev/null
+++ b/client/src/Components/v2/sidebar/Authfooter.tsx
@@ -0,0 +1,183 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import IconButton from "@mui/material/IconButton";
+import Menu from "@mui/material/Menu";
+import MenuItem from "@mui/material/MenuItem";
+import { useTheme } from "@mui/material";
+import { useSelector, useDispatch } from "react-redux";
+import { useTranslation } from "react-i18next";
+import { useState } from "react";
+import { useNavigate } from "react-router";
+import { MoreVertical, LogOut } from "lucide-react";
+import { Avatar, Icon } from "@/Components/v2/design-elements";
+import { clearAuthState } from "@/Features/Auth/authSlice.js";
+import type { RootState } from "@/Types/state.js";
+
+interface AuthFooterProps {
+ collapsed: boolean;
+ accountMenuItems: Array<{
+ name: string;
+ path: string;
+ icon: React.ReactNode;
+ }>;
+}
+
+export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const user = useSelector((state: RootState) => state.auth.user);
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const menuOpen = Boolean(anchorEl);
+ const handleMenuOpen = (e: React.MouseEvent) =>
+ setAnchorEl(e.currentTarget);
+ const handleMenuClose = () => setAnchorEl(null);
+
+ const handleNavigate = (path: string) => {
+ handleMenuClose();
+ navigate(path);
+ };
+
+ const handleLogout = () => {
+ dispatch(clearAuthState());
+ navigate("/login");
+ };
+
+ const getRoleText = () => {
+ const role = user?.role ?? "";
+ if (role.includes("superadmin"))
+ return t("components.sidebar.authFooter.roles.superAdmin");
+ if (role.includes("admin")) return t("components.sidebar.authFooter.roles.admin");
+ if (role.includes("user")) return t("components.sidebar.authFooter.roles.user");
+ if (role.includes("demo")) return t("components.sidebar.authFooter.roles.demoUser");
+ return role;
+ };
+
+ const filteredMenuItems = accountMenuItems.filter((item) => {
+ if (!user) return false;
+ if (item.name === "Password" && user.role?.includes("demo")) return false;
+ if (item.name === "Team" && !user.role?.includes("superadmin")) return false;
+ return true;
+ });
+
+ const menuItemSx = {
+ gap: theme.spacing(2),
+ borderRadius: theme.shape.borderRadius,
+ pl: theme.spacing(4),
+ "& svg": { stroke: theme.palette.text.secondary },
+ };
+
+ return (
+
+
+
+ {!collapsed && (
+
+
+
+ {user?.firstName} {user?.lastName}
+
+
+ {getRoleText()}
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/client/src/Components/v1/Sidebar/components/logo.jsx b/client/src/Components/v2/sidebar/Logo.tsx
similarity index 60%
rename from client/src/Components/v1/Sidebar/components/logo.jsx
rename to client/src/Components/v2/sidebar/Logo.tsx
index a0c0ff84e..d7ab2c751 100644
--- a/client/src/Components/v1/Sidebar/components/logo.jsx
+++ b/client/src/Components/v2/sidebar/Logo.tsx
@@ -1,37 +1,37 @@
-import Stack from "@mui/material/Stack";
+import Stack, { type StackProps } from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
-import { useNavigate } from "react-router";
+import { toggleSidebar } from "@/Features/UI/uiSlice.js";
+import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
-import PropTypes from "prop-types";
+import useSidebar from "@/Hooks/useSidebar";
-const Logo = ({ collapsed }) => {
+export const Logo = (props: StackProps) => {
const { t } = useTranslation();
const theme = useTheme();
- const navigate = useNavigate();
-
+ const dispatch = useDispatch();
+ const { collapsed } = useSidebar();
return (
navigate("/")}
+ onClick={() => {
+ dispatch(toggleSidebar());
+ }}
sx={{ cursor: "pointer" }}
+ {...props}
>
C
@@ -49,9 +49,8 @@ const Logo = ({ collapsed }) => {
{t("common.appName")}
@@ -59,9 +58,3 @@ const Logo = ({ collapsed }) => {
);
};
-
-Logo.propTypes = {
- collapsed: PropTypes.bool,
-};
-
-export default Logo;
diff --git a/client/src/Components/v2/sidebar/Menu.tsx b/client/src/Components/v2/sidebar/Menu.tsx
new file mode 100644
index 000000000..918a82858
--- /dev/null
+++ b/client/src/Components/v2/sidebar/Menu.tsx
@@ -0,0 +1,120 @@
+import { Icon } from "@/Components/v2/design-elements";
+
+import {
+ Globe,
+ Gauge,
+ Link,
+ Bell,
+ FileText,
+ AlertTriangle,
+ Wifi,
+ Wrench,
+ Database,
+ Settings,
+ HelpCircle,
+ MessageCircle,
+ Code,
+ User,
+ Lock,
+ Users,
+} from "lucide-react";
+
+export const getMenu = (t: Function) => {
+ return [
+ {
+ name: t("components.sidebar.menu.uptime"),
+ path: "uptime",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.pagespeed"),
+ path: "pagespeed",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.infrastructure"),
+ path: "infrastructure",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.notifications"),
+ path: "notifications",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.checks"),
+ path: "checks",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.incidents"),
+ path: "incidents",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.statusPages"),
+ path: "status",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.maintenance"),
+ path: "maintenance",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.logs"),
+ path: "logs",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.menu.settings"),
+ icon: ,
+ path: "settings",
+ },
+ ];
+};
+
+export const getBottomMenu = (t: Function) => {
+ return [
+ {
+ name: t("components.sidebar.bottomMenu.support"),
+ path: "support",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.bottomMenu.discussions"),
+ path: "discussions",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.bottomMenu.docs"),
+ path: "docs",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.bottomMenu.changelog"),
+ path: "changelog",
+ icon: ,
+ },
+ ];
+};
+
+export const getAccountMenu = (t: Function) => {
+ return [
+ {
+ name: t("components.sidebar.accountMenu.profile"),
+ path: "account/profile",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.accountMenu.password"),
+ path: "account/password",
+ icon: ,
+ },
+ {
+ name: t("components.sidebar.accountMenu.team"),
+ path: "account/team",
+ icon: ,
+ },
+ ];
+};
diff --git a/client/src/Components/v1/Sidebar/components/navItem.jsx b/client/src/Components/v2/sidebar/NavItem.tsx
similarity index 58%
rename from client/src/Components/v1/Sidebar/components/navItem.jsx
rename to client/src/Components/v2/sidebar/NavItem.tsx
index 170d39bee..756b10d60 100644
--- a/client/src/Components/v1/Sidebar/components/navItem.jsx
+++ b/client/src/Components/v2/sidebar/NavItem.tsx
@@ -3,19 +3,31 @@ import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
-import { useTheme } from "@emotion/react";
-import PropTypes from "prop-types";
+import { useTheme } from "@mui/material/styles";
+import useSidebar from "@/Hooks/useSidebar";
-const NavItem = ({ item, collapsed, selected, onClick }) => {
+export interface NavData {
+ name: string;
+ icon: React.ReactElement;
+}
+
+export const NavItem = ({
+ item,
+ selected,
+ onClick,
+}: {
+ item: NavData;
+ selected: boolean;
+ onClick: (event: React.MouseEvent) => void;
+}) => {
+ const { collapsed } = useSidebar();
const theme = useTheme();
- const iconStroke = selected
- ? theme.palette.primary.contrastText
- : theme.palette.primary.contrastTextTertiary;
+ const iconStroke = selected ? theme.palette.primary.main : theme.palette.text.secondary;
- const buttonBgColor = selected ? theme.palette.secondary.main : "transparent";
+ const buttonBgColor = selected ? theme.palette.action.selected : "transparent";
const buttonBgHoverColor = selected
- ? theme.palette.secondary.main
- : theme.palette.tertiary.main;
+ ? theme.palette.action.selected
+ : theme.palette.action.hover;
const fontWeight = selected ? 600 : 400;
return (
{
{
{item.icon}
@@ -73,7 +96,6 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
>
{
);
};
-
-NavItem.propTypes = {
- item: PropTypes.object,
- collapsed: PropTypes.bool,
- selected: PropTypes.bool,
- onClick: PropTypes.func,
-};
-export default NavItem;
diff --git a/client/src/Components/v1/StarPrompt/index.jsx b/client/src/Components/v2/sidebar/StarPrompt.tsx
similarity index 64%
rename from client/src/Components/v1/StarPrompt/index.jsx
rename to client/src/Components/v2/sidebar/StarPrompt.tsx
index f1dd5e136..5319f6bcc 100644
--- a/client/src/Components/v1/StarPrompt/index.jsx
+++ b/client/src/Components/v2/sidebar/StarPrompt.tsx
@@ -1,18 +1,25 @@
-import React from "react";
import { Typography, IconButton, Stack, Box } from "@mui/material";
-import Icon from "../Icon";
-import { useTheme } from "@emotion/react";
+import { Icon } from "@/Components/v2/design-elements";
+import { X } from "lucide-react";
+
+import { useTheme } from "@mui/material";
import { useSelector, useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
-import { setStarPromptOpen } from "../../../Features/UI/uiSlice.js";
+import { setStarPromptOpen } from "@/Features/UI/uiSlice.js";
+import type { RootState } from "@/Types/state.js";
+import useSidebar from "@/Hooks/useSidebar.js";
-const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) => {
+export const StarPrompt = ({
+ repoUrl = "https://github.com/bluewave-labs/checkmate",
+}: {
+ repoUrl?: string;
+}) => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
- const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true);
- const mode = useSelector((state) => state.ui.mode);
-
+ const isOpen = useSelector((state: RootState) => state.ui?.starPromptOpen ?? true);
+ const mode = useSelector((state: RootState) => state.ui.mode);
+ const { collapsed } = useSidebar();
const handleClose = () => {
dispatch(setStarPromptOpen(false));
};
@@ -21,6 +28,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
window.open(repoUrl, "_blank");
};
+ if (collapsed) return null;
if (!isOpen) return null;
return (
@@ -29,8 +37,8 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
sx={{
width: "100%",
padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
- borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
- borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
+ borderTop: `1px solid ${theme.palette.divider}`,
+ borderBottom: `1px solid ${theme.palette.divider}`,
borderRadius: 0,
gap: theme.spacing(1.5),
}}
@@ -44,15 +52,9 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
>
- {t("starPromptTitle")}
+ {t("components.sidebar.starPrompt.title")}
- {t("starPromptDescription")}
+ {t("components.sidebar.starPrompt.description")}
);
};
-
-export default StarPrompt;
diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx
new file mode 100644
index 000000000..9bd36a853
--- /dev/null
+++ b/client/src/Components/v2/sidebar/index.tsx
@@ -0,0 +1,121 @@
+import { useEffect } from "react";
+import Stack from "@mui/material/Stack";
+import List from "@mui/material/List";
+import Divider from "@mui/material/Divider";
+import { useSidebar } from "@/Hooks/useSidebar.js";
+import { Logo } from "@/Components/v2/sidebar/Logo";
+import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/v2/sidebar/Menu";
+import { NavItem } from "@/Components/v2/sidebar/NavItem";
+import { StarPrompt } from "@/Components/v2/sidebar/StarPrompt";
+import { AuthFooter } from "@/Components/v2/sidebar/Authfooter";
+
+import { useNavigate, useLocation } from "react-router";
+import { useTranslation } from "react-i18next";
+import { useTheme, useMediaQuery } from "@mui/material";
+import { useDispatch } from "react-redux";
+import { setCollapsed } from "@/Features/UI/uiSlice";
+
+const URL_MAP: Record = {
+ support: "https://discord.com/invite/NAb6H3UTjK",
+ discussions: "https://github.com/bluewave-labs/checkmate/discussions",
+ docs: "https://bluewavelabs.gitbook.io/checkmate",
+ changelog: "https://github.com/bluewave-labs/checkmate/releases",
+};
+
+export const Sidebar = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { t } = useTranslation();
+ const { width, transition, collapsed } = useSidebar();
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const menu = getMenu(t);
+ const bottomMenu = getBottomMenu(t);
+ const accountMenu = getAccountMenu(t);
+
+ useEffect(() => {
+ dispatch(setCollapsed({ collapsed: isSmall }));
+ }, [isSmall, dispatch]);
+
+ const handleNavClick = (path: string) => {
+ const url = URL_MAP[path];
+ if (url) {
+ window.open(url, "_blank", "noreferrer");
+ } else {
+ navigate(`/${path}`);
+ }
+ if (isSmall) {
+ dispatch(setCollapsed({ collapsed: true }));
+ }
+ };
+
+ return (
+ t.zIndex.drawer : "auto",
+ }}
+ >
+
+
+ {menu.map((item) => {
+ const selected = location.pathname.startsWith(`/${item.path}`);
+ return (
+ handleNavClick(item.path)}
+ />
+ );
+ })}
+
+
+
+ {bottomMenu.map((item) => {
+ const selected = location.pathname.startsWith(`/${item.path}`);
+ return (
+ handleNavClick(item.path)}
+ />
+ );
+ })}
+
+
+
+
+
+ );
+};
diff --git a/client/src/Hooks/useSidebar.js b/client/src/Hooks/useSidebar.js
deleted file mode 100644
index 97724b417..000000000
--- a/client/src/Hooks/useSidebar.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useSelector } from "react-redux";
-
-// CSS variable names for sidebar widths
-const SIDEBAR_WIDTH_VAR = "var(--env-var-side-bar-width)";
-const SIDEBAR_COLLAPSED_WIDTH_VAR = "var(--env-var-side-bar-collapsed-width)";
-
-// Transition timing for sidebar width changes
-const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)";
-
-/**
- * Hook to get sidebar state and computed width
- * Centralizes sidebar width logic to avoid duplication between Sidebar and HomeLayout
- *
- * @returns {Object} Sidebar state and styles
- * @returns {boolean} collapsed - Whether the sidebar is collapsed
- * @returns {string} width - CSS width value based on collapsed state
- * @returns {string} transition - CSS transition for width changes
- */
-export const useSidebar = () => {
- const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false);
-
- return {
- collapsed,
- width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR,
- transition: SIDEBAR_TRANSITION,
- };
-};
-
-export default useSidebar;
diff --git a/client/src/Hooks/useSidebar.ts b/client/src/Hooks/useSidebar.ts
new file mode 100644
index 000000000..1cf705dd1
--- /dev/null
+++ b/client/src/Hooks/useSidebar.ts
@@ -0,0 +1,25 @@
+import type { RootState } from "@/Types/state";
+import { useSelector } from "react-redux";
+
+// CSS variable names for sidebar widths
+const SIDEBAR_WIDTH_VAR = 250;
+const SIDEBAR_COLLAPSED_WIDTH_VAR = 64;
+
+// Transition timing for sidebar width changes
+const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)";
+
+export const useSidebar = () => {
+ const collapsed = useSelector(
+ (state: RootState) => state.ui.sidebar?.collapsed ?? false
+ );
+
+ return {
+ collapsed,
+ collapsedWidth: SIDEBAR_COLLAPSED_WIDTH_VAR,
+ expandedWidth: SIDEBAR_WIDTH_VAR,
+ width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR,
+ transition: SIDEBAR_TRANSITION,
+ };
+};
+
+export default useSidebar;
diff --git a/client/src/Pages/Account/index.tsx b/client/src/Pages/Account/index.tsx
index b0fd6554e..4a57b539a 100644
--- a/client/src/Pages/Account/index.tsx
+++ b/client/src/Pages/Account/index.tsx
@@ -1,5 +1,5 @@
import { BasePage, Tabs, Tab } from "@/Components/v2/design-elements";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { TabProfile } from "./TabProfile";
import { TabPassword } from "./TabPassword";
@@ -19,6 +19,11 @@ const Account = ({ open = "profile" }: AccountProps) => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(TAB_MAP[open]);
+ // Sync activeTab when open prop changes (e.g., navigating from sidebar)
+ useEffect(() => {
+ setActiveTab(TAB_MAP[open]);
+ }, [open]);
+
return (
{
- const theme = useTheme();
- const navigate = useNavigate();
- const { t } = useTranslation();
-
- // State for tracking connection check status
- const [isCheckingConnection, setIsCheckingConnection] = useState(false);
-
- const handleRetry = React.useCallback(async () => {
- setIsCheckingConnection(true);
- try {
- // Try to connect to the backend with a simple API call
- // We'll use any lightweight endpoint that doesn't require authentication
- await get("/health", { timeout: 5000 });
-
- // If successful, show toast and navigate to login page
- createToast({
- body: t("errorPages.serverUnreachable.toasts.reconnected"),
- });
- navigate("/login");
- } catch (error) {
- // If still unreachable, stay on this page and show toast
- createToast({
- body: t("errorPages.serverUnreachable.toasts.stillUnreachable"),
- });
- } finally {
- setIsCheckingConnection(false);
- }
- }, [navigate, t]);
-
- return (
-
-
-
-
-
- {/* Header with logo */}
-
-
-
- {t("common.appName")}
-
-
-
-
-
-
- .MuiStack-root": {
- border: 1,
- borderRadius: theme.spacing(5),
- borderColor: theme.palette.primary.lowContrast,
- backgroundColor: theme.palette.primary.main,
- padding: {
- xs: theme.spacing(12),
- sm: theme.spacing(20),
- },
- },
- }}
- >
-
-
-
-
-
-
- {t("errorPages.serverUnreachable.description")}
-
-
-
-
-
-
-
-
- );
-};
-
-export default ServerUnreachable;
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 07dc1793c..7d9163c43 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -4,7 +4,7 @@ import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
import { useSelector } from "react-redux";
import { Navigate, Route, Routes as LibRoutes } from "react-router";
-import HomeLayout from "@/Components/v1/Layouts/HomeLayout";
+import RootLayout from "@/Components/v2/layout/RootLayout";
import NotFound from "@/Pages/NotFound";
// Auth
@@ -25,9 +25,6 @@ import PageSpeedDetails from "@/Pages/PageSpeed/Details/";
import Infrastructure from "@/Pages/Infrastructure/Monitors";
import InfrastructureDetails from "@/Pages/Infrastructure/Details/index";
-// Server Status
-import ServerUnreachable from "../Pages/ServerUnreachable.jsx";
-
// Checks
import Checks from "../Pages/Checks/index";
@@ -67,7 +64,7 @@ const Routes = () => {
path="/"
element={
-
+
}
>
@@ -432,10 +429,6 @@ const Routes = () => {
}
/>
- }
- />
void;
+let serverUnreachableCallback: ServerUnreachableCallback | null = null;
+
+export const setServerUnreachableCallback = (
+ callback: ServerUnreachableCallback
+): void => {
+ serverUnreachableCallback = callback;
+};
+
export const initApiClient = (store: StoreType): void => {
storeInstance = store;
@@ -39,8 +48,18 @@ export const initApiClient = (store: StoreType): void => {
}
);
- const onSuccess = (response: AxiosResponse) => response;
+ const onSuccess = (response: AxiosResponse) => {
+ // Server is reachable, hide offline banner if shown
+ serverUnreachableCallback?.(false);
+ return response;
+ };
const onError = (error: AxiosError) => {
+ // Handle network errors (server unreachable)
+ if (error.code === "ERR_NETWORK") {
+ serverUnreachableCallback?.(true);
+ return Promise.reject(error);
+ }
+
if (error.response?.status === 401) {
if (window.location.pathname !== "/login") {
window.location.href = "/login";
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index cbd9ea7b7..d2a066c11 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -325,6 +325,55 @@
"week": "Showing statistics for past 7 days.",
"month": "Showing statistics for past 30 days."
}
+ },
+ "offlineBanner": {
+ "serverUnreachable": "Unable to reach server",
+ "retry": "Retry",
+ "retrying": "Retrying...",
+ "reconnected": "Connection restored"
+ },
+ "sidebar": {
+ "menu": {
+ "uptime": "Uptime",
+ "pagespeed": "Pagespeed",
+ "infrastructure": "Infrastructure",
+ "notifications": "Notifications",
+ "checks": "Checks",
+ "incidents": "Incidents",
+ "statusPages": "Status pages",
+ "maintenance": "Maintenance",
+ "logs": "Logs",
+ "settings": "Settings"
+ },
+ "bottomMenu": {
+ "support": "Support",
+ "discussions": "Discussions",
+ "docs": "Docs",
+ "changelog": "Changelog"
+ },
+ "accountMenu": {
+ "profile": "Profile",
+ "password": "Password",
+ "team": "Team"
+ },
+ "starPrompt": {
+ "title": "Star Checkmate",
+ "description": "See the latest releases and help grow the community on GitHub"
+ },
+ "authFooter": {
+ "navControls": "Controls",
+ "logOut": "Log out",
+ "roles": {
+ "superAdmin": "Super admin",
+ "admin": "Admin",
+ "user": "User",
+ "demoUser": "Demo user"
+ }
+ }
+ },
+ "starPrompt": {
+ "title": "Star Checkmate",
+ "description": "See the latest releases and help grow the community on GitHub"
}
},
"configure": "Configure",
@@ -533,7 +582,7 @@
"notFoundButton": "Go to the main dashboard",
"notifications": {
"fallback": {
- "actionButton": "Let's create your first notification channel!",
+ "actionButton": "Create notification channel!",
"checks": [
"Alert teams about downtime or performance issues",
"Let engineers know when incidents happen",
@@ -970,7 +1019,7 @@
}
},
"fallback": {
- "actionButton": "Let's create your first infrastructure monitor!",
+ "actionButton": "Create a monitor!",
"checks": [
"Track the performance of your servers",
"Identify bottlenecks and optimize usage",
@@ -1038,7 +1087,7 @@
},
"maintenanceWindow": {
"fallback": {
- "actionButton": "Let's create your first maintenance window!",
+ "actionButton": "Create a maintenance window!",
"checks": [
"Mark your maintenance periods",
"Eliminate any misunderstandings",
@@ -1179,7 +1228,7 @@
}
},
"fallback": {
- "actionButton": "Let's create your first PageSpeed monitor!",
+ "actionButton": "Create a monitor!",
"checks": [
"Report on the user experience of a page",
"Help analyze webpage speed",
@@ -1198,7 +1247,7 @@
"Build trust with transparent service monitoring",
"Reduce support requests during incidents"
],
- "actionButton": "Let's create your first status page!"
+ "actionButton": "Create a status page!"
},
"monitorsList": {
"chartTypeHeatmap": "Heatmap",