feat(status-badge): CSS hover tooltip — replaces native title attribute
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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).
This commit is contained in:
120
frontend/src/components/StatusBadge.tsx
Normal file
120
frontend/src/components/StatusBadge.tsx
Normal file
@@ -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<TechniqueStatus, string> = {
|
||||||
|
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<TechniqueStatus, string> = {
|
||||||
|
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<TechniqueStatus, { heading: string; lines: TooltipLine[] }> = {
|
||||||
|
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 (
|
||||||
|
<span className={`relative group inline-flex ${className}`}>
|
||||||
|
{/* The badge itself */}
|
||||||
|
<span
|
||||||
|
className={`inline-flex cursor-help items-center rounded-full border font-medium ${pill} ${BADGE_COLORS[status]}`}
|
||||||
|
>
|
||||||
|
{BADGE_LABELS[status]}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Tooltip — appears above the badge on hover */}
|
||||||
|
<span
|
||||||
|
className="
|
||||||
|
pointer-events-none absolute bottom-full left-1/2 z-50 mb-2
|
||||||
|
w-64 -translate-x-1/2
|
||||||
|
rounded-xl border border-gray-700 bg-gray-900 p-3 shadow-xl
|
||||||
|
opacity-0 transition-opacity duration-150
|
||||||
|
group-hover:opacity-100
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p className="mb-2 text-xs font-semibold text-white">{tip.heading}</p>
|
||||||
|
{tip.lines.map(({ label, text }) => (
|
||||||
|
<div key={label} className="mb-1 last:mb-0">
|
||||||
|
<span className="text-[10px] font-medium uppercase tracking-wide text-gray-500">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<p className="text-[11px] leading-snug text-gray-300">{text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{/* Arrow pointing down */}
|
||||||
|
<span className="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-700" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,14 +27,7 @@ import { useAuth } from "../context/AuthContext";
|
|||||||
import TestFromTemplateForm from "../components/TestFromTemplateForm";
|
import TestFromTemplateForm from "../components/TestFromTemplateForm";
|
||||||
import type { TechniqueStatus, TestState, TestResult } from "../types/models";
|
import type { TechniqueStatus, TestState, TestResult } from "../types/models";
|
||||||
|
|
||||||
const STATUS_TOOLTIPS: Record<TechniqueStatus, string> = {
|
import StatusBadge from "../components/StatusBadge";
|
||||||
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.",
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusBadgeColors: Record<TechniqueStatus, string> = {
|
const statusBadgeColors: Record<TechniqueStatus, string> = {
|
||||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||||
@@ -251,14 +244,7 @@ export default function TechniqueDetailPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-2xl font-bold text-white">{technique.mitre_id}</h1>
|
<h1 className="text-2xl font-bold text-white">{technique.mitre_id}</h1>
|
||||||
<span
|
<StatusBadge status={technique.status_global as TechniqueStatus} />
|
||||||
className={`inline-flex rounded-full border px-2.5 py-0.5 text-xs font-medium cursor-help ${
|
|
||||||
statusBadgeColors[technique.status_global]
|
|
||||||
}`}
|
|
||||||
title={STATUS_TOOLTIPS[technique.status_global]}
|
|
||||||
>
|
|
||||||
{technique.status_global.replace(/_/g, " ")}
|
|
||||||
</span>
|
|
||||||
{technique.review_required && (
|
{technique.review_required && (
|
||||||
<span className="inline-flex items-center gap-1 rounded-full border border-orange-500/30 bg-orange-900/50 px-2.5 py-0.5 text-xs font-medium text-orange-400">
|
<span className="inline-flex items-center gap-1 rounded-full border border-orange-500/30 bg-orange-900/50 px-2.5 py-0.5 text-xs font-medium text-orange-400">
|
||||||
<AlertTriangle className="h-3 w-3" />
|
<AlertTriangle className="h-3 w-3" />
|
||||||
|
|||||||
@@ -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;
|
const PLATFORM_OPTIONS = ["all", "windows", "linux", "macos", "cloud", "network"] as const;
|
||||||
|
|
||||||
|
import StatusBadge from "../components/StatusBadge";
|
||||||
|
|
||||||
const statusBadgeColors: Record<TechniqueStatus, string> = {
|
const statusBadgeColors: Record<TechniqueStatus, string> = {
|
||||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||||
partial: "bg-yellow-900/50 text-yellow-400 border-yellow-500/30",
|
partial: "bg-yellow-900/50 text-yellow-400 border-yellow-500/30",
|
||||||
@@ -26,15 +28,6 @@ const statusBadgeColors: Record<TechniqueStatus, string> = {
|
|||||||
review_required: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
review_required: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_TOOLTIPS: Record<TechniqueStatus, string> = {
|
|
||||||
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() {
|
export default function TechniquesPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [viewMode, setViewMode] = useState<"matrix" | "list">("matrix");
|
const [viewMode, setViewMode] = useState<"matrix" | "list">("matrix");
|
||||||
@@ -247,15 +240,8 @@ export default function TechniquesPage() {
|
|||||||
{tech.tactic?.replace(/-/g, " ") || "—"}
|
{tech.tactic?.replace(/-/g, " ") || "—"}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3 overflow-visible">
|
||||||
<span
|
<StatusBadge status={tech.status_global} />
|
||||||
className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium cursor-help ${
|
|
||||||
statusBadgeColors[tech.status_global]
|
|
||||||
}`}
|
|
||||||
title={STATUS_TOOLTIPS[tech.status_global]}
|
|
||||||
>
|
|
||||||
{tech.status_global.replace(/_/g, " ")}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user