mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 07:24:34 +01:00
304 lines
8.0 KiB
TypeScript
304 lines
8.0 KiB
TypeScript
/**
|
|
* Plugin Page Renderer - Renders plugin pages with security isolation
|
|
*/
|
|
"use client"
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
import { useAuth } from '@/components/providers/auth-provider';
|
|
import { tokenManager } from '@/lib/token-manager';
|
|
import { usePlugin, type PluginInfo } from '../../contexts/PluginContext';
|
|
import { config } from '../../lib/config';
|
|
|
|
interface PluginPageRendererProps {
|
|
pluginId: string;
|
|
pagePath: string;
|
|
componentName?: string;
|
|
}
|
|
|
|
interface PluginIframeProps {
|
|
pluginId: string;
|
|
pagePath: string;
|
|
token: string;
|
|
onLoad?: () => void;
|
|
onError?: (error: string) => void;
|
|
}
|
|
|
|
const PluginIframe: React.FC<PluginIframeProps> = ({
|
|
pluginId,
|
|
pagePath,
|
|
token,
|
|
onLoad,
|
|
onError
|
|
}) => {
|
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const iframe = iframeRef.current;
|
|
if (!iframe) return;
|
|
|
|
// Set up iframe communication
|
|
const handleMessage = (event: MessageEvent) => {
|
|
// Only accept messages from our iframe
|
|
if (event.source !== iframe.contentWindow) return;
|
|
|
|
// Validate origin - should be from our backend
|
|
const allowedOrigins = [
|
|
window.location.origin,
|
|
config.API_BASE_URL,
|
|
config.API_BASE_URL
|
|
].filter(Boolean);
|
|
|
|
if (!allowedOrigins.some(origin => event.origin.startsWith(origin))) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const message = event.data;
|
|
|
|
switch (message.type) {
|
|
case 'plugin_loaded':
|
|
setLoading(false);
|
|
onLoad?.();
|
|
break;
|
|
case 'plugin_error':
|
|
setLoading(false);
|
|
onError?.(message.error || 'Plugin failed to load');
|
|
break;
|
|
case 'plugin_resize':
|
|
if (message.height && iframe) {
|
|
iframe.style.height = `${message.height}px`;
|
|
}
|
|
break;
|
|
case 'plugin_navigate':
|
|
// Handle navigation within plugin
|
|
if (message.path) {
|
|
// Update URL without reload
|
|
const newUrl = `/plugins/${pluginId}${message.path}`;
|
|
window.history.pushState(null, '', newUrl);
|
|
}
|
|
break;
|
|
}
|
|
} catch (err) {
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', handleMessage);
|
|
|
|
return () => {
|
|
window.removeEventListener('message', handleMessage);
|
|
};
|
|
}, [pluginId, onLoad, onError]);
|
|
|
|
const iframeUrl = `/api-internal/v1/plugins/${pluginId}/ui${pagePath}?token=${encodeURIComponent(token)}`;
|
|
|
|
return (
|
|
<div className="relative w-full">
|
|
{loading && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
|
|
<div className="flex items-center gap-2">
|
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
<span className="text-sm text-muted-foreground">Loading plugin...</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<iframe
|
|
ref={iframeRef}
|
|
src={iframeUrl}
|
|
title={`Plugin ${pluginId} - ${pagePath}`}
|
|
className="w-full border-0"
|
|
style={{
|
|
minHeight: '400px',
|
|
maxHeight: '100vh',
|
|
backgroundColor: 'transparent'
|
|
}}
|
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
|
|
onLoad={() => {
|
|
// Iframe loaded, but plugin might still be initializing
|
|
// Wait for plugin_loaded message
|
|
}}
|
|
onError={() => {
|
|
setLoading(false);
|
|
onError?.('Failed to load plugin iframe');
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const PluginComponentRenderer: React.FC<{
|
|
plugin: PluginInfo;
|
|
componentName: string;
|
|
}> = ({ plugin, componentName }) => {
|
|
const { getPluginComponent } = usePlugin();
|
|
|
|
const PluginComponent = getPluginComponent(plugin.id, componentName);
|
|
|
|
if (!PluginComponent) {
|
|
return (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Component '{componentName}' not found in plugin '{plugin.name}'
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="plugin-component-container">
|
|
<PluginComponent />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const PluginPageRenderer: React.FC<PluginPageRendererProps> = ({
|
|
pluginId,
|
|
pagePath,
|
|
componentName
|
|
}) => {
|
|
const { user } = useAuth();
|
|
const token = tokenManager.getAccessToken();
|
|
const {
|
|
installedPlugins,
|
|
getPluginPages,
|
|
isPluginPageAuthorized,
|
|
loading: pluginsLoading
|
|
} = usePlugin();
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Find the plugin
|
|
const plugin = installedPlugins.find(p => p.id === pluginId);
|
|
|
|
// Get plugin pages
|
|
const pluginPages = plugin ? getPluginPages(pluginId) : [];
|
|
const currentPage = pluginPages.find(p => p.path === pagePath);
|
|
|
|
useEffect(() => {
|
|
if (!pluginsLoading) {
|
|
setLoading(false);
|
|
}
|
|
}, [pluginsLoading]);
|
|
|
|
// Loading state
|
|
if (loading || pluginsLoading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="space-y-3">
|
|
<Skeleton className="h-6 w-3/4" />
|
|
<Skeleton className="h-4 w-1/2" />
|
|
<Skeleton className="h-32 w-full" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Plugin not found
|
|
if (!plugin) {
|
|
return (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Plugin '{pluginId}' not found or not installed.
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
// Plugin not enabled or loaded
|
|
if (plugin.status !== 'enabled' || !plugin.loaded) {
|
|
return (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Plugin '{plugin.name}' is not enabled or loaded. Please enable and load the plugin first.
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
// Check authorization
|
|
if (!isPluginPageAuthorized(pluginId, pagePath)) {
|
|
return (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
You are not authorized to view this plugin page.
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
// Page not found
|
|
if (!currentPage && pluginPages.length > 0) {
|
|
return (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Page '{pagePath}' not found in plugin '{plugin.name}'.
|
|
Available pages: {pluginPages.map(p => p.path).join(', ')}
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
// Authentication required
|
|
if (!user || !token) {
|
|
return (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Authentication required to view plugin pages.
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
// Error state
|
|
if (error) {
|
|
return (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Error loading plugin page: {error}
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
// Render component directly if componentName is provided
|
|
if (componentName) {
|
|
return <PluginComponentRenderer plugin={plugin} componentName={componentName} />;
|
|
}
|
|
|
|
// Render plugin page in iframe (default)
|
|
return (
|
|
<div className="space-y-4">
|
|
{currentPage && (
|
|
<div>
|
|
<h1 className="text-2xl font-bold">{currentPage.title || currentPage.name}</h1>
|
|
<p className="text-muted-foreground">
|
|
{plugin.name} v{plugin.version}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<PluginIframe
|
|
pluginId={pluginId}
|
|
pagePath={pagePath}
|
|
token={token}
|
|
onLoad={() => setError(null)}
|
|
onError={setError}
|
|
/>
|
|
</div>
|
|
);
|
|
}; |