Files
liqa/client/src/pages/session/SessionsPage.tsx
T
ars9 86f9ac5603 feat(files): add file management UI for scenarios and runs
- add scenario upload and download UI with typed file API helpers
- show run artifacts on run detail pages after polling completes
- handle multipart uploads and absolute file paths for downloads
2026-04-21 15:58:04 +03:00

147 lines
4.0 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Trash2, Monitor } from 'lucide-react';
import { sessions } from '../../api';
import type { Session } from '../../api';
import {
Badge,
Breadcrumbs,
Button,
Table,
type TableColumn,
type SortDirection,
Timestamp,
UuidBadge,
} from '../../ui';
import { DeleteConfirmationModal } from '../../components/modals';
import styles from '../Page.module.css';
export function SessionsPage() {
const { t } = useTranslation();
usePageTitle(t('sessions.title'));
const navigate = useNavigate();
const [items, setItems] = useState<Session[]>([]);
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((orderBy: string, orderDir: SortDirection, p: number, limit: number) => {
setLoading(true);
sessions
.list(p, limit, orderBy, orderDir === 'asc' ? 'ASC' : 'DESC')
.then((res) => {
setItems(res.data);
setTotal(res.total);
})
.finally(() => setLoading(false));
}, []);
useEffect(() => {
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);
load(sortBy, sortDir, page, pageSize);
setDeleteId(null);
} finally {
setDeleting(false);
}
};
const columns: TableColumn<Session>[] = [
{ key: 'id', header: t('sessions.col_id'), render: (s) => <UuidBadge id={s.id} />, width: 60 },
{
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: 'lastUsedAt',
header: t('sessions.col_lastUsed'),
width: 160,
sortable: true,
render: (s) => (s.lastUsedAt ? <Timestamp value={s.lastUsedAt} /> : '—'),
},
{
key: 'actions',
header: '',
width: 48,
align: 'right',
render: (s) => (
<Button
variant="danger"
size="sm"
title={t('sessions.action_delete')}
onClick={(e) => {
e.stopPropagation();
setDeleteId(s.id);
}}
>
<Trash2 size={14} />
</Button>
),
},
];
return (
<div>
<div className={styles.pageToolbar}>
<Breadcrumbs items={[{ label: t('sessions.title'), icon: <Monitor size={14} /> }]} />
</div>
<Table
columns={columns}
data={items}
rowKey={(s) => s.id}
loading={loading}
emptyMessage={t('sessions.empty')}
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}`)}
/>
<DeleteConfirmationModal
open={deleteId != null}
title={t('sessions.action_delete')}
message={t('common.confirm_delete_session')}
onClose={() => setDeleteId(null)}
onConfirm={handleDelete}
isDeleting={deleting}
/>
</div>
);
}