hook up offline banner

This commit is contained in:
Alex Holliday
2026-02-10 23:53:06 +00:00
parent 0503c25663
commit 10bfdc9da1
6 changed files with 59 additions and 211 deletions
@@ -1,30 +1,39 @@
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
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;
onRetry?: () => void;
isRetrying?: boolean;
}
export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerProps) => {
export const OfflineBanner = ({ visible }: OfflineBannerProps) => {
const theme = useTheme();
const { t } = useTranslation();
const [shouldRender, setShouldRender] = useState(visible);
const [isAnimating, setIsAnimating] = useState(false);
if (!visible) {
return null;
}
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: 0,
top: isAnimating ? 0 : "-100%",
left: 0,
right: 0,
zIndex: theme.zIndex.snackbar,
@@ -32,6 +41,7 @@ export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerPro
color: theme.palette.error.contrastText,
px: theme.spacing(8),
py: theme.spacing(4),
transition: "top 1s ease-in-out",
}}
>
<Stack
@@ -47,29 +57,6 @@ export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerPro
>
{t("components.offlineBanner.serverUnreachable")}
</Typography>
{onRetry && (
<Button
size="small"
variant="outlined"
onClick={onRetry}
disabled={isRetrying}
sx={{
color: "inherit",
borderColor: "currentColor",
minWidth: "auto",
py: 0.5,
px: 2,
"&:hover": {
borderColor: "currentColor",
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
}}
>
{isRetrying
? t("components.offlineBanner.retrying")
: t("components.offlineBanner.retry")}
</Button>
)}
</Stack>
</Box>
);
+31 -1
View File
@@ -1,10 +1,12 @@
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;
@@ -15,6 +17,34 @@ const AppLayout = ({ children }: AppLayoutProps) => {
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={{
@@ -29,7 +59,7 @@ const AppLayout = ({ children }: AppLayoutProps) => {
}}
>
<ThemeProvider theme={v2theme}>
<OfflineBanner visible={false} />
<OfflineBanner visible={serverUnreachable} />
</ThemeProvider>
{children}
</Box>
-166
View File
@@ -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
View File
@@ -25,9 +25,6 @@ import PageSpeedDetails from "@/Pages/PageSpeed/Details/";
import Infrastructure from "@/Pages/Infrastructure/Monitors";
import InfrastructureDetails from "@/Pages/Infrastructure/Details/index";
// Server Status
import ServerUnreachable from "../Pages/ServerUnreachable.jsx";
// Checks
import Checks from "../Pages/Checks/index";
@@ -432,10 +429,6 @@ const Routes = () => {
}
/>
<Route
path="/server-unreachable"
element={<ServerUnreachable />}
/>
<Route
path="*"
element={
+5 -1
View File
@@ -48,7 +48,11 @@ 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") {
+5 -5
View File
@@ -582,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",
@@ -1019,7 +1019,7 @@
}
},
"fallback": {
"actionButton": "Let's create your first infrastructure monitor!",
"actionButton": "Create a monitor!",
"checks": [
"Track the performance of your servers",
"Identify bottlenecks and optimize usage",
@@ -1087,7 +1087,7 @@
},
"maintenanceWindow": {
"fallback": {
"actionButton": "Let's create your first maintenance window!",
"actionButton": "Create a maintenance window!",
"checks": [
"Mark your maintenance periods",
"Eliminate any misunderstandings",
@@ -1228,7 +1228,7 @@
}
},
"fallback": {
"actionButton": "Let's create your first PageSpeed monitor!",
"actionButton": "Create a monitor!",
"checks": [
"Report on the user experience of a page",
"Help analyze webpage speed",
@@ -1247,7 +1247,7 @@
"Build trust with transparent service monitoring",
"Reduce support requests during incidents"
],
"actionButton": "Let's create your first status page!"
"actionButton": "Create a status page!"
},
"monitorsList": {
"chartTypeHeatmap": "Heatmap",