fix(jira): show test Jira tickets on technique page (correct entity model)
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Techniques don't have their own Jira tickets — tickets exist on tests and campaigns. The previous JiraLinkPanel entityType='technique' always returned empty. Backend: add entity_ids (list) filter to GET /jira/links so multiple test IDs can be fetched in a single request. Frontend API: listJiraLinks() accepts entity_ids[] and serialises them as repeated query params (required by FastAPI List[UUID] parsing). TechniqueDetailPage: replace JiraLinkPanel with TechniqueJiraSection — a dedicated read-only component that: - Takes technique.tests (already loaded) - Batch-fetches all test Jira links in one request - Shows test name + ticket key + status + priority + open-in-Jira link - Hides itself when no tickets exist (avoids empty panel) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,9 +21,10 @@ import {
|
||||
} from "lucide-react";
|
||||
import { getTechniqueByMitreId, markTechniqueReviewed } from "../api/techniques";
|
||||
import { getTemplatesByTechnique } from "../api/test-templates";
|
||||
import { listJiraLinks, type JiraLink } from "../api/jira";
|
||||
import { getJiraConfig } from "../api/settings";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import TestFromTemplateForm from "../components/TestFromTemplateForm";
|
||||
import JiraLinkPanel from "../components/JiraLinkPanel";
|
||||
import type { TechniqueStatus, TestState, TestResult } from "../types/models";
|
||||
|
||||
const statusBadgeColors: Record<TechniqueStatus, string> = {
|
||||
@@ -50,6 +51,102 @@ const testResultBadgeColors: Record<TestResult, string> = {
|
||||
partially_detected: "bg-yellow-900/50 text-yellow-400 border-yellow-500/30",
|
||||
};
|
||||
|
||||
/* ── TechniqueJiraSection ─────────────────────────────────────────────
|
||||
Techniques don't have their own Jira tickets — tickets live on tests.
|
||||
This component fetches all test-linked Jira tickets for this technique
|
||||
in a single batched request and shows them read-only.
|
||||
─────────────────────────────────────────────────────────────────── */
|
||||
function TechniqueJiraSection({ technique }: { technique: ReturnType<typeof Object.create> }) {
|
||||
const tests: { id: string; name: string }[] = (technique as any).tests ?? [];
|
||||
|
||||
const testIds = tests.map((t) => t.id);
|
||||
|
||||
const { data: links = [], isLoading } = useQuery({
|
||||
queryKey: ["jira-links-technique", testIds],
|
||||
queryFn: () =>
|
||||
listJiraLinks({ entity_type: "test", entity_ids: testIds }),
|
||||
enabled: testIds.length > 0,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const { data: jiraConfig } = useQuery({
|
||||
queryKey: ["jira-config"],
|
||||
queryFn: getJiraConfig,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const jiraBase = jiraConfig?.url?.replace(/\/$/, "") ?? "https://jira.atlassian.com";
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
"To Do": "bg-gray-700 text-gray-300",
|
||||
"In Progress": "bg-blue-900/50 text-blue-400",
|
||||
"Done": "bg-green-900/50 text-green-400",
|
||||
};
|
||||
|
||||
// Build a map entity_id → test name
|
||||
const testById = Object.fromEntries(tests.map((t) => [t.id, t.name]));
|
||||
|
||||
// Only render if there's something to show (or we're loading)
|
||||
if (!isLoading && links.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-white">
|
||||
<ExternalLink className="h-5 w-5 text-blue-400" />
|
||||
Jira Tickets
|
||||
</h2>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-500" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{links.map((link: JiraLink) => (
|
||||
<div
|
||||
key={link.id}
|
||||
className="flex items-center justify-between rounded-lg border border-gray-700 bg-gray-800/30 px-4 py-3"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Test name */}
|
||||
<p className="text-xs text-gray-500 truncate mb-1">
|
||||
{testById[link.entity_id] ?? "Test"}
|
||||
</p>
|
||||
{/* Jira ticket info */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-mono text-sm font-medium text-blue-400">
|
||||
{link.jira_issue_key}
|
||||
</span>
|
||||
{link.jira_status && (
|
||||
<span className={`rounded px-1.5 py-0.5 text-xs ${statusColors[link.jira_status] ?? "bg-gray-700 text-gray-300"}`}>
|
||||
{link.jira_status}
|
||||
</span>
|
||||
)}
|
||||
{link.jira_priority && (
|
||||
<span className="text-xs text-gray-500">{link.jira_priority}</span>
|
||||
)}
|
||||
{link.jira_assignee && (
|
||||
<span className="text-xs text-gray-500">→ {link.jira_assignee}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={`${jiraBase}/browse/${link.jira_issue_key}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-3 shrink-0 rounded p-1.5 text-gray-500 hover:bg-gray-700 hover:text-blue-400 transition-colors"
|
||||
title="Open in Jira"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TechniqueDetailPage() {
|
||||
const { mitreId } = useParams<{ mitreId: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -561,10 +658,8 @@ export default function TechniqueDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jira Integration */}
|
||||
{technique && (
|
||||
<JiraLinkPanel entityType="technique" entityId={technique.id} readOnly label={technique.name} />
|
||||
)}
|
||||
{/* Jira Integration — shows tickets linked to tests of this technique */}
|
||||
{technique && <TechniqueJiraSection technique={technique} />}
|
||||
|
||||
{/* Template instantiation modal */}
|
||||
{templateFormId && technique && (
|
||||
|
||||
Reference in New Issue
Block a user