Spaces:
Paused
Paused
| /** | |
| * Widget Schema Viewer | |
| * | |
| * Komponent der visualiserer en widgets metadata/schema. | |
| * Viser hvad widgeten kræver, leverer og kan konfigureres med. | |
| */ | |
| import { useState } from 'react'; | |
| import { | |
| Info, | |
| ArrowDownToLine, | |
| ArrowUpFromLine, | |
| Settings, | |
| Link2, | |
| Tag, | |
| Zap, | |
| Database, | |
| RefreshCw | |
| } from 'lucide-react'; | |
| import { WidgetMetadata } from '@/types/WidgetSchema'; | |
| import { widgetRegistry } from '@/lib/widgetRegistry'; | |
| import { cn } from '@/lib/utils'; | |
| interface WidgetSchemaViewerProps { | |
| widgetId: string; | |
| compact?: boolean; | |
| className?: string; | |
| } | |
| export function WidgetSchemaViewer({ widgetId, compact = false, className }: WidgetSchemaViewerProps) { | |
| const [activeTab, setActiveTab] = useState<'requires' | 'provides' | 'config' | 'relations'>('requires'); | |
| const metadata = widgetRegistry.get(widgetId); | |
| if (!metadata) { | |
| return ( | |
| <div className={cn("p-4 border border-border/30 rounded-lg bg-card/50", className)}> | |
| <p className="text-muted-foreground text-sm">Widget metadata ikke tilgængelig</p> | |
| </div> | |
| ); | |
| } | |
| if (compact) { | |
| return <CompactView metadata={metadata} className={className} />; | |
| } | |
| return ( | |
| <div className={cn("border border-border/30 rounded-lg bg-card/50 overflow-hidden", className)}> | |
| {/* Header */} | |
| <div className="p-4 border-b border-border/30 bg-gradient-to-r from-primary/10 to-transparent"> | |
| <div className="flex items-start justify-between"> | |
| <div> | |
| <h3 className="font-mono text-lg text-foreground">{metadata.name}</h3> | |
| <p className="text-sm text-muted-foreground mt-1">{metadata.description}</p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span className={cn( | |
| "px-2 py-0.5 rounded text-xs font-mono", | |
| metadata.priority === 'critical' && "bg-destructive/20 text-destructive", | |
| metadata.priority === 'high' && "bg-warning/20 text-warning", | |
| metadata.priority === 'medium' && "bg-primary/20 text-primary", | |
| metadata.priority === 'low' && "bg-muted text-muted-foreground" | |
| )}> | |
| {metadata.priority || 'medium'} | |
| </span> | |
| <span className="text-xs text-muted-foreground font-mono">v{metadata.version}</span> | |
| </div> | |
| </div> | |
| {/* Tags */} | |
| <div className="flex flex-wrap gap-1 mt-3"> | |
| {metadata.tags.map(tag => ( | |
| <span key={tag} className="px-2 py-0.5 bg-secondary/50 rounded text-xs text-secondary-foreground flex items-center gap-1"> | |
| <Tag className="h-3 w-3" /> | |
| {tag} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Tabs */} | |
| <div className="flex border-b border-border/30"> | |
| <TabButton | |
| active={activeTab === 'requires'} | |
| onClick={() => setActiveTab('requires')} | |
| icon={ArrowDownToLine} | |
| label="Kræver" | |
| count={metadata.requires.length} | |
| /> | |
| <TabButton | |
| active={activeTab === 'provides'} | |
| onClick={() => setActiveTab('provides')} | |
| icon={ArrowUpFromLine} | |
| label="Leverer" | |
| count={metadata.provides.length} | |
| /> | |
| <TabButton | |
| active={activeTab === 'config'} | |
| onClick={() => setActiveTab('config')} | |
| icon={Settings} | |
| label="Konfiguration" | |
| count={metadata.config.length} | |
| /> | |
| <TabButton | |
| active={activeTab === 'relations'} | |
| onClick={() => setActiveTab('relations')} | |
| icon={Link2} | |
| label="Relationer" | |
| count={metadata.relations.length} | |
| /> | |
| </div> | |
| {/* Content */} | |
| <div className="p-4 max-h-80 overflow-y-auto"> | |
| {activeTab === 'requires' && ( | |
| <div className="space-y-3"> | |
| {metadata.requires.length === 0 ? ( | |
| <p className="text-muted-foreground text-sm">Ingen data-afhængigheder</p> | |
| ) : ( | |
| metadata.requires.map(req => ( | |
| <div key={req.id} className="p-3 bg-background/50 rounded-lg border border-border/20"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-mono text-sm text-foreground">{req.name}</span> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-xs px-2 py-0.5 bg-primary/20 text-primary rounded font-mono"> | |
| {req.type} | |
| </span> | |
| {req.required && ( | |
| <span className="text-xs px-2 py-0.5 bg-destructive/20 text-destructive rounded"> | |
| påkrævet | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| <p className="text-xs text-muted-foreground mt-1">{req.description}</p> | |
| {req.refreshRate && ( | |
| <div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground"> | |
| <RefreshCw className="h-3 w-3" /> | |
| Opdateres hver {req.refreshRate / 1000}s | |
| </div> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| )} | |
| {activeTab === 'provides' && ( | |
| <div className="space-y-3"> | |
| {metadata.provides.length === 0 ? ( | |
| <p className="text-muted-foreground text-sm">Ingen outputs defineret</p> | |
| ) : ( | |
| metadata.provides.map(cap => ( | |
| <div key={cap.id} className="p-3 bg-background/50 rounded-lg border border-border/20"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-mono text-sm text-foreground">{cap.name}</span> | |
| <span className="text-xs px-2 py-0.5 bg-accent/20 text-accent-foreground rounded font-mono"> | |
| {cap.outputType} | |
| </span> | |
| </div> | |
| <p className="text-xs text-muted-foreground mt-1">{cap.description}</p> | |
| {cap.triggers && cap.triggers.length > 0 && ( | |
| <div className="flex items-center gap-1 mt-2 flex-wrap"> | |
| <Zap className="h-3 w-3 text-warning" /> | |
| {cap.triggers.map(trigger => ( | |
| <span key={trigger} className="text-xs px-1.5 py-0.5 bg-warning/10 text-warning rounded"> | |
| {trigger} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| )} | |
| {activeTab === 'config' && ( | |
| <div className="space-y-3"> | |
| {metadata.config.length === 0 ? ( | |
| <p className="text-muted-foreground text-sm">Ingen konfigurerbare indstillinger</p> | |
| ) : ( | |
| metadata.config.map(cfg => ( | |
| <div key={cfg.id} className="p-3 bg-background/50 rounded-lg border border-border/20"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-mono text-sm text-foreground">{cfg.name}</span> | |
| <span className="text-xs px-2 py-0.5 bg-secondary/50 text-secondary-foreground rounded font-mono"> | |
| {cfg.type} | |
| </span> | |
| </div> | |
| <p className="text-xs text-muted-foreground mt-1">{cfg.description}</p> | |
| <div className="mt-2 text-xs"> | |
| <span className="text-muted-foreground">Default: </span> | |
| <span className="font-mono text-primary">{String(cfg.default)}</span> | |
| </div> | |
| {cfg.options && ( | |
| <div className="flex flex-wrap gap-1 mt-2"> | |
| {cfg.options.map(opt => ( | |
| <span key={String(opt.value)} className="text-xs px-1.5 py-0.5 bg-muted rounded"> | |
| {opt.label} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| )} | |
| {activeTab === 'relations' && ( | |
| <div className="space-y-3"> | |
| {metadata.relations.length === 0 ? ( | |
| <p className="text-muted-foreground text-sm">Ingen definerede relationer</p> | |
| ) : ( | |
| metadata.relations.map((rel, idx) => ( | |
| <div key={idx} className="p-3 bg-background/50 rounded-lg border border-border/20"> | |
| <div className="flex items-center gap-2"> | |
| <span className={cn( | |
| "text-xs px-2 py-0.5 rounded font-mono", | |
| rel.type === 'depends_on' && "bg-blue-500/20 text-blue-400", | |
| rel.type === 'provides_to' && "bg-green-500/20 text-green-400", | |
| rel.type === 'synchronizes' && "bg-purple-500/20 text-purple-400", | |
| rel.type === 'triggers' && "bg-orange-500/20 text-orange-400", | |
| rel.type === 'aggregates' && "bg-cyan-500/20 text-cyan-400" | |
| )}> | |
| {rel.type} | |
| </span> | |
| <span className="font-mono text-sm text-foreground">{rel.targetWidgetId}</span> | |
| </div> | |
| <p className="text-xs text-muted-foreground mt-1">{rel.description}</p> | |
| {rel.dataFlow && ( | |
| <div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground"> | |
| <Database className="h-3 w-3" /> | |
| Data: {rel.dataFlow} | |
| </div> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Tab button component | |
| function TabButton({ | |
| active, | |
| onClick, | |
| icon: Icon, | |
| label, | |
| count | |
| }: { | |
| active: boolean; | |
| onClick: () => void; | |
| icon: React.ElementType; | |
| label: string; | |
| count: number; | |
| }) { | |
| return ( | |
| <button | |
| onClick={onClick} | |
| className={cn( | |
| "flex items-center gap-2 px-4 py-2 text-sm transition-colors", | |
| active | |
| ? "bg-primary/10 text-primary border-b-2 border-primary" | |
| : "text-muted-foreground hover:text-foreground hover:bg-muted/50" | |
| )} | |
| > | |
| <Icon className="h-4 w-4" /> | |
| {label} | |
| <span className={cn( | |
| "px-1.5 py-0.5 rounded-full text-xs", | |
| active ? "bg-primary/20" : "bg-muted" | |
| )}> | |
| {count} | |
| </span> | |
| </button> | |
| ); | |
| } | |
| // Compact view for inline display | |
| function CompactView({ metadata, className }: { metadata: WidgetMetadata; className?: string }) { | |
| return ( | |
| <div className={cn("p-2 border border-border/30 rounded bg-card/50", className)}> | |
| <div className="flex items-center gap-2 text-xs"> | |
| <Info className="h-3 w-3 text-primary" /> | |
| <span className="font-mono text-foreground">{metadata.name}</span> | |
| <span className="text-muted-foreground">•</span> | |
| <span className="text-muted-foreground"> | |
| {metadata.requires.length} inputs, {metadata.provides.length} outputs | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| } | |