fix: use createPortal for DetailPanel instead of context+useEffect
The previous approach used useEffect+context to hoist DetailPanel content to the AppShell level, but the dependency-free useEffect caused a re-render loop that broke sidebar navigation. Replace with createPortal: pages render DetailPanel inline in their JSX but portal it to a target div (#detail-panel-portal) at the AppShell level. No state lifting, no re-render loops. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react'
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useParams, useNavigate } from 'react-router'
|
||||
import {
|
||||
DataTable,
|
||||
@@ -22,7 +23,6 @@ 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
|
||||
@@ -175,7 +175,6 @@ const SHORTCUTS = [
|
||||
export default function Dashboard() {
|
||||
const { appId, routeId } = useParams<{ appId: string; routeId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const { setDetail } = useDetailPanelSlot()
|
||||
const [selectedId, setSelectedId] = useState<string | undefined>()
|
||||
const [panelOpen, setPanelOpen] = useState(false)
|
||||
const [sortField, setSortField] = useState<string>('startTime')
|
||||
@@ -416,128 +415,93 @@ export default function Dashboard() {
|
||||
{/* Shortcuts bar */}
|
||||
<ShortcutsBar shortcuts={SHORTCUTS} />
|
||||
|
||||
{/* Detail panel — hoisted to AppShell via context */}
|
||||
<DashboardDetailPanel
|
||||
open={panelOpen}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
selectedRow={selectedRow}
|
||||
detail={detail}
|
||||
errorClass={errorClass}
|
||||
errorMsg={errorMsg}
|
||||
routeNodes={routeNodes}
|
||||
flatProcs={flatProcs}
|
||||
navigate={navigate}
|
||||
setDetail={setDetail}
|
||||
/>
|
||||
{/* Detail panel — portaled to AppShell level for proper slide-in */}
|
||||
{selectedRow && detail && document.getElementById('detail-panel-portal') &&
|
||||
createPortal(
|
||||
<DetailPanel
|
||||
open={panelOpen}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
title={`${detail.routeId} \u2014 ${selectedRow.executionId.slice(0, 12)}`}
|
||||
>
|
||||
<div className={styles.panelSection}>
|
||||
<button
|
||||
className={styles.openDetailLink}
|
||||
onClick={() => navigate(`/exchanges/${detail.executionId}`)}
|
||||
>
|
||||
Open full details →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Overview</div>
|
||||
<div className={styles.overviewGrid}>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Status</span>
|
||||
<span className={styles.statusCell}>
|
||||
<StatusDot variant={statusToVariant(detail.status)} />
|
||||
<span>{statusLabel(detail.status)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Duration</span>
|
||||
<MonoText size="sm">{formatDuration(detail.durationMs)}</MonoText>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Route</span>
|
||||
<span>{detail.routeId}</span>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Agent</span>
|
||||
<MonoText size="sm">{detail.agentId ?? '\u2014'}</MonoText>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Correlation</span>
|
||||
<MonoText size="xs">{detail.correlationId ?? '\u2014'}</MonoText>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Timestamp</span>
|
||||
<MonoText size="xs">{detail.startTime ? new Date(detail.startTime).toISOString() : '\u2014'}</MonoText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Errors</div>
|
||||
<div className={styles.errorBlock}>
|
||||
<div className={styles.errorClass}>{errorClass}</div>
|
||||
<div className={styles.errorMessage}>{errorMsg}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Route Flow</div>
|
||||
{routeNodes.length > 0 ? (
|
||||
<RouteFlow nodes={routeNodes} />
|
||||
) : (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No diagram available</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>
|
||||
Processor Timeline
|
||||
<span className={styles.panelSectionMeta}>{formatDuration(detail.durationMs)}</span>
|
||||
</div>
|
||||
{flatProcs.length > 0 ? (
|
||||
<ProcessorTimeline
|
||||
processors={flatProcs}
|
||||
totalMs={detail.durationMs}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No processor data</div>
|
||||
)}
|
||||
</div>
|
||||
</DetailPanel>,
|
||||
document.getElementById('detail-panel-portal')!,
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 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(
|
||||
<DetailPanel
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={`${detail.routeId} \u2014 ${selectedRow.executionId.slice(0, 12)}`}
|
||||
>
|
||||
<div className={styles.panelSection}>
|
||||
<button
|
||||
className={styles.openDetailLink}
|
||||
onClick={() => navigate(`/exchanges/${detail.executionId}`)}
|
||||
>
|
||||
Open full details →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Overview</div>
|
||||
<div className={styles.overviewGrid}>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Status</span>
|
||||
<span className={styles.statusCell}>
|
||||
<StatusDot variant={statusToVariant(detail.status)} />
|
||||
<span>{statusLabel(detail.status)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Duration</span>
|
||||
<MonoText size="sm">{formatDuration(detail.durationMs)}</MonoText>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Route</span>
|
||||
<span>{detail.routeId}</span>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Agent</span>
|
||||
<MonoText size="sm">{detail.agentId ?? '\u2014'}</MonoText>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Correlation</span>
|
||||
<MonoText size="xs">{detail.correlationId ?? '\u2014'}</MonoText>
|
||||
</div>
|
||||
<div className={styles.overviewRow}>
|
||||
<span className={styles.overviewLabel}>Timestamp</span>
|
||||
<MonoText size="xs">{detail.startTime ? new Date(detail.startTime).toISOString() : '\u2014'}</MonoText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Errors</div>
|
||||
<div className={styles.errorBlock}>
|
||||
<div className={styles.errorClass}>{errorClass}</div>
|
||||
<div className={styles.errorMessage}>{errorMsg}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>Route Flow</div>
|
||||
{routeNodes.length > 0 ? (
|
||||
<RouteFlow nodes={routeNodes} />
|
||||
) : (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No diagram available</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.panelSection}>
|
||||
<div className={styles.panelSectionTitle}>
|
||||
Processor Timeline
|
||||
<span className={styles.panelSectionMeta}>{formatDuration(detail.durationMs)}</span>
|
||||
</div>
|
||||
{flatProcs.length > 0 ? (
|
||||
<ProcessorTimeline
|
||||
processors={flatProcs}
|
||||
totalMs={detail.durationMs}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No processor data</div>
|
||||
)}
|
||||
</div>
|
||||
</DetailPanel>
|
||||
)
|
||||
return () => setDetail(null)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user