fix: use createPortal for DetailPanel instead of context+useEffect
Some checks failed
CI / build (push) Successful in 1m21s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 53s
CI / deploy-feature (push) Has been cancelled
CI / deploy (push) Has been cancelled

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:
hsiegeln
2026-03-24 18:38:59 +01:00
parent 5de792744e
commit f3241e904f
4 changed files with 106 additions and 192 deletions

View File

@@ -1,4 +1,5 @@
import { useState, useMemo, useEffect } from 'react';
import { useState, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useParams, Link } from 'react-router';
import {
StatCard, StatusDot, Badge, MonoText, ProgressBar,
@@ -9,7 +10,6 @@ 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 ──────────────────────────────────────────────────────────────────
@@ -219,7 +219,6 @@ 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<AgentInstance | null>(null);
const [panelOpen, setPanelOpen] = useState(false);
@@ -505,41 +504,18 @@ export default function AgentHealth() {
</div>
)}
{/* Detail panel — hoisted to AppShell via context */}
<AgentDetailPanelSlot
open={panelOpen}
onClose={() => { setPanelOpen(false); setSelectedInstance(null); }}
selectedInstance={selectedInstance}
detailTabs={detailTabs}
setDetail={setDetail}
/>
{/* Detail panel — portaled to AppShell level for proper slide-in */}
{selectedInstance && document.getElementById('detail-panel-portal') &&
createPortal(
<DetailPanel
open={panelOpen}
onClose={() => { setPanelOpen(false); setSelectedInstance(null); }}
title={selectedInstance.name ?? selectedInstance.id}
tabs={detailTabs}
/>,
document.getElementById('detail-panel-portal')!,
)
}
</div>
);
}
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(
<DetailPanel
open={open}
onClose={onClose}
title={selectedInstance.name ?? selectedInstance.id}
tabs={detailTabs}
/>
);
return () => setDetail(null);
});
return null;
}