import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Play, Pencil, Plus, History, Trash2, Upload } from 'lucide-react'; import { stringify as yamlStringify } from 'yaml'; import { scenarios, steps, scenarioCredentials, credentials as credentialsApi } from '../../api'; import type { Scenario, ScenarioStep, ScenarioCredential, Credential } from '../../api'; import { Badge, Breadcrumbs, Button, Card, DescriptionList, Input, Notification, Select, Table, Timestamp, UuidBadge, type TableColumn, } from '../../ui'; import styles from '../Page.module.css'; const STEP_TYPE_VARIANT: Record = { login: 'success', exec: 'info', sign: 'warning', }; export function ScenarioDetailPage() { const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [scenario, setScenario] = useState<(Scenario & { steps: ScenarioStep[] }) | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Credentials state const [scenarioCreds, setScenarioCreds] = useState([]); const [allCredentials, setAllCredentials] = useState([]); const [addCredOpen, setAddCredOpen] = useState(false); const [addCredId, setAddCredId] = useState(''); const [addAlias, setAddAlias] = useState(''); const [addCredError, setAddCredError] = useState(''); const [addAliasError, setAddAliasError] = useState(''); const [addCredSaving, setAddCredSaving] = useState(false); useEffect(() => { if (!id) return; scenarios .get(id) .then((s) => { setScenario(s); setScenarioCreds(s.scenarioCredentials ?? []); }) .catch((err: Error) => setError(err.message)) .finally(() => setLoading(false)); credentialsApi .list(1, 200) .then((r) => setAllCredentials(r.data)) .catch(() => {}); }, [id]); const handleRun = async () => { if (!scenario) return; const run = await scenarios.run(scenario.id); navigate(`/scenarios/${id}/runs/${run.id}`); }; const handleDelete = async () => { if (!scenario) return; await scenarios.remove(scenario.id); navigate('/scenarios'); }; const handleExport = async () => { if (!scenario) return; const data = await scenarios.exportScenario(scenario.id); const blob = new Blob([yamlStringify(data)], { type: 'application/yaml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${scenario.name}.yaml`; a.click(); URL.revokeObjectURL(url); }; const handleDeleteStep = async (stepId: string) => { await steps.remove(id!, stepId); setScenario((prev) => prev ? { ...prev, steps: prev.steps.filter((s) => s.id !== stepId) } : prev, ); }; const handleAddCredential = async (e: React.FormEvent) => { e.preventDefault(); let valid = true; if (!addCredId) { setAddCredError(t('scenarios.cred_form_cred_required')); valid = false; } if (!addAlias.trim()) { setAddAliasError(t('scenarios.cred_form_alias_required')); valid = false; } if (!valid) return; setAddCredSaving(true); try { const sc = await scenarioCredentials.add(id!, addCredId, addAlias.trim()); // Re-fetch the credential object since the response may not include it const full = await scenarioCredentials.list(id!); setScenarioCreds(full); void sc; setAddCredOpen(false); setAddCredId(''); setAddAlias(''); } catch (err) { setAddCredError((err as Error).message); } finally { setAddCredSaving(false); } }; const handleRemoveCredential = async (scCredId: string) => { await scenarioCredentials.remove(id!, scCredId); setScenarioCreds((prev) => prev.filter((sc) => sc.id !== scCredId)); }; const stepColumns: TableColumn[] = [ { key: 'order', header: t('scenarios.step_order'), render: (s) => s.order, width: 60 }, { key: 'type', header: t('scenarios.step_type'), width: 90, render: (s) => {s.type}, }, { key: 'title', header: t('scenarios.step_title'), render: (s) => s.title ?? {'—'}, }, { key: 'updated', header: t('scenarios.step_updated'), width: 140, render: (s) => , }, { key: 'actions', header: '', width: 80, align: 'right', render: (s) => ( ), }, ]; const credColumns: TableColumn[] = [ { key: 'alias', header: t('scenarios.cred_col_alias'), render: (sc) => sc.alias }, { key: 'name', header: t('scenarios.cred_col_name'), render: (sc) => sc.credential?.name ?? `#${sc.credentialId}`, }, { key: 'added', header: t('scenarios.cred_col_added'), width: 140, render: (sc) => , }, { key: 'actions', header: '', width: 60, align: 'right', render: (sc) => ( ), }, ]; return (
navigate('/scenarios') }, { label: scenario?.name ?? `#${id}` }, ]} /> {scenario && (
)}
{error && ( {error.startsWith('404') ? t('errors.not_found_scenario', { id }) : error} )} {loading &&

{t('scenarios.loading')}

} {scenario && ( <> }, { term: t('scenarios.field_name'), detail: scenario.name }, { term: t('scenarios.field_steps'), detail: scenario.steps.length }, { term: t('scenarios.field_created'), detail: , }, { term: t('scenarios.field_updated'), detail: , }, ]} /> {scenario.steps.length > 0 && (

{t('scenarios.steps_heading')}

s.id} loading={false} emptyMessage="" /> )} {scenario.steps.length === 0 && (

{t('scenarios.steps_heading')}

)} {/* ── Credentials section ──────────────────────────────────── */}

{t('scenarios.credentials_heading')}

{addCredOpen && (
{ setAddAlias(e.target.value); setAddAliasError(''); }} error={addAliasError || undefined} />
)} {scenarioCreds.length > 0 ? (
sc.id} loading={false} emptyMessage="" /> ) : (

{t('scenarios.cred_empty')}

)} )} ); }