diff --git a/client/src/Components/v2/design-elements/OfflineBanner.tsx b/client/src/Components/v2/design-elements/OfflineBanner.tsx index fa5690c12..062b1ca0d 100644 --- a/client/src/Components/v2/design-elements/OfflineBanner.tsx +++ b/client/src/Components/v2/design-elements/OfflineBanner.tsx @@ -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 ( {t("components.offlineBanner.serverUnreachable")} - {onRetry && ( - - )} ); diff --git a/client/src/Components/v2/layout/AppLayout.tsx b/client/src/Components/v2/layout/AppLayout.tsx index 3ae2729e3..7c9c3c903 100644 --- a/client/src/Components/v2/layout/AppLayout.tsx +++ b/client/src/Components/v2/layout/AppLayout.tsx @@ -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(null); + + useEffect(() => { + setServerUnreachableCallback(setServerUnreachable); + }, []); + + useEffect(() => { + if (serverUnreachable) { + retryIntervalRef.current = window.setInterval(async () => { + try { + await get("/health", { timeout: 5000 }); + } catch { + // NO_OP + } + }, 5000); + } else if (retryIntervalRef.current) { + clearInterval(retryIntervalRef.current); + retryIntervalRef.current = null; + } + + return () => { + if (retryIntervalRef.current) { + clearInterval(retryIntervalRef.current); + } + }; + }, [serverUnreachable]); + return ( { }} > - + {children} diff --git a/client/src/Pages/ServerUnreachable.jsx b/client/src/Pages/ServerUnreachable.jsx deleted file mode 100644 index 5051ee95f..000000000 --- a/client/src/Pages/ServerUnreachable.jsx +++ /dev/null @@ -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 ( - - - - - - {/* Header with logo */} - - - - {t("common.appName")} - - - - - - - .MuiStack-root": { - border: 1, - borderRadius: theme.spacing(5), - borderColor: theme.palette.primary.lowContrast, - backgroundColor: theme.palette.primary.main, - padding: { - xs: theme.spacing(12), - sm: theme.spacing(20), - }, - }, - }} - > - - - - - - - {t("errorPages.serverUnreachable.description")} - - - - - - - - - ); -}; - -export default ServerUnreachable; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 0a66b0cf3..7d9163c43 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -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 = () => { } /> - } - /> { } ); - 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") { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 86b220998..d2a066c11 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -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",