Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/quickstart/server-statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ spec:
resources:
requests:
storage: 1Gi

71 changes: 37 additions & 34 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
<Provider store={store}>
<Router>
<div className="app-container">
<div className="sidebar">
<NavigationBar />
</div>
<GlobalErrorBoundaryWithHooks>
<Provider store={store}>
<Router>
<div className="app-container">
<div className="sidebar">
<NavigationBar />
</div>

<div className="main-content">
<div className="main">
<SelectServer />
<br />
{IsManager && <br />}
<Route path="/" exact component={AgentList} />
<Route path="/clusters" exact component={ClusterList} />
<Route path="/federations" exact component={FederationList} />
<Route path="/trustbundle" exact component={TrustBundleCreate} />
<Route path="/federation/create" exact component={FederationCreate} />
<Route path="/agents" exact component={AgentList} />
<Route path="/entries" exact component={EntryList} />
<RenderOnAdminRole>
<Route path="/entry/create" exact component={EntryCreate} />
<Route path="/agent/createjointoken" exact component={CreateJoinToken} />
<Route path="/cluster/clustermanagement" exact component={ClusterManagement} />
</RenderOnAdminRole>
<Route path="/tornjak/serverinfo" exact component={TornjakServerInfo} />
<Route path="/tornjak/dashboard" exact component={TornjakDashBoardStyled} />
<Route
path="/tornjak/dashboard/details/:entity"
render={(props) => (<DashboardDetailsRender {...props} params={props.match.params} />)}
/>
<Route path="/server/manage" exact component={ServerManagement} />
<br /><br /><br />
<div className="main-content">
<div className="main">
<SelectServer />
<br />
{IsManager && <br />}
<Route path="/" exact component={AgentList} />
<Route path="/clusters" exact component={ClusterList} />
<Route path="/federations" exact component={FederationList} />
<Route path="/trustbundle" exact component={TrustBundleCreate} />
<Route path="/federation/create" exact component={FederationCreate} />
<Route path="/agents" exact component={AgentList} />
<Route path="/entries" exact component={EntryList} />
<RenderOnAdminRole>
<Route path="/entry/create" exact component={EntryCreate} />
<Route path="/agent/createjointoken" exact component={CreateJoinToken} />
<Route path="/cluster/clustermanagement" exact component={ClusterManagement} />
</RenderOnAdminRole>
<Route path="/tornjak/serverinfo" exact component={TornjakServerInfo} />
<Route path="/tornjak/dashboard" exact component={TornjakDashBoardStyled} />
<Route
path="/tornjak/dashboard/details/:entity"
render={(props) => (<DashboardDetailsRender {...props} params={props.match.params} />)}
/>
<Route path="/server/manage" exact component={ServerManagement} />
<br /><br /><br />
</div>
</div>
</div>
</div>
</Router>
</Provider>
</Router>
</Provider>
</GlobalErrorBoundaryWithHooks>
</div>
)
}
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/Utils/axiosSetup.ts
Original file line number Diff line number Diff line change
@@ -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);
}
);
5 changes: 4 additions & 1 deletion frontend/src/components/agent-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ class AgentList extends Component<AgentListProp, AgentListState> {
{this.props.globalErrorMessage !== "OK" &&
<div className="alert-primary" role="alert">
<pre>
{this.props.globalErrorMessage}
{typeof this.props.globalErrorMessage === 'string'
? this.props.globalErrorMessage
: JSON.stringify(this.props.globalErrorMessage, null, 2)
}
</pre>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<GlobalErrorBoundaryProps, GlobalErrorState> {
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<GlobalErrorState> {
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<GlobalErrorState>) => {
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 (
<ServerDownError
customMessage={customServerDownMessage}
onReloadPage={this.handleReloadPage}
/>
);
}
}
}

const GlobalErrorBoundaryWrapper: React.FC<GlobalErrorBoundaryProps> = (props) => {
const errorBoundaryRef = useRef<GlobalErrorBoundary>(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 <GlobalErrorBoundary ref={errorBoundaryRef} {...props} />;
};

export { GlobalErrorBoundaryWrapper as GlobalErrorBoundaryWithHooks };
Original file line number Diff line number Diff line change
@@ -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<ServerDownErrorProps> = ({
customMessage,
onReloadPage,
}) => {
const defaultMessage = 'The server is currently unavailable. Please try again later.';

return (
<div className={styles.errorContainer}>
<Tile className={styles.errorTile}>
<div className={styles.iconContainer}>
<CloudOffline className={styles.serverOfflineIcon} />

<h2 className={styles.title}>
Server Unavailable
</h2>

<p className={styles.message}>
{customMessage || defaultMessage}
</p>

<p className={styles.description}>
Please ensure the server is running and try again.
</p>
</div>

<div className={styles.actionButtons}>
<Button
kind="secondary"
renderIcon={Restart}
onClick={onReloadPage}>
Reload Page
</Button>
</div>
</Tile>
</div>
);
};
11 changes: 11 additions & 0 deletions frontend/src/components/error-boundary/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
22 changes: 22 additions & 0 deletions frontend/src/components/error-boundary/error.types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
15 changes: 15 additions & 0 deletions frontend/src/components/error-boundary/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading