feat(phase-20): navigation, error handling, integration tests, and V2 docs (T-132 to T-135)
This commit is contained in:
@@ -22,6 +22,7 @@ import type { TestResult, TeamSide, TestTimelineEntry } from "../types/models";
|
||||
import TestDetailHeader from "../components/test-detail/TestDetailHeader";
|
||||
import TeamTabs from "../components/test-detail/TeamTabs";
|
||||
import ValidationModal from "../components/test-detail/ValidationModal";
|
||||
import ConfirmDialog from "../components/ConfirmDialog";
|
||||
|
||||
// ── Page Component ─────────────────────────────────────────────────
|
||||
|
||||
@@ -38,6 +39,8 @@ export default function TestDetailPage() {
|
||||
side: "red" | "blue";
|
||||
}>({ open: false, side: "red" });
|
||||
|
||||
const [confirmReopen, setConfirmReopen] = useState(false);
|
||||
|
||||
const [redDraft, setRedDraft] = useState({
|
||||
procedure_text: "",
|
||||
tool_used: "",
|
||||
@@ -96,7 +99,19 @@ export default function TestDetailPage() {
|
||||
|
||||
const showToast = useCallback((message: string, type: "success" | "error") => {
|
||||
setToast({ message, type });
|
||||
setTimeout(() => setToast(null), 3500);
|
||||
setTimeout(() => setToast(null), 5000);
|
||||
}, []);
|
||||
|
||||
/** Extract a user-friendly error message from Axios or generic errors. */
|
||||
const extractError = useCallback((err: unknown): string => {
|
||||
if (err && typeof err === "object" && "response" in err) {
|
||||
const resp = (err as { response?: { data?: { detail?: string | { message?: string } } } }).response;
|
||||
const detail = resp?.data?.detail;
|
||||
if (typeof detail === "string") return detail;
|
||||
if (detail && typeof detail === "object" && "message" in detail) return (detail as { message: string }).message;
|
||||
}
|
||||
if (err instanceof Error) return err.message;
|
||||
return "An unexpected error occurred";
|
||||
}, []);
|
||||
|
||||
const invalidateAll = useCallback(() => {
|
||||
@@ -120,7 +135,7 @@ export default function TestDetailPage() {
|
||||
invalidateAll();
|
||||
showToast("Red Team fields saved", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const saveBlueMutation = useMutation({
|
||||
@@ -133,7 +148,7 @@ export default function TestDetailPage() {
|
||||
invalidateAll();
|
||||
showToast("Blue Team fields saved", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
// State transitions
|
||||
@@ -143,7 +158,7 @@ export default function TestDetailPage() {
|
||||
invalidateAll();
|
||||
showToast("Test execution started", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const submitRedMutation = useMutation({
|
||||
@@ -152,7 +167,7 @@ export default function TestDetailPage() {
|
||||
invalidateAll();
|
||||
showToast("Submitted to Blue Team", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const submitBlueMutation = useMutation({
|
||||
@@ -161,7 +176,7 @@ export default function TestDetailPage() {
|
||||
invalidateAll();
|
||||
showToast("Submitted for review", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const validateRedLeadMutation = useMutation({
|
||||
@@ -172,7 +187,7 @@ export default function TestDetailPage() {
|
||||
setValidationModal({ open: false, side: "red" });
|
||||
showToast("Red Lead validation submitted", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const validateBlueLeadMutation = useMutation({
|
||||
@@ -183,16 +198,20 @@ export default function TestDetailPage() {
|
||||
setValidationModal({ open: false, side: "blue" });
|
||||
showToast("Blue Lead validation submitted", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
const reopenMutation = useMutation({
|
||||
mutationFn: () => reopenTest(testId!),
|
||||
onSuccess: () => {
|
||||
invalidateAll();
|
||||
setConfirmReopen(false);
|
||||
showToast("Test reopened", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => {
|
||||
setConfirmReopen(false);
|
||||
showToast(extractError(err), "error");
|
||||
},
|
||||
});
|
||||
|
||||
// Evidence upload
|
||||
@@ -203,7 +222,7 @@ export default function TestDetailPage() {
|
||||
invalidateAll();
|
||||
showToast("Evidence uploaded", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => showToast(extractError(err), "error"),
|
||||
});
|
||||
|
||||
// ── Handlers ───────────────────────────────────────────────────
|
||||
@@ -322,7 +341,7 @@ export default function TestDetailPage() {
|
||||
onSubmitRed={() => submitRedMutation.mutate()}
|
||||
onSubmitBlue={() => submitBlueMutation.mutate()}
|
||||
onOpenValidateModal={(side) => setValidationModal({ open: true, side })}
|
||||
onReopen={() => reopenMutation.mutate()}
|
||||
onReopen={() => setConfirmReopen(true)}
|
||||
/>
|
||||
|
||||
{/* Content: Tabs + Sidebar */}
|
||||
@@ -426,6 +445,18 @@ export default function TestDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm Reopen Dialog */}
|
||||
<ConfirmDialog
|
||||
open={confirmReopen}
|
||||
title="Reopen Test"
|
||||
message="This will move the test back to Draft state and clear all validation decisions. The Red/Blue workflow will need to be restarted. Are you sure?"
|
||||
confirmLabel="Reopen"
|
||||
variant="warning"
|
||||
isLoading={reopenMutation.isPending}
|
||||
onConfirm={() => reopenMutation.mutate()}
|
||||
onCancel={() => setConfirmReopen(false)}
|
||||
/>
|
||||
|
||||
{/* Validation Modal */}
|
||||
{validationModal.open && (
|
||||
<ValidationModal
|
||||
|
||||
Reference in New Issue
Block a user