diff --git a/docs/quickstart/server-statefulset.yaml b/docs/quickstart/server-statefulset.yaml
index 60f2490b..61ac1181 100644
--- a/docs/quickstart/server-statefulset.yaml
+++ b/docs/quickstart/server-statefulset.yaml
@@ -61,3 +61,4 @@ spec:
resources:
requests:
storage: 1Gi
+
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4a51c881..efcd03aa 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -19,49 +19,52 @@ import TornjakServerInfo from "./components/tornjak-server-info";
import TornjakDashBoardStyled from "./components/dashboard/tornjak-dashboard";
import DashboardDetailsRender from 'components/dashboard/dashboard-details-render';
import RenderOnAdminRole from 'components/RenderOnAdminRole'
+import { GlobalErrorBoundaryWithHooks } from 'components/error-boundary'
import './App.css';
import 'react-toastify/dist/ReactToastify.css';
function App() {
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
- {IsManager &&
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
()}
- />
-
-
+
+
+
+
+ {IsManager &&
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ()}
+ />
+
+
+
-
-
-
+
+
+
)
}
diff --git a/frontend/src/Utils/axiosSetup.ts b/frontend/src/Utils/axiosSetup.ts
new file mode 100644
index 00000000..9b3163d6
--- /dev/null
+++ b/frontend/src/Utils/axiosSetup.ts
@@ -0,0 +1,24 @@
+import axios from 'axios';
+
+// Callback to notify app when server is down
+let onServerDownCallback: (() => void) | null = null;
+
+export const setServerDownHandler = (callback: () => void) => {
+ onServerDownCallback = callback;
+};
+
+// Axios response interceptor to catch network/server down errors
+axios.interceptors.response.use(
+ response => response,
+ error => {
+ // If no response (network error or server down)
+ if (!error.response) {
+ console.warn('Axios interceptor detected network/server down');
+
+ if (onServerDownCallback) {
+ onServerDownCallback();
+ }
+ }
+ return Promise.reject(error);
+ }
+);
\ No newline at end of file
diff --git a/frontend/src/components/agent-list.tsx b/frontend/src/components/agent-list.tsx
index 3ed0a78b..8088ab0e 100644
--- a/frontend/src/components/agent-list.tsx
+++ b/frontend/src/components/agent-list.tsx
@@ -133,7 +133,10 @@ class AgentList extends Component {
{this.props.globalErrorMessage !== "OK" &&
- {this.props.globalErrorMessage}
+ {typeof this.props.globalErrorMessage === 'string'
+ ? this.props.globalErrorMessage
+ : JSON.stringify(this.props.globalErrorMessage, null, 2)
+ }
diff --git a/frontend/src/components/error-boundary/components/GlobalErrorBoundary.tsx b/frontend/src/components/error-boundary/components/GlobalErrorBoundary.tsx
new file mode 100644
index 00000000..629e5885
--- /dev/null
+++ b/frontend/src/components/error-boundary/components/GlobalErrorBoundary.tsx
@@ -0,0 +1,94 @@
+import React, { Component, ReactNode, useEffect, useRef } from 'react';
+import {
+ GlobalErrorBoundaryProps,
+ GlobalErrorState,
+ AppError,
+} from '../error.types';
+import { classifyError, isServerError } from '../utils';
+import { ServerDownError } from './ServerDownError';
+import { setServerDownHandler } from '../../../Utils/axiosSetup';
+
+export class GlobalErrorBoundary extends Component {
+ private isUnmounted = false;
+
+ constructor(props: GlobalErrorBoundaryProps) {
+ super(props);
+ this.state = {
+ hasError: false,
+ isServerDown: false,
+ error: undefined,
+ };
+ }
+
+ componentWillUnmount() {
+ this.isUnmounted = true;
+ }
+
+ static getDerivedStateFromError(error: Error): Partial {
+ const appError: AppError = classifyError(error);
+ console.log('Error classified:', appError.message, appError.type);
+ return {
+ hasError: true,
+ error: appError,
+ isServerDown: isServerError(appError),
+ };
+ }
+
+ // Methods to be called by the wrapper component
+ public updateState = (newState: Partial) => {
+ if (!this.isUnmounted) {
+ this.setState((prevState) => ({ ...prevState, ...newState }));
+ }
+ };
+
+ public resetError = () => {
+ this.setState({
+ hasError: false,
+ error: undefined,
+ isServerDown: false
+ });
+ };
+
+ private handleReloadPage = () => {
+ window.location.reload();
+ };
+
+ render(): ReactNode {
+ const { hasError, isServerDown } = this.state;
+ const { customServerDownMessage, children } = this.props;
+
+ if (!hasError) {
+ return children;
+ }
+
+ if (isServerDown) {
+ return (
+
+ );
+ }
+ }
+}
+
+const GlobalErrorBoundaryWrapper: React.FC = (props) => {
+ const errorBoundaryRef = useRef(null);
+ useEffect(() => {
+ setServerDownHandler(() => {
+ errorBoundaryRef.current?.updateState({
+ hasError: true,
+ isServerDown: true,
+ error: {
+ message: props.customServerDownMessage || 'Unable to connect to server',
+ type: 'server',
+ timestamp: new Date(),
+ } as AppError,
+ });
+ });
+ }, [props.customServerDownMessage]);
+
+ return ;
+};
+
+export { GlobalErrorBoundaryWrapper as GlobalErrorBoundaryWithHooks };
\ No newline at end of file
diff --git a/frontend/src/components/error-boundary/components/ServerDownError.tsx b/frontend/src/components/error-boundary/components/ServerDownError.tsx
new file mode 100644
index 00000000..536155d4
--- /dev/null
+++ b/frontend/src/components/error-boundary/components/ServerDownError.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { Tile, Button, Loading } from 'carbon-components-react';
+import { CloudOffline, Restart } from '@carbon/icons-react';
+import styles from '../styles/ServerDownError.module.css';
+
+interface ServerDownErrorProps {
+ customMessage?: string;
+ onReloadPage: () => void;
+}
+
+export const ServerDownError: React.FC = ({
+ customMessage,
+ onReloadPage,
+}) => {
+ const defaultMessage = 'The server is currently unavailable. Please try again later.';
+
+ return (
+
+
+
+
+
+
+ Server Unavailable
+
+
+
+ {customMessage || defaultMessage}
+
+
+
+ Please ensure the server is running and try again.
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/error-boundary/constants.ts b/frontend/src/components/error-boundary/constants.ts
new file mode 100644
index 00000000..9c9887bc
--- /dev/null
+++ b/frontend/src/components/error-boundary/constants.ts
@@ -0,0 +1,11 @@
+export const ERROR_MESSAGES = {
+ SERVER_DOWN: 'Unable to connect to server',
+ NETWORK_ERROR: 'Network connection error occurred',
+ UNEXPECTED_ERROR: 'An unexpected error occurred'
+} as const;
+
+export const ERROR_TYPES = {
+ NETWORK: 'network',
+ SERVER: 'server',
+ UNKNOWN: 'unknown'
+} as const;
\ No newline at end of file
diff --git a/frontend/src/components/error-boundary/error.types.ts b/frontend/src/components/error-boundary/error.types.ts
new file mode 100644
index 00000000..fbc3ba3b
--- /dev/null
+++ b/frontend/src/components/error-boundary/error.types.ts
@@ -0,0 +1,22 @@
+export interface AppError {
+ message: string;
+ type: 'server' | 'network' | 'unknown';
+ statusCode?: number;
+ timestamp: Date;
+ details?: any;
+}
+
+export interface GlobalErrorState {
+ hasError: boolean;
+ error?: AppError;
+ isServerDown: boolean;
+}
+
+export interface GlobalErrorBoundaryProps {
+ children: React.ReactNode;
+
+ onServerError?: (error: AppError) => void;
+
+ customServerDownMessage?: string;
+ customNetworkErrorMessage?: string;
+}
\ No newline at end of file
diff --git a/frontend/src/components/error-boundary/index.ts b/frontend/src/components/error-boundary/index.ts
new file mode 100644
index 00000000..ad48116c
--- /dev/null
+++ b/frontend/src/components/error-boundary/index.ts
@@ -0,0 +1,15 @@
+export { GlobalErrorBoundary } from './components/GlobalErrorBoundary';
+export { GlobalErrorBoundaryWithHooks } from './components/GlobalErrorBoundary';
+export { ServerDownError } from './components/ServerDownError';
+
+export type {
+ AppError,
+ GlobalErrorState,
+ GlobalErrorBoundaryProps
+} from './error.types';
+
+export { classifyError, isServerError } from './utils';
+export { ERROR_MESSAGES, ERROR_TYPES } from './constants';
+
+// Default export
+export { GlobalErrorBoundaryWithHooks as default } from './components/GlobalErrorBoundary';
\ No newline at end of file
diff --git a/frontend/src/components/error-boundary/styles/ServerDownError.module.css b/frontend/src/components/error-boundary/styles/ServerDownError.module.css
new file mode 100644
index 00000000..cf4e4ff8
--- /dev/null
+++ b/frontend/src/components/error-boundary/styles/ServerDownError.module.css
@@ -0,0 +1,74 @@
+.errorContainer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ padding: 2rem;
+ background-color: #f4f4f4;
+}
+
+.errorTile {
+ max-width: 500px;
+ width: 100%;
+ text-align: center;
+ padding: 3rem 2rem;
+}
+
+.iconContainer {
+ margin-bottom: 2rem;
+}
+
+.serverOfflineIcon {
+ color: #fa4d56;
+ margin-bottom: 1rem;
+}
+
+.loadingContainer {
+ margin-bottom: 2rem;
+}
+
+.title {
+ font-size: 1.75rem;
+ font-weight: 600;
+ color: #161616;
+ margin-bottom: 1rem;
+}
+
+.message {
+ font-size: 1rem;
+ color: #525252;
+ margin-bottom: 1rem;
+ line-height: 1.5;
+}
+
+.description {
+ font-size: 0.875rem;
+ color: #6f6f6f;
+ margin-bottom: 2rem;
+}
+
+.actionButtons {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+@media (max-width: 768px) {
+ .errorContainer {
+ padding: 1rem;
+ }
+
+ .errorTile {
+ padding: 2rem 1rem;
+ }
+
+ .title {
+ font-size: 1.5rem;
+ }
+
+ .actionButtons {
+ flex-direction: column;
+ align-items: center;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/error-boundary/utils.ts b/frontend/src/components/error-boundary/utils.ts
new file mode 100644
index 00000000..cdd19770
--- /dev/null
+++ b/frontend/src/components/error-boundary/utils.ts
@@ -0,0 +1,26 @@
+import { AppError } from './error.types';
+import { ERROR_MESSAGES, ERROR_TYPES } from './constants';
+
+export const classifyError = (error: any): AppError => {
+ const timestamp = new Date();
+
+ if (error.isAxiosError && (!error.response || error.code === 'ERR_NETWORK')) {
+ return {
+ message: ERROR_MESSAGES.SERVER_DOWN,
+ type: ERROR_TYPES.SERVER,
+ timestamp,
+ details: { originalError: error.message }
+ };
+ }
+
+ return {
+ message: error?.message || ERROR_MESSAGES.UNEXPECTED_ERROR,
+ type: ERROR_TYPES.UNKNOWN,
+ timestamp,
+ details: error
+ };
+};
+
+export const isServerError = (error: AppError): boolean => {
+ return error.type === ERROR_TYPES.SERVER || error.type === ERROR_TYPES.NETWORK;
+};
\ No newline at end of file
diff --git a/resources/start_tornjak_server.sh b/resources/start_tornjak_server.sh
new file mode 100755
index 00000000..32ea7b87
--- /dev/null
+++ b/resources/start_tornjak_server.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+cd docs/quickstart
+cp server-statefulset-examples/backend-sidecar-server-statefulset.yaml server-statefulset.yaml
+
+kubectl apply -f spire-namespace.yaml \
+ -f server-account.yaml \
+ -f spire-bundle-configmap.yaml \
+ -f tornjak-configmap.yaml \
+ -f server-cluster-role.yaml \
+ -f server-configmap.yaml \
+ -f server-statefulset.yaml \
+ -f server-service.yaml
+
+sleep 5s
+
+kubectl get statefulset --namespace spire
+
+kubectl apply \
+ -f agent-account.yaml \
+ -f agent-cluster-role.yaml \
+ -f agent-configmap.yaml \
+ -f agent-daemonset.yaml
+
+sleep 5s
+
+kubectl get daemonset --namespace spire
+
+kubectl exec -n spire -c spire-server spire-server-0 -- \
+ /opt/spire/bin/spire-server entry create \
+ -spiffeID spiffe://example.org/ns/spire/sa/spire-agent \
+ -selector k8s_psat:cluster:demo-cluster \
+ -selector k8s_psat:agent_ns:spire \
+ -selector k8s_psat:agent_sa:spire-agent \
+ -node
+
+kubectl exec -n spire -c spire-server spire-server-0 -- \
+ /opt/spire/bin/spire-server entry create \
+ -spiffeID spiffe://example.org/ns/default/sa/default \
+ -parentID spiffe://example.org/ns/spire/sa/spire-agent \
+ -selector k8s:ns:default \
+ -selector k8s:sa:default
+
+sleep 5s
+
+kubectl apply -f client-deployment.yaml
+
+kubectl exec -it $(kubectl get pods -o=jsonpath='{.items[0].metadata.name}' \
+ -l app=client) -- /opt/spire/bin/spire-agent api fetch -socketPath /run/spire/sockets/agent.sock
+
+sleep 5s
+
+kubectl -n spire describe pod spire-server-0 | grep "Image:"
+
+kubectl -n spire port-forward spire-server-0 10000:10000
\ No newline at end of file