diff --git a/ui/src/components/DetailPanelContext.tsx b/ui/src/components/DetailPanelContext.tsx new file mode 100644 index 00000000..a6651744 --- /dev/null +++ b/ui/src/components/DetailPanelContext.tsx @@ -0,0 +1,23 @@ +import { createContext, useContext, useState, type ReactNode } from 'react'; + +interface DetailPanelContextValue { + detail: ReactNode; + setDetail: (node: ReactNode) => void; +} + +const DetailPanelContext = createContext(null); + +export function DetailPanelProvider({ children }: { children: ReactNode }) { + const [detail, setDetail] = useState(null); + return ( + + {children} + + ); +} + +export function useDetailPanelSlot(): DetailPanelContextValue { + const ctx = useContext(DetailPanelContext); + if (!ctx) throw new Error('useDetailPanelSlot must be used within DetailPanelProvider'); + return ctx; +} diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index f3abaef2..47cc1e81 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -5,6 +5,7 @@ import { useRouteCatalog } from '../api/queries/catalog'; import { useAgents } from '../api/queries/agents'; import { useAuthStore } from '../auth/auth-store'; import { useMemo, useCallback } from 'react'; +import { DetailPanelProvider, useDetailPanelSlot } from './DetailPanelContext'; function healthToColor(health: string): string { switch (health) { @@ -68,6 +69,7 @@ function LayoutContent() { const { data: agents } = useAgents(); const { username, logout } = useAuthStore(); const { open: paletteOpen, setOpen: setPaletteOpen } = useCommandPalette(); + const { detail } = useDetailPanelSlot(); const sidebarApps: SidebarApp[] = useMemo(() => { if (!catalog) return []; @@ -122,6 +124,7 @@ function LayoutContent() { apps={sidebarApps} /> } + detail={detail} > - + + + diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index 1e9a8cc8..224b9c30 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { useParams, Link } from 'react-router'; import { StatCard, StatusDot, Badge, MonoText, ProgressBar, @@ -9,6 +9,7 @@ import styles from './AgentHealth.module.css'; import { useAgents, useAgentEvents } from '../../api/queries/agents'; import { useAgentMetrics } from '../../api/queries/agent-metrics'; import type { AgentInstance } from '../../api/types'; +import { useDetailPanelSlot } from '../../components/DetailPanelContext'; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -218,6 +219,7 @@ export default function AgentHealth() { const { appId } = useParams(); const { data: agents } = useAgents(undefined, appId); const { data: events } = useAgentEvents(appId); + const { setDetail } = useDetailPanelSlot(); const [selectedInstance, setSelectedInstance] = useState(null); const [panelOpen, setPanelOpen] = useState(false); @@ -503,18 +505,41 @@ export default function AgentHealth() { )} - {/* Detail panel */} - {selectedInstance && ( - { - setPanelOpen(false); - setSelectedInstance(null); - }} - title={selectedInstance.name ?? selectedInstance.id} - tabs={detailTabs} - /> - )} + {/* Detail panel — hoisted to AppShell via context */} + { setPanelOpen(false); setSelectedInstance(null); }} + selectedInstance={selectedInstance} + detailTabs={detailTabs} + setDetail={setDetail} + /> ); } + +function AgentDetailPanelSlot({ + open, onClose, selectedInstance, detailTabs, setDetail, +}: { + open: boolean + onClose: () => void + selectedInstance: AgentInstance | null + detailTabs: any + setDetail: (node: React.ReactNode) => void +}) { + useEffect(() => { + if (!selectedInstance) { + setDetail(null); + return; + } + setDetail( + + ); + return () => setDetail(null); + }); + return null; +} diff --git a/ui/src/pages/Dashboard/Dashboard.tsx b/ui/src/pages/Dashboard/Dashboard.tsx index f5f58f01..949707b1 100644 --- a/ui/src/pages/Dashboard/Dashboard.tsx +++ b/ui/src/pages/Dashboard/Dashboard.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react' +import { useState, useMemo, useCallback, useEffect } from 'react' import { useParams, useNavigate } from 'react-router' import { DataTable, @@ -22,6 +22,7 @@ import { import { useDiagramLayout } from '../../api/queries/diagrams' import type { ExecutionSummary } from '../../api/types' import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping' +import { useDetailPanelSlot } from '../../components/DetailPanelContext' import styles from './Dashboard.module.css' // Row type extends ExecutionSummary with an `id` field for DataTable @@ -174,6 +175,7 @@ const SHORTCUTS = [ export default function Dashboard() { const { appId, routeId } = useParams<{ appId: string; routeId: string }>() const navigate = useNavigate() + const { setDetail } = useDetailPanelSlot() const [selectedId, setSelectedId] = useState() const [panelOpen, setPanelOpen] = useState(false) const [sortField, setSortField] = useState('startTime') @@ -414,95 +416,128 @@ export default function Dashboard() { {/* Shortcuts bar */} - {/* Detail panel */} - {selectedRow && detail && ( - setPanelOpen(false)} - title={`${detail.routeId} \u2014 ${selectedRow.executionId.slice(0, 12)}`} - > - {/* Link to full detail page */} -
- -
- - {/* Overview */} -
-
Overview
-
-
- Status - - - {statusLabel(detail.status)} - -
-
- Duration - {formatDuration(detail.durationMs)} -
-
- Route - {detail.routeId} -
-
- Agent - {detail.agentId ?? '\u2014'} -
-
- Correlation - {detail.correlationId ?? '\u2014'} -
-
- Timestamp - {detail.startTime ? new Date(detail.startTime).toISOString() : '\u2014'} -
-
-
- - {/* Errors */} - {errorMsg && ( -
-
Errors
-
-
{errorClass}
-
{errorMsg}
-
-
- )} - - {/* Route Flow */} -
-
Route Flow
- {routeNodes.length > 0 ? ( - - ) : ( -
No diagram available
- )} -
- - {/* Processor Timeline */} -
-
- Processor Timeline - {formatDuration(detail.durationMs)} -
- {flatProcs.length > 0 ? ( - - ) : ( -
No processor data
- )} -
-
- )} + {/* Detail panel — hoisted to AppShell via context */} + setPanelOpen(false)} + selectedRow={selectedRow} + detail={detail} + errorClass={errorClass} + errorMsg={errorMsg} + routeNodes={routeNodes} + flatProcs={flatProcs} + navigate={navigate} + setDetail={setDetail} + /> ) } + +/** Renders the DetailPanel into the AppShell detail slot via context. */ +function DashboardDetailPanel({ + open, onClose, selectedRow, detail, errorClass, errorMsg, routeNodes, flatProcs, navigate, setDetail, +}: { + open: boolean + onClose: () => void + selectedRow: Row | undefined + detail: any + errorClass: string + errorMsg: string + routeNodes: RouteNode[] + flatProcs: any[] + navigate: (path: string) => void + setDetail: (node: React.ReactNode) => void +}) { + useEffect(() => { + if (!selectedRow || !detail) { + setDetail(null) + return + } + setDetail( + +
+ +
+ +
+
Overview
+
+
+ Status + + + {statusLabel(detail.status)} + +
+
+ Duration + {formatDuration(detail.durationMs)} +
+
+ Route + {detail.routeId} +
+
+ Agent + {detail.agentId ?? '\u2014'} +
+
+ Correlation + {detail.correlationId ?? '\u2014'} +
+
+ Timestamp + {detail.startTime ? new Date(detail.startTime).toISOString() : '\u2014'} +
+
+
+ + {errorMsg && ( +
+
Errors
+
+
{errorClass}
+
{errorMsg}
+
+
+ )} + +
+
Route Flow
+ {routeNodes.length > 0 ? ( + + ) : ( +
No diagram available
+ )} +
+ +
+
+ Processor Timeline + {formatDuration(detail.durationMs)} +
+ {flatProcs.length > 0 ? ( + + ) : ( +
No processor data
+ )} +
+
+ ) + return () => setDetail(null) + }) + + return null +}