Files
mimic/frontend/src/pages/EngagementDetailPage.tsx
Knacky 100441bdeb feat: ExportEngagementButton + exports API client
Add split-button dropdown [Export ▼] on EngagementDetailPage that
downloads engagement as Markdown, CSV, or PDF via
GET /api/engagements/<id>/export?format=md|csv|pdf.

Both halves open the dropdown (no default left-click action).
RBAC-gated with canEditEngagements (admin + redteam only).
Loading state per item, toast on error, click-outside + Escape close.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:04:49 +02:00

85 lines
3.2 KiB
TypeScript

import { Link, useParams } from 'react-router-dom';
import { extractApiError } from '@/api/client';
import { useAuth } from '@/hooks/useAuth';
import { useEngagement } from '@/hooks/useEngagements';
import { LoadingState } from '@/components/LoadingState';
import { ErrorState } from '@/components/ErrorState';
import { StatusBadge } from '@/components/StatusBadge';
import { SimulationList } from '@/components/SimulationList';
import { ExportEngagementButton } from '@/components/ExportEngagementButton';
export function EngagementDetailPage(): JSX.Element {
const { id } = useParams<{ id: string }>();
const numericId = id ? Number(id) : undefined;
const { canEditEngagements } = useAuth();
const detail = useEngagement(numericId);
if (detail.isLoading) return <LoadingState label="Loading engagement…" />;
if (detail.isError) {
return (
<ErrorState
message={extractApiError(detail.error, 'Could not load engagement')}
onRetry={() => detail.refetch()}
/>
);
}
if (!detail.data) return <ErrorState message="Engagement not found" />;
const eng = detail.data;
return (
<div className="flex flex-col gap-xl">
<header className="flex items-start justify-between gap-md">
<div className="flex flex-col gap-sm">
<Link to="/engagements" className="btn-text-link text-[14px]">
Back to engagements
</Link>
<h1 className="text-[44px] font-medium leading-none">{eng.name}</h1>
<div className="flex items-center gap-md">
<StatusBadge status={eng.status} />
<span className="text-[14px] text-graphite">
Created by <span className="text-ink">{eng.created_by.username}</span>
</span>
</div>
</div>
{canEditEngagements ? (
<div className="flex items-center gap-sm">
<ExportEngagementButton engagementId={eng.id} />
<Link to={`/engagements/${eng.id}/edit`} className="btn-outline">
Edit
</Link>
</div>
) : null}
</header>
<section className="grid grid-cols-1 md:grid-cols-2 gap-md">
<div className="card-product">
<h2 className="text-[20px] font-medium mb-md">Schedule</h2>
<dl className="grid grid-cols-2 gap-md text-[14px]">
<dt className="text-graphite">Start date</dt>
<dd className="text-ink">{eng.start_date}</dd>
<dt className="text-graphite">End date</dt>
<dd className="text-ink">{eng.end_date ?? '—'}</dd>
<dt className="text-graphite">Status</dt>
<dd className="text-ink capitalize">{eng.status}</dd>
<dt className="text-graphite">Created at</dt>
<dd className="text-ink">{eng.created_at}</dd>
</dl>
</div>
<div className="card-product">
<h2 className="text-[20px] font-medium mb-md">Description</h2>
<p className="text-[16px] text-charcoal whitespace-pre-line">
{eng.description?.trim() ? eng.description : 'No description provided.'}
</p>
</div>
</section>
<section>
<SimulationList engagementId={eng.id} />
</section>
</div>
);
}