feat(table,runs,sessions): add server-side sorting and pagination
- add sortable column support to Table with asc/desc/unsorted icons - active sort icon uses --color-primary; no header background change - Table supports server-side mode via total/page/onPageChange props - wire server-side sort+pagination in ScenariosPage, AllRunsPage, RunsPage, SessionsPage - remove steps count column from run tables - fix server: RunsQueryDto now extends PaginationQueryDto with orderBy/orderDir - fix findAllRuns to use QueryBuilder; sort by scenario.name via JOIN - add per-resource typed query DTOs with @IsIn allowlist on orderBy - prevents SQL injection and returns 400 for unknown orderBy values
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
Button,
|
||||
Table,
|
||||
type TableColumn,
|
||||
type SortDirection,
|
||||
Timestamp,
|
||||
UuidBadge,
|
||||
} from '../../ui';
|
||||
@@ -23,25 +24,36 @@ export function SessionsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [sortBy, setSortBy] = useState('id');
|
||||
const [sortDir, setSortDir] = useState<SortDirection>('desc');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const load = useCallback(() => {
|
||||
const load = useCallback((orderBy: string, orderDir: SortDirection, p: number, limit: number) => {
|
||||
setLoading(true);
|
||||
sessions
|
||||
.list()
|
||||
.then((res) => setItems(res.data))
|
||||
.list(p, limit, orderBy, orderDir === 'asc' ? 'ASC' : 'DESC')
|
||||
.then((res) => { setItems(res.data); setTotal(res.total); })
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
load(sortBy, sortDir, page, pageSize);
|
||||
}, [load, sortBy, sortDir, page, pageSize]);
|
||||
|
||||
const handleSort = (col: string, dir: SortDirection) => {
|
||||
setSortBy(col);
|
||||
setSortDir(dir);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteId) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await sessions.remove(deleteId);
|
||||
setLoading(true);
|
||||
load();
|
||||
load(sortBy, sortDir, page, pageSize);
|
||||
setDeleteId(null);
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
@@ -50,17 +62,19 @@ export function SessionsPage() {
|
||||
|
||||
const columns: TableColumn<Session>[] = [
|
||||
{ key: 'id', header: t('sessions.col_id'), render: (s) => <UuidBadge id={s.id} />, width: 60 },
|
||||
{ key: 'name', header: t('sessions.col_name'), render: (s) => s.sessionName },
|
||||
{ key: 'sessionName', header: t('sessions.col_name'), sortable: true, render: (s) => s.sessionName },
|
||||
{
|
||||
key: 'status',
|
||||
header: t('sessions.col_status'),
|
||||
width: 100,
|
||||
sortable: true,
|
||||
render: (s) => <Badge variant={s.status === 'open' ? 'success' : 'error'}>{s.status}</Badge>,
|
||||
},
|
||||
{
|
||||
key: 'lastUsed',
|
||||
key: 'lastUsedAt',
|
||||
header: t('sessions.col_lastUsed'),
|
||||
width: 160,
|
||||
sortable: true,
|
||||
render: (s) => (s.lastUsedAt ? <Timestamp value={s.lastUsedAt} /> : '—'),
|
||||
},
|
||||
{
|
||||
@@ -95,7 +109,13 @@ export function SessionsPage() {
|
||||
rowKey={(s) => s.id}
|
||||
loading={loading}
|
||||
emptyMessage={t('sessions.empty')}
|
||||
pageSize={10}
|
||||
sortBy={sortBy}
|
||||
sortDir={sortDir}
|
||||
onSort={handleSort}
|
||||
total={total}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
onPageSizeChange={(s) => { setPageSize(s); setPage(1); }}
|
||||
pageSizeOptions={[10, 25, 50]}
|
||||
onRowClick={(s) => navigate(`/sessions/${s.id}`)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user