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 () => {
|
it('opens debug trace detail on demand instead of preloading the first trace inline', async () => {
|
||||||
let root!: WebTestRenderer;
|
let root!: WebTestRenderer;
|
||||||
|
|
||||||
|
|||||||
+161
-126
@@ -622,6 +622,7 @@ export default function ProxyLogs() {
|
|||||||
const [showDebugSettingsModal, setShowDebugSettingsModal] = useState(false);
|
const [showDebugSettingsModal, setShowDebugSettingsModal] = useState(false);
|
||||||
const [debugPanelLoading, setDebugPanelLoading] = useState(false);
|
const [debugPanelLoading, setDebugPanelLoading] = useState(false);
|
||||||
const [debugPanelSaving, setDebugPanelSaving] = useState(false);
|
const [debugPanelSaving, setDebugPanelSaving] = useState(false);
|
||||||
|
const [debugTracePanelExpanded, setDebugTracePanelExpanded] = useState(true);
|
||||||
const [debugSettings, setDebugSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
|
const [debugSettings, setDebugSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
|
||||||
const [debugDraftSettings, setDebugDraftSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
|
const [debugDraftSettings, setDebugDraftSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
|
||||||
const [debugTraces, setDebugTraces] = useState<ProxyDebugTraceListItem[]>([]);
|
const [debugTraces, setDebugTraces] = useState<ProxyDebugTraceListItem[]>([]);
|
||||||
@@ -994,6 +995,9 @@ export default function ProxyLogs() {
|
|||||||
const updated = await api.updateRuntimeSettings(buildProxyDebugSettingsPayload(nextSettings));
|
const updated = await api.updateRuntimeSettings(buildProxyDebugSettingsPayload(nextSettings));
|
||||||
const normalized = normalizeProxyDebugSettings(updated);
|
const normalized = normalizeProxyDebugSettings(updated);
|
||||||
applyLoadedDebugSettings(normalized, { syncDraft: true });
|
applyLoadedDebugSettings(normalized, { syncDraft: true });
|
||||||
|
if (normalized.proxyDebugTraceEnabled) {
|
||||||
|
setDebugTracePanelExpanded(true);
|
||||||
|
}
|
||||||
if (options?.closeAfterSave) {
|
if (options?.closeAfterSave) {
|
||||||
setShowDebugSettingsModal(false);
|
setShowDebugSettingsModal(false);
|
||||||
}
|
}
|
||||||
@@ -1542,6 +1546,29 @@ export default function ProxyLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={debugSettings.proxyDebugTraceEnabled ? 'btn btn-ghost btn-ghost-active' : 'btn btn-ghost'}
|
className={debugSettings.proxyDebugTraceEnabled ? 'btn btn-ghost btn-ghost-active' : 'btn btn-ghost'}
|
||||||
@@ -1586,135 +1613,143 @@ export default function ProxyLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card" style={{ marginBottom: 12, padding: 12, overflowX: 'auto' }}>
|
<div
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
|
className={`anim-collapse ${debugTracePanelExpanded ? 'is-open' : ''}`.trim()}
|
||||||
<div>
|
data-debug-trace-panel-body
|
||||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-primary)' }}>最近调试追踪</div>
|
style={{ marginBottom: debugTracePanelExpanded ? 12 : 0 }}
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 4 }}>
|
>
|
||||||
最多抓最近 20 条,列表分页每页 5 条;打开详情后各段内容可按需展开和收起。
|
<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>
|
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
|
{debugPanelLoading && debugTraces.length === 0 ? (
|
||||||
{debugSettings.proxyDebugTraceEnabled ? '开启中,结果会自动刷新' : '尚未开启'}
|
<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>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user