- 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
147 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
}
|