sprint/2-simulations #3

Merged
knacky merged 8 commits from sprint/2-simulations into main 2026-05-26 10:14:36 +00:00
3 changed files with 13 additions and 8 deletions
Showing only changes of commit c9032a9057 - Show all commits

View File

@@ -37,12 +37,16 @@ export function MitreTechniquePicker({
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLUListElement>(null); const listRef = useRef<HTMLUListElement>(null);
// True once we've synced the first real techniqueId from props (parent/API load).
// After that we stop reacting to null, so keystrokes that emit onChange(null,null)
// don't propagate back and wipe the input mid-stroke.
const hasHydratedFromProps = useRef(false);
// Sync display when selection changes from outside (initial load)
useEffect(() => { useEffect(() => {
if (techniqueId && techniqueName) { if (techniqueId && techniqueName) {
setInputValue(`${techniqueId}${techniqueName}`); setInputValue(`${techniqueId}${techniqueName}`);
} else if (!techniqueId) { hasHydratedFromProps.current = true;
} else if (!techniqueId && !hasHydratedFromProps.current) {
setInputValue(''); setInputValue('');
} }
}, [techniqueId, techniqueName]); }, [techniqueId, techniqueName]);

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { extractApiError } from '@/api/client'; import { extractApiError } from '@/api/client';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useEngagementSimulations } from '@/hooks/useSimulations'; import { useEngagementSimulations } from '@/hooks/useSimulations';
@@ -19,6 +19,7 @@ function formatDate(value: string | null): string {
export function SimulationList({ engagementId }: SimulationListProps): JSX.Element { export function SimulationList({ engagementId }: SimulationListProps): JSX.Element {
const { data, isLoading, isError, error, refetch } = useEngagementSimulations(engagementId); const { data, isLoading, isError, error, refetch } = useEngagementSimulations(engagementId);
const { canEditEngagements } = useAuth(); const { canEditEngagements } = useAuth();
const navigate = useNavigate();
if (isLoading) return <LoadingState label="Loading simulations…" />; if (isLoading) return <LoadingState label="Loading simulations…" />;
@@ -82,14 +83,13 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
key={sim.id} key={sim.id}
className="border-b border-hairline last:border-0 hover:bg-cloud cursor-pointer" className="border-b border-hairline last:border-0 hover:bg-cloud cursor-pointer"
onClick={() => onClick={() =>
(window.location.href = `/engagements/${engagementId}/simulations/${sim.id}/edit`) navigate(`/engagements/${engagementId}/simulations/${sim.id}/edit`)
} }
> >
<td className="px-xl py-md"> <td className="px-xl py-md">
<Link <Link
to={`/engagements/${engagementId}/simulations/${sim.id}/edit`} to={`/engagements/${engagementId}/simulations/${sim.id}/edit`}
className="text-ink font-medium hover:underline" className="text-ink font-medium hover:underline"
onClick={(e) => e.stopPropagation()}
> >
{sim.name} {sim.name}
</Link> </Link>

View File

@@ -86,7 +86,7 @@ export function SimulationFormPage(): JSX.Element {
description: s.description ?? '', description: s.description ?? '',
commands: s.commands ?? '', commands: s.commands ?? '',
prerequisites: s.prerequisites ?? '', prerequisites: s.prerequisites ?? '',
executed_at: s.executed_at ? s.executed_at.replace(' ', 'T').slice(0, 16) : '', executed_at: s.executed_at ? s.executed_at.slice(0, 16) : '',
execution_result: s.execution_result ?? '', execution_result: s.execution_result ?? '',
}); });
setSoc({ setSoc({
@@ -117,6 +117,7 @@ export function SimulationFormPage(): JSX.Element {
const socCanEdit = isSoc && (status === 'review_required' || status === 'done'); const socCanEdit = isSoc && (status === 'review_required' || status === 'done');
const socBlocked = isSoc && (status === 'pending' || status === 'in_progress'); const socBlocked = isSoc && (status === 'pending' || status === 'in_progress');
const canSaveSoc = socCanEdit || canEditEngagements;
const rtDisabled = !canEditRT; const rtDisabled = !canEditRT;
const socDisabled = !canEditEngagements && !socCanEdit; const socDisabled = !canEditEngagements && !socCanEdit;
@@ -395,7 +396,7 @@ export function SimulationFormPage(): JSX.Element {
{/* SOC card */} {/* SOC card */}
<form <form
id="soc-form" id="soc-form"
onSubmit={socCanEdit ? onSaveSOC : canEditEngagements ? onSaveSOC : (e) => e.preventDefault()} onSubmit={canSaveSoc ? onSaveSOC : (e) => e.preventDefault()}
noValidate noValidate
className="card-product flex flex-col gap-md" className="card-product flex flex-col gap-md"
> >
@@ -441,7 +442,7 @@ export function SimulationFormPage(): JSX.Element {
/> />
</FormField> </FormField>
{(socCanEdit || canEditEngagements) && ( {canSaveSoc && (
<div className="flex items-center gap-md pt-sm border-t border-hairline"> <div className="flex items-center gap-md pt-sm border-t border-hairline">
<button type="submit" form="soc-form" className="btn-primary" disabled={submitting}> <button type="submit" form="soc-form" className="btn-primary" disabled={submitting}>
{updateMutation.isPending ? 'Saving…' : 'Save SOC'} {updateMutation.isPending ? 'Saving…' : 'Save SOC'}