fix: use server-side sorting for paginated tables
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 1m10s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

Upgrade @cameleer/design-system to v0.1.1 which adds onSortChange
callback to DataTable. Wire it up in Dashboard (exchanges), AuditLog,
and RouteDetail (recent executions) so sorting triggers a new API
request with sortField/sortDir instead of only sorting the current page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-24 17:05:06 +01:00
parent aa3d9f375b
commit 48455cd559
5 changed files with 39 additions and 8 deletions

8
ui/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "ui", "name": "ui",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@cameleer/design-system": "^0.0.3", "@cameleer/design-system": "^0.1.1",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",
"react": "^19.2.4", "react": "^19.2.4",
@@ -276,9 +276,9 @@
} }
}, },
"node_modules/@cameleer/design-system": { "node_modules/@cameleer/design-system": {
"version": "0.0.3", "version": "0.1.1",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.0.3/design-system-0.0.3.tgz", "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.1/design-system-0.1.1.tgz",
"integrity": "sha512-x1mZvgYz7j57xFB26pMh9hn5waSJA1CcRWTgkzleLfaO/CmhekLup1HHlbh0b9SxVci6g2HzbcJldr4kvM1yzg==", "integrity": "sha512-5PNIDuw9vcM0BVQIdiJQalWoMJPUZj01f6rX3unHDCF1gE1mBHCFZ41RPy2QsuKmjgZ1azenXXdnHL/XGVostQ==",
"dependencies": { "dependencies": {
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View File

@@ -14,7 +14,7 @@
"generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts" "generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
}, },
"dependencies": { "dependencies": {
"@cameleer/design-system": "^0.0.3", "@cameleer/design-system": "^0.1.1",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",
"react": "^19.2.4", "react": "^19.2.4",

View File

@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { import {
Badge, DateRangePicker, Input, Select, MonoText, CodeBlock, DataTable, Badge, DateRangePicker, Input, Select, MonoText, CodeBlock, DataTable,
} from '@cameleer/design-system'; } from '@cameleer/design-system';
@@ -60,6 +60,14 @@ export default function AuditLogPage() {
const [categoryFilter, setCategoryFilter] = useState(''); const [categoryFilter, setCategoryFilter] = useState('');
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [sortField, setSortField] = useState<string>('timestamp');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
const handleSortChange = useCallback((key: string, dir: 'asc' | 'desc') => {
setSortField(key);
setSortDir(dir);
setPage(0);
}, []);
const { data } = useAuditLog({ const { data } = useAuditLog({
username: userFilter || undefined, username: userFilter || undefined,
@@ -67,6 +75,8 @@ export default function AuditLogPage() {
search: searchFilter || undefined, search: searchFilter || undefined,
from: dateRange.start.toISOString(), from: dateRange.start.toISOString(),
to: dateRange.end.toISOString(), to: dateRange.end.toISOString(),
sort: sortField,
order: sortDir,
page, page,
size: 25, size: 25,
}); });
@@ -122,6 +132,7 @@ export default function AuditLogPage() {
sortable sortable
flush flush
pageSize={25} pageSize={25}
onSortChange={handleSortChange}
rowAccent={(row) => row.result === 'FAILURE' ? 'error' : undefined} rowAccent={(row) => row.result === 'FAILURE' ? 'error' : undefined}
expandedContent={(row) => ( expandedContent={(row) => (
<div className={styles.expandedDetail}> <div className={styles.expandedDetail}>

View File

@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react' import { useState, useMemo, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router' import { useParams, useNavigate } from 'react-router'
import { import {
DataTable, DataTable,
@@ -176,12 +176,19 @@ export default function Dashboard() {
const navigate = useNavigate() const navigate = useNavigate()
const [selectedId, setSelectedId] = useState<string | undefined>() const [selectedId, setSelectedId] = useState<string | undefined>()
const [panelOpen, setPanelOpen] = useState(false) const [panelOpen, setPanelOpen] = useState(false)
const [sortField, setSortField] = useState<string>('startTime')
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
const { timeRange, statusFilters } = useGlobalFilters() const { timeRange, statusFilters } = useGlobalFilters()
const timeFrom = timeRange.start.toISOString() const timeFrom = timeRange.start.toISOString()
const timeTo = timeRange.end.toISOString() const timeTo = timeRange.end.toISOString()
const timeWindowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000 const timeWindowSeconds = (timeRange.end.getTime() - timeRange.start.getTime()) / 1000
const handleSortChange = useCallback((key: string, dir: 'asc' | 'desc') => {
setSortField(key)
setSortDir(dir)
}, [])
// ─── API hooks ─────────────────────────────────────────────────────────── // ─── API hooks ───────────────────────────────────────────────────────────
const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId) const { data: stats } = useExecutionStats(timeFrom, timeTo, routeId, appId)
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId) const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, routeId, appId)
@@ -191,6 +198,8 @@ export default function Dashboard() {
timeTo, timeTo,
routeId: routeId || undefined, routeId: routeId || undefined,
application: appId || undefined, application: appId || undefined,
sortField,
sortDir,
offset: 0, offset: 0,
limit: 50, limit: 50,
}, },
@@ -385,6 +394,7 @@ export default function Dashboard() {
selectedId={selectedId} selectedId={selectedId}
sortable sortable
flush flush
onSortChange={handleSortChange}
rowAccent={handleRowAccent} rowAccent={handleRowAccent}
expandedContent={(row: Row) => expandedContent={(row: Row) =>
row.errorMessage ? ( row.errorMessage ? (

View File

@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { useParams, useNavigate, Link } from 'react-router'; import { useParams, useNavigate, Link } from 'react-router';
import { import {
KpiStrip, KpiStrip,
@@ -260,6 +260,13 @@ export default function RouteDetail() {
const timeTo = timeRange.end.toISOString(); const timeTo = timeRange.end.toISOString();
const [activeTab, setActiveTab] = useState('performance'); const [activeTab, setActiveTab] = useState('performance');
const [recentSortField, setRecentSortField] = useState<string>('startTime');
const [recentSortDir, setRecentSortDir] = useState<'asc' | 'desc'>('desc');
const handleRecentSortChange = useCallback((key: string, dir: 'asc' | 'desc') => {
setRecentSortField(key);
setRecentSortDir(dir);
}, []);
// ── API queries ──────────────────────────────────────────────────────────── // ── API queries ────────────────────────────────────────────────────────────
const { data: catalog } = useRouteCatalog(); const { data: catalog } = useRouteCatalog();
@@ -272,6 +279,8 @@ export default function RouteDetail() {
timeTo, timeTo,
routeId: routeId || undefined, routeId: routeId || undefined,
application: appId || undefined, application: appId || undefined,
sortField: recentSortField,
sortDir: recentSortDir,
offset: 0, offset: 0,
limit: 50, limit: 50,
}); });
@@ -560,6 +569,7 @@ export default function RouteDetail() {
onRowClick={(row) => navigate(`/exchanges/${row.executionId}`)} onRowClick={(row) => navigate(`/exchanges/${row.executionId}`)}
sortable sortable
pageSize={20} pageSize={20}
onSortChange={handleRecentSortChange}
/> />
)} )}
</div> </div>