mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-19 16:08:39 -05:00
hook up offline banner
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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={
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user