'use client' import { useState, useRef } from 'react' import { useMissionControl } from '@/store' import { WIDGET_CATALOG, getDefaultLayout, getAvailableWidgets, getWidgetById } from '@/lib/dashboard-widgets' import { Button } from '@/components/ui/button' import type { DashboardData } from './widget-primitives' import { MetricCardsWidget } from './widgets/metric-cards-widget' import { RuntimeHealthWidget } from './widgets/runtime-health-widget' import { GatewayHealthWidget } from './widgets/gateway-health-widget' import { SessionWorkbenchWidget } from './widgets/session-workbench-widget' import { EventStreamWidget } from './widgets/event-stream-widget' import { TaskFlowWidget } from './widgets/task-flow-widget' import { GithubSignalWidget } from './widgets/github-signal-widget' import { SecurityAuditWidget } from './widgets/security-audit-widget' import { MaintenanceWidget } from './widgets/maintenance-widget' import { QuickActionsWidget } from './widgets/quick-actions-widget' const WIDGET_COMPONENTS: Record> = { 'metric-cards': MetricCardsWidget, 'runtime-health': RuntimeHealthWidget, 'gateway-health': GatewayHealthWidget, 'session-workbench': SessionWorkbenchWidget, 'event-stream': EventStreamWidget, 'task-flow': TaskFlowWidget, 'github-signal': GithubSignalWidget, 'security-audit': SecurityAuditWidget, 'maintenance': MaintenanceWidget, 'quick-actions': QuickActionsWidget, } // Map widget defaultSize to CSS grid column spans const SIZE_CLASSES: Record = { sm: 'xl:col-span-6', md: 'xl:col-span-4', lg: 'xl:col-span-8', full: 'xl:col-span-12', } export function WidgetGrid({ data }: { data: DashboardData }) { const { dashboardLayout, setDashboardLayout, dashboardMode } = useMissionControl() const mode = dashboardMode === 'local' ? 'local' : 'full' const [customizing, setCustomizing] = useState(false) const [dragId, setDragId] = useState(null) const [dragOverId, setDragOverId] = useState(null) const dragCounter = useRef(0) const defaults = getDefaultLayout(mode) const activeLayout = dashboardLayout ?? defaults const available = getAvailableWidgets(mode) // Filter layout to only include widgets valid for current mode const validLayout = activeLayout.filter((id) => { const w = getWidgetById(id) return w && w.modes.includes(mode) }) // Widgets not in current layout but available for this mode const hiddenWidgets = available.filter((w) => !validLayout.includes(w.id)) const handleDragStart = (e: React.DragEvent, widgetId: string) => { setDragId(widgetId) e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/plain', widgetId) } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' } const handleDragEnter = (_e: React.DragEvent, widgetId: string) => { dragCounter.current++ setDragOverId(widgetId) } const handleDragLeave = () => { dragCounter.current-- if (dragCounter.current <= 0) { setDragOverId(null) dragCounter.current = 0 } } const handleDrop = (e: React.DragEvent, targetId: string) => { e.preventDefault() dragCounter.current = 0 setDragOverId(null) setDragId(null) const sourceId = e.dataTransfer.getData('text/plain') if (!sourceId || sourceId === targetId) return setDashboardLayout((currentLayout) => { const activeLayout = currentLayout ?? defaults const nextLayout = activeLayout.filter((id) => { const widget = getWidgetById(id) return !!widget && widget.modes.includes(mode) }) const sourceIdx = nextLayout.indexOf(sourceId) const targetIdx = nextLayout.indexOf(targetId) if (sourceIdx === -1) { if (targetIdx === -1) return nextLayout nextLayout.splice(targetIdx + 1, 0, sourceId) return nextLayout } if (targetIdx === -1) return nextLayout nextLayout.splice(sourceIdx, 1) nextLayout.splice(targetIdx, 0, sourceId) return nextLayout }) } const handleDragEnd = () => { setDragId(null) setDragOverId(null) dragCounter.current = 0 } const addWidget = (widgetId: string) => { setDashboardLayout((currentLayout) => { const activeLayout = currentLayout ?? defaults const nextLayout = activeLayout.filter((id) => { const widget = getWidgetById(id) return !!widget && widget.modes.includes(mode) }) return nextLayout.includes(widgetId) ? nextLayout : [...nextLayout, widgetId] }) } const removeWidget = (widgetId: string) => { setDashboardLayout((currentLayout) => { const activeLayout = currentLayout ?? defaults return activeLayout.filter((id) => id !== widgetId) }) } const resetToDefaults = () => { setDashboardLayout(null) } // Group widgets into rows based on their sizes // full-width widgets get their own row; sm/md/lg widgets flow in a 12-col grid const renderWidgets = () => { const elements: React.ReactNode[] = [] let rowWidgets: { id: string; size: string }[] = [] let rowSpan = 0 const flushRow = () => { if (rowWidgets.length === 0) return elements.push(
{rowWidgets.map(({ id, size }) => renderWidget(id, SIZE_CLASSES[size] || 'xl:col-span-4'))}
) rowWidgets = [] rowSpan = 0 } for (const widgetId of validLayout) { const widget = getWidgetById(widgetId) if (!widget) continue const Component = WIDGET_COMPONENTS[widgetId] if (!Component) continue if (widget.defaultSize === 'full') { flushRow() elements.push(renderWidget(widgetId, '')) } else { const span = widget.defaultSize === 'sm' ? 6 : widget.defaultSize === 'md' ? 4 : 8 if (rowSpan + span > 12) flushRow() rowWidgets.push({ id: widgetId, size: widget.defaultSize }) rowSpan += span } } flushRow() return elements } const renderWidget = (widgetId: string, colClass: string) => { const Component = WIDGET_COMPONENTS[widgetId] const widget = getWidgetById(widgetId) if (!Component || !widget) return null const isDragging = dragId === widgetId const isDragOver = dragOverId === widgetId return (
handleDragStart(e, widgetId)} onDragOver={handleDragOver} onDragEnter={(e) => handleDragEnter(e, widgetId)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, widgetId)} onDragEnd={handleDragEnd} > {customizing && (
:::
)}
) } return (
{renderWidgets()} {/* Customize mode: hidden widgets + controls */} {customizing && hiddenWidgets.length > 0 && (

Available Widgets

{hiddenWidgets.map((widget) => ( ))}
)} {/* Customize controls bar */}
{customizing && ( )}
) }