diff --git a/client/src/App.tsx b/client/src/App.tsx
index ef3625cad..0a6b364f8 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -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() {
diff --git a/client/src/Components/v1/Layouts/AppLayout/index.jsx b/client/src/Components/v1/Layouts/AppLayout/index.jsx
deleted file mode 100644
index 0dbfb1e7a..000000000
--- a/client/src/Components/v1/Layouts/AppLayout/index.jsx
+++ /dev/null
@@ -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 (
-
- {children}
-
- );
-};
-
-AppLayout.propTypes = {
- children: PropTypes.node,
-};
-
-export default AppLayout;
diff --git a/client/src/Components/v2/design-elements/OfflineBanner.tsx b/client/src/Components/v2/design-elements/OfflineBanner.tsx
new file mode 100644
index 000000000..fa5690c12
--- /dev/null
+++ b/client/src/Components/v2/design-elements/OfflineBanner.tsx
@@ -0,0 +1,76 @@
+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";
+
+interface OfflineBannerProps {
+ visible: boolean;
+ onRetry?: () => void;
+ isRetrying?: boolean;
+}
+
+export const OfflineBanner = ({ visible, onRetry, isRetrying }: OfflineBannerProps) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {t("components.offlineBanner.serverUnreachable")}
+
+ {onRetry && (
+
+ )}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx
index b7b1d484e..9a4b97a44 100644
--- a/client/src/Components/v2/design-elements/index.tsx
+++ b/client/src/Components/v2/design-elements/index.tsx
@@ -19,3 +19,4 @@ export * from "./Gauge";
export * from "./Tabs";
export * from "./SplitBox";
export * from "./TextLink";
+export * from "./OfflineBanner";
diff --git a/client/src/Components/v2/layout/AppLayout.tsx b/client/src/Components/v2/layout/AppLayout.tsx
new file mode 100644
index 000000000..1310863f9
--- /dev/null
+++ b/client/src/Components/v2/layout/AppLayout.tsx
@@ -0,0 +1,39 @@
+import Box from "@mui/material/Box";
+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";
+
+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;
+
+ return (
+
+
+
+ {children}
+
+
+ );
+};
+
+export default AppLayout;
diff --git a/client/src/Components/v2/layout/RootLayout.tsx b/client/src/Components/v2/layout/RootLayout.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/client/src/Components/v2/sidebar/index.tsx b/client/src/Components/v2/sidebar/index.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts
index c39839558..4b2f4971f 100644
--- a/client/src/Utils/ApiClient.ts
+++ b/client/src/Utils/ApiClient.ts
@@ -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;
@@ -41,6 +50,12 @@ export const initApiClient = (store: StoreType): void => {
const onSuccess = (response: AxiosResponse) => 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";
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index cbd9ea7b7..096fabd9d 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -325,6 +325,12 @@
"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"
}
},
"configure": "Configure",