diff --git a/core/http/react-ui/e2e/traces-pagination.spec.js b/core/http/react-ui/e2e/traces-pagination.spec.js index c6d7b89a1744..a7e56afd4de9 100644 --- a/core/http/react-ui/e2e/traces-pagination.spec.js +++ b/core/http/react-ui/e2e/traces-pagination.spec.js @@ -79,4 +79,30 @@ test.describe('Traces - bounded list and on-demand detail', () => { await expect(page.locator('text=hello from the response body')).toBeVisible() await expect(page.locator('text=203.0.113.9').first()).toBeVisible() }) + + test('keeps the expanded trace open when a refresh prepends a new row', async ({ page }) => { + await page.locator('tr', { hasText: '/v1/chat/completions' }).first().click() + await expect(page.locator('text=hello from the request body')).toBeVisible() + + await page.route('**/api/traces?*', (route) => { + route.fulfill({ + contentType: 'application/json', + headers: { 'X-Total-Count': '843' }, + body: JSON.stringify([ + { + id: '8', + request: { method: 'GET', path: '/v1/models', body: null }, + response: { status: 200, body: null }, + }, + ...LIST_BODY, + ]), + }) + }) + + await page.getByRole('button', { name: 'Refresh' }).click() + + await expect(page.locator('text=hello from the request body')).toBeVisible() + const originalRow = page.locator('tr', { hasText: '/v1/chat/completions' }).first() + await expect(originalRow.locator('i.fa-chevron-down')).toBeVisible() + }) }) diff --git a/core/http/react-ui/src/pages/Traces.jsx b/core/http/react-ui/src/pages/Traces.jsx index f1bfc76bd774..0f57b6790234 100644 --- a/core/http/react-ui/src/pages/Traces.jsx +++ b/core/http/react-ui/src/pages/Traces.jsx @@ -342,7 +342,7 @@ export default function Traces() { const [apiCount, setApiCount] = useState(0) const [backendCount, setBackendCount] = useState(0) const [loading, setLoading] = useState(true) - const [expandedRow, setExpandedRow] = useState(null) + const [expandedTraceId, setExpandedTraceId] = useState(null) // detail holds the full record for the currently expanded row, fetched on // demand from /api/traces/:id (the list response omits the bodies). const [detail, setDetail] = useState(null) @@ -360,7 +360,8 @@ export default function Traces() { duration: (a, b) => (a.duration || 0) - (b.duration || 0), } const toggleSort = (key) => { - setExpandedRow(null) + setExpandedTraceId(null) + setDetail(null) setSort(s => s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' }) } const sortableTh = (key, label, props = {}) => ( @@ -433,20 +434,20 @@ export default function Traces() { useEffect(() => { setLoading(true) - setExpandedRow(null) + setExpandedTraceId(null) setDetail(null) fetchTraces() }, [fetchTraces]) // Expanding a row pulls the full record (bodies, data fields, audio // snippets) that the list response deliberately omits. - const toggleRow = useCallback(async (index, row) => { - if (expandedRow === index) { - setExpandedRow(null) + const toggleRow = useCallback(async (row) => { + if (expandedTraceId === row?.id) { + setExpandedTraceId(null) setDetail(null) return } - setExpandedRow(index) + setExpandedTraceId(row?.id ?? null) setDetail(null) if (!row?.id) return try { @@ -457,7 +458,7 @@ export default function Traces() { } catch { // Fall back to the summary view; the row still renders what it has. } - }, [expandedRow, activeTab]) + }, [expandedTraceId, activeTab]) // Auto-refresh every 5 seconds useEffect(() => { @@ -470,7 +471,7 @@ export default function Traces() { if (activeTab === 'api') await tracesApi.clear() else await tracesApi.clearBackend() setTraces([]) - setExpandedRow(null) + setExpandedTraceId(null) setDetail(null) addToast('Traces cleared', 'success') } catch (err) { @@ -500,7 +501,7 @@ export default function Traces() { } // Reset sort + expansion when switching trace tabs (columns differ). - useEffect(() => { setSort({ key: null, dir: 'asc' }); setExpandedRow(null); setDetail(null) }, [activeTab]) + useEffect(() => { setSort({ key: null, dir: 'asc' }); setExpandedTraceId(null); setDetail(null) }, [activeTab]) const sortedTraces = sort.key && TRACE_SORT[sort.key] ? [...traces].sort((a, b) => sort.dir === 'asc' ? TRACE_SORT[sort.key](a, b) : TRACE_SORT[sort.key](b, a)) @@ -635,9 +636,9 @@ export default function Traces() { {sortedTraces.map((trace, i) => ( - - toggleRow(i, trace)} className="clickable"> - + + toggleRow(trace)} className="clickable"> + {trace.request?.method || '-'} {trace.request?.path || '-'} {trace.user_name || trace.user_id || '-'} @@ -648,7 +649,7 @@ export default function Traces() { : } - {expandedRow === i && ( + {expandedTraceId === trace.id && ( @@ -674,9 +675,9 @@ export default function Traces() { {sortedTraces.map((trace, i) => ( - - toggleRow(i, trace)} className="clickable"> - + + toggleRow(trace)} className="clickable"> + {trace.type || '-'} {formatDateTime(trace.timestamp)} {trace.model_name || '-'} @@ -690,7 +691,7 @@ export default function Traces() { : } - {expandedRow === i && ( + {expandedTraceId === trace.id && (