From 5de792744e9d2fe4c6fe862123faad8c61041d57 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:28:03 +0100 Subject: [PATCH] fix: hoist DetailPanel into AppShell detail slot for proper slide-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DetailPanel is a flex sibling that slides in from the right — it must be rendered at the AppShell level via the detail prop, not inside the page content. Add DetailPanelContext so pages can push their panel content up to LayoutShell, which passes it to AppShell.detail. Applied to Dashboard (exchange detail) and AgentHealth (instance detail). Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/components/DetailPanelContext.tsx | 23 +++ ui/src/components/LayoutShell.tsx | 7 +- ui/src/pages/AgentHealth/AgentHealth.tsx | 51 ++++-- ui/src/pages/Dashboard/Dashboard.tsx | 215 +++++++++++++---------- 4 files changed, 192 insertions(+), 104 deletions(-) create mode 100644 ui/src/components/DetailPanelContext.tsx 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 +}