fix: 4 improvements — campaign test deletion, review queue triggers, technique link, Jira read-only
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

1. Campaign test deletion: removing a test from a campaign now also
   deletes the underlying Test record and recalculates technique status.

2. Review Queue triggers: review_required=True is now also set when
   - Sigma/Elastic detection rules are imported for a technique
   - A test is validated (coverage status changes)

3. Test detail — Technique link: 'Technique' entry added at the top of
   the Details sidebar showing MITRE ID + name as a clickable link to
   /techniques/{mitre_id}.

4. Jira panel — read-only on test page: added readOnly + label props to
   JiraLinkPanel. TestDetailPage now passes readOnly=true and the test
   name as label, hiding Link Issue / Sync / Unlink controls (automatic
   Jira creation only — no manual management).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-29 11:18:55 +02:00
parent 727b8af7fd
commit 1b513b050e
6 changed files with 118 additions and 40 deletions

View File

@@ -26,6 +26,10 @@ import { useDebounce } from "../hooks/useDebounce";
interface JiraLinkPanelProps {
entityType: JiraLinkEntityType;
entityId: string;
/** If true, hides all management controls (Link Issue, Sync, Unlink). */
readOnly?: boolean;
/** Optional label shown under the Jira header (e.g. the test name). */
label?: string;
}
const priorityColors: Record<string, string> = {
@@ -42,7 +46,7 @@ const statusColors: Record<string, string> = {
"Done": "bg-green-900/50 text-green-400",
};
export default function JiraLinkPanel({ entityType, entityId }: JiraLinkPanelProps) {
export default function JiraLinkPanel({ entityType, entityId, readOnly = false, label }: JiraLinkPanelProps) {
const queryClient = useQueryClient();
const [showSearch, setShowSearch] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
@@ -120,29 +124,36 @@ export default function JiraLinkPanel({ entityType, entityId }: JiraLinkPanelPro
return (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="flex items-center gap-2 text-lg font-semibold text-white">
<Link2 className="h-5 w-5 text-blue-400" />
Jira
</h2>
<button
onClick={() => setShowSearch(!showSearch)}
className="flex items-center gap-1 rounded-lg border border-gray-700 px-3 py-1.5 text-xs text-gray-300 hover:border-blue-500/50 hover:text-blue-400 transition-colors"
>
{showSearch ? (
<>
<X className="h-3.5 w-3.5" /> Cancel
</>
) : (
<>
<Plus className="h-3.5 w-3.5" /> Link Issue
</>
<div className="mb-4 flex items-start justify-between">
<div>
<h2 className="flex items-center gap-2 text-lg font-semibold text-white">
<Link2 className="h-5 w-5 text-blue-400" />
Jira
</h2>
{label && (
<p className="mt-0.5 text-xs text-gray-500 truncate max-w-xs">{label}</p>
)}
</button>
</div>
{!readOnly && (
<button
onClick={() => setShowSearch(!showSearch)}
className="flex items-center gap-1 rounded-lg border border-gray-700 px-3 py-1.5 text-xs text-gray-300 hover:border-blue-500/50 hover:text-blue-400 transition-colors"
>
{showSearch ? (
<>
<X className="h-3.5 w-3.5" /> Cancel
</>
) : (
<>
<Plus className="h-3.5 w-3.5" /> Link Issue
</>
)}
</button>
)}
</div>
{/* Search panel */}
{showSearch && (
{/* Search panel — only in edit mode */}
{!readOnly && showSearch && (
<div className="mb-4 rounded-lg border border-gray-700 bg-gray-800/50 p-3 space-y-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
@@ -245,16 +256,18 @@ export default function JiraLinkPanel({ entityType, entityId }: JiraLinkPanelPro
</div>
<div className="flex shrink-0 items-center gap-1">
<button
onClick={() => syncMutation.mutate(link.id)}
disabled={syncMutation.isPending}
title="Sync from Jira"
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-blue-400 transition-colors"
>
<RefreshCw
className={`h-3.5 w-3.5 ${syncMutation.isPending ? "animate-spin" : ""}`}
/>
</button>
{!readOnly && (
<button
onClick={() => syncMutation.mutate(link.id)}
disabled={syncMutation.isPending}
title="Sync from Jira"
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-blue-400 transition-colors"
>
<RefreshCw
className={`h-3.5 w-3.5 ${syncMutation.isPending ? "animate-spin" : ""}`}
/>
</button>
)}
<a
href={`${jiraBaseUrl}/browse/${link.jira_issue_key}`}
target="_blank"
@@ -264,14 +277,16 @@ export default function JiraLinkPanel({ entityType, entityId }: JiraLinkPanelPro
>
<ExternalLink className="h-3.5 w-3.5" />
</a>
<button
onClick={() => deleteMutation.mutate(link.id)}
disabled={deleteMutation.isPending}
title="Unlink"
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-red-400 transition-colors"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
{!readOnly && (
<button
onClick={() => deleteMutation.mutate(link.id)}
disabled={deleteMutation.isPending}
title="Unlink"
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-red-400 transition-colors"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
)}
</div>
</div>
</div>

View File

@@ -440,6 +440,26 @@ export default function TestDetailPage() {
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<h2 className="mb-4 text-lg font-semibold text-white">Details</h2>
<dl className="space-y-4">
{test.technique_mitre_id && (
<div>
<dt className="text-xs font-medium uppercase text-gray-500">Technique</dt>
<dd className="mt-1">
<button
onClick={() => navigate(`/techniques/${test.technique_mitre_id}`)}
className="flex items-start gap-1.5 text-left group"
>
<span className="font-mono text-xs text-cyan-400 shrink-0 group-hover:underline">
{test.technique_mitre_id}
</span>
{test.technique_name && (
<span className="text-xs text-gray-400 group-hover:text-gray-300 transition-colors">
\u2014 {test.technique_name}
</span>
)}
</button>
</dd>
</div>
)}
<div>
<dt className="text-xs font-medium uppercase text-gray-500">Description</dt>
<dd className="mt-1">
@@ -539,7 +559,7 @@ export default function TestDetailPage() {
)}
{/* Jira Integration */}
<JiraLinkPanel entityType="test" entityId={testId!} />
<JiraLinkPanel entityType="test" entityId={testId!} readOnly label={test.name} />
{/* Phase Timeline (read-only, with Tempo sync) */}
<TestPhaseTimeline test={test} testId={testId} />