2026-07-23 19:01:18 +02:00
|
|
|
|
import React, { Suspense, lazy, Component, ReactNode } from 'react';
|
|
|
|
|
|
import { Loader2 } from 'lucide-react';
|
2026-08-13 21:57:59 +02:00
|
|
|
|
import { logError } from '@/utils/errorLogger';
|
2026-07-23 19:01:18 +02:00
|
|
|
|
|
|
|
|
|
|
// ── Error Boundary ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
interface PluginErrorBoundaryProps {
|
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
|
pluginName: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface PluginErrorBoundaryState {
|
|
|
|
|
|
hasError: boolean;
|
2026-08-13 21:57:59 +02:00
|
|
|
|
error: Error | null;
|
|
|
|
|
|
traceId: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function generateTraceId(): string {
|
|
|
|
|
|
return 'plugin-err-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
2026-07-23 19:01:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class PluginErrorBoundary extends Component<PluginErrorBoundaryProps, PluginErrorBoundaryState> {
|
2026-08-13 21:57:59 +02:00
|
|
|
|
state: PluginErrorBoundaryState = { hasError: false, error: null, traceId: null };
|
2026-07-23 19:01:18 +02:00
|
|
|
|
|
2026-08-13 21:57:59 +02:00
|
|
|
|
static getDerivedStateFromError(error: Error): PluginErrorBoundaryState {
|
|
|
|
|
|
return { hasError: true, error, traceId: generateTraceId() };
|
2026-07-23 19:01:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 21:57:59 +02:00
|
|
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
|
|
|
|
logError(error, {
|
|
|
|
|
|
componentStack: errorInfo.componentStack,
|
|
|
|
|
|
pluginName: this.props.pluginName,
|
|
|
|
|
|
trace_id: this.state.traceId,
|
|
|
|
|
|
source: 'PluginErrorBoundary',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleRetry = (): void => {
|
|
|
|
|
|
this.setState({ hasError: false, error: null, traceId: null });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-23 19:01:18 +02:00
|
|
|
|
render() {
|
|
|
|
|
|
if (this.state.hasError) {
|
|
|
|
|
|
return (
|
2026-08-13 21:57:59 +02:00
|
|
|
|
<div
|
|
|
|
|
|
className="flex flex-col items-center justify-center min-h-[300px] p-8 text-center"
|
|
|
|
|
|
role="alert"
|
|
|
|
|
|
aria-live="assertive"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="text-danger-500 text-4xl mb-3" aria-hidden="true">⚠️</div>
|
|
|
|
|
|
<h2 className="text-lg font-bold text-secondary-900 mb-2">
|
|
|
|
|
|
Plugin konnte nicht geladen werden
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p className="text-sm text-secondary-600 mb-3">
|
|
|
|
|
|
Das Plugin „{this.props.pluginName}" hat einen Fehler verursacht.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{this.state.traceId && (
|
|
|
|
|
|
<p className="text-xs text-secondary-400 mb-3 font-mono" data-testid="plugin-error-trace-id">
|
|
|
|
|
|
Trace-ID: {this.state.traceId}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{this.state.error && (
|
|
|
|
|
|
<details className="mb-4 max-w-lg text-left text-secondary-500">
|
|
|
|
|
|
<summary className="cursor-pointer text-sm hover:text-secondary-700">
|
|
|
|
|
|
Fehlerdetails
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<pre className="mt-2 p-3 bg-secondary-50 rounded text-xs overflow-auto max-h-32">
|
|
|
|
|
|
{this.state.error.message}
|
|
|
|
|
|
{this.state.error.stack ? `\n\n${this.state.error.stack}` : ''}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
</details>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={this.handleRetry}
|
|
|
|
|
|
className="inline-flex items-center px-4 py-2 bg-primary-600 text-white text-sm font-medium rounded-md hover:bg-primary-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 transition-colors min-h-touch"
|
|
|
|
|
|
>
|
|
|
|
|
|
Erneut versuchen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => window.location.reload()}
|
|
|
|
|
|
className="inline-flex items-center px-4 py-2 bg-secondary-100 text-secondary-700 text-sm font-medium rounded-md hover:bg-secondary-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-secondary-400 transition-colors min-h-touch"
|
|
|
|
|
|
>
|
|
|
|
|
|
Neu laden
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-07-23 19:01:18 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.props.children;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Lazy Component Cache ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const componentCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>();
|
|
|
|
|
|
|
|
|
|
|
|
function getLazyComponent(componentPath: string): React.LazyExoticComponent<React.ComponentType<any>> {
|
|
|
|
|
|
if (componentCache.has(componentPath)) {
|
|
|
|
|
|
return componentCache.get(componentPath)!;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 19:29:48 +02:00
|
|
|
|
// Convert @/pages/Mail to ../../pages/Mail for dynamic import
|
|
|
|
|
|
// PluginLoader.tsx is in src/components/plugins/, so ../../ resolves to src/
|
|
|
|
|
|
const importPath = componentPath.replace(/^@\//, '../../');
|
2026-07-23 19:01:18 +02:00
|
|
|
|
|
|
|
|
|
|
const LazyComp = lazy(() =>
|
|
|
|
|
|
import(/* @vite-ignore */ importPath).then((m) => ({
|
|
|
|
|
|
default: m.default || m[Object.keys(m)[0]],
|
|
|
|
|
|
}))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
componentCache.set(componentPath, LazyComp);
|
|
|
|
|
|
return LazyComp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── PluginPage Component ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
interface PluginPageProps {
|
|
|
|
|
|
component: string;
|
|
|
|
|
|
pluginName: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* PluginPage — dynamically loads a plugin page component using React.lazy().
|
|
|
|
|
|
* Wraps the lazy component in Suspense with a centered spinner fallback
|
|
|
|
|
|
* and an ErrorBoundary that shows a fallback message on failure.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function PluginPage({ component, pluginName }: PluginPageProps) {
|
|
|
|
|
|
const LazyComp = getLazyComponent(component);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PluginErrorBoundary pluginName={pluginName}>
|
|
|
|
|
|
<Suspense
|
|
|
|
|
|
fallback={
|
|
|
|
|
|
<div className="flex items-center justify-center min-h-[50vh]">
|
|
|
|
|
|
<Loader2 className="animate-spin h-8 w-8 text-primary-500" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<LazyComp />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
</PluginErrorBoundary>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|