From 2bfcc7e58cba89403ea568efe515e24a010a65ed Mon Sep 17 00:00:00 2001 From: kitos Date: Tue, 2 Jun 2026 10:42:13 +0200 Subject: [PATCH] =?UTF-8?q?feat(status-badge):=20CSS=20hover=20tooltip=20?= =?UTF-8?q?=E2=80=94=20replaces=20native=20title=20attribute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit title= attribute tooltip is browser-native, tiny, and often invisible. New StatusBadge component uses a Tailwind group-hover absolute panel that appears immediately on hover with: - Clear heading per status - 'Meaning' and 'Action' lines - Arrow pointing to the badge - 200ms fade-in transition Used in TechniquesPage (list table) and TechniqueDetailPage (header). --- frontend/src/components/StatusBadge.tsx | 120 +++++++++++++++++++++ frontend/src/pages/TechniqueDetailPage.tsx | 18 +--- frontend/src/pages/TechniquesPage.tsx | 22 +--- 3 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 frontend/src/components/StatusBadge.tsx diff --git a/frontend/src/components/StatusBadge.tsx b/frontend/src/components/StatusBadge.tsx new file mode 100644 index 0000000..52d51ff --- /dev/null +++ b/frontend/src/components/StatusBadge.tsx @@ -0,0 +1,120 @@ +/** + * StatusBadge — renders a technique coverage status pill with a + * CSS-only hover tooltip explaining what the status means. + */ + +import type { TechniqueStatus } from "../types/models"; + +const BADGE_COLORS: Record = { + validated: "bg-green-900/50 text-green-400 border-green-500/30", + partial: "bg-yellow-900/50 text-yellow-400 border-yellow-500/30", + in_progress: "bg-blue-900/50 text-blue-400 border-blue-500/30", + not_covered: "bg-red-900/50 text-red-400 border-red-500/30", + not_evaluated: "bg-gray-800/50 text-gray-400 border-gray-600/30", + review_required:"bg-orange-900/50 text-orange-400 border-orange-500/30", +}; + +const BADGE_LABELS: Record = { + validated: "Validated", + partial: "Partial", + in_progress: "In Progress", + not_covered: "Not Covered", + not_evaluated: "Not Evaluated", + review_required:"Review Required", +}; + +interface TooltipLine { label: string; text: string } + +const TOOLTIPS: Record = { + validated: { + heading: "✅ Validated", + lines: [ + { label: "Meaning", text: "≥2 tests executed. Blue Team detected the attack in all of them." }, + { label: "Action", text: "Maintain and re-test periodically." }, + ], + }, + partial: { + heading: "🟡 Partial", + lines: [ + { label: "Meaning", text: "Only 1 validated test, some tests still pending, or detection was not 100%." }, + { label: "Action", text: "Run more tests and ensure all are validated with 'detected'." }, + ], + }, + in_progress: { + heading: "🔵 In Progress", + lines: [ + { label: "Meaning", text: "Tests exist but none have been validated yet (draft, executing, or in review)." }, + { label: "Action", text: "Complete the Red/Blue validation workflow." }, + ], + }, + not_covered: { + heading: "🔴 Not Covered", + lines: [ + { label: "Meaning", text: "Tests were run but Blue Team did NOT detect the attack — coverage gap." }, + { label: "Action", text: "Improve detection rules and re-test." }, + ], + }, + not_evaluated: { + heading: "⚫ Not Evaluated", + lines: [ + { label: "Meaning", text: "No tests have been created for this technique yet." }, + { label: "Action", text: "Create a test from the available templates." }, + ], + }, + review_required: { + heading: "🟠 Review Required", + lines: [ + { label: "Meaning", text: "MITRE updated this technique, or new intel / detection rules were added." }, + { label: "Action", text: "A lead should review the changes and mark as reviewed." }, + ], + }, +}; + +interface StatusBadgeProps { + status: TechniqueStatus; + /** Extra classes on the outer wrapper (e.g. sizing) */ + className?: string; + /** Size variant — defaults to 'md' */ + size?: "sm" | "md"; +} + +export default function StatusBadge({ status, className = "", size = "md" }: StatusBadgeProps) { + const tip = TOOLTIPS[status]; + const pill = size === "sm" + ? "px-2 py-0.5 text-[10px]" + : "px-2.5 py-0.5 text-xs"; + + return ( + + {/* The badge itself */} + + {BADGE_LABELS[status]} + + + {/* Tooltip — appears above the badge on hover */} + +

{tip.heading}

+ {tip.lines.map(({ label, text }) => ( +
+ + {label} + +

{text}

+
+ ))} + {/* Arrow pointing down */} + + +
+ ); +} diff --git a/frontend/src/pages/TechniqueDetailPage.tsx b/frontend/src/pages/TechniqueDetailPage.tsx index 1fda807..5e72721 100644 --- a/frontend/src/pages/TechniqueDetailPage.tsx +++ b/frontend/src/pages/TechniqueDetailPage.tsx @@ -27,14 +27,7 @@ import { useAuth } from "../context/AuthContext"; import TestFromTemplateForm from "../components/TestFromTemplateForm"; import type { TechniqueStatus, TestState, TestResult } from "../types/models"; -const STATUS_TOOLTIPS: Record = { - validated: "✅ Validated — ≥2 tests executed and detected by Blue Team. Technique is covered.", - partial: "🟡 Partial — Some tests done but not all detected, only 1 validated test, or some tests still pending. More testing needed.", - in_progress: "🔵 In Progress — Tests exist but none validated yet (draft, executing, or under review).", - not_covered: "🔴 Not Covered — Tests were run but Blue Team did not detect the attack. Coverage gap.", - not_evaluated: "⚫ Not Evaluated — No tests created for this technique yet.", - review_required:"🟠 Review Required — Technique was recently updated or new intel/rules detected. Needs review.", -}; +import StatusBadge from "../components/StatusBadge"; const statusBadgeColors: Record = { validated: "bg-green-900/50 text-green-400 border-green-500/30", @@ -251,14 +244,7 @@ export default function TechniqueDetailPage() {

{technique.mitre_id}

- - {technique.status_global.replace(/_/g, " ")} - + {technique.review_required && ( diff --git a/frontend/src/pages/TechniquesPage.tsx b/frontend/src/pages/TechniquesPage.tsx index 6f9f46d..047fd9a 100644 --- a/frontend/src/pages/TechniquesPage.tsx +++ b/frontend/src/pages/TechniquesPage.tsx @@ -17,6 +17,8 @@ const STATUS_OPTIONS: { value: TechniqueStatus | "all"; label: string; color: st const PLATFORM_OPTIONS = ["all", "windows", "linux", "macos", "cloud", "network"] as const; +import StatusBadge from "../components/StatusBadge"; + const statusBadgeColors: Record = { validated: "bg-green-900/50 text-green-400 border-green-500/30", partial: "bg-yellow-900/50 text-yellow-400 border-yellow-500/30", @@ -26,15 +28,6 @@ const statusBadgeColors: Record = { review_required: "bg-orange-900/50 text-orange-400 border-orange-500/30", }; -const STATUS_TOOLTIPS: Record = { - validated: "✅ Validated — ≥2 tests executed and detected by Blue Team. Technique is covered.", - partial: "🟡 Partial — Some tests done but not all detected, only 1 validated test, or some tests still pending. More testing needed.", - in_progress: "🔵 In Progress — Tests exist but none validated yet (draft, executing, or under review).", - not_covered: "🔴 Not Covered — Tests were run but Blue Team did not detect the attack. Coverage gap.", - not_evaluated: "⚫ Not Evaluated — No tests created for this technique yet.", - review_required:"🟠 Review Required — Technique was recently updated or new intel/rules detected. Needs review.", -}; - export default function TechniquesPage() { const navigate = useNavigate(); const [viewMode, setViewMode] = useState<"matrix" | "list">("matrix"); @@ -247,15 +240,8 @@ export default function TechniquesPage() { {tech.tactic?.replace(/-/g, " ") || "—"} - - - {tech.status_global.replace(/_/g, " ")} - + + ))}