make proxy trace panel collapsible (#327)

This commit is contained in:
Cita
2026-03-30 16:32:26 +08:00
committed by GitHub
parent 26d87db2cb
commit 0d2c4501c2
2 changed files with 229 additions and 126 deletions
@@ -390,6 +390,74 @@ describe('ProxyLogs server-driven page', () => {
}
});
it('allows collapsing and expanding the debug trace panel to reduce page footprint', async () => {
let root!: WebTestRenderer;
try {
await act(async () => {
root = create(
<MemoryRouter initialEntries={['/logs']}>
<ToastProvider>
<ProxyLogs />
</ToastProvider>
</MemoryRouter>,
);
});
await flushMicrotasks();
const toggleButton = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& node.props['data-debug-trace-panel-toggle'] === true
));
const panelBody = root.root.find((node) => (
node.type === 'div'
&& node.props['data-debug-trace-panel-body'] === true
));
expect(toggleButton.props['aria-expanded']).toBe(true);
expect(String(panelBody.props.className || '')).toContain('is-open');
await act(async () => {
toggleButton.props.onClick();
});
await flushMicrotasks();
const collapsedToggleButton = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& node.props['data-debug-trace-panel-toggle'] === true
));
const collapsedPanelBody = root.root.find((node) => (
node.type === 'div'
&& node.props['data-debug-trace-panel-body'] === true
));
expect(collapsedToggleButton.props['aria-expanded']).toBe(false);
expect(String(collapsedPanelBody.props.className || '')).not.toContain('is-open');
await act(async () => {
collapsedToggleButton.props.onClick();
});
await flushMicrotasks();
const expandedToggleButton = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& node.props['data-debug-trace-panel-toggle'] === true
));
const expandedPanelBody = root.root.find((node) => (
node.type === 'div'
&& node.props['data-debug-trace-panel-body'] === true
));
expect(expandedToggleButton.props['aria-expanded']).toBe(true);
expect(String(expandedPanelBody.props.className || '')).toContain('is-open');
} finally {
root?.unmount();
}
});
it('opens debug trace detail on demand instead of preloading the first trace inline', async () => {
let root!: WebTestRenderer;
+161 -126
View File
@@ -622,6 +622,7 @@ export default function ProxyLogs() {
const [showDebugSettingsModal, setShowDebugSettingsModal] = useState(false);
const [debugPanelLoading, setDebugPanelLoading] = useState(false);
const [debugPanelSaving, setDebugPanelSaving] = useState(false);
const [debugTracePanelExpanded, setDebugTracePanelExpanded] = useState(true);
const [debugSettings, setDebugSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
const [debugDraftSettings, setDebugDraftSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
const [debugTraces, setDebugTraces] = useState<ProxyDebugTraceListItem[]>([]);
@@ -994,6 +995,9 @@ export default function ProxyLogs() {
const updated = await api.updateRuntimeSettings(buildProxyDebugSettingsPayload(nextSettings));
const normalized = normalizeProxyDebugSettings(updated);
applyLoadedDebugSettings(normalized, { syncDraft: true });
if (normalized.proxyDebugTraceEnabled) {
setDebugTracePanelExpanded(true);
}
if (options?.closeAfterSave) {
setShowDebugSettingsModal(false);
}
@@ -1542,6 +1546,29 @@ export default function ProxyLogs() {
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button
type="button"
className="btn btn-ghost"
style={{ border: '1px solid var(--color-border)' }}
aria-expanded={debugTracePanelExpanded}
data-debug-trace-panel-toggle
onClick={() => setDebugTracePanelExpanded((current) => !current)}
>
<svg
width="14"
height="14"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style={{
transform: debugTracePanelExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
}}
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
{debugTracePanelExpanded ? '收起追踪面板' : '展开追踪面板'}
</button>
<button
type="button"
className={debugSettings.proxyDebugTraceEnabled ? 'btn btn-ghost btn-ghost-active' : 'btn btn-ghost'}
@@ -1586,135 +1613,143 @@ export default function ProxyLogs() {
</div>
</div>
<div className="card" style={{ marginBottom: 12, padding: 12, overflowX: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-primary)' }}>最近调试追踪</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 4 }}>
最多抓最近 20 条,列表分页每页 5 条;打开详情后各段内容可按需展开和收起。
<div
className={`anim-collapse ${debugTracePanelExpanded ? 'is-open' : ''}`.trim()}
data-debug-trace-panel-body
style={{ marginBottom: debugTracePanelExpanded ? 12 : 0 }}
>
<div className="anim-collapse-inner">
<div className="card" style={{ padding: 12, overflowX: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-primary)' }}>最近调试追踪</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 4 }}>
最多抓最近 20 条,列表分页每页 5 条;打开详情后各段内容可按需展开和收起。
</div>
</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
{debugSettings.proxyDebugTraceEnabled ? '开启中,结果会自动刷新' : '尚未开启'}
</div>
</div>
</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
{debugSettings.proxyDebugTraceEnabled ? '开启中,结果会自动刷新' : '尚未开启'}
{debugPanelLoading && debugTraces.length === 0 ? (
<div style={{ color: 'var(--color-text-muted)', fontSize: 13, paddingBottom: 12 }}>加载调试追踪中...</div>
) : debugTraces.length === 0 ? (
<div
style={{
padding: 14,
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--color-border-light)',
background: 'var(--color-bg)',
color: 'var(--color-text-muted)',
fontSize: 12,
lineHeight: 1.6,
}}
>
{debugSettings.proxyDebugTraceEnabled
? '暂时还没有新追踪。这里只显示开启后产生的新请求,等下一次代理请求进入就会出现在这里。'
: '调试追踪尚未开启。点击上方“开启调试”或“调试设置”后,新的代理请求会出现在这里。'}
</div>
) : isMobile ? (
<div className="mobile-card-list">
{visibleDebugTraces.map((trace) => (
<MobileCard
key={trace.id}
title={trace.sessionId || `trace-${trace.id}`}
subtitle={formatDateTimeLocal(trace.createdAt)}
compact
headerActions={renderTraceStatusBadge(trace)}
footerActions={(
<button
type="button"
className="btn btn-link"
onClick={() => openDebugTraceDetailModal(trace.id)}
>
查看详情
</button>
)}
>
<MobileField label="模型" value={trace.requestedModel || '-'} />
<MobileField label="下游路径" value={trace.downstreamPath || '-'} />
<MobileField label="上游路径" value={trace.finalUpstreamPath || '-'} />
<MobileField label="客户端" value={trace.clientKind || '-'} />
</MobileCard>
))}
</div>
) : (
<table className="data-table" style={{ width: '100%' }}>
<thead>
<tr>
<th>时间</th>
<th>Session</th>
<th>模型</th>
<th>下游路径</th>
<th>上游路径</th>
<th>客户端</th>
<th>{tr('状态')}</th>
<th style={{ textAlign: 'right' }}>操作</th>
</tr>
</thead>
<tbody>
{visibleDebugTraces.map((trace) => (
<tr key={trace.id}>
<td style={{ fontSize: 12, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums', color: 'var(--color-text-secondary)' }}>
{formatDateTimeLocal(trace.createdAt)}
</td>
<td style={{ fontSize: 12, fontWeight: 600 }}>{trace.sessionId || `trace-${trace.id}`}</td>
<td style={{ fontSize: 12 }}>{trace.requestedModel || '-'}</td>
<td style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>{trace.downstreamPath || '-'}</td>
<td style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>{trace.finalUpstreamPath || '-'}</td>
<td style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>{trace.clientKind || '-'}</td>
<td>{renderTraceStatusBadge(trace)}</td>
<td style={{ textAlign: 'right' }}>
<button
type="button"
className="btn btn-link"
onClick={() => openDebugTraceDetailModal(trace.id)}
>
查看详情
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
{debugTraces.length > 0 ? (
<div className="pagination" style={{ marginTop: 12 }}>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginRight: 'auto' }}>
显示第 {debugTraceDisplayedStart} - {debugTraceDisplayedEnd} 条,共 {debugTraces.length} 条
</div>
<button
className="pagination-btn"
aria-label="调试追踪上一页"
disabled={safeDebugTracePage <= 1}
onClick={() => setDebugTracePage((current) => current - 1)}
>
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
</button>
{Array.from({ length: debugTraceTotalPages }, (_, index) => index + 1).map((num) => (
<button
key={`debug-trace-page-${num}`}
className={`pagination-btn ${safeDebugTracePage === num ? 'active' : ''}`}
onClick={() => setDebugTracePage(num)}
>
{num}
</button>
))}
<button
className="pagination-btn"
aria-label="调试追踪下一页"
disabled={safeDebugTracePage >= debugTraceTotalPages}
onClick={() => setDebugTracePage((current) => current + 1)}
>
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
</button>
</div>
) : null}
</div>
</div>
{debugPanelLoading && debugTraces.length === 0 ? (
<div style={{ color: 'var(--color-text-muted)', fontSize: 13, paddingBottom: 12 }}>加载调试追踪中...</div>
) : debugTraces.length === 0 ? (
<div
style={{
padding: 14,
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--color-border-light)',
background: 'var(--color-bg)',
color: 'var(--color-text-muted)',
fontSize: 12,
lineHeight: 1.6,
}}
>
{debugSettings.proxyDebugTraceEnabled
? '暂时还没有新追踪。这里只显示开启后产生的新请求,等下一次代理请求进入就会出现在这里。'
: '调试追踪尚未开启。点击上方“开启调试”或“调试设置”后,新的代理请求会出现在这里。'}
</div>
) : isMobile ? (
<div className="mobile-card-list">
{visibleDebugTraces.map((trace) => (
<MobileCard
key={trace.id}
title={trace.sessionId || `trace-${trace.id}`}
subtitle={formatDateTimeLocal(trace.createdAt)}
compact
headerActions={renderTraceStatusBadge(trace)}
footerActions={(
<button
type="button"
className="btn btn-link"
onClick={() => openDebugTraceDetailModal(trace.id)}
>
查看详情
</button>
)}
>
<MobileField label="模型" value={trace.requestedModel || '-'} />
<MobileField label="下游路径" value={trace.downstreamPath || '-'} />
<MobileField label="上游路径" value={trace.finalUpstreamPath || '-'} />
<MobileField label="客户端" value={trace.clientKind || '-'} />
</MobileCard>
))}
</div>
) : (
<table className="data-table" style={{ width: '100%' }}>
<thead>
<tr>
<th>时间</th>
<th>Session</th>
<th>模型</th>
<th>下游路径</th>
<th>上游路径</th>
<th>客户端</th>
<th>{tr('状态')}</th>
<th style={{ textAlign: 'right' }}>操作</th>
</tr>
</thead>
<tbody>
{visibleDebugTraces.map((trace) => (
<tr key={trace.id}>
<td style={{ fontSize: 12, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums', color: 'var(--color-text-secondary)' }}>
{formatDateTimeLocal(trace.createdAt)}
</td>
<td style={{ fontSize: 12, fontWeight: 600 }}>{trace.sessionId || `trace-${trace.id}`}</td>
<td style={{ fontSize: 12 }}>{trace.requestedModel || '-'}</td>
<td style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>{trace.downstreamPath || '-'}</td>
<td style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>{trace.finalUpstreamPath || '-'}</td>
<td style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>{trace.clientKind || '-'}</td>
<td>{renderTraceStatusBadge(trace)}</td>
<td style={{ textAlign: 'right' }}>
<button
type="button"
className="btn btn-link"
onClick={() => openDebugTraceDetailModal(trace.id)}
>
查看详情
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
{debugTraces.length > 0 ? (
<div className="pagination" style={{ marginTop: 12 }}>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginRight: 'auto' }}>
显示第 {debugTraceDisplayedStart} - {debugTraceDisplayedEnd} 条,共 {debugTraces.length} 条
</div>
<button
className="pagination-btn"
aria-label="调试追踪上一页"
disabled={safeDebugTracePage <= 1}
onClick={() => setDebugTracePage((current) => current - 1)}
>
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
</button>
{Array.from({ length: debugTraceTotalPages }, (_, index) => index + 1).map((num) => (
<button
key={`debug-trace-page-${num}`}
className={`pagination-btn ${safeDebugTracePage === num ? 'active' : ''}`}
onClick={() => setDebugTracePage(num)}
>
{num}
</button>
))}
<button
className="pagination-btn"
aria-label="调试追踪下一页"
disabled={safeDebugTracePage >= debugTraceTotalPages}
onClick={() => setDebugTracePage((current) => current + 1)}
>
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
</button>
</div>
) : null}
</div>
{isMobile ? (