Spaces:
Paused
Paused
| import { useEffect, useState } from 'react'; | |
| import { | |
| Rss, Activity, TrendingUp, AlertTriangle, Globe, | |
| Database, Zap, BarChart3, LineChart, Map, Target, Radar, Grid3X3, | |
| RefreshCw, Brain, Wifi, WifiOff, Download, ExternalLink | |
| } from 'lucide-react'; | |
| import { useWidgetCommunication } from '@/contexts/WidgetContext'; | |
| import { cn } from '@/lib/utils'; | |
| import { useHealthData, useHealingStatus, useHyperEvents as useBackendHyperEvents } from '@/hooks/useBackendData'; | |
| import { useLiveData, useAlerts, useHyperEvents, useSystemMetrics, useSourcesHealth } from '@/hooks/useLiveData'; | |
| import AutonomousMetricsWidget from '@/widgets/AutonomousMetricsWidget'; | |
| import SourceDiscoveryWidget from '@/widgets/SourceDiscoveryWidget'; | |
| import ApprovalQueueWidget from '@/widgets/ApprovalQueueWidget'; | |
| interface InteractiveWidgetContentProps { | |
| type: string; | |
| widgetId: string; | |
| } | |
| // ============================================================ | |
| // SOURCE RECOMMENDATION PANEL - Suggests additional data sources | |
| // ============================================================ | |
| const SourceRecommendationPanel = ({ | |
| sources, | |
| onFetch | |
| }: { | |
| sources: Array<{ id: string; name: string; description: string; isAvailable: boolean; requiresApproval: boolean }>; | |
| onFetch: (id: string) => void; | |
| }) => { | |
| if (sources.length === 0) return null; | |
| const unavailableSources = sources.filter(s => !s.isAvailable); | |
| if (unavailableSources.length === 0) return null; | |
| return ( | |
| <div className="mt-3 pt-3 border-t border-border/30"> | |
| <p className="text-xs text-muted-foreground mb-2 flex items-center gap-1"> | |
| <ExternalLink className="w-3 h-3" /> | |
| Anbefalede kilder: | |
| </p> | |
| <div className="space-y-1"> | |
| {unavailableSources.slice(0, 3).map(source => ( | |
| <button | |
| key={source.id} | |
| onClick={() => onFetch(source.id)} | |
| className="w-full flex items-center justify-between p-1.5 bg-secondary/20 hover:bg-secondary/40 rounded text-xs transition-colors" | |
| > | |
| <span className="text-foreground">{source.name}</span> | |
| <span className="text-primary flex items-center gap-1"> | |
| <Download className="w-3 h-3" /> | |
| Hent | |
| </span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // CONNECTION STATUS INDICATOR | |
| // ============================================================ | |
| const ConnectionIndicator = ({ status }: { status: 'connected' | 'connecting' | 'disconnected' }) => ( | |
| <div className="flex items-center gap-1 text-xs"> | |
| {status === 'connected' ? ( | |
| <><Wifi className="w-3 h-3 text-green-400" /><span className="text-green-400">Live</span></> | |
| ) : status === 'connecting' ? ( | |
| <><Wifi className="w-3 h-3 text-primary animate-pulse" /><span className="text-primary">Connecting...</span></> | |
| ) : ( | |
| <><WifiOff className="w-3 h-3 text-destructive" /><span className="text-destructive">Offline</span></> | |
| )} | |
| </div> | |
| ); | |
| // ============================================================ | |
| // NEWS WIDGET - Using HyperLog Events | |
| // ============================================================ | |
| const NewsWidget = ({ widgetId }: { widgetId: string }) => { | |
| const { filters, sharedData, selectThreat } = useWidgetCommunication(widgetId); | |
| const { data, isLoading, connectionStatus, recommendedSources, fetchRecommendedSource } = useHyperEvents(15000); | |
| // Transform HyperLog events into news items | |
| const newsItems = (data?.events || []) | |
| .filter((e: any) => ['THOUGHT', 'CRITICAL_DECISION', 'DELEGATION', 'threat:detected'].includes(e.type)) | |
| .slice(0, 10) | |
| .map((e: any, i: number) => ({ | |
| id: e.id || `event-${i}`, | |
| title: e.content || e.payload?.message || 'System Event', | |
| severity: e.type === 'CRITICAL_DECISION' ? 'critical' : e.type === 'threat:detected' ? 'high' : 'medium', | |
| time: formatRelativeTime(e.timestamp), | |
| region: e.metadata?.region || 'system', | |
| agent: e.agent || 'System', | |
| })); | |
| const filteredNews = newsItems.filter((item: any) => { | |
| if (filters.severity && filters.severity !== 'all' && item.severity !== filters.severity) return false; | |
| if (filters.searchQuery && !item.title.toLowerCase().includes(filters.searchQuery.toLowerCase())) return false; | |
| return true; | |
| }); | |
| if (isLoading && newsItems.length === 0) { | |
| return <div className="text-xs text-muted-foreground animate-pulse">Indlæser nyheder...</div>; | |
| } | |
| return ( | |
| <div className="space-y-2"> | |
| <div className="flex justify-between items-center mb-2"> | |
| <ConnectionIndicator status={connectionStatus} /> | |
| </div> | |
| {filteredNews.length === 0 ? ( | |
| <p className="text-xs text-muted-foreground">Ingen nyheder matcher filtrene</p> | |
| ) : ( | |
| filteredNews.slice(0, 4).map((item: any) => ( | |
| <button | |
| key={item.id} | |
| onClick={() => selectThreat({ id: item.id, name: item.title, severity: item.severity })} | |
| className={cn( | |
| "w-full p-2 bg-secondary/30 border-l-2 border-primary/50 text-left text-xs transition-all hover:border-primary hover:bg-secondary/50", | |
| sharedData.selectedThreat?.id === item.id && "border-primary bg-primary/10" | |
| )} | |
| > | |
| <span className="text-foreground">{item.title}</span> | |
| <div className="flex gap-2 mt-1 text-muted-foreground"> | |
| <span className={item.severity === 'critical' ? 'text-destructive' : item.severity === 'high' ? 'text-orange-400' : 'text-primary'}> | |
| {item.severity} | |
| </span> | |
| <span>• {item.time}</span> | |
| <span className="text-muted-foreground/60">• {item.agent}</span> | |
| </div> | |
| </button> | |
| )) | |
| )} | |
| <SourceRecommendationPanel sources={recommendedSources} onFetch={fetchRecommendedSource} /> | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // ALERTS WIDGET - Using Live Alerts | |
| // ============================================================ | |
| const AlertsWidget = ({ widgetId }: { widgetId: string }) => { | |
| const { filters, sharedData, selectAlert, highlightIP } = useWidgetCommunication(widgetId); | |
| const { data: liveAlerts, isLoading, connectionStatus, recommendedSources, fetchRecommendedSource } = useAlerts(10000); | |
| // Fallback to HyperLog for alerts if no dedicated alerts endpoint | |
| const { data: hyperData } = useHyperEvents(15000); | |
| // Combine and transform alerts | |
| const alerts = (liveAlerts || []).length > 0 | |
| ? liveAlerts | |
| : (hyperData?.events || []) | |
| .filter((e: any) => ['system.alert', 'security.alert', 'failure:recorded', 'failure:recurring'].includes(e.type)) | |
| .slice(0, 10) | |
| .map((e: any, i: number) => ({ | |
| id: e.id || `alert-${i}`, | |
| type: e.type === 'failure:recurring' ? 'CRITICAL' : e.type === 'security.alert' ? 'WARNING' : 'INFO', | |
| msg: e.content || e.payload?.message || 'Alert', | |
| time: formatRelativeTime(e.timestamp), | |
| ip: e.metadata?.ip || e.payload?.sourceId || null, | |
| severity: e.type.includes('failure') ? 'critical' : 'medium', | |
| })); | |
| const filteredAlerts = alerts.filter((a: any) => { | |
| if (filters.severity && filters.severity !== 'all') { | |
| const severityMap: Record<string, string> = { CRITICAL: 'critical', WARNING: 'high', INFO: 'medium' }; | |
| if (severityMap[a.type] !== filters.severity) return false; | |
| } | |
| if (filters.searchQuery && !a.msg.toLowerCase().includes(filters.searchQuery.toLowerCase())) return false; | |
| return true; | |
| }); | |
| if (isLoading && alerts.length === 0) { | |
| return <div className="text-xs text-muted-foreground animate-pulse">Indlæser alerts...</div>; | |
| } | |
| return ( | |
| <div className="space-y-2"> | |
| <ConnectionIndicator status={connectionStatus} /> | |
| {filteredAlerts.length === 0 ? ( | |
| <p className="text-xs text-muted-foreground">Ingen aktive alerts</p> | |
| ) : ( | |
| filteredAlerts.slice(0, 5).map((a: any) => ( | |
| <button | |
| key={a.id} | |
| onClick={() => { | |
| selectAlert({ id: a.id, type: a.type, message: a.msg }); | |
| if (a.ip) highlightIP(a.ip); | |
| }} | |
| className={cn( | |
| "w-full p-2 border-l-2 text-xs text-left transition-all", | |
| a.type === 'CRITICAL' ? 'border-destructive bg-destructive/10 hover:bg-destructive/20' : | |
| a.type === 'WARNING' ? 'border-orange-400 bg-orange-400/10 hover:bg-orange-400/20' : | |
| 'border-primary bg-primary/10 hover:bg-primary/20', | |
| sharedData.activeAlert?.id === a.id && "ring-1 ring-foreground" | |
| )} | |
| > | |
| <span className={a.type === 'CRITICAL' ? 'text-destructive' : a.type === 'WARNING' ? 'text-orange-400' : 'text-primary'}> | |
| {a.type} | |
| </span> | |
| <span className="text-muted-foreground ml-2">{a.time}</span> | |
| <p className="text-foreground mt-1">{a.msg}</p> | |
| {a.ip && ( | |
| <span className={cn( | |
| "text-xs mt-1 inline-block", | |
| sharedData.highlightedIP === a.ip ? "text-orange-400" : "text-muted-foreground" | |
| )}> | |
| IP: {a.ip} | |
| </span> | |
| )} | |
| </button> | |
| )) | |
| )} | |
| <SourceRecommendationPanel sources={recommendedSources} onFetch={fetchRecommendedSource} /> | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // EVENTS WIDGET - Using Live HyperLog | |
| // ============================================================ | |
| const EventsWidget = ({ widgetId }: { widgetId: string }) => { | |
| const { sharedData, highlightIP } = useWidgetCommunication(widgetId); | |
| const { data, connectionStatus } = useHyperEvents(10000); | |
| const events = (data?.events || []).slice(0, 8).map((e: any) => ({ | |
| type: e.type?.split('.')[0]?.toUpperCase().slice(0, 4) || 'SYS', | |
| msg: e.content || e.payload?.message || 'Event', | |
| ip: e.metadata?.ip || null, | |
| })); | |
| return ( | |
| <div className="space-y-1"> | |
| <ConnectionIndicator status={connectionStatus} /> | |
| <div className="font-mono text-xs space-y-1 mt-2"> | |
| {events.length === 0 ? ( | |
| <p className="text-muted-foreground">Ingen events...</p> | |
| ) : ( | |
| events.map((e: any, i: number) => ( | |
| <button | |
| key={i} | |
| onClick={() => e.ip && highlightIP(e.ip)} | |
| className={cn( | |
| "w-full flex gap-2 p-1 bg-secondary/20 text-left transition-all", | |
| e.ip && "hover:bg-secondary/40 cursor-pointer", | |
| e.ip && sharedData.highlightedIP === e.ip && "bg-orange-400/20" | |
| )} | |
| > | |
| <span className="text-primary">[{e.type}]</span> | |
| <span className="text-foreground truncate">{e.msg}</span> | |
| </button> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // METRICS WIDGET - Using System Metrics | |
| // ============================================================ | |
| const MetricsWidget = ({ widgetId }: { widgetId: string }) => { | |
| const { filters, sharedData } = useWidgetCommunication(widgetId); | |
| const { data, connectionStatus, recommendedSources, fetchRecommendedSource } = useSystemMetrics(15000); | |
| const { data: statsData } = useLiveData({ sourceType: 'autonomousStats', pollInterval: 30000 }); | |
| // Combine system metrics with autonomous stats | |
| const metrics = [ | |
| { | |
| label: "Decisions", | |
| value: statsData?.totalDecisions || statsData?.decisions?.total || 0, | |
| change: `${((statsData?.successRate || 0) * 100).toFixed(0)}%` | |
| }, | |
| { | |
| label: "Patterns", | |
| value: statsData?.patternsLearned || statsData?.patterns?.total || 0, | |
| change: "+∞" | |
| }, | |
| { | |
| label: "CPU", | |
| value: `${data?.cpu || 0}%`, | |
| change: "" | |
| }, | |
| { | |
| label: "Memory", | |
| value: `${data?.memory || 0}%`, | |
| change: "" | |
| }, | |
| ]; | |
| return ( | |
| <div> | |
| <ConnectionIndicator status={connectionStatus} /> | |
| <div className="grid grid-cols-2 gap-2 mt-2"> | |
| {metrics.map((m, i) => ( | |
| <div key={i} className={cn( | |
| "p-2 bg-secondary/30 text-center transition-all", | |
| sharedData.selectedThreat && i === 0 && "ring-1 ring-destructive" | |
| )}> | |
| <div className="text-lg font-mono text-primary">{m.value}</div> | |
| <div className="text-xs text-muted-foreground">{m.label}</div> | |
| {m.change && <div className="text-xs text-green-400">{m.change}</div>} | |
| </div> | |
| ))} | |
| </div> | |
| <SourceRecommendationPanel sources={recommendedSources} onFetch={fetchRecommendedSource} /> | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // GLOBAL WIDGET - Region Threats (Knowledge Graph) | |
| // ============================================================ | |
| const GlobalWidget = ({ widgetId }: { widgetId: string }) => { | |
| const { filters, sharedData, selectRegion } = useWidgetCommunication(widgetId); | |
| const { data, connectionStatus, recommendedSources, fetchRecommendedSource } = useLiveData({ | |
| sourceType: 'knowledge', | |
| pollInterval: 60000, | |
| transform: (raw) => raw.compiled?.insights || raw.insights || [] | |
| }); | |
| // Transform knowledge insights into regional threat data | |
| const regions = [ | |
| { id: "north-america", name: "North America", threats: 0, up: true }, | |
| { id: "europe", name: "Europe", threats: 0, up: false }, | |
| { id: "asia-pacific", name: "Asia Pacific", threats: 0, up: true }, | |
| { id: "middle-east", name: "Middle East", threats: 0, up: false }, | |
| ].map(r => ({ | |
| ...r, | |
| threats: Math.floor(Math.random() * 200) + 50, // Placeholder - connect to real geo data | |
| })); | |
| const filteredRegions = regions.filter(r => { | |
| if (filters.region && r.id !== filters.region) return false; | |
| return true; | |
| }); | |
| return ( | |
| <div className="space-y-2"> | |
| <ConnectionIndicator status={connectionStatus} /> | |
| {filteredRegions.map((r) => ( | |
| <button | |
| key={r.id} | |
| onClick={() => selectRegion(r.id)} | |
| className={cn( | |
| "w-full flex justify-between p-2 bg-secondary/30 text-xs transition-all hover:bg-secondary/50", | |
| sharedData.selectedRegion === r.id && "bg-primary/20 border-l-2 border-primary" | |
| )} | |
| > | |
| <span className="text-foreground">{r.name}</span> | |
| <span className="text-primary"> | |
| {r.threats} {r.up ? '↑' : '↓'} | |
| </span> | |
| </button> | |
| ))} | |
| <SourceRecommendationPanel sources={recommendedSources} onFetch={fetchRecommendedSource} /> | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // DATA STREAMS WIDGET - Using Sources Health | |
| // ============================================================ | |
| const DataStreamsWidget = ({ widgetId }: { widgetId: string }) => { | |
| const { sharedData } = useWidgetCommunication(widgetId); | |
| const { data: sources, connectionStatus } = useSourcesHealth(15000); | |
| const streams = (sources || []).slice(0, 5).map((s: any) => ({ | |
| name: s.name || 'Unknown', | |
| rate: s.latency ? `${s.latency}ms` : 'N/A', | |
| ok: s.healthy !== false, | |
| })); | |
| // Fallback if no sources | |
| if (streams.length === 0) { | |
| streams.push( | |
| { name: "Database", rate: "12ms", ok: true }, | |
| { name: "MCP Tools", rate: "45ms", ok: true }, | |
| { name: "Agent Pipeline", rate: "89ms", ok: !sharedData.activeAlert } | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-2"> | |
| <ConnectionIndicator status={connectionStatus} /> | |
| {streams.map((s: any, i: number) => ( | |
| <div key={i} className="flex justify-between items-center p-2 bg-secondary/30 text-xs"> | |
| <span className="text-foreground">{s.name}</span> | |
| <div className="flex items-center gap-2"> | |
| <span className="font-mono text-primary">{s.rate}</span> | |
| <div className={`w-2 h-2 rounded-full ${s.ok ? 'bg-green-400' : 'bg-orange-400 animate-pulse'}`} /> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // STATISTICS WIDGET | |
| // ============================================================ | |
| const StatisticsWidget = ({ widgetId }: { widgetId: string }) => { | |
| const { filters } = useWidgetCommunication(widgetId); | |
| const { data } = useLiveData({ sourceType: 'autonomousStats', pollInterval: 30000 }); | |
| const severityFilter = filters.severity; | |
| const stats = [ | |
| { label: "Success Rate", value: ((data?.successRate || 0.85) * 100).toFixed(0) }, | |
| { label: "Pattern Match", value: Math.min(((data?.patternsLearned || 50) / 100) * 100, 100).toFixed(0) }, | |
| { label: "Source Health", value: ((data?.healthySources || 4) / (data?.totalSources || 5) * 100).toFixed(0) }, | |
| ]; | |
| return ( | |
| <div className="space-y-2"> | |
| {stats.map((s, i) => ( | |
| <div key={i} className="space-y-1"> | |
| <div className="flex justify-between text-xs"> | |
| <span className="text-muted-foreground">{s.label}</span> | |
| <span className="text-foreground">{s.value}%</span> | |
| </div> | |
| <div className="h-1.5 bg-secondary rounded-full overflow-hidden"> | |
| <div | |
| className="h-full bg-primary rounded-full transition-all" | |
| style={{ width: `${s.value}%` }} | |
| /> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // TRENDS WIDGET | |
| // ============================================================ | |
| const TrendsWidget = () => { | |
| const { data } = useLiveData({ sourceType: 'decisionHistory', pollInterval: 60000 }); | |
| // Generate trend data from decisions | |
| const decisions = data || []; | |
| const trendData = [20, 35, 45, 80, 65, 90, 70].map((base, i) => { | |
| const decision = decisions[i]; | |
| return decision?.success ? base + 10 : base; | |
| }); | |
| return ( | |
| <div className="h-20 flex items-end justify-between gap-1"> | |
| {trendData.map((v, i) => ( | |
| <div | |
| key={i} | |
| className="flex-1 bg-primary/60 hover:bg-primary rounded-t transition-colors cursor-pointer" | |
| style={{ height: `${v}%` }} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // THREAT MAP WIDGET | |
| // ============================================================ | |
| const ThreatMapWidget = ({ widgetId }: { widgetId: string }) => { | |
| const { sharedData } = useWidgetCommunication(widgetId); | |
| return ( | |
| <div className="aspect-video bg-secondary/30 border border-border/50 relative"> | |
| {[ | |
| { x: 20, y: 30, region: "north-america" }, | |
| { x: 45, y: 25, region: "europe" }, | |
| { x: 70, y: 40, region: "asia-pacific" }, | |
| ].map((p, i) => ( | |
| <div | |
| key={i} | |
| className={cn( | |
| "absolute w-2 h-2 rounded-full bg-destructive transition-all", | |
| sharedData.selectedRegion === p.region ? "w-4 h-4 animate-ping" : "animate-pulse" | |
| )} | |
| style={{ left: `${p.x}%`, top: `${p.y}%` }} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // DATABASE WIDGET - Real Backend Data | |
| // ============================================================ | |
| const DatabaseWidget = () => { | |
| const { data: healthData, isLoading, refresh } = useHealthData(10000); | |
| const { data: healingData } = useHealingStatus(15000); | |
| const databases = healthData?.services | |
| ? Object.entries(healthData.services).map(([name, info]: [string, any]) => ({ | |
| name: name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, ' '), | |
| status: info.healthy ? 'online' : 'offline', | |
| latency: info.latency | |
| })) | |
| : [ | |
| { name: "Backend", status: healthData?.status === 'healthy' ? 'online' : 'checking', latency: undefined } | |
| ]; | |
| return ( | |
| <div className="space-y-2"> | |
| <div className="flex justify-between items-center text-xs text-muted-foreground mb-1"> | |
| <span>Live Backend Status</span> | |
| <button onClick={refresh} className="p-1 hover:bg-secondary rounded"> | |
| <RefreshCw className={cn("h-3 w-3", isLoading && "animate-spin")} /> | |
| </button> | |
| </div> | |
| {databases.map((db: any, i: number) => ( | |
| <div key={i} className="flex justify-between items-center p-2 bg-secondary/30 text-xs"> | |
| <span className="text-foreground">{db.name}</span> | |
| <div className="flex items-center gap-2"> | |
| {db.latency && <span className="text-muted-foreground">{db.latency}ms</span>} | |
| <span className={ | |
| db.status === 'online' ? 'text-green-400' : | |
| db.status === 'offline' ? 'text-destructive' : | |
| 'text-primary animate-pulse' | |
| }> | |
| {db.status} | |
| </span> | |
| </div> | |
| </div> | |
| ))} | |
| {healingData && ( | |
| <div className="mt-2 p-2 bg-primary/10 border border-primary/20 text-xs"> | |
| <span className="text-primary">Self-Healing:</span> | |
| <span className={healingData.healthy ? 'text-green-400 ml-2' : 'text-destructive ml-2'}> | |
| {healingData.healthy ? 'Active' : 'Issues detected'} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // SIMPLE WIDGETS (Visual indicators) | |
| // ============================================================ | |
| const TargetWidget = () => ( | |
| <div className="aspect-square max-w-24 mx-auto relative"> | |
| {[100, 75, 50, 25].map((size, i) => ( | |
| <div | |
| key={i} | |
| className="absolute rounded-full border border-destructive/30" | |
| style={{ | |
| width: `${size}%`, | |
| height: `${size}%`, | |
| left: `${(100 - size) / 2}%`, | |
| top: `${(100 - size) / 2}%`, | |
| }} | |
| /> | |
| ))} | |
| <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-2 h-2 bg-destructive rounded-full animate-pulse" /> | |
| </div> | |
| ); | |
| const RadarWidget = () => ( | |
| <div className="aspect-square max-w-24 mx-auto relative bg-secondary/30 rounded-full overflow-hidden"> | |
| {[100, 66, 33].map((size, i) => ( | |
| <div | |
| key={i} | |
| className="absolute rounded-full border border-primary/20" | |
| style={{ | |
| width: `${size}%`, | |
| height: `${size}%`, | |
| left: `${(100 - size) / 2}%`, | |
| top: `${(100 - size) / 2}%`, | |
| }} | |
| /> | |
| ))} | |
| <div | |
| className="absolute top-1/2 left-1/2 w-1/2 h-0.5 bg-gradient-to-r from-primary to-transparent origin-left animate-spin" | |
| style={{ animationDuration: '3s' }} | |
| /> | |
| </div> | |
| ); | |
| const GridWidget = () => { | |
| const { data } = useSourcesHealth(30000); | |
| const sources = data || []; | |
| // Generate grid based on source health | |
| const grid = Array.from({ length: 16 }).map((_, i) => { | |
| const source = sources[i % sources.length]; | |
| return source ? source.healthy !== false : Math.random() > 0.75; | |
| }); | |
| return ( | |
| <div className="grid grid-cols-4 gap-0.5"> | |
| {grid.map((active, i) => ( | |
| <div | |
| key={i} | |
| className={`aspect-square ${active ? 'bg-green-400/40' : 'bg-destructive/60 animate-pulse'}`} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| // ============================================================ | |
| // HELPER FUNCTIONS | |
| // ============================================================ | |
| function formatRelativeTime(timestamp: number | string): string { | |
| const now = Date.now(); | |
| const ts = typeof timestamp === 'string' ? new Date(timestamp).getTime() : timestamp; | |
| const diff = now - ts; | |
| if (diff < 60000) return 'Nu'; | |
| if (diff < 3600000) return `${Math.floor(diff / 60000)}m`; | |
| if (diff < 86400000) return `${Math.floor(diff / 3600000)}t`; | |
| return `${Math.floor(diff / 86400000)}d`; | |
| } | |
| // ============================================================ | |
| // MAIN COMPONENT | |
| // ============================================================ | |
| const InteractiveWidgetContent = ({ type, widgetId }: InteractiveWidgetContentProps) => { | |
| const widgets: Record<string, JSX.Element> = { | |
| news: <NewsWidget widgetId={widgetId} />, | |
| datastreams: <DataStreamsWidget widgetId={widgetId} />, | |
| metrics: <MetricsWidget widgetId={widgetId} />, | |
| alerts: <AlertsWidget widgetId={widgetId} />, | |
| global: <GlobalWidget widgetId={widgetId} />, | |
| database: <DatabaseWidget />, | |
| events: <EventsWidget widgetId={widgetId} />, | |
| statistics: <StatisticsWidget widgetId={widgetId} />, | |
| trends: <TrendsWidget />, | |
| threatmap: <ThreatMapWidget widgetId={widgetId} />, | |
| target: <TargetWidget />, | |
| radar: <RadarWidget />, | |
| grid: <GridWidget />, | |
| 'autonomous-metrics': <AutonomousMetricsWidget />, | |
| 'source-discovery': <SourceDiscoveryWidget />, | |
| 'approval-queue': <ApprovalQueueWidget />, | |
| }; | |
| return widgets[type] || <div className="text-muted-foreground text-sm">Widget not found</div>; | |
| }; | |
| export default InteractiveWidgetContent; | |