forked from boos/BoosAPI
make proxy trace panel collapsible (#327)
This commit is contained in:
@@ -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
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user