- Add ScenarioDetailPage with DescriptionList, steps table, run/delete actions - Add /scenarios/:id route - Add i18n keys for scenario detail fields, steps columns, and 404 error - Add 'comfortable' and 'inline' layout variants to DescriptionList (with compact as default stacked) - Apply layout="comfortable" to session, environment, and scenario detail pages - Apply layout="compact" to EnvironmentsPage card DescriptionList - Refactor EnvironmentsPage card to use ContextMenu (view/edit/delete) with icons - Add ScenariosPage clickable rows, Play icon (primary) and Trash2 icon (danger) action buttons - Add Page.module.css sectionHeading and stepsSection utility classes - Fix Notification.tsx and Table.tsx formatting (prettier)
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Trash2 } from 'lucide-react';
|
|
import { sessions } from '../api';
|
|
import type { Session } from '../api';
|
|
import { Badge, Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp } from '../ui';
|
|
import styles from './Page.module.css';
|
|
|
|
export function SessionDetailPage() {
|
|
const { t } = useTranslation();
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const [session, setSession] = useState<Session | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
sessions
|
|
.get(Number(id))
|
|
.then(setSession)
|
|
.catch((err: Error) => setError(err.message))
|
|
.finally(() => setLoading(false));
|
|
}, [id]);
|
|
|
|
const handleDelete = async () => {
|
|
if (!session) return;
|
|
await sessions.remove(session.id);
|
|
navigate('/sessions');
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className={styles.pageToolbar}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: t('sessions.title'), onClick: () => navigate('/sessions') },
|
|
{ label: session?.sessionName ?? `#${id}` },
|
|
]}
|
|
/>
|
|
{session && (
|
|
<Button variant="danger" size="sm" onClick={handleDelete}>
|
|
<Trash2 size={14} />
|
|
{t('sessions.action_delete')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<Notification
|
|
variant="error"
|
|
title={error.startsWith('404') ? t('errors.not_found') : undefined}
|
|
>
|
|
{error.startsWith('404') ? t('errors.not_found_session', { id }) : error}
|
|
</Notification>
|
|
)}
|
|
{loading && <p className={styles.muted}>{t('sessions.loading')}</p>}
|
|
|
|
{session && (
|
|
<Card>
|
|
<DescriptionList
|
|
layout="comfortable"
|
|
items={[
|
|
{ term: t('sessions.field_id'), detail: session.id },
|
|
{ term: t('sessions.field_name'), detail: session.sessionName },
|
|
{
|
|
term: t('sessions.field_status'),
|
|
detail: (
|
|
<Badge variant={session.status === 'open' ? 'success' : 'error'}>
|
|
{session.status}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
term: t('sessions.field_token'),
|
|
detail: <code className={styles.codeInline}>{session.token}</code>,
|
|
},
|
|
{
|
|
term: t('sessions.field_lastUsed'),
|
|
detail: session.lastUsedAt ? <Timestamp value={session.lastUsedAt} /> : '—',
|
|
},
|
|
{
|
|
term: t('sessions.field_created'),
|
|
detail: <Timestamp value={session.createdAt} />,
|
|
},
|
|
{
|
|
term: t('sessions.field_updated'),
|
|
detail: <Timestamp value={session.updatedAt} />,
|
|
},
|
|
]}
|
|
/>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|