feat(frontend): add BackLink helper + dedup 3 hand-rolled instances

BackLink: ArrowLeft (14px) + caption-md text-graphite hover:text-primary,
instant. Replaces inline Link patterns in EngagementDetailPage (already
committed), SimulationFormPage, and TemplateFormPage. Also migrates
SimulationFormPage Done/SOC banners to AlertBanner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-21 22:11:17 +02:00
parent 59eaa342a9
commit 9a8c5f52ab
3 changed files with 29 additions and 20 deletions

View File

@@ -0,0 +1,17 @@
import { Link } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
import type { ReactNode } from 'react';
interface BackLinkProps {
to: string;
children: ReactNode;
}
export function BackLink({ to, children }: BackLinkProps): JSX.Element {
return (
<Link to={to} className="inline-flex items-center gap-xxs caption-md text-graphite hover:text-primary">
<ArrowLeft size={14} aria-hidden />
{children}
</Link>
);
}

View File

@@ -22,6 +22,8 @@ import { MitreTechniquesField } from '@/components/MitreTechniquesField';
import { ExecuteViaC2Modal } from '@/components/ExecuteViaC2Modal'; import { ExecuteViaC2Modal } from '@/components/ExecuteViaC2Modal';
import { ImportC2HistoryModal } from '@/components/ImportC2HistoryModal'; import { ImportC2HistoryModal } from '@/components/ImportC2HistoryModal';
import { C2TasksPanel } from '@/components/C2TasksPanel'; import { C2TasksPanel } from '@/components/C2TasksPanel';
import { AlertBanner } from '@/components/AlertBanner';
import { BackLink } from '@/components/BackLink';
interface RedteamFormState { interface RedteamFormState {
name: string; name: string;
@@ -238,9 +240,7 @@ export function SimulationFormPage(): JSX.Element {
return ( return (
<div className="flex flex-col gap-xl max-w-2xl"> <div className="flex flex-col gap-xl max-w-2xl">
<header> <header>
<Link to={`/engagements/${engagementId}`} className="btn-text-link text-[14px]"> <BackLink to={`/engagements/${engagementId}`}>Back to engagement</BackLink>
Back to engagement
</Link>
<h1 className="text-[32px] font-medium leading-none mt-sm">New simulation</h1> <h1 className="text-[32px] font-medium leading-none mt-sm">New simulation</h1>
</header> </header>
@@ -279,9 +279,7 @@ export function SimulationFormPage(): JSX.Element {
<div className="flex flex-col gap-xl"> <div className="flex flex-col gap-xl">
<header className="flex items-start justify-between gap-md"> <header className="flex items-start justify-between gap-md">
<div className="flex flex-col gap-sm"> <div className="flex flex-col gap-sm">
<Link to={`/engagements/${engagementId}`} className="btn-text-link text-[14px]"> <BackLink to={`/engagements/${engagementId}`}>Back to engagement</BackLink>
Back to engagement
</Link>
<h1 className="text-[32px] font-medium leading-none">{rt.name || simulation?.name}</h1> <h1 className="text-[32px] font-medium leading-none">{rt.name || simulation?.name}</h1>
{status ? ( {status ? (
<div className="flex items-center gap-md"> <div className="flex items-center gap-md">
@@ -298,22 +296,17 @@ export function SimulationFormPage(): JSX.Element {
{/* Done banner */} {/* Done banner */}
{isDone && ( {isDone && (
<div <AlertBanner variant="success">
role="status"
className="rounded-none px-xl py-md bg-cloud border border-hairline text-[14px] text-charcoal"
>
This simulation is <strong>done</strong> and read-only. Use Reopen to make changes. This simulation is <strong>done</strong> and read-only. Use Reopen to make changes.
</div> </AlertBanner>
)} )}
{/* SOC banner */} {/* SOC banner */}
{socBlocked && ( {socBlocked && (
<div <div data-testid="soc-blocked-banner">
role="alert" <AlertBanner variant="warn">
data-testid="soc-blocked-banner" Simulation not yet ready for review the red team must mark it as &quot;Review required&quot; before you can fill in the SOC section.
className="rounded-none px-xl py-md bg-fog border border-hairline text-[14px] text-charcoal" </AlertBanner>
>
Simulation not yet ready for review the red team must mark it as &quot;Review required&quot; before you can fill in the SOC section.
</div> </div>
)} )}

View File

@@ -9,6 +9,7 @@ import { FormField, TextArea, TextInput } from '@/components/FormField';
import { LoadingState } from '@/components/LoadingState'; import { LoadingState } from '@/components/LoadingState';
import { ErrorState } from '@/components/ErrorState'; import { ErrorState } from '@/components/ErrorState';
import { ConfirmDialog } from '@/components/ConfirmDialog'; import { ConfirmDialog } from '@/components/ConfirmDialog';
import { BackLink } from '@/components/BackLink';
import { MitreTechniqueTag, MitreTacticTag } from '@/components/MitreTechniqueTag'; import { MitreTechniqueTag, MitreTacticTag } from '@/components/MitreTechniqueTag';
import { MitreTechniquePicker } from '@/components/MitreTechniquePicker'; import { MitreTechniquePicker } from '@/components/MitreTechniquePicker';
import { MitreMatrixModal } from '@/components/MitreMatrixModal'; import { MitreMatrixModal } from '@/components/MitreMatrixModal';
@@ -127,9 +128,7 @@ export function TemplateFormPage(): JSX.Element {
<div className="flex flex-col gap-xl"> <div className="flex flex-col gap-xl">
<header className="flex items-start justify-between gap-md"> <header className="flex items-start justify-between gap-md">
<div className="flex flex-col gap-sm"> <div className="flex flex-col gap-sm">
<Link to="/admin/templates" className="btn-text-link text-[14px]"> <BackLink to="/admin/templates">Back to templates</BackLink>
Back to templates
</Link>
<h1 className="text-[32px] font-medium leading-none"> <h1 className="text-[32px] font-medium leading-none">
{isNew ? 'New template' : (existing.data?.name ?? 'Edit template')} {isNew ? 'New template' : (existing.data?.name ?? 'Edit template')}
</h1> </h1>