mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-21 00:48:45 -05:00
Merge branch 'develop' into settings-zod-schema
This commit is contained in:
+1
-1
@@ -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() {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.alert {
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
.alert,
|
||||
.alert button,
|
||||
.alert .MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||
import Icon from "../Icon";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Icons mapping for different alert variants.
|
||||
* @type {Object<string, JSX.Element>}
|
||||
*/
|
||||
|
||||
const icons = {
|
||||
info: (
|
||||
<Icon
|
||||
name="Info"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
error: (
|
||||
<Icon
|
||||
name="AlertCircle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
warning: (
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {'info' | 'error' | 'warning'} props.variant - The type of alert.
|
||||
* @param {string} [props.title] - The title of the alert.
|
||||
* @param {string} [props.body] - The body text of the alert.
|
||||
* @param {boolean} [props.isToast] - Indicates if the alert is used as a toast notification.
|
||||
* @param {boolean} [props.hasIcon] - Whether to display an icon in the alert.
|
||||
* @param {function} props.onClick - Toast dismiss function.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
const theme = useTheme();
|
||||
/* TODO
|
||||
Do we need other variants for alert?
|
||||
*/
|
||||
|
||||
const text = theme.palette.secondary.contrastText;
|
||||
const border = theme.palette.alert.contrastText;
|
||||
const bg = theme.palette.alert.main;
|
||||
const icon = icons[variant];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems={hasIcon ? "" : "center"}
|
||||
className="alert row-stack"
|
||||
gap={theme.spacing(8)}
|
||||
sx={{
|
||||
padding: hasIcon ? theme.spacing(8) : `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
backgroundColor: bg,
|
||||
border: `solid 1px ${border}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}}
|
||||
>
|
||||
{hasIcon && <Box sx={{ color: text }}>{icon}</Box>}
|
||||
<Stack
|
||||
direction="column"
|
||||
gap="2px"
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{title && (
|
||||
<Typography sx={{ fontWeight: "700", color: `${text}` }}>{title}</Typography>
|
||||
)}
|
||||
{body && (
|
||||
<Typography sx={{ fontWeight: "400", color: `${text}` }}>{body}</Typography>
|
||||
)}
|
||||
{hasIcon && isToast && (
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
fontWeight: "600",
|
||||
width: "fit-content",
|
||||
mt: theme.spacing(4),
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
{isToast && (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
alignSelf: "flex-start",
|
||||
ml: "auto",
|
||||
mr: "-5px",
|
||||
mt: hasIcon ? "-5px" : 0,
|
||||
padding: "5px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="X"
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Alert.propTypes = {
|
||||
variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired,
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
isToast: PropTypes.bool,
|
||||
hasIcon: PropTypes.bool,
|
||||
onClick: function (props, propName, componentName) {
|
||||
if (props.isToast && !props[propName]) {
|
||||
return new Error(
|
||||
`Prop '${propName}' is required when 'isToast' is true in '${componentName}'.`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
@@ -1,36 +0,0 @@
|
||||
import Icon from "../Icon";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<Icon
|
||||
name="ChevronsLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (type === "long") {
|
||||
return (
|
||||
<Icon
|
||||
name="ArrowLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Icon
|
||||
name="ChevronLeft"
|
||||
color={color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ArrowLeft.propTypes = {
|
||||
color: PropTypes.string,
|
||||
type: PropTypes.oneOf(["double", "long", "default"]),
|
||||
};
|
||||
export default ArrowLeft;
|
||||
@@ -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
|
||||
* <Avatar src="assets/img" first="Alex" last="Holliday" small />
|
||||
*/
|
||||
|
||||
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 (
|
||||
<MuiAvatar
|
||||
onClick={onClick}
|
||||
alt={`${user?.firstName} ${user?.lastName}`}
|
||||
/* TODO What is the /static/images/avatar/2.jpg ?*/
|
||||
src={src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"}
|
||||
sx={{
|
||||
fontSize: small ? "16px" : "22px",
|
||||
fontWeight: 400,
|
||||
color: theme.palette.accent.contrastText,
|
||||
backgroundColor: theme.palette.accent.main, // Same BG color as checkmate BG in sidebar
|
||||
display: "inline-flex",
|
||||
/*
|
||||
TODO not sure what this is for*/
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
border: `${border}px solid rgba(255,255,255,0.2)`,
|
||||
borderRadius: "50%",
|
||||
},
|
||||
...style,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{user.firstName?.charAt(0)}
|
||||
{user.lastName?.charAt(0) || ""}
|
||||
</MuiAvatar>
|
||||
);
|
||||
};
|
||||
|
||||
Avatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
small: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { logger } from "../../../Utils/Logger.js";
|
||||
import { useLazyGet } from "@/Hooks/UseApi";
|
||||
|
||||
const withAdminCheck = (WrappedComponent) => {
|
||||
const WithAdminCheck = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const [superAdminExists, setSuperAdminExists] = useState(false);
|
||||
const [hasChecked, setHasChecked] = useState(false);
|
||||
const { get: checkSuperAdmin, loading: isChecking } = useLazyGet();
|
||||
|
||||
useEffect(() => {
|
||||
checkSuperAdmin("/auth/users/superadmin")
|
||||
.then((response) => {
|
||||
if (response?.data === true) {
|
||||
navigate("/login");
|
||||
} else {
|
||||
setSuperAdminExists(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setHasChecked(true);
|
||||
});
|
||||
}, [navigate, checkSuperAdmin]);
|
||||
|
||||
if (!hasChecked || isChecking) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
superAdminExists={superAdminExists}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const wrappedComponentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || "Component";
|
||||
WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`;
|
||||
|
||||
return WithAdminCheck;
|
||||
};
|
||||
|
||||
export default withAdminCheck;
|
||||
@@ -1,6 +0,0 @@
|
||||
.label {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
}
|
||||
@@ -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 (
|
||||
<Box
|
||||
className="label"
|
||||
sx={{
|
||||
borderRadius: borderRadius,
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
color: theme.palette.primary.contrastText,
|
||||
padding: padding,
|
||||
...styles,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{label}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
* <ColoredLabel label="Label" color="#FF0000" />
|
||||
*/
|
||||
|
||||
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 (
|
||||
<BaseLabel
|
||||
label={label}
|
||||
styles={{
|
||||
color: color,
|
||||
borderColor: borderColor,
|
||||
backgroundColor: bgColor,
|
||||
}}
|
||||
></BaseLabel>
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
* <StatusLabel status="up" text="Active" />
|
||||
*/
|
||||
|
||||
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 (
|
||||
<BaseLabel
|
||||
label={text}
|
||||
styles={{
|
||||
color: theme.palette[themeColor].main,
|
||||
borderColor: theme.palette[themeColor].lowContrast,
|
||||
...customStyles,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
bgcolor={theme.palette[themeColor].lowContrast}
|
||||
borderRadius="0%"
|
||||
marginRight="1px"
|
||||
/>
|
||||
</BaseLabel>
|
||||
);
|
||||
};
|
||||
|
||||
StatusLabel.propTypes = {
|
||||
status: PropTypes.oneOf([
|
||||
"up",
|
||||
"down",
|
||||
"paused",
|
||||
"pending",
|
||||
"cannot resolve",
|
||||
"published",
|
||||
"unpublished",
|
||||
]),
|
||||
text: PropTypes.string,
|
||||
customStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
export { BaseLabel, ColoredLabel, StatusLabel };
|
||||
@@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
backgroundColor: theme.palette.primaryBackground.main,
|
||||
backgroundImage: ui?.mode === "dark" ? `url("${BackgroundSVG}")` : "none",
|
||||
backgroundSize: "100% 100%",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
color: theme.palette.primary.contrastText,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
AppLayout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<Stack
|
||||
className="home-layout"
|
||||
flexDirection="row"
|
||||
>
|
||||
<Sidebar />
|
||||
{/* Spacer for fixed sidebar */}
|
||||
<Box
|
||||
sx={{
|
||||
width,
|
||||
flexShrink: 0,
|
||||
transition,
|
||||
}}
|
||||
/>
|
||||
<Stack className="home-content-wrapper">
|
||||
<Outlet />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeLayout;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ProtectedRoute is a wrapper component that ensures only authenticated users
|
||||
* can access the wrapped content. It checks authentication status (e.g., from Redux or Context).
|
||||
* If the user is authenticated, it renders the children; otherwise, it redirects to the login page.
|
||||
*
|
||||
* @param {Object} props - The props passed to the ProtectedRoute component.
|
||||
* @param {React.ReactNode} props.children - The children to render if the user is authenticated.
|
||||
* @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
|
||||
*/
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
return authState.authToken ? (
|
||||
children
|
||||
) : (
|
||||
<Navigate
|
||||
to="/login"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProtectedRoute.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ProtectedRoute is a wrapper component that ensures only authenticated users
|
||||
* can access the wrapped content. It checks authentication status (e.g., from Redux or Context).
|
||||
* If the user is authenticated, it renders the children; otherwise, it redirects to the login page.
|
||||
*
|
||||
* @param {Object} props - The props passed to the ProtectedRoute component.
|
||||
* @param {React.ReactNode} props.children - The children to render if the user is authenticated.
|
||||
* @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
|
||||
*/
|
||||
|
||||
const RoleProtectedRoute = ({ roles, children }) => {
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const userRoles = authState?.user?.role || [];
|
||||
const canAccess = userRoles.some((role) => roles.includes(role));
|
||||
|
||||
return canAccess ? (
|
||||
children
|
||||
) : (
|
||||
<Navigate
|
||||
to="/uptime"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
RoleProtectedRoute.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
roles: PropTypes.array,
|
||||
};
|
||||
|
||||
export default RoleProtectedRoute;
|
||||
@@ -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) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
onClick={() => {
|
||||
closePopup();
|
||||
navigate(item.path);
|
||||
}}
|
||||
sx={{
|
||||
gap: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
"& svg": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
));
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
height="var(--env-var-side-bar-auth-footer-height)"
|
||||
alignItems="center"
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(8)}
|
||||
gap={theme.spacing(2)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
boxSizing={"border-box"}
|
||||
>
|
||||
<Avatar
|
||||
small={true}
|
||||
onClick={(e) => collapsed && openPopup(e)}
|
||||
sx={{
|
||||
cursor: collapsed ? "pointer" : "default",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(2)}
|
||||
minWidth={0}
|
||||
maxWidth={collapsed ? 0 : "100%"}
|
||||
sx={{
|
||||
opacity: collapsed ? 0 : 1,
|
||||
transition: "opacity 300ms ease, max-width 300ms ease",
|
||||
transitionDelay: collapsed ? "0ms" : "300ms",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
ml={theme.spacing(2)}
|
||||
sx={{
|
||||
maxWidth: "50%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontWeight={500}
|
||||
lineHeight={1}
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontSize={"var(--env-var-font-size-small)"}
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
sx={{ textTransform: "capitalize", opacity: 0.8 }}
|
||||
>
|
||||
{getRoleDisplayText(authState.user, t)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Tooltip
|
||||
title={t("navControls")}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
ml: "50px",
|
||||
"&:focus": { outline: "none" },
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onClick={(event) => openPopup(event)}
|
||||
>
|
||||
<Icon
|
||||
name="MoreVertical"
|
||||
size={22}
|
||||
color="primary.contrastTextTertiary"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Menu
|
||||
className="sidebar-popup"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={closePopup}
|
||||
disableScrollLock
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
marginTop: theme.spacing(-4),
|
||||
marginLeft: collapsed ? theme.spacing(2) : 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
MenuListProps={{
|
||||
sx: {
|
||||
p: 2,
|
||||
"& li": { m: 0 },
|
||||
"& li:has(.MuiBox-root):hover": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{collapsed && (
|
||||
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
|
||||
<Box
|
||||
mb={theme.spacing(2)}
|
||||
sx={{
|
||||
minWidth: "50%",
|
||||
maxWidth: "max-content",
|
||||
overflow: "visible",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
textOverflow: "clip",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
fontSize: 12,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{authState.user?.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* TODO Do we need two dividers? */}
|
||||
{collapsed && <Divider />}
|
||||
{/* <Divider /> */}
|
||||
{renderAccountMenuItems(authState.user, accountMenuItems)}
|
||||
<MenuItem
|
||||
onClick={logout}
|
||||
sx={{
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="LogOut"
|
||||
size={20}
|
||||
color="primary.contrastTextTertiary"
|
||||
/>
|
||||
{t("menu.logOut", "Log out")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
AuthFooter.propTypes = {
|
||||
collapsed: PropTypes.bool,
|
||||
accountMenuItems: PropTypes.array,
|
||||
};
|
||||
|
||||
export default AuthFooter;
|
||||
@@ -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 ? (
|
||||
<ArrowRight
|
||||
height={theme.spacing(8)}
|
||||
width={theme.spacing(8)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
/>
|
||||
) : (
|
||||
<ArrowLeft
|
||||
height={theme.spacing(8)}
|
||||
width={theme.spacing(8)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
/* TODO 60 is a magic number. if logo chnges size this might break */
|
||||
top: 60,
|
||||
right: 0,
|
||||
transform: `translate(50%, 0)`,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
p: theme.spacing(2.5),
|
||||
|
||||
"&:focus": { outline: "none" },
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(toggleSidebar());
|
||||
}}
|
||||
>
|
||||
{arrowIcon}
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
CollapseButton.propTypes = {
|
||||
collapsed: PropTypes.bool.isRequired,
|
||||
};
|
||||
export default CollapseButton;
|
||||
@@ -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: <Icon name="Globe" /> },
|
||||
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <Icon name="Gauge" /> },
|
||||
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Icon name="Link" /> },
|
||||
{
|
||||
name: t("menu.notifications"),
|
||||
path: "notifications",
|
||||
icon: <Icon name="Bell" />,
|
||||
},
|
||||
{ name: t("menu.checks"), path: "checks", icon: <Icon name="FileText" /> },
|
||||
{ name: t("menu.incidents"), path: "incidents", icon: <Icon name="AlertTriangle" /> },
|
||||
{ name: t("menu.statusPages"), path: "status", icon: <Icon name="Wifi" /> },
|
||||
{ name: t("menu.maintenance"), path: "maintenance", icon: <Icon name="Wrench" /> },
|
||||
{ name: t("menu.logs"), path: "logs", icon: <Icon name="Database" /> },
|
||||
{
|
||||
name: t("menu.settings"),
|
||||
icon: <Icon name="Settings" />,
|
||||
path: "settings",
|
||||
},
|
||||
];
|
||||
|
||||
const getOtherMenuItems = (t) => [
|
||||
{ name: t("menu.support"), path: "support", icon: <Icon name="HelpCircle" /> },
|
||||
{
|
||||
name: t("menu.discussions"),
|
||||
path: "discussions",
|
||||
icon: <Icon name="MessageCircle" />,
|
||||
},
|
||||
{ name: t("menu.docs"), path: "docs", icon: <Icon name="FileText" /> },
|
||||
{ name: t("menu.changelog"), path: "changelog", icon: <Icon name="Code" /> },
|
||||
];
|
||||
|
||||
const getAccountMenuItems = (t) => [
|
||||
{ name: t("menu.profile"), path: "account/profile", icon: <Icon name="User" /> },
|
||||
{ name: t("menu.password"), path: "account/password", icon: <Icon name="Lock" /> },
|
||||
{ name: t("menu.team"), path: "account/team", icon: <Icon name="Users" /> },
|
||||
];
|
||||
|
||||
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 (
|
||||
<Stack
|
||||
height="100vh"
|
||||
width={width}
|
||||
component="aside"
|
||||
position="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
paddingTop={theme.spacing(6)}
|
||||
paddingBottom={theme.spacing(6)}
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
transition,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<CollapseButton collapsed={collapsed} />
|
||||
<Logo collapsed={collapsed} />
|
||||
<List
|
||||
component="nav"
|
||||
aria-labelledby="nested-menu-subheader"
|
||||
disablePadding
|
||||
sx={{
|
||||
px: theme.spacing(6),
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{menu.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
selected={selected}
|
||||
onClick={() => navigate(`/${item.path}`)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
{!collapsed && <StarPrompt />}
|
||||
<List
|
||||
component="nav"
|
||||
disablePadding
|
||||
sx={{ px: theme.spacing(6) }}
|
||||
>
|
||||
{otherMenuItems.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
selected={selected}
|
||||
onClick={() => {
|
||||
const url = URL_MAP[item.path];
|
||||
if (url) {
|
||||
window.open(url, "_blank", "noreferrer");
|
||||
} else {
|
||||
navigate(`/${item.path}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
|
||||
<AuthFooter
|
||||
collapsed={collapsed}
|
||||
accountMenuItems={accountMenuItems}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,98 +0,0 @@
|
||||
import { useTheme } from "@mui/material";
|
||||
import "./index.css";
|
||||
|
||||
const SunAndMoonIcon = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<svg
|
||||
className="sun-and-moon"
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<mask
|
||||
className="moon"
|
||||
id="moon-mask"
|
||||
>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle
|
||||
cx="24"
|
||||
cy="10"
|
||||
r="6"
|
||||
fill="#000"
|
||||
/>
|
||||
</mask>
|
||||
<circle
|
||||
className="sun"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="6"
|
||||
fill={theme.palette.primary.contrastTextSecondary}
|
||||
mask="url(#moon-mask)"
|
||||
/>
|
||||
<g
|
||||
className="sun-beams"
|
||||
stroke={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
<line
|
||||
x1="12"
|
||||
y1="1"
|
||||
x2="12"
|
||||
y2="3"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
y1="21"
|
||||
x2="12"
|
||||
y2="23"
|
||||
/>
|
||||
<line
|
||||
x1="4.22"
|
||||
y1="4.22"
|
||||
x2="5.64"
|
||||
y2="5.64"
|
||||
/>
|
||||
<line
|
||||
x1="18.36"
|
||||
y1="18.36"
|
||||
x2="19.78"
|
||||
y2="19.78"
|
||||
/>
|
||||
<line
|
||||
x1="1"
|
||||
y1="12"
|
||||
x2="3"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="21"
|
||||
y1="12"
|
||||
x2="23"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="4.22"
|
||||
y1="19.78"
|
||||
x2="5.64"
|
||||
y2="18.36"
|
||||
/>
|
||||
<line
|
||||
x1="18.36"
|
||||
y1="5.64"
|
||||
x2="19.78"
|
||||
y2="4.22"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default SunAndMoonIcon;
|
||||
@@ -1,64 +0,0 @@
|
||||
.sun-and-moon > :is(.moon, .sun, .sun-beams) {
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.theme-toggle .sun-and-moon > .sun-beams {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.theme-dark .sun-and-moon > .sun {
|
||||
transform: scale(1.75);
|
||||
}
|
||||
|
||||
.theme-dark .sun-and-moon > .sun-beams {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.theme-dark .sun-and-moon > .moon > circle {
|
||||
transform: translateX(-7px);
|
||||
}
|
||||
|
||||
@supports (cx: 1) {
|
||||
.theme-dark .sun-and-moon > .moon > circle {
|
||||
cx: 17;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.sun-and-moon > .sun {
|
||||
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
}
|
||||
|
||||
.sun-and-moon > .sun-beams {
|
||||
transition:
|
||||
transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55),
|
||||
opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
|
||||
.sun-and-moon .moon > circle {
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@supports (cx: 1) {
|
||||
.sun-and-moon .moon > circle {
|
||||
transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-dark .sun-and-moon > .sun {
|
||||
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
transition-duration: 0.25s;
|
||||
transform: scale(1.75);
|
||||
}
|
||||
|
||||
.theme-dark .sun-and-moon > .sun-beams {
|
||||
transition-duration: 0.15s;
|
||||
transform: rotateZ(-25deg);
|
||||
}
|
||||
|
||||
.theme-dark .sun-and-moon > .moon > circle {
|
||||
transition-duration: 0.5s;
|
||||
transition-delay: 0.25s;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* ThemeSwitch Component
|
||||
* Dark and Light Theme Switch
|
||||
* Original Code: https://web.dev/patterns/theming/theme-switch
|
||||
* License: Apache License 2.0
|
||||
* Copyright © Google LLC
|
||||
*
|
||||
* This code has been adapted for use in this project.
|
||||
* Apache License: https://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
import { IconButton } from "@mui/material";
|
||||
import SunAndMoonIcon from "./SunAndMoonIcon.jsx";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setMode } from "../../../Features/UI/uiSlice.js";
|
||||
import "./index.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const ThemeSwitch = ({ width = 48, height = 48, color }) => {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleTheme = () => {
|
||||
dispatch(setMode(mode === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
id="theme-toggle"
|
||||
title={t("common.buttons.toggleTheme")}
|
||||
className={`theme-${mode}`}
|
||||
aria-label="auto"
|
||||
aria-live="polite"
|
||||
onClick={toggleTheme}
|
||||
sx={{
|
||||
width,
|
||||
height,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
"& svg >:is(circle, g)": {
|
||||
fill: color,
|
||||
stroke: color,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SunAndMoonIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSwitch;
|
||||
@@ -0,0 +1,48 @@
|
||||
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<HTMLDivElement>) => 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<string>();
|
||||
useEffect(() => {
|
||||
if (user.avatarImage) {
|
||||
setImage(`data:image/png;base64,${user.avatarImage}`);
|
||||
}
|
||||
}, [user?.avatarImage]);
|
||||
|
||||
return (
|
||||
<MuiAvatar
|
||||
onClick={onClick}
|
||||
alt={`${user?.firstName} ${user?.lastName}`}
|
||||
src={src ? src : user?.avatarImage ? image : undefined}
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastText,
|
||||
fontSize: small ? "16px" : "22px",
|
||||
fontWeight: 400,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
display: "inline-flex",
|
||||
|
||||
...style,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{user.firstName?.charAt(0)}
|
||||
{user.lastName?.charAt(0) || ""}
|
||||
</MuiAvatar>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ interface IconProps {
|
||||
icon: LucideIcon;
|
||||
size?: number;
|
||||
strokeWidth?: number;
|
||||
stroke?: string;
|
||||
}
|
||||
|
||||
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => {
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: isAnimating ? 0 : "-100%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: theme.zIndex.snackbar,
|
||||
backgroundColor: theme.palette.error.main,
|
||||
color: theme.palette.error.contrastText,
|
||||
px: theme.spacing(8),
|
||||
py: theme.spacing(4),
|
||||
transition: "top 1s ease-in-out",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<WifiOff size={20} />
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight={500}
|
||||
>
|
||||
{t("components.offlineBanner.serverUnreachable")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -105,6 +105,29 @@ export const PausedStatusBox = ({ n }: { n: number }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const MaintenanceStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.maintenance")}
|
||||
n={n}
|
||||
color={theme.palette.warning.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const InitializingStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.initializing")}
|
||||
n={n}
|
||||
color={theme.palette.warning.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TotalChecksBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -138,15 +161,3 @@ export const UpChecksBox = ({ n }: { n: number }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InitializingStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.initializing")}
|
||||
n={n}
|
||||
color={theme.palette.warning.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,33 +11,24 @@ import { useTranslation } from "react-i18next";
|
||||
export const ValueTypes = ["positive", "negative", "neutral"] as const;
|
||||
export type ValueType = (typeof ValueTypes)[number];
|
||||
|
||||
export const StatusLabel = ({
|
||||
status,
|
||||
isActive,
|
||||
sx,
|
||||
}: {
|
||||
status: MonitorStatus;
|
||||
isActive?: boolean;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProps }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const palette = getStatusPalette(status);
|
||||
|
||||
const determineStatus = (
|
||||
isActive: boolean | undefined,
|
||||
status: MonitorStatus
|
||||
): string => {
|
||||
if (isActive === false) {
|
||||
const determineStatus = (status: MonitorStatus): string => {
|
||||
if (status === "up") {
|
||||
return t("pages.common.monitors.status.up");
|
||||
} else if (status === "down") {
|
||||
return t("pages.common.monitors.status.down");
|
||||
} else if (status === "maintenance") {
|
||||
return t("pages.common.monitors.status.maintenance");
|
||||
} else if (status === "paused") {
|
||||
return t("pages.common.monitors.status.paused");
|
||||
} else if (status === "initializing") {
|
||||
return t("pages.common.monitors.status.initializing");
|
||||
}
|
||||
|
||||
if (status === true) {
|
||||
return t("pages.common.monitors.status.up");
|
||||
}
|
||||
if (status === false) {
|
||||
return t("pages.common.monitors.status.down");
|
||||
}
|
||||
return t("pages.common.monitors.status.initializing");
|
||||
};
|
||||
|
||||
@@ -64,9 +55,7 @@ export const StatusLabel = ({
|
||||
borderRadius="50%"
|
||||
marginRight="5px"
|
||||
/>
|
||||
<Typography textTransform={"capitalize"}>
|
||||
{determineStatus(isActive, status)}
|
||||
</Typography>
|
||||
<Typography textTransform={"capitalize"}>{determineStatus(status)}</Typography>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,3 +19,5 @@ export * from "./Gauge";
|
||||
export * from "./Tabs";
|
||||
export * from "./SplitBox";
|
||||
export * from "./TextLink";
|
||||
export * from "./OfflineBanner";
|
||||
export * from "./Avatar";
|
||||
|
||||
@@ -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<number | null>(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 (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
// @ts-expect-error custom palette property
|
||||
backgroundColor: theme.palette.primaryBackground.main,
|
||||
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
|
||||
backgroundSize: "100% 100%",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
color: theme.palette.primary.contrastText,
|
||||
}}
|
||||
>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<OfflineBanner visible={serverUnreachable} />
|
||||
</ThemeProvider>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -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 (
|
||||
<Stack flexDirection="row">
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<Sidebar />
|
||||
</ThemeProvider>
|
||||
<Stack
|
||||
flex={1}
|
||||
padding={6}
|
||||
overflow={"hidden"}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark"
|
||||
? "rgba(255, 255, 255, 0.01)"
|
||||
: "rgba(0, 0, 0, 0.01)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingLeft: isSmall ? `${collapsedWidth + 12}px` : 12,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
maxWidth={1280}
|
||||
width="100%"
|
||||
paddingY={theme.spacing(6)}
|
||||
flex={1}
|
||||
>
|
||||
<Outlet />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
InitializingStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import Stack from "@mui/material/Stack";
|
||||
|
||||
import type { MonitorsSummary } from "@/Types/Monitor";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
interface MonitorsSummaryProps {
|
||||
summary: MonitorsSummary | null;
|
||||
}
|
||||
|
||||
export const HeaderMonitorsSummary = ({ summary }: MonitorsSummaryProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
<InitializingStatusBox n={summary?.initializingMonitors || 0} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -11,3 +11,4 @@ export * from "./charts/PiePageSpeedLegend";
|
||||
export * from "./charts/HistogramPageSpeedDetails";
|
||||
export * from "./charts/HistogramPageSpeedDetailsTooltip";
|
||||
export * from "./charts/HistogramInfrastructure";
|
||||
export * from "./HeaderMonitorsSummary";
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import type { UserRole } from "@/Types/User";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
return authState.authToken ? (
|
||||
children
|
||||
) : (
|
||||
<Navigate
|
||||
to="/login"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface RoleProtectedRouteProps {
|
||||
roles: UserRole[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const RoleProtectedRoute = ({ roles, children }: RoleProtectedRouteProps) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
const userRoles = authState?.user?.role || [];
|
||||
const canAccess = userRoles.some((role) => roles.includes(role));
|
||||
|
||||
return canAccess ? (
|
||||
children
|
||||
) : (
|
||||
<Navigate
|
||||
to="/uptime"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 | HTMLElement>(null);
|
||||
|
||||
const menuOpen = Boolean(anchorEl);
|
||||
const handleMenuOpen = (e: React.MouseEvent<HTMLElement>) =>
|
||||
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 (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(8)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Avatar
|
||||
small
|
||||
onClick={collapsed ? handleMenuOpen : undefined}
|
||||
sx={{ cursor: collapsed ? "pointer" : "default" }}
|
||||
/>
|
||||
|
||||
{!collapsed && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
minWidth={0}
|
||||
flex={1}
|
||||
>
|
||||
<Stack
|
||||
minWidth={0}
|
||||
flex={1}
|
||||
>
|
||||
<Typography
|
||||
fontWeight={500}
|
||||
noWrap
|
||||
sx={{ color: theme.palette.text.primary }}
|
||||
>
|
||||
{user?.firstName} {user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
noWrap
|
||||
sx={{ textTransform: "capitalize", color: theme.palette.text.secondary }}
|
||||
>
|
||||
{getRoleText()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
sx={{
|
||||
"&:focus": { outline: "none" },
|
||||
"& svg": { stroke: theme.palette.text.secondary },
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={MoreVertical}
|
||||
size={22}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={menuOpen}
|
||||
onClose={handleMenuClose}
|
||||
disableScrollLock
|
||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
slotProps={{
|
||||
paper: { sx: { mt: theme.spacing(-4), ml: collapsed ? theme.spacing(2) : 0 } },
|
||||
}}
|
||||
MenuListProps={{ sx: { p: 2, "& li": { m: 0 } } }}
|
||||
sx={{ ml: theme.spacing(4) }}
|
||||
>
|
||||
{collapsed && (
|
||||
<Box
|
||||
px={2}
|
||||
pb={2}
|
||||
sx={{ pointerEvents: "none" }}
|
||||
>
|
||||
<Typography
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
>
|
||||
{user?.firstName} {user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize={12}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{getRoleText()}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{filteredMenuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
onClick={() => handleNavigate(item.path)}
|
||||
sx={menuItemSx}
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
sx={menuItemSx}
|
||||
>
|
||||
<Icon
|
||||
icon={LogOut}
|
||||
size={20}
|
||||
/>
|
||||
{t("components.sidebar.authFooter.logOut")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
+15
-22
@@ -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 (
|
||||
<Stack
|
||||
pt={theme.spacing(6)}
|
||||
pb={theme.spacing(12)}
|
||||
pl={theme.spacing(8)}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={() => navigate("/")}
|
||||
onClick={() => {
|
||||
dispatch(toggleSidebar());
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
{...props}
|
||||
>
|
||||
<Typography
|
||||
pl={theme.spacing("1px")}
|
||||
minWidth={theme.spacing(16)}
|
||||
minHeight={theme.spacing(16)}
|
||||
display={"flex"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
backgroundColor={theme.palette.accent.main}
|
||||
bgcolor={theme.palette.primary.main}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
color={theme.palette.accent.contrastText}
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontSize={18}
|
||||
>
|
||||
C
|
||||
@@ -49,9 +49,8 @@ const Logo = ({ collapsed }) => {
|
||||
<Typography
|
||||
lineHeight={1}
|
||||
mt={theme.spacing(2)}
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontSize={"var(--env-var-font-size-medium-plus)"}
|
||||
sx={{ fontWeight: 500 }}
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
>
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
@@ -59,9 +58,3 @@ const Logo = ({ collapsed }) => {
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Logo.propTypes = {
|
||||
collapsed: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
@@ -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: <Icon icon={Globe} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.pagespeed"),
|
||||
path: "pagespeed",
|
||||
icon: <Icon icon={Gauge} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.infrastructure"),
|
||||
path: "infrastructure",
|
||||
icon: <Icon icon={Link} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.notifications"),
|
||||
path: "notifications",
|
||||
icon: <Icon icon={Bell} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.checks"),
|
||||
path: "checks",
|
||||
icon: <Icon icon={FileText} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.incidents"),
|
||||
path: "incidents",
|
||||
icon: <Icon icon={AlertTriangle} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.statusPages"),
|
||||
path: "status",
|
||||
icon: <Icon icon={Wifi} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.maintenance"),
|
||||
path: "maintenance",
|
||||
icon: <Icon icon={Wrench} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.logs"),
|
||||
path: "logs",
|
||||
icon: <Icon icon={Database} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.settings"),
|
||||
icon: <Icon icon={Settings} />,
|
||||
path: "settings",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getBottomMenu = (t: Function) => {
|
||||
return [
|
||||
{
|
||||
name: t("components.sidebar.bottomMenu.support"),
|
||||
path: "support",
|
||||
icon: <Icon icon={HelpCircle} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.bottomMenu.discussions"),
|
||||
path: "discussions",
|
||||
icon: <Icon icon={MessageCircle} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.bottomMenu.docs"),
|
||||
path: "docs",
|
||||
icon: <Icon icon={FileText} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.bottomMenu.changelog"),
|
||||
path: "changelog",
|
||||
icon: <Icon icon={Code} />,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getAccountMenu = (t: Function) => {
|
||||
return [
|
||||
{
|
||||
name: t("components.sidebar.accountMenu.profile"),
|
||||
path: "account/profile",
|
||||
icon: <Icon icon={User} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.accountMenu.password"),
|
||||
path: "account/password",
|
||||
icon: <Icon icon={Lock} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.accountMenu.team"),
|
||||
path: "account/team",
|
||||
icon: <Icon icon={Users} />,
|
||||
},
|
||||
];
|
||||
};
|
||||
+36
-22
@@ -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 (
|
||||
<Tooltip
|
||||
@@ -38,10 +50,14 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
|
||||
<ListItemButton
|
||||
sx={{
|
||||
backgroundColor: buttonBgColor,
|
||||
backgroundImage: "none",
|
||||
border: 1,
|
||||
borderColor: "transparent",
|
||||
"&:hover": {
|
||||
backgroundColor: buttonBgHoverColor,
|
||||
backgroundImage: "none",
|
||||
},
|
||||
height: 37,
|
||||
height: 32,
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
@@ -52,13 +68,20 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
color: iconStroke,
|
||||
"& svg": {
|
||||
height: 20,
|
||||
width: 20,
|
||||
height: 16,
|
||||
width: 16,
|
||||
opacity: 0.81,
|
||||
transition: "stroke 0.2s ease",
|
||||
},
|
||||
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
|
||||
stroke: iconStroke,
|
||||
},
|
||||
".MuiListItemButton-root:hover &": {
|
||||
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
|
||||
stroke: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
@@ -73,7 +96,6 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
sx={{
|
||||
fontWeight: fontWeight,
|
||||
opacity: 0.9,
|
||||
@@ -86,11 +108,3 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
NavItem.propTypes = {
|
||||
item: PropTypes.object,
|
||||
collapsed: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
export default NavItem;
|
||||
+22
-27
@@ -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" })
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color:
|
||||
mode === "dark"
|
||||
? theme.palette.primary.contrastText
|
||||
: theme.palette.text.primary,
|
||||
mt: theme.spacing(3),
|
||||
}}
|
||||
mt={theme.spacing(3)}
|
||||
>
|
||||
{t("starPromptTitle")}
|
||||
{t("components.sidebar.starPrompt.title")}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
@@ -60,7 +62,6 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
padding: 0,
|
||||
marginTop: theme.spacing(-5),
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent",
|
||||
opacity: 0.8,
|
||||
@@ -68,23 +69,19 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="X"
|
||||
icon={X}
|
||||
size={20}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: "0.938rem",
|
||||
lineHeight: 1.5,
|
||||
mb: 1,
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{t("starPromptDescription")}
|
||||
{t("components.sidebar.starPrompt.description")}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
@@ -106,5 +103,3 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default StarPrompt;
|
||||
@@ -0,0 +1,131 @@
|
||||
import { useEffect } from "react";
|
||||
import Backdrop from "@mui/material/Backdrop";
|
||||
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<string, string> = {
|
||||
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 (
|
||||
<>
|
||||
<Backdrop
|
||||
open={!collapsed && isSmall}
|
||||
onClick={() => dispatch(setCollapsed({ collapsed: true }))}
|
||||
sx={{ zIndex: 999 }}
|
||||
/>
|
||||
<Stack
|
||||
component="aside"
|
||||
position={isSmall ? "fixed" : "sticky"}
|
||||
top={0}
|
||||
left={0}
|
||||
minHeight={"100vh"}
|
||||
maxHeight={"100vh"}
|
||||
paddingTop={theme.spacing(6)}
|
||||
paddingBottom={theme.spacing(6)}
|
||||
gap={theme.spacing(6)}
|
||||
borderRight={`1px solid ${theme.palette.divider}`}
|
||||
width={width}
|
||||
sx={{
|
||||
touchAction: "none",
|
||||
transition: transition,
|
||||
zIndex: 1000,
|
||||
backdropFilter: "blur(8px)",
|
||||
}}
|
||||
>
|
||||
<List
|
||||
component="nav"
|
||||
disablePadding
|
||||
sx={{
|
||||
px: theme.spacing(6),
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Logo
|
||||
pt={theme.spacing(8)}
|
||||
pb={theme.spacing(10)}
|
||||
/>
|
||||
{menu.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
selected={selected}
|
||||
onClick={() => handleNavClick(item.path)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<StarPrompt />
|
||||
<List
|
||||
component="nav"
|
||||
disablePadding
|
||||
sx={{ px: theme.spacing(6) }}
|
||||
>
|
||||
{bottomMenu.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
selected={selected}
|
||||
onClick={() => handleNavClick(item.path)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider sx={{ borderColor: theme.palette.divider }} />
|
||||
|
||||
<AuthFooter
|
||||
collapsed={collapsed}
|
||||
accountMenuItems={accountMenu}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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<number>(TAB_MAP[open]);
|
||||
|
||||
// Sync activeTab when open prop changes (e.g., navigating from sidebar)
|
||||
useEffect(() => {
|
||||
setActiveTab(TAB_MAP[open]);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<Tabs
|
||||
|
||||
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
|
||||
import { useRegisterForm } from "@/Hooks/useRegisterForm";
|
||||
import type { RegisterFormData } from "@/Validation/register";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
import { usePost, useGet } from "@/Hooks/UseApi";
|
||||
import { setAuthState } from "@/Features/Auth/authSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
@@ -32,11 +32,21 @@ const RegisterPage = () => {
|
||||
const { post: verifyToken } = usePost<{ token: string }, InviteVerifyResponse>();
|
||||
const hasVerified = useRef(false);
|
||||
|
||||
const { data: superAdminExists, isLoading: isCheckingAdmin } = useGet<boolean>(
|
||||
token ? null : "/auth/users/superadmin"
|
||||
);
|
||||
|
||||
const { control, handleSubmit, setError, reset } = useForm<RegisterFormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaults,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (superAdminExists === true) {
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
}, [superAdminExists, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || hasVerified.current) return;
|
||||
hasVerified.current = true;
|
||||
@@ -53,6 +63,8 @@ const RegisterPage = () => {
|
||||
});
|
||||
}, [token]);
|
||||
|
||||
if (isCheckingAdmin) return null;
|
||||
|
||||
const onSubmit = async (data: RegisterFormData) => {
|
||||
if (loading) return;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/Components/v2/design-elements";
|
||||
import Box from "@mui/material/Box";
|
||||
import type { Header } from "@/Components/v2/design-elements/Table";
|
||||
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
@@ -52,7 +52,7 @@ export const ChecksTable = ({
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status as MonitorStatus} />;
|
||||
return <StatusLabel status={row.status === true ? "up" : "down"} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -195,12 +195,7 @@ export const InfraMonitorsTable = ({
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
isActive={row.isActive}
|
||||
/>
|
||||
);
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import {
|
||||
MonitorBasePageWithStates,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { MonitorBasePageWithStates } from "@/Components/v2/design-elements";
|
||||
import { HeaderCreate } from "@/Components/v2/common";
|
||||
import { ControlsFilter } from "@/Components/v2/monitors";
|
||||
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/v2/monitors";
|
||||
import { TextField, Dialog } from "@/Components/v2/inputs";
|
||||
|
||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||
@@ -99,7 +94,7 @@ const InfrastructureMonitors = () => {
|
||||
{ refreshInterval: 5000, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { summary, count } = monitorsWithChecksData ?? {};
|
||||
const { summary, count } = monitorsWithChecksData ?? { summary: null, count: 0 };
|
||||
const isLoading = monitorsWithChecksLoading;
|
||||
|
||||
// Check if any filters are active
|
||||
@@ -138,14 +133,7 @@ const InfrastructureMonitors = () => {
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
</Stack>
|
||||
<HeaderMonitorsSummary summary={summary} />
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
justifyContent={isSmall ? "flex-start" : "space-between"}
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import {
|
||||
MonitorBasePageWithStates,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
PageSpeedKeyPriorityFallback,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { Dialog } from "@/Components/v2/inputs";
|
||||
import { HeaderCreate } from "@/Components/v2/common";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
|
||||
import type { AppSettingsResponse } from "@/Types/Settings";
|
||||
import { HeaderMonitorsSummary } from "@/Components/v2/monitors";
|
||||
|
||||
const PageSpeedMonitorsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
|
||||
@@ -48,8 +43,8 @@ const PageSpeedMonitorsPage = () => {
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
|
||||
const monitors = monitorsData?.monitors;
|
||||
const monitorsCount = monitorsData?.count;
|
||||
const summary = monitorsData?.summary;
|
||||
const monitorsCount = monitorsData?.count ?? 0;
|
||||
const summary = monitorsData?.summary ?? null;
|
||||
|
||||
const isLoading = monitorsIsLoading || settingsIsLoading;
|
||||
|
||||
@@ -80,14 +75,7 @@ const PageSpeedMonitorsPage = () => {
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
</Stack>
|
||||
<HeaderMonitorsSummary summary={summary} />
|
||||
<PageSpeedMonitorsTable
|
||||
monitors={monitors || []}
|
||||
refetch={refetch}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Typography, Button, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { get } from "@/Utils/ApiClient";
|
||||
import Alert from "@/Components/v1/Alert/index.jsx";
|
||||
import { createToast } from "@/Utils/toastUtils.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Background from "@/assets/Images/background-grid.svg?react";
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx";
|
||||
import LanguageSelector from "@/Components/LanguageSelector.jsx";
|
||||
|
||||
const ServerUnreachable = () => {
|
||||
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 (
|
||||
<Stack
|
||||
className="login-page auth"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
|
||||
{/* Header with logo */}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
>
|
||||
<LanguageSelector />
|
||||
<ThemeSwitch />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
rowGap={theme.spacing(8)}
|
||||
sx={{
|
||||
"& > .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),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
spacing={theme.spacing(6)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: theme.spacing(220),
|
||||
mx: "auto",
|
||||
"& .alert.row-stack": {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
variant="error"
|
||||
body={t("errorPages.serverUnreachable.alertBox")}
|
||||
hasIcon={true}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={theme.spacing(2)}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
align="center"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("errorPages.serverUnreachable.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ mt: theme.spacing(4) }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleRetry}
|
||||
disabled={isCheckingConnection}
|
||||
className="dashboard-style-button"
|
||||
sx={{
|
||||
px: theme.spacing(6),
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`,
|
||||
"&.MuiButtonBase-root": {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`,
|
||||
},
|
||||
"&.MuiButton-root": {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isCheckingConnection
|
||||
? t("errorPages.serverUnreachable.retryButton.processing")
|
||||
: t("errorPages.serverUnreachable.retryButton.default")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerUnreachable;
|
||||
@@ -7,7 +7,6 @@ import { StatusLabel, BaseBox } from "@/Components/v2/design-elements";
|
||||
import { SwitchComponent } from "@/Components/v2/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { determineState } from "@/Utils/MonitorUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
@@ -54,7 +53,6 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
)}
|
||||
|
||||
{monitors?.map((monitor) => {
|
||||
const status = determineState(monitor);
|
||||
return (
|
||||
<BaseBox
|
||||
key={monitor.id}
|
||||
@@ -87,10 +85,7 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<StatusLabel
|
||||
status={status === "up"}
|
||||
isActive={monitor.isActive}
|
||||
/>
|
||||
<StatusLabel status={monitor.status} />
|
||||
</Stack>
|
||||
{statusPage.showCharts !== false && (
|
||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||
|
||||
@@ -12,16 +12,16 @@ const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => {
|
||||
icon: <AlertTriangle size={24} />,
|
||||
};
|
||||
|
||||
if (monitors.every((monitor) => monitor.status === true)) {
|
||||
if (monitors.every((monitor) => monitor.status === "up")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allUp");
|
||||
monitorsStatus.color = theme.palette.success.main;
|
||||
monitorsStatus.icon = <CircleCheck size={24} />;
|
||||
return monitorsStatus;
|
||||
} else if (monitors.every((monitor) => monitor.status === false)) {
|
||||
} else if (monitors.every((monitor) => monitor.status === "down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
return monitorsStatus;
|
||||
} else if (monitors.some((monitor) => monitor.status === false)) {
|
||||
} else if (monitors.some((monitor) => monitor.status === "down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.degraded");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
return monitorsStatus;
|
||||
|
||||
@@ -15,7 +15,7 @@ const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
id: "status",
|
||||
content: t("common.table.headers.status"),
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status} />;
|
||||
return <StatusLabel status={row.status === true ? "up" : "down"} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -186,12 +186,7 @@ export const MonitorTable = ({
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
isActive={row.isActive}
|
||||
/>
|
||||
);
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { ControlsFilter } from "@/Components/v2/monitors";
|
||||
import {
|
||||
MonitorBasePageWithStates,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/v2/monitors";
|
||||
import { MonitorBasePageWithStates } from "@/Components/v2/design-elements";
|
||||
import { TextField, Dialog } from "@/Components/v2/inputs";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable";
|
||||
@@ -99,7 +94,11 @@ const UptimeMonitorsPage = () => {
|
||||
{ refreshInterval: 5000, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {};
|
||||
const {
|
||||
monitors: monitorsWithChecks,
|
||||
summary,
|
||||
count,
|
||||
} = monitorsWithChecksData ?? { monitors: null, summary: null, count: 0 };
|
||||
|
||||
// Delete hook
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
@@ -150,14 +149,8 @@ const UptimeMonitorsPage = () => {
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
</Stack>
|
||||
|
||||
<HeaderMonitorsSummary summary={summary} />
|
||||
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
|
||||
+23
-27
@@ -4,14 +4,14 @@ 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
|
||||
import AuthLogin from "@/Pages/Auth/Login";
|
||||
import AuthRegister from "@/Pages/Auth/Register";
|
||||
import AuthForgotPassword from "@/Pages/Auth/Recovery";
|
||||
import AuthSetNewPassword from "../Pages/Auth/SetNewPassword";
|
||||
import AuthSetNewPassword from "@/Pages/Auth/SetNewPassword";
|
||||
|
||||
// Uptime
|
||||
import Uptime from "@/Pages/Uptime/Monitors";
|
||||
@@ -23,43 +23,43 @@ import PageSpeedDetails from "@/Pages/PageSpeed/Details/";
|
||||
|
||||
// Infrastructure
|
||||
import Infrastructure from "@/Pages/Infrastructure/Monitors";
|
||||
import InfrastructureDetails from "@/Pages/Infrastructure/Details/index";
|
||||
|
||||
// Server Status
|
||||
import ServerUnreachable from "../Pages/ServerUnreachable.jsx";
|
||||
import InfrastructureDetails from "@/Pages/Infrastructure/Details";
|
||||
|
||||
// Checks
|
||||
import Checks from "../Pages/Checks/index";
|
||||
import Checks from "@/Pages/Checks";
|
||||
|
||||
// Incidents
|
||||
import Incidents from "../Pages/Incidents/";
|
||||
import Incidents from "@/Pages/Incidents";
|
||||
|
||||
// Status pages
|
||||
import CreateStatus from "../Pages/StatusPage/Create/";
|
||||
import StatusPages from "../Pages/StatusPage/StatusPages";
|
||||
import Status from "../Pages/StatusPage/Status";
|
||||
import CreateStatus from "@/Pages/StatusPage/Create/";
|
||||
import StatusPages from "@/Pages/StatusPage/StatusPages";
|
||||
import Status from "@/Pages/StatusPage/Status";
|
||||
|
||||
import Notifications from "../Pages/Notifications";
|
||||
import CreateNotifications from "../Pages/Notifications/create";
|
||||
import Notifications from "@/Pages/Notifications";
|
||||
import CreateNotifications from "@/Pages/Notifications/create";
|
||||
|
||||
// Settings
|
||||
import Account from "@/Pages/Account";
|
||||
import EditUser from "../Pages/Account/EditUser";
|
||||
import Settings from "../Pages/Settings";
|
||||
import EditUser from "@/Pages/Account/EditUser";
|
||||
import Settings from "@/Pages/Settings";
|
||||
|
||||
import Maintenance from "../Pages/Maintenance";
|
||||
import Maintenance from "@/Pages/Maintenance";
|
||||
import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create";
|
||||
|
||||
import ProtectedRoute from "../Components/v1/ProtectedRoute";
|
||||
import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute";
|
||||
import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
|
||||
import Logs from "../Pages/Logs";
|
||||
// Logs & Diagnostics
|
||||
import Logs from "@/Pages/Logs";
|
||||
|
||||
// Routing
|
||||
import {
|
||||
ProtectedRoute,
|
||||
RoleProtectedRoute,
|
||||
} from "@/Components/v2/routing/RouteProtected";
|
||||
|
||||
import CreateMonitor from "@/Pages/CreateMonitor";
|
||||
|
||||
const Routes = () => {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const AdminCheckedRegister = withAdminCheck(AuthRegister);
|
||||
const v2theme = mode === "light" ? lightTheme : darkTheme;
|
||||
return (
|
||||
<LibRoutes>
|
||||
@@ -67,7 +67,7 @@ const Routes = () => {
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<HomeLayout />
|
||||
<RootLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
@@ -383,7 +383,7 @@ const Routes = () => {
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<AdminCheckedRegister />
|
||||
<AuthRegister />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
@@ -432,10 +432,6 @@ const Routes = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/server-unreachable"
|
||||
element={<ServerUnreachable />}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
|
||||
export type MonitorStatus = boolean | undefined;
|
||||
|
||||
export const MonitorTypes = [
|
||||
"http",
|
||||
@@ -13,6 +12,15 @@ export const MonitorTypes = [
|
||||
] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = [
|
||||
"up",
|
||||
"down",
|
||||
"paused",
|
||||
"initializing",
|
||||
"maintenance",
|
||||
] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export interface MonitorThresholds {
|
||||
usage_cpu?: number;
|
||||
usage_memory?: number;
|
||||
@@ -28,7 +36,7 @@ export interface Monitor {
|
||||
teamId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
status?: boolean;
|
||||
status: MonitorStatus;
|
||||
statusWindow: boolean[];
|
||||
statusWindowSize: number;
|
||||
statusWindowThreshold: number;
|
||||
@@ -66,6 +74,8 @@ export interface MonitorsSummary {
|
||||
upMonitors: number;
|
||||
downMonitors: number;
|
||||
pausedMonitors: number;
|
||||
initializingMonitors: number;
|
||||
maintenanceMonitors: number;
|
||||
}
|
||||
|
||||
export interface MonitorsWithChecksResponse {
|
||||
|
||||
@@ -15,6 +15,15 @@ type StoreType = {
|
||||
let storeInstance: StoreType | null = null;
|
||||
let interceptorsInitialized = false;
|
||||
|
||||
type ServerUnreachableCallback = (unreachable: boolean) => 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";
|
||||
|
||||
@@ -2,13 +2,6 @@ import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor";
|
||||
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
|
||||
import type { ValueType } from "@/Components/v2/design-elements/StatusLabel";
|
||||
|
||||
export const determineState = (monitor: Monitor) => {
|
||||
if (typeof monitor === "undefined") return "pending";
|
||||
if (monitor?.isActive === false) return "paused";
|
||||
if (monitor?.status === undefined) return "pending";
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
};
|
||||
|
||||
export const getMonitorPath = (type: MonitorType): string => {
|
||||
const pathMap: Record<MonitorType, string> = {
|
||||
http: "uptime",
|
||||
@@ -24,10 +17,10 @@ export const getMonitorPath = (type: MonitorType): string => {
|
||||
};
|
||||
|
||||
export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
|
||||
if (status === true) {
|
||||
if (status === "up") {
|
||||
return "success";
|
||||
}
|
||||
if (status === false) {
|
||||
if (status === "down") {
|
||||
return "error";
|
||||
}
|
||||
return "warning";
|
||||
@@ -43,11 +36,11 @@ export const getValuePalette = (value: ValueType): PaletteKey => {
|
||||
};
|
||||
|
||||
export const getStatusColor = (status: MonitorStatus, theme: any): string => {
|
||||
if (status === true) {
|
||||
if (status === "up") {
|
||||
return theme.palette.success.light;
|
||||
}
|
||||
|
||||
if (status === false) {
|
||||
if (status === "down") {
|
||||
return theme.palette.error.light;
|
||||
}
|
||||
|
||||
@@ -101,9 +94,9 @@ export const getStatusPageHeaderConfig = (
|
||||
return { paletteKey: "error", message: "No monitors available" };
|
||||
}
|
||||
|
||||
const allUp = monitors.every((monitor) => monitor.status === true);
|
||||
const anyDown = monitors.some((monitor) => monitor.status === false);
|
||||
const allDown = monitors.every((monitor) => monitor.status === false);
|
||||
const allUp = monitors.every((monitor) => monitor.status === "up");
|
||||
const anyDown = monitors.some((monitor) => monitor.status === "down");
|
||||
const allDown = monitors.every((monitor) => monitor.status === "down");
|
||||
|
||||
if (allUp)
|
||||
return {
|
||||
|
||||
@@ -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",
|
||||
@@ -726,6 +775,7 @@
|
||||
"status": {
|
||||
"down": "down",
|
||||
"initializing": "initializing",
|
||||
"maintenance": "maintenance",
|
||||
"paused": "paused",
|
||||
"total": "total",
|
||||
"up": "up"
|
||||
@@ -970,7 +1020,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 +1088,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 +1229,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 +1248,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",
|
||||
|
||||
@@ -206,6 +206,7 @@ export const initializeServices = async ({
|
||||
buffer: bufferService,
|
||||
incidentService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
});
|
||||
|
||||
const superSimpleQueue = await SuperSimpleQueue.create({
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Schema, model, Types, type UpdateQuery } from "mongoose";
|
||||
import type { Monitor, MonitorMatchMethod, MonitorThresholds, CheckSnapshot } from "@/types/monitor.js";
|
||||
import { MonitorTypes } from "@/types/monitor.js";
|
||||
import Check from "./Check.js";
|
||||
import MonitorStats from "./MonitorStats.js";
|
||||
import StatusPage from "./StatusPage.js";
|
||||
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
||||
|
||||
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
||||
|
||||
@@ -68,8 +65,9 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
type: String,
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
type: String,
|
||||
enum: MonitorStatuses,
|
||||
default: "initializing",
|
||||
},
|
||||
statusWindow: {
|
||||
type: [Boolean],
|
||||
|
||||
@@ -55,7 +55,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
query.isActive = filter === "true";
|
||||
break;
|
||||
case "status":
|
||||
query.status = filter === "true";
|
||||
query.status = filter;
|
||||
break;
|
||||
case "type":
|
||||
query.type = filter;
|
||||
@@ -181,7 +181,13 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
{
|
||||
$set: {
|
||||
isActive: { $not: "$isActive" },
|
||||
status: "$$REMOVE",
|
||||
status: {
|
||||
$cond: {
|
||||
if: { $eq: ["$status", "paused"] },
|
||||
then: "initializing",
|
||||
else: "paused",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -223,17 +229,27 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
totalMonitors: { $sum: 1 },
|
||||
upMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", true] }, 1, 0],
|
||||
$cond: [{ $eq: ["$status", "up"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
downMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", false] }, 1, 0],
|
||||
$cond: [{ $eq: ["$status", "down"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
pausedMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
|
||||
$cond: [{ $eq: ["$status", "paused"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
initializingMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", "initializing"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
maintenanceMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", "maintenance"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -242,7 +258,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
];
|
||||
|
||||
const [summary] = await MonitorModel.aggregate(pipeline);
|
||||
return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 };
|
||||
return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0, initializingMonitors: 0, maintenanceMonitors: 0 };
|
||||
};
|
||||
|
||||
findGroupsByTeamId = async (teamId: string): Promise<string[]> => {
|
||||
@@ -284,7 +300,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
teamId: toStringId(doc.teamId),
|
||||
name: doc.name,
|
||||
description: doc.description ?? undefined,
|
||||
status: doc.status ?? undefined,
|
||||
status: doc.status ?? "initializing",
|
||||
statusWindow: doc.statusWindow ?? [],
|
||||
statusWindowSize: doc.statusWindowSize,
|
||||
statusWindowThreshold: doc.statusWindowThreshold,
|
||||
@@ -331,45 +347,13 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
|
||||
const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification));
|
||||
|
||||
const checks: Check[] = (doc.checks ?? []).map((check: any) => ({
|
||||
id: toStringId(check._id),
|
||||
metadata: {
|
||||
monitorId: toStringId(check.metadata?.monitorId),
|
||||
teamId: toStringId(check.metadata?.teamId),
|
||||
type: check.metadata?.type,
|
||||
},
|
||||
status: check.status,
|
||||
responseTime: check.responseTime,
|
||||
timings: check.timings,
|
||||
statusCode: check.statusCode,
|
||||
message: check.message,
|
||||
ack: check.ack,
|
||||
ackAt: check.ackAt ?? null,
|
||||
expiry: toDateString(check.expiry),
|
||||
cpu: check.cpu,
|
||||
memory: check.memory,
|
||||
disk: check.disk,
|
||||
host: check.host,
|
||||
errors: check.errors,
|
||||
capture: check.capture,
|
||||
net: check.net,
|
||||
accessibility: check.accessibility,
|
||||
bestPractices: check.bestPractices,
|
||||
seo: check.seo,
|
||||
performance: check.performance,
|
||||
audits: check.audits,
|
||||
__v: check.__v,
|
||||
createdAt: toDateString(check.createdAt),
|
||||
updatedAt: toDateString(check.updatedAt),
|
||||
}));
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
userId: toStringId(doc.userId),
|
||||
teamId: toStringId(doc.teamId),
|
||||
name: doc.name,
|
||||
description: doc.description ?? undefined,
|
||||
status: doc.status ?? undefined,
|
||||
status: doc.status ?? "initializing",
|
||||
statusWindow: doc.statusWindow ?? [],
|
||||
statusWindowSize: doc.statusWindowSize,
|
||||
statusWindowThreshold: doc.statusWindowThreshold,
|
||||
|
||||
@@ -46,7 +46,7 @@ class IncidentService {
|
||||
handleIncident = async (monitor: Monitor, code: number): Promise<Incident | null> => {
|
||||
const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId);
|
||||
// Monitor is down, create an incident
|
||||
if (monitor.status === false) {
|
||||
if (monitor.status === "down") {
|
||||
if (activeIncident) {
|
||||
return activeIncident;
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Monitor } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
||||
import IncidentService from "@/service/business/incidentService.js";
|
||||
import { IMaintenanceWindowsRepository } from "@/repositories/index.js";
|
||||
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||
|
||||
class SuperSimpleQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -16,6 +16,7 @@ class SuperSimpleQueueHelper {
|
||||
private buffer: any;
|
||||
private incidentService: IncidentService;
|
||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
@@ -26,6 +27,7 @@ class SuperSimpleQueueHelper {
|
||||
buffer,
|
||||
incidentService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
}: {
|
||||
logger: any;
|
||||
networkService: INetworkService;
|
||||
@@ -35,6 +37,7 @@ class SuperSimpleQueueHelper {
|
||||
buffer: any;
|
||||
incidentService: IncidentService;
|
||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
}) {
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
@@ -44,6 +47,7 @@ class SuperSimpleQueueHelper {
|
||||
this.notificationsService = notificationsService;
|
||||
this.incidentService = incidentService;
|
||||
this.maintenanceWindowsRepository = maintenanceWindowsRepository;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -59,7 +63,8 @@ class SuperSimpleQueueHelper {
|
||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" });
|
||||
}
|
||||
|
||||
// Step 1. Check for maintenacne window, if found, skip the check
|
||||
// Step 1. Check for maintenance window, if found, skip the check
|
||||
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
|
||||
if (maintenanceWindowActive) {
|
||||
this.logger.debug({
|
||||
@@ -67,6 +72,9 @@ class SuperSimpleQueueHelper {
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
});
|
||||
if (monitor.status !== "maintenance") {
|
||||
await this.monitorsRepository.updateById(monitorId, teamId, { status: "maintenance" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ export const buildHardwareEmail = async (emailService: any, monitor: Monitor, al
|
||||
};
|
||||
|
||||
export const buildEmail = async (emailService: any, monitor: Monitor): Promise<string> => {
|
||||
const template = monitor.status === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
const template = monitor.status === "up" ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url };
|
||||
const html = await emailService.buildEmail(template, context);
|
||||
return html;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js";
|
||||
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js";
|
||||
import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
|
||||
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
|
||||
|
||||
export interface INotificationsService {
|
||||
createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
|
||||
findById: (id: string, teamId: string) => Promise<Notification>;
|
||||
@@ -11,7 +12,7 @@ export interface INotificationsService {
|
||||
handleNotifications: (
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
prevStatus: boolean | undefined,
|
||||
prevStatus: MonitorStatus,
|
||||
statusChanged: boolean
|
||||
) => Promise<boolean>;
|
||||
|
||||
@@ -228,12 +229,7 @@ export class NotificationsService implements INotificationsService {
|
||||
return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus);
|
||||
};
|
||||
|
||||
handleNotifications = async (
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
prevStatus: boolean | undefined,
|
||||
statusChanged: boolean
|
||||
) => {
|
||||
handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, prevStatus: MonitorStatus, statusChanged: boolean) => {
|
||||
const { type } = monitor;
|
||||
const payload = monitorStatusResponse.payload as HardwareStatusPayload;
|
||||
// If this is a non-hardeware type monitor and status did not change, we're done
|
||||
|
||||
@@ -2,7 +2,6 @@ import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import MonitorStats from "../../db/models/MonitorStats.js";
|
||||
import { CheckModel } from "@/db/models/index.js";
|
||||
import type {
|
||||
CheckErrorInfo,
|
||||
Monitor,
|
||||
MonitorStatusResponse,
|
||||
StatusChangeResult,
|
||||
@@ -15,7 +14,6 @@ const SERVICE_NAME = "StatusService";
|
||||
|
||||
export interface IStatusService {
|
||||
updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise<boolean>;
|
||||
getStatusString(status: boolean | undefined): string;
|
||||
handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise<void>;
|
||||
updateMonitorStatus(
|
||||
statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>,
|
||||
@@ -39,7 +37,7 @@ export class StatusService implements IStatusService {
|
||||
return StatusService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) {
|
||||
async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: MonitorStatusResponse }) {
|
||||
try {
|
||||
const monitorId = monitor.id;
|
||||
const { responseTime, status } = networkResponse;
|
||||
@@ -60,7 +58,7 @@ export class StatusService implements IStatusService {
|
||||
// Update stats
|
||||
|
||||
// Last response time
|
||||
stats.lastResponseTime = responseTime;
|
||||
stats.lastResponseTime = responseTime ?? 0;
|
||||
|
||||
// Avg response time:
|
||||
let avgResponseTime = stats.avgResponseTime;
|
||||
@@ -111,12 +109,6 @@ export class StatusService implements IStatusService {
|
||||
}
|
||||
}
|
||||
|
||||
getStatusString = (status: boolean | undefined) => {
|
||||
if (status === true) return "up";
|
||||
if (status === false) return "down";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => {
|
||||
try {
|
||||
let savedCheck;
|
||||
@@ -208,9 +200,7 @@ export class StatusService implements IStatusService {
|
||||
monitor.recentChecks.shift();
|
||||
}
|
||||
|
||||
if (monitor.status === undefined || monitor.status === null) {
|
||||
monitor.status = status;
|
||||
}
|
||||
monitor.status = status === true ? "up" : "down";
|
||||
|
||||
const prevStatus = monitor.status;
|
||||
let newStatus = monitor.status;
|
||||
@@ -233,13 +223,13 @@ export class StatusService implements IStatusService {
|
||||
const failureRate = (failures / monitor.statusWindow.length) * 100;
|
||||
|
||||
// If threshold has been met and the monitor is not already down, mark down:
|
||||
if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) {
|
||||
newStatus = false;
|
||||
if (failureRate >= monitor.statusWindowThreshold && monitor.status !== "down") {
|
||||
newStatus = "down";
|
||||
statusChanged = true;
|
||||
}
|
||||
// If the failure rate is below the threshold and the monitor is down, recover:
|
||||
else if (failureRate < monitor.statusWindowThreshold && monitor.status === false) {
|
||||
newStatus = true;
|
||||
else if (failureRate < monitor.statusWindowThreshold && monitor.status === "down") {
|
||||
newStatus = "up";
|
||||
statusChanged = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Check } from "@/types/check.js";
|
||||
import type { CheckSnapshot } from "@/types/check.js";
|
||||
export type { CheckSnapshot } from "@/types/check.js";
|
||||
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance"] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export interface MonitorThresholds {
|
||||
usage_cpu?: number;
|
||||
usage_memory?: number;
|
||||
@@ -20,7 +22,7 @@ export interface Monitor {
|
||||
teamId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
status?: boolean;
|
||||
status: MonitorStatus;
|
||||
statusWindow: boolean[];
|
||||
statusWindowSize: number;
|
||||
statusWindowThreshold: number;
|
||||
@@ -56,6 +58,8 @@ export interface MonitorsSummary {
|
||||
upMonitors: number;
|
||||
downMonitors: number;
|
||||
pausedMonitors: number;
|
||||
initializingMonitors: number;
|
||||
maintenanceMonitors: number;
|
||||
}
|
||||
|
||||
export interface MonitorsWithChecksByTeamIdResult {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
Monitor,
|
||||
MonitorMatchMethod,
|
||||
MonitorType,
|
||||
MonitorStatus,
|
||||
} from "@/types/index.js";
|
||||
|
||||
export interface MonitorStatusResponse<T = any> {
|
||||
@@ -106,7 +107,7 @@ export interface MonitorPayloadMap {
|
||||
export type StatusChangeResult = {
|
||||
monitor: Monitor;
|
||||
statusChanged: boolean;
|
||||
prevStatus: boolean | undefined;
|
||||
prevStatus: MonitorStatus;
|
||||
code: number;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user