Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions core/http/react-ui/e2e/traces-pagination.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
37 changes: 19 additions & 18 deletions core/http/react-ui/src/pages/Traces.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = {}) => (
Expand Down Expand Up @@ -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 {
Expand All @@ -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(() => {
Expand All @@ -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) {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -635,9 +636,9 @@ export default function Traces() {
</thead>
<tbody>
{sortedTraces.map((trace, i) => (
<React.Fragment key={i}>
<tr onClick={() => toggleRow(i, trace)} className="clickable">
<td><i className={`fas fa-chevron-${expandedRow === i ? 'down' : 'right'} text-xs`} /></td>
<React.Fragment key={trace.id ?? i}>
<tr onClick={() => toggleRow(trace)} className="clickable">
<td><i className={`fas fa-chevron-${expandedTraceId === trace.id ? 'down' : 'right'} text-xs`} /></td>
<td><span className="badge badge-info">{trace.request?.method || '-'}</span></td>
<td className="text-mono text-sm">{trace.request?.path || '-'}</td>
<td className="text-sub cell-clip" title={trace.user_name || trace.user_id || ''}>{trace.user_name || trace.user_id || '-'}</td>
Expand All @@ -648,7 +649,7 @@ export default function Traces() {
: <i className="fas fa-check-circle text-success" />}
</td>
</tr>
{expandedRow === i && (
{expandedTraceId === trace.id && (
<tr>
<td colSpan="6" className="p-0">
<ApiTraceDetail trace={detail && detail.id === trace.id ? detail : trace} />
Expand All @@ -674,9 +675,9 @@ export default function Traces() {
</thead>
<tbody>
{sortedTraces.map((trace, i) => (
<React.Fragment key={i}>
<tr onClick={() => toggleRow(i, trace)} className="clickable">
<td><i className={`fas fa-chevron-${expandedRow === i ? 'down' : 'right'} text-xs`} /></td>
<React.Fragment key={trace.id ?? i}>
<tr onClick={() => toggleRow(trace)} className="clickable">
<td><i className={`fas fa-chevron-${expandedTraceId === trace.id ? 'down' : 'right'} text-xs`} /></td>
<td><span style={typeBadgeStyle(trace.type)}>{trace.type || '-'}</span></td>
<td className="text-sub nowrap">{formatDateTime(trace.timestamp)}</td>
<td className="text-mono text-sm">{trace.model_name || '-'}</td>
Expand All @@ -690,7 +691,7 @@ export default function Traces() {
: <i className="fas fa-check-circle text-success" />}
</td>
</tr>
{expandedRow === i && (
{expandedTraceId === trace.id && (
<tr>
<td colSpan="7" className="p-0">
<BackendTraceDetail trace={detail && detail.id === trace.id ? detail : trace} />
Expand Down
Loading