diff --git a/.gitignore b/.gitignore
index 2299e795a..f6e292bc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@
.VSCodeCounter
*.sh
mongo
-node_modules/
\ No newline at end of file
+node_modules/
+docs/architecture
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
index 84fc4c3a6..038814543 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -2472,11 +2472,13 @@
}
},
"node_modules/axios": {
- "version": "1.13.4",
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 0a6b364f8..a8a25669e 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,4 +1,4 @@
-import { useEffect, type CSSProperties } from "react";
+import { type CSSProperties } from "react";
import { useSelector } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
@@ -6,7 +6,6 @@ import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
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/v2/layout/AppLayout";
import type { RootState } from "@/Types/state";
@@ -14,13 +13,6 @@ import type { RootState } from "@/Types/state";
function App() {
const mode = useSelector((state: RootState) => state.ui.mode);
- // Cleanup
- useEffect(() => {
- return () => {
- logger.cleanup();
- };
- }, []);
-
const theme = mode === "light" ? lightTheme : darkTheme;
return (
diff --git a/client/src/Components/WalletProvider/index.css b/client/src/Components/WalletProvider/index.css
deleted file mode 100644
index f3da31efc..000000000
--- a/client/src/Components/WalletProvider/index.css
+++ /dev/null
@@ -1,53 +0,0 @@
-.wallet-adapter-dropdown {
- width: 100%;
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: center;
- gap: var(--env-var-spacing-1);
-}
-
-.wallet-adapter-button {
- display: flex;
- align-items: center;
- justify-content: center;
- width: auto;
- height: var(--env-var-height-2) !important;
- font-size: var(--env-var-font-size-medium-plus) !important;
- font-weight: 500 !important;
- margin: 0;
- padding: calc(
- (var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2
- )
- var(--env-var-spacing-1);
- white-space: nowrap;
-}
-
-.wallet-adapter-modal-title {
- font-size: var(--font-size-h5) !important;
-}
-
-/* Separator styling */
-.wallet-adapter-modal-divider {
- background-color: var(--border-color) !important;
- margin: var(--spacing-md) 0 !important;
-}
-
-/* Responsive fixes */
-@media (max-width: 1200px) {
- .wallet-adapter-button {
- font-size: var(--env-var-font-size-medium) !important;
- padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium) * 1.2) / 2)
- var(--env-var-spacing-1-minus) !important;
- }
-}
-
-@media (max-width: 900px) {
- .wallet-adapter-modal-wrapper {
- flex-direction: column !important;
- }
-
- .wallet-adapter-modal-divider {
- margin: var(--spacing-sm) 0 !important;
- }
-}
diff --git a/client/src/Components/WalletProvider/index.jsx b/client/src/Components/WalletProvider/index.jsx
deleted file mode 100644
index 13cc83b07..000000000
--- a/client/src/Components/WalletProvider/index.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-// import { useMemo } from "react";
-// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
-// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
-// import {
-// UnsafeBurnerWalletAdapter,
-// PhantomWalletAdapter,
-// } from "@solana/wallet-adapter-wallets";
-
-// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
-// import { clusterApiUrl } from "@solana/web3.js";
-// import PropTypes from "prop-types";
-// import "./index.css";
-
-// // Default styles that can be overridden by your app
-// import "@solana/wallet-adapter-react-ui/styles.css";
-
-// export const Wallet = ({ children }) => {
-// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
-// const network = WalletAdapterNetwork.Mainnet;
-
-// // You can also provide a custom RPC endpoint.
-// const endpoint = useMemo(() => clusterApiUrl(network), [network]);
-
-// const wallets = useMemo(
-// () => [new PhantomWalletAdapter()],
-// // eslint-disable-next-line react-hooks/exhaustive-deps
-// [network]
-// );
-
-// return (
-//
-//
-// {children}
-//
-//
-// );
-// };
-
-// Wallet.propTypes = {
-// children: PropTypes.node,
-// };
-
-const Wallet = ({ children }) => {
- return children;
-};
-
-export default Wallet;
diff --git a/client/src/Components/v1/Alert/index.css b/client/src/Components/v1/Alert/index.css
deleted file mode 100644
index 233b5c466..000000000
--- a/client/src/Components/v1/Alert/index.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.alert {
- margin: 0;
- width: fit-content;
-}
-.alert,
-.alert button,
-.alert .MuiTypography-root {
- font-size: var(--env-var-font-size-medium);
-}
diff --git a/client/src/Components/v1/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx
deleted file mode 100644
index dd3b2f9f2..000000000
--- a/client/src/Components/v1/Alert/index.jsx
+++ /dev/null
@@ -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}
- */
-
-const icons = {
- info: (
-
- ),
- error: (
-
- ),
- warning: (
-
- ),
-};
-
-/**
- * @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 (
-
- {hasIcon && {icon}}
-
- {title && (
- {title}
- )}
- {body && (
- {body}
- )}
- {hasIcon && isToast && (
-
- )}
-
- {isToast && (
-
-
-
- )}
-
- );
-};
-
-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;
diff --git a/client/src/Components/v1/Breadcrumbs/index.css b/client/src/Components/v1/Breadcrumbs/index.css
index abb3c0a28..022db9a83 100644
--- a/client/src/Components/v1/Breadcrumbs/index.css
+++ b/client/src/Components/v1/Breadcrumbs/index.css
@@ -6,7 +6,6 @@
min-height: 16px;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li a {
- font-size: var(--env-var-font-size-medium);
font-weight: 400;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) {
diff --git a/client/src/Components/v1/HOC/withAdminCheck.jsx b/client/src/Components/v1/HOC/withAdminCheck.jsx
deleted file mode 100644
index 6343b2e4e..000000000
--- a/client/src/Components/v1/HOC/withAdminCheck.jsx
+++ /dev/null
@@ -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 (
-
- );
- };
- const wrappedComponentName =
- WrappedComponent.displayName || WrappedComponent.name || "Component";
- WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`;
-
- return WithAdminCheck;
-};
-
-export default withAdminCheck;
diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx
index 0f96a2ff6..ec2522d17 100644
--- a/client/src/Components/v1/Inputs/Select/index.jsx
+++ b/client/src/Components/v1/Inputs/Select/index.jsx
@@ -64,7 +64,6 @@ const Select = ({
const theme = useTheme();
const getItemValue = (item) => item?._id ?? item?.id;
const itemStyles = {
- fontSize: "var(--env-var-font-size-medium)",
color: theme.palette.primary.contrastTextTertiary,
borderRadius: theme.shape.borderRadius,
margin: theme.spacing(2),
diff --git a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx
index 078684405..2b3abab0f 100644
--- a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx
+++ b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx
@@ -18,7 +18,6 @@ export const HttpAdornment = ({ https }) => {
>
diff --git a/client/src/Components/v1/ProtectedRoute/index.jsx b/client/src/Components/v1/ProtectedRoute/index.jsx
deleted file mode 100644
index 313500338..000000000
--- a/client/src/Components/v1/ProtectedRoute/index.jsx
+++ /dev/null
@@ -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
- ) : (
-
- );
-};
-
-ProtectedRoute.propTypes = {
- children: PropTypes.element.isRequired,
-};
-
-export default ProtectedRoute;
diff --git a/client/src/Components/v1/RoleProtectedRoute/index.jsx b/client/src/Components/v1/RoleProtectedRoute/index.jsx
deleted file mode 100644
index c7b7dfc12..000000000
--- a/client/src/Components/v1/RoleProtectedRoute/index.jsx
+++ /dev/null
@@ -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
- ) : (
-
- );
-};
-
-RoleProtectedRoute.propTypes = {
- children: PropTypes.element.isRequired,
- roles: PropTypes.array,
-};
-
-export default RoleProtectedRoute;
diff --git a/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx b/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx
deleted file mode 100644
index 21256e114..000000000
--- a/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useTheme } from "@mui/material";
-import "./index.css";
-
-const SunAndMoonIcon = () => {
- const theme = useTheme();
-
- return (
-
- );
-};
-
-export default SunAndMoonIcon;
diff --git a/client/src/Components/v1/ThemeSwitch/index.css b/client/src/Components/v1/ThemeSwitch/index.css
deleted file mode 100644
index d5db1c1ca..000000000
--- a/client/src/Components/v1/ThemeSwitch/index.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/client/src/Components/v1/ThemeSwitch/index.jsx b/client/src/Components/v1/ThemeSwitch/index.jsx
deleted file mode 100644
index 60763da90..000000000
--- a/client/src/Components/v1/ThemeSwitch/index.jsx
+++ /dev/null
@@ -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 (
- :is(circle, g)": {
- fill: color,
- stroke: color,
- },
- }}
- >
-
-
- );
-};
-
-export default ThemeSwitch;
diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx
index 74c06f8f3..2dd1160ad 100644
--- a/client/src/Components/v2/design-elements/Avatar.tsx
+++ b/client/src/Components/v2/design-elements/Avatar.tsx
@@ -31,6 +31,7 @@ export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => {
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,
diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/v2/design-elements/MonitorStatus.tsx
index 3d5e81845..09cd95f0f 100644
--- a/client/src/Components/v2/design-elements/MonitorStatus.tsx
+++ b/client/src/Components/v2/design-elements/MonitorStatus.tsx
@@ -23,7 +23,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
- maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{monitor.name}
@@ -40,7 +39,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
- maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{formatUrl(monitor?.url)}
diff --git a/client/src/Components/v2/design-elements/PulseDot.tsx b/client/src/Components/v2/design-elements/PulseDot.tsx
index ab792b628..1f5b3401e 100644
--- a/client/src/Components/v2/design-elements/PulseDot.tsx
+++ b/client/src/Components/v2/design-elements/PulseDot.tsx
@@ -1,4 +1,17 @@
-import { Box, Stack, useTheme } from "@mui/material";
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import { useTheme, keyframes } from "@mui/material";
+
+const ripple = keyframes`
+ from {
+ opacity: 1;
+ transform: scale(0);
+ }
+ to {
+ opacity: 0;
+ transform: scale(2);
+ }
+`;
interface PulseDotProps {
color: string;
@@ -28,7 +41,7 @@ export const PulseDot = ({ color }: PulseDotProps) => {
height: "100%",
backgroundColor: "inherit",
borderRadius: "50%",
- animation: "ripple 1.8s ease-out infinite",
+ animation: `${ripple} 1.8s ease-out infinite`,
},
"&::after": {
content: `""`,
diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx
index bf7979cca..73a881736 100644
--- a/client/src/Components/v2/design-elements/StatusBox.tsx
+++ b/client/src/Components/v2/design-elements/StatusBox.tsx
@@ -2,7 +2,6 @@ import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/v2/design-elements";
-import Background from "@/assets/Images/background-grid.svg?react";
import { useTranslation } from "react-i18next";
import type { SxProps } from "@mui/material";
@@ -18,6 +17,7 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
return (
{
>
-
-
+ top={0}
+ left={0}
+ right={0}
+ bottom={0}
+ sx={{
+ pointerEvents: "none",
+ backgroundImage: `
+ linear-gradient(${theme.palette.divider} 1px, transparent 1px),
+ linear-gradient(90deg, ${theme.palette.divider} 1px, transparent 1px)
+ `,
+ backgroundSize: "24px 24px",
+ maskImage:
+ "linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)",
+ WebkitMaskImage:
+ "linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)",
+ }}
+ />
{children}
);
@@ -105,6 +117,40 @@ export const PausedStatusBox = ({ n }: { n: number }) => {
/>
);
};
+export const MaintenanceStatusBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
+export const InitializingStatusBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
+export const BreachedStatusBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
+
export const TotalChecksBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -138,15 +184,3 @@ export const UpChecksBox = ({ n }: { n: number }) => {
/>
);
};
-
-export const InitializingStatusBox = ({ n }: { n: number }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- return (
-
- );
-};
diff --git a/client/src/Components/v2/design-elements/StatusLabel.tsx b/client/src/Components/v2/design-elements/StatusLabel.tsx
index d7212ddff..19bb8d157 100644
--- a/client/src/Components/v2/design-elements/StatusLabel.tsx
+++ b/client/src/Components/v2/design-elements/StatusLabel.tsx
@@ -11,33 +11,26 @@ 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 === "breached") {
+ return t("pages.common.monitors.status.breached");
+ } 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 +57,7 @@ export const StatusLabel = ({
borderRadius="50%"
marginRight="5px"
/>
-
- {determineStatus(isActive, status)}
-
+ {determineStatus(status)}
);
};
diff --git a/client/src/Components/v1/I18nLoader/index.jsx b/client/src/Components/v2/i18nLoader/index.tsx
similarity index 63%
rename from client/src/Components/v1/I18nLoader/index.jsx
rename to client/src/Components/v2/i18nLoader/index.tsx
index 676e63d7b..f0555afed 100644
--- a/client/src/Components/v1/I18nLoader/index.jsx
+++ b/client/src/Components/v2/i18nLoader/index.tsx
@@ -1,8 +1,9 @@
-import i18n from "../../../Utils/i18n.js";
+import i18n from "@/Utils/i18n.js";
import { useSelector } from "react-redux";
import { useEffect } from "react";
+import type { RootState } from "@/store";
const I18nLoader = () => {
- const language = useSelector((state) => state.ui.language ?? "en");
+ const language = useSelector((state: RootState) => state.ui.language ?? "en");
useEffect(() => {
if (language && i18n.language !== language) {
diff --git a/client/src/Components/v2/inputs/ImageUpload.tsx b/client/src/Components/v2/inputs/ImageUpload.tsx
index 6c9fc1783..0d0b77b40 100644
--- a/client/src/Components/v2/inputs/ImageUpload.tsx
+++ b/client/src/Components/v2/inputs/ImageUpload.tsx
@@ -38,11 +38,11 @@ export const ImageUpload = ({
const isValidType = accept.some((type) => file.type.includes(type));
const isValidSize = file.size <= maxSize;
if (!isValidType) {
- setLocalError(t("common.errors.invalidFileFormat"));
+ setLocalError(t("components.imageUpload.errors.invalidFileFormat"));
return;
}
if (!isValidSize) {
- setLocalError(t("common.errors.invalidFileSize"));
+ setLocalError(t("components.imageUpload.errors.invalidFileSize"));
return;
}
setLocalError(null);
@@ -137,15 +137,15 @@ export const ImageUpload = ({
color="primary"
fontWeight={500}
>
- {t("common.imageUpload.clickToUpload")}
+ {t("components.imageUpload.clickToUpload")}
{" "}
- {t("common.imageUpload.orDragAndDrop")}
+ {t("components.imageUpload.orDragAndDrop")}
- {accept.join(", ").toUpperCase()} • {t("common.imageUpload.maxSize")}{" "}
+ {accept.join(", ").toUpperCase()} • {t("components.imageUpload.maxSize")}{" "}
{Math.round(maxSize / 1024 / 1024)}MB
diff --git a/client/src/Components/v2/inputs/Slider.tsx b/client/src/Components/v2/inputs/Slider.tsx
index 97a16ac2c..83037ab54 100644
--- a/client/src/Components/v2/inputs/Slider.tsx
+++ b/client/src/Components/v2/inputs/Slider.tsx
@@ -41,7 +41,10 @@ export const SliderInput = forwardRef(
"& .MuiSlider-thumb": {
backgroundColor: "#fff",
"&:hover, &.Mui-focusVisible": {
- boxShadow: `0 0 0 8px ${theme.palette.primary.main}20`,
+ boxShadow: "none",
+ },
+ "&:active": {
+ boxShadow: "none",
},
},
"& .MuiSlider-valueLabel": {
diff --git a/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx b/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx
new file mode 100644
index 000000000..38a6adfde
--- /dev/null
+++ b/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx
@@ -0,0 +1,35 @@
+import {
+ UpStatusBox,
+ DownStatusBox,
+ PausedStatusBox,
+ InitializingStatusBox,
+ BreachedStatusBox,
+} 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;
+ showBreached?: boolean;
+}
+
+export const HeaderMonitorsSummary = ({
+ summary,
+ showBreached = false,
+}: MonitorsSummaryProps) => {
+ const theme = useTheme();
+ return (
+
+
+
+ {showBreached && }
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx
index 1c25c339e..d3aa62a69 100644
--- a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx
+++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx
@@ -3,7 +3,7 @@ import Typography from "@mui/material/Typography";
import { BaseChart } from "@/Components/v2/design-elements";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
-import { formatDateWithTz } from "@/Utils/timeUtilsLegacy";
+import { formatDateWithTz } from "@/Utils/TimeUtils";
import { useTranslation } from "react-i18next";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts";
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx
index e2856d6a9..9496395f6 100644
--- a/client/src/Components/v2/monitors/index.tsx
+++ b/client/src/Components/v2/monitors/index.tsx
@@ -11,3 +11,4 @@ export * from "./charts/PiePageSpeedLegend";
export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
export * from "./charts/HistogramInfrastructure";
+export * from "./HeaderMonitorsSummary";
diff --git a/client/src/Components/v2/routing/RouteProtected.tsx b/client/src/Components/v2/routing/RouteProtected.tsx
new file mode 100644
index 000000000..7064c26a4
--- /dev/null
+++ b/client/src/Components/v2/routing/RouteProtected.tsx
@@ -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
+ ) : (
+
+ );
+};
+
+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
+ ) : (
+
+ );
+};
diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx
index 62ea5610b..4724ed97d 100644
--- a/client/src/Components/v2/sidebar/Authfooter.tsx
+++ b/client/src/Components/v2/sidebar/Authfooter.tsx
@@ -76,7 +76,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
alignItems="center"
py={theme.spacing(4)}
px={theme.spacing(8)}
- gap={theme.spacing(2)}
+ gap={theme.spacing(4)}
>
{
};
return (
- t.zIndex.drawer : "auto",
- }}
- >
-
+ dispatch(setCollapsed({ collapsed: true }))}
+ sx={{ zIndex: 999 }}
+ />
+
-
- {menu.map((item) => {
- const selected = location.pathname.startsWith(`/${item.path}`);
- return (
- handleNavClick(item.path)}
- />
- );
- })}
-
-
-
- {bottomMenu.map((item) => {
- const selected = location.pathname.startsWith(`/${item.path}`);
- return (
- handleNavClick(item.path)}
- />
- );
- })}
-
-
+
+
+ {menu.map((item) => {
+ const selected = location.pathname.startsWith(`/${item.path}`);
+ return (
+ handleNavClick(item.path)}
+ />
+ );
+ })}
+
+
+
+ {bottomMenu.map((item) => {
+ const selected = location.pathname.startsWith(`/${item.path}`);
+ return (
+ handleNavClick(item.path)}
+ />
+ );
+ })}
+
+
-
-
+
+
+ >
);
};
diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js
deleted file mode 100644
index 0e8ca8608..000000000
--- a/client/src/Features/UI/uiSlice.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import { createSlice } from "@reduxjs/toolkit";
-
-const initialMode = window?.matchMedia?.("(prefers-color-scheme: dark)")?.matches
- ? "dark"
- : "light";
-
-const initialState = {
- monitors: {
- rowsPerPage: 10,
- },
- team: {
- rowsPerPage: 5,
- },
- maintenance: {
- rowsPerPage: 5,
- },
- infrastructure: {
- rowsPerPage: 5,
- },
- logs: {
- rowsPerPage: 15,
- },
- sidebar: {
- collapsed: false,
- },
- mode: initialMode,
- showURL: false,
- greeting: { index: 0, lastUpdate: null },
- timezone: "America/Toronto",
- distributedUptimeEnabled: false,
- language: "en",
- starPromptOpen: true,
- chartType: "histogram",
-};
-
-const uiSlice = createSlice({
- name: "ui",
- initialState,
- reducers: {
- setDistributedUptimeEnabled: (state, action) => {
- state.distributedUptimeEnabled = action.payload;
- },
- setRowsPerPage: (state, action) => {
- const { table, value } = action.payload;
- if (!state[table]) {
- state[table] = {};
- }
- state[table].rowsPerPage = value;
- },
- toggleSidebar: (state) => {
- state.sidebar.collapsed = !state.sidebar.collapsed;
- },
- setCollapsed: (state, action) => {
- const { collapsed } = action.payload;
- state.sidebar.collapsed = collapsed;
- },
- setMode: (state, action) => {
- state.mode = action.payload;
- },
- setShowURL: (state, action) => {
- state.showURL = action.payload;
- },
- setGreeting(state, action) {
- if (!state.greeting) {
- state.greeting = { index: 0, lastUpdate: null };
- }
- state.greeting.index = action.payload.index;
- state.greeting.lastUpdate = action.payload.lastUpdate;
- },
- setTimezone(state, action) {
- state.timezone = action.payload.timezone;
- },
- setLanguage: (state, action) => {
- state.language = action.payload;
- },
- setStarPromptOpen: (state, action) => {
- state.starPromptOpen = action.payload;
- },
- setChartType: (state, action) => {
- state.chartType = action.payload;
- },
- },
-});
-
-export default uiSlice.reducer;
-export const {
- setRowsPerPage,
- toggleSidebar,
- setCollapsed,
- setMode,
- setShowURL,
- setGreeting,
- setTimezone,
- setDistributedUptimeEnabled,
- setLanguage,
- setStarPromptOpen,
- setChartType,
-} = uiSlice.actions;
diff --git a/client/src/Features/UI/uiSlice.ts b/client/src/Features/UI/uiSlice.ts
new file mode 100644
index 000000000..84e8248db
--- /dev/null
+++ b/client/src/Features/UI/uiSlice.ts
@@ -0,0 +1,119 @@
+import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
+
+type ThemeMode = "light" | "dark";
+type ChartType = "histogram" | "line";
+type TableName = "monitors" | "team" | "maintenance" | "infrastructure" | "logs";
+
+interface TableState {
+ rowsPerPage: number;
+}
+
+interface SidebarState {
+ collapsed: boolean;
+}
+
+interface UIState {
+ monitors: TableState;
+ team: TableState;
+ maintenance: TableState;
+ infrastructure: TableState;
+ logs: TableState;
+ sidebar: SidebarState;
+ mode: ThemeMode;
+ showURL: boolean;
+ timezone: string;
+ distributedUptimeEnabled: boolean;
+ language: string;
+ starPromptOpen: boolean;
+ chartType: ChartType;
+}
+
+const initialMode: ThemeMode = window?.matchMedia?.("(prefers-color-scheme: dark)")
+ ?.matches
+ ? "dark"
+ : "light";
+
+const initialState: UIState = {
+ monitors: {
+ rowsPerPage: 10,
+ },
+ team: {
+ rowsPerPage: 5,
+ },
+ maintenance: {
+ rowsPerPage: 5,
+ },
+ infrastructure: {
+ rowsPerPage: 5,
+ },
+ logs: {
+ rowsPerPage: 15,
+ },
+ sidebar: {
+ collapsed: false,
+ },
+ mode: initialMode,
+ showURL: false,
+ timezone: "America/Toronto",
+ distributedUptimeEnabled: false,
+ language: "en",
+ starPromptOpen: true,
+ chartType: "histogram",
+};
+
+const uiSlice = createSlice({
+ name: "ui",
+ initialState,
+ reducers: {
+ setDistributedUptimeEnabled: (state, action: PayloadAction) => {
+ state.distributedUptimeEnabled = action.payload;
+ },
+ setRowsPerPage: (
+ state,
+ action: PayloadAction<{ table: TableName; value: number }>
+ ) => {
+ const { table, value } = action.payload;
+ state[table].rowsPerPage = value;
+ },
+ toggleSidebar: (state) => {
+ state.sidebar.collapsed = !state.sidebar.collapsed;
+ },
+ setCollapsed: (state, action: PayloadAction<{ collapsed: boolean }>) => {
+ state.sidebar.collapsed = action.payload.collapsed;
+ },
+ setMode: (state, action: PayloadAction) => {
+ state.mode = action.payload;
+ },
+ setShowURL: (state, action: PayloadAction) => {
+ state.showURL = action.payload;
+ },
+
+ setTimezone: (state, action: PayloadAction<{ timezone: string }>) => {
+ state.timezone = action.payload.timezone;
+ },
+ setLanguage: (state, action: PayloadAction) => {
+ state.language = action.payload;
+ },
+ setStarPromptOpen: (state, action: PayloadAction) => {
+ state.starPromptOpen = action.payload;
+ },
+ setChartType: (state, action: PayloadAction) => {
+ state.chartType = action.payload;
+ },
+ },
+});
+
+export type { UIState, ThemeMode, ChartType, TableName };
+export default uiSlice.reducer;
+export const {
+ setRowsPerPage,
+ toggleSidebar,
+ setCollapsed,
+ setMode,
+ setShowURL,
+ setTimezone,
+ setDistributedUptimeEnabled,
+ setLanguage,
+ setStarPromptOpen,
+ setChartType,
+} = uiSlice.actions;
diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts
index 153cf9f19..633fcb5c9 100644
--- a/client/src/Hooks/useMonitorForm.ts
+++ b/client/src/Hooks/useMonitorForm.ts
@@ -83,10 +83,10 @@ export const useMonitorForm = ({
type: "hardware",
url: data?.url || "",
secret: data?.secret || "",
- cpuAlertThreshold: data?.cpuAlertThreshold ?? 80,
- memoryAlertThreshold: data?.memoryAlertThreshold ?? 80,
- diskAlertThreshold: data?.diskAlertThreshold ?? 80,
- tempAlertThreshold: data?.tempAlertThreshold ?? 80,
+ cpuAlertThreshold: data?.cpuAlertThreshold ?? 100,
+ memoryAlertThreshold: data?.memoryAlertThreshold ?? 100,
+ diskAlertThreshold: data?.diskAlertThreshold ?? 100,
+ tempAlertThreshold: data?.tempAlertThreshold ?? 100,
selectedDisks: data?.selectedDisks || [],
};
break;
diff --git a/client/src/Pages/Auth/Register/index.tsx b/client/src/Pages/Auth/Register/index.tsx
index 75d27649f..3c8a4e4bc 100644
--- a/client/src/Pages/Auth/Register/index.tsx
+++ b/client/src/Pages/Auth/Register/index.tsx
@@ -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(
+ token ? null : "/auth/users/superadmin"
+ );
+
const { control, handleSubmit, setError, reset } = useForm({
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;
diff --git a/client/src/Pages/Auth/SetNewPassword/index.tsx b/client/src/Pages/Auth/SetNewPassword/index.tsx
index 09bd565f1..2e99be254 100644
--- a/client/src/Pages/Auth/SetNewPassword/index.tsx
+++ b/client/src/Pages/Auth/SetNewPassword/index.tsx
@@ -97,27 +97,27 @@ const SetNewPasswordPage = () => {
/>
@@ -127,7 +127,7 @@ const SetNewPasswordPage = () => {
fullWidth
loading={loading}
>
- {t("auth.forgotPassword.buttons.resetPassword")}
+ {t("common.buttons.resetPassword")}
{
- return ;
+ return ;
},
},
{
diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx
index 3972b8ebb..1c111e23d 100644
--- a/client/src/Pages/CreateMonitor/index.tsx
+++ b/client/src/Pages/CreateMonitor/index.tsx
@@ -329,11 +329,16 @@ const CreateMonitorPage = () => {
render={({ field, fieldState }) => (
field.onChange(Number(e.target.value) || 0)}
+ value={field.value === 0 ? "" : field.value}
+ onChange={(e) => {
+ const val = e.target.value;
+ field.onChange(val === "" ? 0 : Number(val));
+ }}
type="number"
- fieldLabel={t("portToMonitor")}
- placeholder="5173"
+ fieldLabel={t("pages.createMonitor.form.general.option.port.label")}
+ placeholder={t(
+ "pages.createMonitor.form.general.option.port.placeholder"
+ )}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
@@ -351,10 +356,12 @@ const CreateMonitorPage = () => {
)}
/>
}
/>
+ {/* Alert Thresholds - only for hardware type */}
+ {generalSettingsConfig.showSecret && (
+
+ (
+ `${value}%`}
+ />
+ )}
+ />
+ (
+ `${value}%`}
+ />
+ )}
+ />
+ (
+ `${value}%`}
+ />
+ )}
+ />
+ (
+ `${value}°C`}
+ />
+ )}
+ />
+
+ }
+ />
+ )}
+
{
"pages.createMonitor.form.advanced.option.matchMethod.label"
)}
>
- {t("matchMethodOptions.equal")}
-
- {t("matchMethodOptions.include")}
+
+ {t(
+ "pages.createMonitor.form.advanced.option.matchMethod.equal"
+ )}
+
+
+ {t(
+ "pages.createMonitor.form.advanced.option.matchMethod.include"
+ )}
+
+
+ {t(
+ "pages.createMonitor.form.advanced.option.matchMethod.regex"
+ )}
- {t("matchMethodOptions.regex")}
)}
/>
diff --git a/client/src/Pages/Incidents/Components/DialogResolution.tsx b/client/src/Pages/Incidents/Components/DialogResolution.tsx
index 648af52a9..a102b1de3 100644
--- a/client/src/Pages/Incidents/Components/DialogResolution.tsx
+++ b/client/src/Pages/Incidents/Components/DialogResolution.tsx
@@ -46,10 +46,9 @@ export const DialogResolution = ({
return (