diff --git a/frontend/src/pages/EngagementDetailPage.tsx b/frontend/src/pages/EngagementDetailPage.tsx index f0ff78e..d8216d4 100644 --- a/frontend/src/pages/EngagementDetailPage.tsx +++ b/frontend/src/pages/EngagementDetailPage.tsx @@ -1,4 +1,5 @@ import { useParams, Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { extractApiError } from '@/api/client'; import { useAuth } from '@/hooks/useAuth'; import { useEngagement } from '@/hooks/useEngagements'; @@ -11,10 +12,12 @@ import { SimulationList } from '@/components/SimulationList'; import { ExportEngagementButton } from '@/components/ExportEngagementButton'; import { BackLink } from '@/components/BackLink'; import { Tabs } from '@/components/Tabs'; +import { formatDateTime } from '@/lib/format'; type TabId = 'description' | 'simulations'; export function EngagementDetailPage(): JSX.Element { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const numericId = id ? Number(id) : undefined; const { canEditEngagements } = useAuth(); @@ -25,35 +28,36 @@ export function EngagementDetailPage(): JSX.Element { const [activeTabRaw, setActiveTab] = useHashTab('description'); const activeTab = activeTabRaw as TabId; - if (detail.isLoading) return ; + if (detail.isLoading) return ; if (detail.isError) { return ( detail.refetch()} /> ); } - if (!detail.data) return ; + if (!detail.data) return ; const eng = detail.data; const simCount = simsQuery.data?.length; const tabs = [ - { id: 'description', label: 'Description' }, - { id: 'simulations', label: 'Simulations', count: simCount }, + { id: 'description', label: t('engagement.detail.tabs.description') }, + { id: 'simulations', label: t('engagement.detail.tabs.simulations'), count: simCount }, ]; return (
- Back to engagements + {t('engagement.detail.backTo')}

{eng.name}

- Created by {eng.created_by.username} + {t('engagement.detail.createdBy')}{' '} + {eng.created_by.username}
@@ -73,25 +77,25 @@ export function EngagementDetailPage(): JSX.Element {
-
Start date
+
{t('engagement.detail.schedule.startDate')}
{eng.start_date}
-
End date
+
{t('engagement.detail.schedule.endDate')}
{eng.end_date ?? '—'}
-
Status
+
{t('engagement.detail.schedule.status')}
-
Created at
-
{eng.created_at}
+
{t('engagement.detail.schedule.createdAt')}
+
{formatDateTime(eng.created_at)}
{canEditEngagements ? ( - Edit + {t('engagement.detail.edit')} ) : null}

-

Description

+

{t('engagement.detail.tabs.description')}

- {eng.description?.trim() ? eng.description : 'No description provided.'} + {eng.description?.trim() ? eng.description : t('engagement.detail.noDescription')}

)} diff --git a/frontend/src/pages/EngagementFormPage.tsx b/frontend/src/pages/EngagementFormPage.tsx index 3047437..b99c340 100644 --- a/frontend/src/pages/EngagementFormPage.tsx +++ b/frontend/src/pages/EngagementFormPage.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, type FormEvent } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { extractApiError } from '@/api/client'; import type { EngagementInput, EngagementStatus } from '@/api/types'; import { @@ -13,12 +14,7 @@ import { FormField, Select, TextArea, TextInput } from '@/components/FormField'; import { LoadingState } from '@/components/LoadingState'; import { ErrorState } from '@/components/ErrorState'; import { C2ConfigCard } from '@/components/C2ConfigCard'; - -const STATUS_OPTIONS: { value: EngagementStatus; label: string }[] = [ - { value: 'planned', label: 'Planned' }, - { value: 'active', label: 'Active' }, - { value: 'closed', label: 'Closed' }, -]; +import { engagementStatusLabel } from '@/i18n/status'; interface FormState { name: string; @@ -36,17 +32,8 @@ const EMPTY: FormState = { status: 'planned', }; -function validate(state: FormState): Partial> { - const errors: Partial> = {}; - if (!state.name.trim()) errors.name = 'Name is required'; - if (!state.start_date) errors.start_date = 'Start date is required'; - if (state.end_date && state.start_date && state.end_date < state.start_date) { - errors.end_date = 'End date must be on or after start date'; - } - return errors; -} - export function EngagementFormPage(): JSX.Element { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const editing = Boolean(id); const numericId = id ? Number(id) : undefined; @@ -62,6 +49,22 @@ export function EngagementFormPage(): JSX.Element { const [errors, setErrors] = useState>>({}); const [submitError, setSubmitError] = useState(null); + const STATUS_OPTIONS: { value: EngagementStatus; label: string }[] = [ + { value: 'planned', label: engagementStatusLabel('planned') }, + { value: 'active', label: engagementStatusLabel('active') }, + { value: 'closed', label: engagementStatusLabel('closed') }, + ]; + + function validate(state: FormState): Partial> { + const errs: Partial> = {}; + if (!state.name.trim()) errs.name = t('engagement.form.validation.nameRequired'); + if (!state.start_date) errs.start_date = t('engagement.form.validation.startDateRequired'); + if (state.end_date && state.start_date && state.end_date < state.start_date) { + errs.end_date = t('engagement.form.validation.endDateAfterStart'); + } + return errs; + } + // Hydrate edit form when data arrives. useEffect(() => { if (editing && detail.data) { @@ -75,11 +78,11 @@ export function EngagementFormPage(): JSX.Element { } }, [editing, detail.data]); - if (editing && detail.isLoading) return ; + if (editing && detail.isLoading) return ; if (editing && detail.isError) { return ( detail.refetch()} /> ); @@ -109,15 +112,15 @@ export function EngagementFormPage(): JSX.Element { try { if (editing && numericId) { await patchMutation.mutateAsync(payload); - push('Engagement updated', 'success'); + push(t('engagement.form.toast.updated'), 'success'); navigate(`/engagements/${numericId}`); } else { const created = await createMutation.mutateAsync(payload); - push('Engagement created', 'success'); + push(t('engagement.form.toast.created'), 'success'); navigate(`/engagements/${created.id}`); } } catch (err) { - setSubmitError(extractApiError(err, 'Could not save engagement')); + setSubmitError(extractApiError(err, t('engagement.form.error.save'))); } }; @@ -127,12 +130,10 @@ export function EngagementFormPage(): JSX.Element {

- {editing ? 'Edit engagement' : 'New engagement'} + {editing ? t('engagement.form.title.edit') : t('engagement.form.title.new')}

- {editing - ? 'Update the engagement metadata.' - : 'Create a new red team mission to host simulations.'} + {editing ? t('engagement.form.subtitle.edit') : t('engagement.form.subtitle.new')}

@@ -144,7 +145,7 @@ export function EngagementFormPage(): JSX.Element { } >
- + - +