234 lines
7.0 KiB
TypeScript
234 lines
7.0 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Play, Plus, Trash2, Download, ClipboardList } from 'lucide-react';
|
|
import { parse as yamlParse } from 'yaml';
|
|
import { environments, scenarios } from '../../api';
|
|
import type { Environment, Scenario } from '../../api';
|
|
import {
|
|
Breadcrumbs,
|
|
Button,
|
|
Modal,
|
|
Select,
|
|
Table,
|
|
type TableColumn,
|
|
Timestamp,
|
|
UuidBadge,
|
|
useToast,
|
|
} from '../../ui';
|
|
import styles from '../Page.module.css';
|
|
|
|
export function ScenariosPage() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const toast = useToast();
|
|
const [items, setItems] = useState<Scenario[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
|
const [deleting, setDeleting] = useState(false);
|
|
const [runScenarioId, setRunScenarioId] = useState<string | null>(null);
|
|
const [running, setRunning] = useState(false);
|
|
const [saveSessionFlag, setSaveSessionFlag] = useState(false);
|
|
const [envs, setEnvs] = useState<Environment[]>([]);
|
|
const [selectedEnvId, setSelectedEnvId] = useState('');
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const load = useCallback(() => {
|
|
Promise.all([scenarios.list(), environments.list(1, 200)])
|
|
.then(([scenarioRes, envRes]) => {
|
|
setItems(scenarioRes.data);
|
|
setEnvs(envRes.data);
|
|
})
|
|
.catch((err: Error) => setError(err.message))
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
const openRunModal = (id: string) => {
|
|
setRunScenarioId(id);
|
|
if (!selectedEnvId && envs.length > 0) {
|
|
setSelectedEnvId(envs[0].id);
|
|
}
|
|
};
|
|
|
|
const handleRun = async () => {
|
|
if (!runScenarioId || !selectedEnvId) return;
|
|
setRunning(true);
|
|
try {
|
|
const run = await scenarios.run(runScenarioId, selectedEnvId, saveSessionFlag);
|
|
toast.success('Scenario run started');
|
|
navigate(`/scenarios/${runScenarioId}/runs/${run.id}`);
|
|
setRunScenarioId(null);
|
|
setSaveSessionFlag(false);
|
|
} catch (err) {
|
|
toast.error((err as Error).message);
|
|
} finally {
|
|
setRunning(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!deleteId) return;
|
|
setDeleting(true);
|
|
try {
|
|
await scenarios.remove(deleteId);
|
|
setLoading(true);
|
|
load();
|
|
setDeleteId(null);
|
|
} finally {
|
|
setDeleting(false);
|
|
}
|
|
};
|
|
|
|
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
e.target.value = '';
|
|
try {
|
|
const text = await file.text();
|
|
const payload = yamlParse(text) as unknown;
|
|
const imported = await scenarios.importScenario(payload);
|
|
navigate(`/scenarios/${imported.id}`);
|
|
} catch (err) {
|
|
setError((err as Error).message);
|
|
}
|
|
};
|
|
|
|
const columns: TableColumn<Scenario>[] = [
|
|
{ key: 'id', header: t('scenarios.col_id'), render: (s) => <UuidBadge id={s.id} />, width: 60 },
|
|
{ key: 'name', header: t('scenarios.col_name'), render: (s) => s.name },
|
|
{
|
|
key: 'updated',
|
|
header: t('scenarios.col_updated'),
|
|
width: 140,
|
|
render: (s) => <Timestamp value={s.updatedAt} />,
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: '',
|
|
width: 100,
|
|
align: 'right',
|
|
render: (s) => (
|
|
<span style={{ display: 'inline-flex', gap: 6 }}>
|
|
<Button
|
|
size="sm"
|
|
title={t('scenarios.action_run')}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
openRunModal(s.id);
|
|
}}
|
|
>
|
|
<Play size={14} />
|
|
</Button>
|
|
<Button
|
|
variant="danger"
|
|
size="sm"
|
|
title={t('scenarios.action_delete')}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setDeleteId(s.id);
|
|
}}
|
|
>
|
|
<Trash2 size={14} />
|
|
</Button>
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<div className={styles.pageToolbar}>
|
|
<Breadcrumbs items={[{ label: t('scenarios.title'), icon: <ClipboardList size={14} /> }]} />
|
|
<div className={styles.toolbarActions}>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept=".yaml,.yml,.json"
|
|
style={{ display: 'none' }}
|
|
onChange={handleImport}
|
|
/>
|
|
<Button variant="secondary" size="sm" onClick={() => fileInputRef.current?.click()}>
|
|
<Download size={14} />
|
|
{t('scenarios.action_import')}
|
|
</Button>
|
|
<Button size="sm" onClick={() => navigate('/scenarios/new')}>
|
|
<Plus size={14} />
|
|
{t('scenarios.action_add')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<Table
|
|
columns={columns}
|
|
data={items}
|
|
rowKey={(s) => s.id}
|
|
loading={loading}
|
|
emptyMessage={t('scenarios.empty')}
|
|
pageSize={10}
|
|
pageSizeOptions={[10, 25, 50]}
|
|
onRowClick={(s) => navigate(`/scenarios/${s.id}`)}
|
|
/>
|
|
|
|
<Modal
|
|
open={deleteId != null}
|
|
title={t('scenarios.action_delete')}
|
|
onClose={() => !deleting && setDeleteId(null)}
|
|
footer={(
|
|
<>
|
|
<Button variant="secondary" onClick={() => setDeleteId(null)} disabled={deleting}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="danger" onClick={handleDelete} disabled={deleting}>
|
|
{t('scenarios.action_delete')}
|
|
</Button>
|
|
</>
|
|
)}
|
|
>
|
|
Are you sure you want to delete this scenario?
|
|
</Modal>
|
|
|
|
<Modal
|
|
open={runScenarioId != null}
|
|
title={t('scenarios.run_modal_title')}
|
|
onClose={() => !running && setRunScenarioId(null)}
|
|
footer={(
|
|
<>
|
|
<Button variant="secondary" onClick={() => setRunScenarioId(null)} disabled={running}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="primary" onClick={handleRun} disabled={running || !selectedEnvId}>
|
|
{t('scenarios.action_run')}
|
|
</Button>
|
|
</>
|
|
)}
|
|
>
|
|
{envs.length === 0 ? (
|
|
<p>{t('scenarios.run_modal_no_env')}</p>
|
|
) : (
|
|
<>
|
|
<Select
|
|
label={t('scenarios.run_modal_env_label')}
|
|
value={selectedEnvId}
|
|
onChange={(e) => setSelectedEnvId(e.target.value)}
|
|
options={envs.map((env) => ({ value: env.id, label: env.name }))}
|
|
/>
|
|
<label style={{ display: 'flex', alignItems: 'center', marginTop: '1rem', gap: '0.5rem' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={saveSessionFlag}
|
|
onChange={(e) => setSaveSessionFlag(e.target.checked)}
|
|
/>
|
|
<span>Save session</span>
|
|
</label>
|
|
</>
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|