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