feat(frontend): i18n simulation pages (SimulationFormPage, SimulationList)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { ChevronDown, Plus } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { extractApiError } from '@/api/client';
|
||||
import type { SimulationTemplate } from '@/api/types';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useEngagementSimulations, useCreateSimulation } from '@/hooks/useSimulations';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { formatDateTime } from '@/lib/format';
|
||||
import { LoadingState } from './LoadingState';
|
||||
import { ErrorState } from './ErrorState';
|
||||
import { EmptyState } from './EmptyState';
|
||||
@@ -16,12 +18,8 @@ interface SimulationListProps {
|
||||
engagementId: number;
|
||||
}
|
||||
|
||||
function formatDate(value: string | null): string {
|
||||
if (!value) return '—';
|
||||
return value.replace('T', ' ').slice(0, 16);
|
||||
}
|
||||
|
||||
function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { push } = useToast();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -61,10 +59,10 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
|
||||
try {
|
||||
const sim = await createMutation.mutateAsync({ name: template.name, template_id: template.id });
|
||||
setShowPicker(false);
|
||||
push(`Created "${sim.name}" from template`, 'success');
|
||||
push(t('simulation.form.toast.created'), 'success');
|
||||
navigate(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
|
||||
} catch (err) {
|
||||
push(extractApiError(err, 'Could not create simulation from template'), 'error');
|
||||
push(extractApiError(err, t('simulation.form.error.create')), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,7 +75,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
|
||||
onClick={handleBlank}
|
||||
data-testid="new-simulation-btn"
|
||||
>
|
||||
<Plus size={14} aria-hidden /> New
|
||||
<Plus size={14} aria-hidden /> {t('simulation.list.new')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -133,16 +131,17 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
|
||||
}
|
||||
|
||||
export function SimulationList({ engagementId }: SimulationListProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isError, error, refetch } = useEngagementSimulations(engagementId);
|
||||
const { canEditEngagements } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (isLoading) return <LoadingState label="Loading simulations…" />;
|
||||
if (isLoading) return <LoadingState label={t('common.loading')} />;
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<ErrorState
|
||||
message={extractApiError(error, 'Could not load simulations')}
|
||||
message={extractApiError(error, t('simulation.list.error'))}
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
);
|
||||
@@ -151,8 +150,8 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="No simulations yet"
|
||||
description="Create the first simulation to start tracking red team tests."
|
||||
title={t('simulation.list.empty.title')}
|
||||
description={t('simulation.list.empty.desc')}
|
||||
action={
|
||||
canEditEngagements ? (
|
||||
<NewSimulationDropdown engagementId={engagementId} />
|
||||
@@ -165,7 +164,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
|
||||
return (
|
||||
<div className="flex flex-col gap-md">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-[24px] font-medium text-ink">Simulations</h2>
|
||||
<h2 className="text-[24px] font-medium text-ink">{t('engagement.detail.tabs.simulations')}</h2>
|
||||
{canEditEngagements ? (
|
||||
<NewSimulationDropdown engagementId={engagementId} />
|
||||
) : null}
|
||||
@@ -175,10 +174,10 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
|
||||
<table className="table-compact w-full text-left">
|
||||
<thead className="bg-cloud border-b border-hairline">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>MITRE</th>
|
||||
<th>Status</th>
|
||||
<th>Executed at</th>
|
||||
<th>{t('simulation.list.col.name')}</th>
|
||||
<th>{t('simulation.list.col.mitre')}</th>
|
||||
<th>{t('simulation.list.col.status')}</th>
|
||||
<th>{t('simulation.list.col.executedAt')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -202,8 +201,8 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
|
||||
<td className="text-charcoal font-mono">
|
||||
{(() => {
|
||||
const items = [
|
||||
...(sim.tactics ?? []).map((t) => t.id),
|
||||
...sim.techniques.map((t) => t.id),
|
||||
...(sim.tactics ?? []).map((tactic) => tactic.id),
|
||||
...sim.techniques.map((tech) => tech.id),
|
||||
];
|
||||
if (items.length === 0) return '—';
|
||||
if (items.length === 1) return items[0];
|
||||
@@ -214,7 +213,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
|
||||
<SimulationStatusBadge status={sim.status} />
|
||||
</td>
|
||||
<td className="text-charcoal font-mono">
|
||||
{formatDate(sim.executed_at)}
|
||||
{sim.executed_at ? formatDateTime(sim.executed_at) : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, type FormEvent } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Save, RotateCcw } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { extractApiError } from '@/api/client';
|
||||
import type { SimulationPatchInput } from '@/api/types';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
@@ -58,6 +59,7 @@ const EMPTY_SOC: SocFormState = {
|
||||
};
|
||||
|
||||
export function SimulationFormPage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { eid, sid } = useParams<{ eid: string; sid: string }>();
|
||||
const engagementId = eid ? Number(eid) : undefined;
|
||||
const simulationId = sid ? Number(sid) : undefined;
|
||||
@@ -115,11 +117,11 @@ export function SimulationFormPage(): JSX.Element {
|
||||
}
|
||||
}, [isNew, detail.data]);
|
||||
|
||||
if (!isNew && detail.isLoading) return <LoadingState label="Loading simulation…" />;
|
||||
if (!isNew && detail.isLoading) return <LoadingState label={t('simulation.form.loading')} />;
|
||||
if (!isNew && detail.isError) {
|
||||
return (
|
||||
<ErrorState
|
||||
message={extractApiError(detail.error, 'Could not load simulation')}
|
||||
message={extractApiError(detail.error, t('simulation.form.error.load'))}
|
||||
onRetry={() => detail.refetch()}
|
||||
/>
|
||||
);
|
||||
@@ -148,13 +150,13 @@ export function SimulationFormPage(): JSX.Element {
|
||||
e.preventDefault();
|
||||
setNameError(null);
|
||||
setSubmitError(null);
|
||||
if (!rt.name.trim()) { setNameError('Name is required'); return; }
|
||||
if (!rt.name.trim()) { setNameError(t('simulation.form.validation.nameRequired')); return; }
|
||||
try {
|
||||
const created = await createMutation.mutateAsync({ name: rt.name.trim() });
|
||||
push('Simulation created', 'success');
|
||||
push(t('simulation.form.toast.created'), 'success');
|
||||
navigate(`/engagements/${engagementId}/simulations/${created.id}/edit`);
|
||||
} catch (err) {
|
||||
setSubmitError(extractApiError(err, 'Could not create simulation'));
|
||||
setSubmitError(extractApiError(err, t('simulation.form.error.create')));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,7 +164,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
e.preventDefault();
|
||||
setNameError(null);
|
||||
setSubmitError(null);
|
||||
if (!rt.name.trim()) { setNameError('Name is required'); return; }
|
||||
if (!rt.name.trim()) { setNameError(t('simulation.form.validation.nameRequired')); return; }
|
||||
const patch: SimulationPatchInput = {
|
||||
name: rt.name.trim(),
|
||||
description: rt.description.trim() || null,
|
||||
@@ -173,9 +175,9 @@ export function SimulationFormPage(): JSX.Element {
|
||||
};
|
||||
try {
|
||||
await updateMutation.mutateAsync(patch);
|
||||
push('Simulation updated', 'success');
|
||||
push(t('simulation.form.toast.updated'), 'success');
|
||||
} catch (err) {
|
||||
setSubmitError(extractApiError(err, 'Could not update simulation'));
|
||||
setSubmitError(extractApiError(err, t('simulation.form.error.update')));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -190,36 +192,36 @@ export function SimulationFormPage(): JSX.Element {
|
||||
};
|
||||
try {
|
||||
await updateMutation.mutateAsync(patch);
|
||||
push('SOC report updated', 'success');
|
||||
push(t('simulation.form.toast.socUpdated'), 'success');
|
||||
} catch (err) {
|
||||
setSubmitError(extractApiError(err, 'Could not update SOC fields'));
|
||||
setSubmitError(extractApiError(err, t('simulation.form.error.soc')));
|
||||
}
|
||||
};
|
||||
|
||||
const onMarkReview = async () => {
|
||||
try {
|
||||
await transitionMutation.mutateAsync('review_required');
|
||||
push('Simulation marked for review', 'success');
|
||||
push(t('simulation.form.toast.markedReview'), 'success');
|
||||
} catch (err) {
|
||||
push(extractApiError(err, 'Transition failed'), 'error');
|
||||
push(extractApiError(err, t('simulation.form.error.transition')), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = async () => {
|
||||
try {
|
||||
await transitionMutation.mutateAsync('done');
|
||||
push('Simulation closed', 'success');
|
||||
push(t('simulation.form.toast.closed'), 'success');
|
||||
} catch (err) {
|
||||
push(extractApiError(err, 'Transition failed'), 'error');
|
||||
push(extractApiError(err, t('simulation.form.error.transition')), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const onReopen = async () => {
|
||||
try {
|
||||
await transitionMutation.mutateAsync('review_required');
|
||||
push('Simulation reopened', 'success');
|
||||
push(t('simulation.form.toast.reopened'), 'success');
|
||||
} catch (err) {
|
||||
push(extractApiError(err, 'Transition failed'), 'error');
|
||||
push(extractApiError(err, t('simulation.form.error.transition')), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,10 +229,10 @@ export function SimulationFormPage(): JSX.Element {
|
||||
setShowDeleteConfirm(false);
|
||||
try {
|
||||
await deleteMutation.mutateAsync(simulationId as number);
|
||||
push('Simulation deleted', 'success');
|
||||
push(t('simulation.form.toast.deleted'), 'success');
|
||||
navigate(`/engagements/${engagementId}`);
|
||||
} catch (err) {
|
||||
push(extractApiError(err, 'Could not delete simulation'), 'error');
|
||||
push(extractApiError(err, t('simulation.form.error.delete')), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -240,12 +242,12 @@ export function SimulationFormPage(): JSX.Element {
|
||||
return (
|
||||
<div className="flex flex-col gap-xl max-w-2xl">
|
||||
<header>
|
||||
<BackLink to={`/engagements/${engagementId}`}>Back to engagement</BackLink>
|
||||
<h1 className="text-[32px] font-medium leading-none mt-sm">New simulation</h1>
|
||||
<BackLink to={`/engagements/${engagementId}`}>{t('engagement.detail.backTo')}</BackLink>
|
||||
<h1 className="text-[32px] font-medium leading-none mt-sm">{t('simulation.form.title.new')}</h1>
|
||||
</header>
|
||||
|
||||
<form onSubmit={onSubmitNew} noValidate className="card-product flex flex-col gap-md">
|
||||
<FormField label="Name" htmlFor="sim-name" required error={nameError}>
|
||||
<FormField label={t('simulation.form.field.name')} htmlFor="sim-name" required error={nameError}>
|
||||
<TextInput
|
||||
id="sim-name"
|
||||
name="name"
|
||||
@@ -260,11 +262,11 @@ export function SimulationFormPage(): JSX.Element {
|
||||
) : null}
|
||||
|
||||
<div className="flex items-center gap-md pt-sm">
|
||||
<button type="submit" className="btn-primary" disabled={submitting}>
|
||||
{submitting ? 'Creating…' : 'Create simulation'}
|
||||
<button type="submit" className="btn-primary" disabled={submitting} data-testid="create-sim-btn">
|
||||
{submitting ? t('simulation.form.btn.creating') : t('simulation.form.btn.create')}
|
||||
</button>
|
||||
<Link to={`/engagements/${engagementId}`} className="btn-outline-ink">
|
||||
Cancel
|
||||
{t('simulation.form.btn.cancel')}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
@@ -279,14 +281,15 @@ export function SimulationFormPage(): JSX.Element {
|
||||
<div className="flex flex-col gap-xl">
|
||||
<header className="flex items-start justify-between gap-md">
|
||||
<div className="flex flex-col gap-sm">
|
||||
<BackLink to={`/engagements/${engagementId}`}>Back to engagement</BackLink>
|
||||
<BackLink to={`/engagements/${engagementId}`}>{t('engagement.detail.backTo')}</BackLink>
|
||||
<h1 className="text-[32px] font-medium leading-none">{rt.name || simulation?.name}</h1>
|
||||
{status ? (
|
||||
<div className="flex items-center gap-md">
|
||||
<SimulationStatusBadge status={status} />
|
||||
{simulation?.created_by && (
|
||||
<span className="text-[14px] text-graphite">
|
||||
Created by <span className="text-ink">{simulation.created_by.username}</span>
|
||||
{t('engagement.detail.createdBy')}{' '}
|
||||
<span className="text-ink">{simulation.created_by.username}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -297,7 +300,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
{/* Done banner */}
|
||||
{isDone && (
|
||||
<AlertBanner variant="success">
|
||||
This simulation is <strong>done</strong> and read-only. Use Reopen to make changes.
|
||||
{t('simulation.form.banner.done')}
|
||||
</AlertBanner>
|
||||
)}
|
||||
|
||||
@@ -305,7 +308,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
{socBlocked && (
|
||||
<div data-testid="soc-blocked-banner">
|
||||
<AlertBanner variant="warn">
|
||||
Simulation not yet ready for review — the red team must mark it as "Review required" before you can fill in the SOC section.
|
||||
{t('simulation.form.banner.socNotReady')}
|
||||
</AlertBanner>
|
||||
</div>
|
||||
)}
|
||||
@@ -321,9 +324,9 @@ export function SimulationFormPage(): JSX.Element {
|
||||
noValidate
|
||||
className="card-product flex flex-col gap-md"
|
||||
>
|
||||
<h2 className="text-[20px] font-medium text-ink">Red Team</h2>
|
||||
<h2 className="text-[20px] font-medium text-ink">{t('simulation.form.header.redTeam')}</h2>
|
||||
|
||||
<FormField label="Name" htmlFor="sim-name" required error={nameError}>
|
||||
<FormField label={t('simulation.form.field.name')} htmlFor="sim-name" required error={nameError}>
|
||||
<TextInput
|
||||
id="sim-name"
|
||||
name="name"
|
||||
@@ -335,7 +338,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
</FormField>
|
||||
|
||||
<div className="flex flex-col gap-xs">
|
||||
<span className="text-[14px] font-medium text-ink">MITRE Techniques & Tactics</span>
|
||||
<span className="text-[14px] font-medium text-ink">{t('simulation.form.field.mitre')}</span>
|
||||
<MitreTechniquesField
|
||||
value={simulation?.techniques ?? []}
|
||||
tactics={simulation?.tactics ?? []}
|
||||
@@ -345,7 +348,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField label="Description" htmlFor="sim-description">
|
||||
<FormField label={t('simulation.form.field.description')} htmlFor="sim-description">
|
||||
<TextArea
|
||||
id="sim-description"
|
||||
name="description"
|
||||
@@ -355,7 +358,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Commands" htmlFor="sim-commands" hint="One command per line">
|
||||
<FormField label={t('simulation.form.field.commands')} htmlFor="sim-commands" hint={t('simulation.form.field.commandsHint')}>
|
||||
<TextArea
|
||||
id="sim-commands"
|
||||
name="commands"
|
||||
@@ -366,7 +369,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Prerequisites" htmlFor="sim-prerequisites">
|
||||
<FormField label={t('simulation.form.field.prerequisites')} htmlFor="sim-prerequisites">
|
||||
<TextArea
|
||||
id="sim-prerequisites"
|
||||
name="prerequisites"
|
||||
@@ -376,7 +379,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Executed at" htmlFor="sim-executed-at">
|
||||
<FormField label={t('simulation.form.field.executedAt')} htmlFor="sim-executed-at">
|
||||
<TextInput
|
||||
id="sim-executed-at"
|
||||
type="datetime-local"
|
||||
@@ -387,7 +390,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Execution result" htmlFor="sim-exec-result">
|
||||
<FormField label={t('simulation.form.field.executionResult')} htmlFor="sim-exec-result">
|
||||
<TextArea
|
||||
id="sim-exec-result"
|
||||
name="execution_result"
|
||||
@@ -406,7 +409,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
className="btn-outline"
|
||||
onClick={() => setShowC2Modal(true)}
|
||||
>
|
||||
Execute via C2
|
||||
{t('simulation.form.btn.executeC2')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -414,7 +417,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
className="btn-outline"
|
||||
onClick={() => setShowImportModal(true)}
|
||||
>
|
||||
Import C2 history
|
||||
{t('simulation.form.btn.importC2History')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -433,9 +436,9 @@ export function SimulationFormPage(): JSX.Element {
|
||||
noValidate
|
||||
className="card-product flex flex-col gap-md"
|
||||
>
|
||||
<h2 className="text-[20px] font-medium text-ink">SOC</h2>
|
||||
<h2 className="text-[20px] font-medium text-ink">{t('simulation.form.header.soc')}</h2>
|
||||
|
||||
<FormField label="Log source" htmlFor="sim-log-source">
|
||||
<FormField label={t('simulation.form.field.logSource')} htmlFor="sim-log-source">
|
||||
<TextInput
|
||||
id="sim-log-source"
|
||||
name="log_source"
|
||||
@@ -445,7 +448,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Logs" htmlFor="sim-logs">
|
||||
<FormField label={t('simulation.form.field.logs')} htmlFor="sim-logs">
|
||||
<TextArea
|
||||
id="sim-logs"
|
||||
name="logs"
|
||||
@@ -455,7 +458,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="SOC comment" htmlFor="sim-soc-comment">
|
||||
<FormField label={t('simulation.form.field.socComment')} htmlFor="sim-soc-comment">
|
||||
<TextArea
|
||||
id="sim-soc-comment"
|
||||
name="soc_comment"
|
||||
@@ -465,7 +468,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Incident number" htmlFor="sim-incident">
|
||||
<FormField label={t('simulation.form.field.incidentNumber')} htmlFor="sim-incident">
|
||||
<TextInput
|
||||
id="sim-incident"
|
||||
name="incident_number"
|
||||
@@ -493,7 +496,7 @@ export function SimulationFormPage(): JSX.Element {
|
||||
data-testid="reopen-btn"
|
||||
>
|
||||
<RotateCcw size={14} aria-hidden />
|
||||
Reopen
|
||||
{t('simulation.form.btn.reopen')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -501,13 +504,13 @@ export function SimulationFormPage(): JSX.Element {
|
||||
{!isDone && canEditRT && (
|
||||
<button type="submit" form="rt-form" className="btn-primary" disabled={submitting}>
|
||||
<Save size={14} aria-hidden />
|
||||
{updateMutation.isPending ? 'Saving…' : 'Save'}
|
||||
{updateMutation.isPending ? t('simulation.form.btn.saving') : t('simulation.form.btn.save')}
|
||||
</button>
|
||||
)}
|
||||
{!isDone && canSaveSoc && (
|
||||
<button type="submit" form="soc-form" className="btn-primary" disabled={submitting}>
|
||||
<Save size={14} aria-hidden />
|
||||
{updateMutation.isPending ? 'Saving…' : 'Save SOC'}
|
||||
{updateMutation.isPending ? t('simulation.form.btn.saving') : t('simulation.form.btn.saveSoc')}
|
||||
</button>
|
||||
)}
|
||||
{showMarkReview && (
|
||||
@@ -516,8 +519,9 @@ export function SimulationFormPage(): JSX.Element {
|
||||
className="btn-outline"
|
||||
onClick={onMarkReview}
|
||||
disabled={transitionMutation.isPending}
|
||||
data-testid="mark-review-btn"
|
||||
>
|
||||
Mark for review
|
||||
{t('simulation.form.btn.markReview')}
|
||||
</button>
|
||||
)}
|
||||
{showClose && (
|
||||
@@ -526,8 +530,9 @@ export function SimulationFormPage(): JSX.Element {
|
||||
className="btn-outline"
|
||||
onClick={onClose}
|
||||
disabled={transitionMutation.isPending}
|
||||
data-testid="close-btn"
|
||||
>
|
||||
Close
|
||||
{t('simulation.form.btn.close')}
|
||||
</button>
|
||||
)}
|
||||
{!isDone && canEditEngagements && simulationId && (
|
||||
@@ -536,18 +541,19 @@ export function SimulationFormPage(): JSX.Element {
|
||||
className="btn-text-link text-bloom-deep ml-auto"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={submitting}
|
||||
data-testid="delete-btn"
|
||||
>
|
||||
Delete
|
||||
{t('simulation.form.btn.delete')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDeleteConfirm && (
|
||||
<ConfirmDialog
|
||||
title="Delete simulation"
|
||||
description="This action is permanent. The simulation will be deleted forever."
|
||||
confirmLabel="Delete"
|
||||
cancelLabel="Cancel"
|
||||
title={t('simulation.form.deleteConfirm.title')}
|
||||
description={t('simulation.form.deleteConfirm.desc')}
|
||||
confirmLabel={t('simulation.form.deleteConfirm.confirm')}
|
||||
cancelLabel={t('simulation.form.deleteConfirm.cancel')}
|
||||
destructive
|
||||
onConfirm={onDelete}
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
|
||||
Reference in New Issue
Block a user