From aa63e0aa420f543ce118357eab694ecbdb66e351 Mon Sep 17 00:00:00 2001 From: funnywolf Date: Sat, 27 Jun 2026 23:47:39 +0800 Subject: [PATCH] Improve dashboard big-screen UX Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/pages/Dashboard.css | 342 +++++++++++++++++++++++++++++-- frontend/src/pages/Dashboard.tsx | 238 +++++++++++++++++---- 2 files changed, 527 insertions(+), 53 deletions(-) diff --git a/frontend/src/pages/Dashboard.css b/frontend/src/pages/Dashboard.css index 37f73de..0ec1954 100644 --- a/frontend/src/pages/Dashboard.css +++ b/frontend/src/pages/Dashboard.css @@ -31,14 +31,14 @@ position: relative; display: flex; flex-direction: column; - gap: 14px; + gap: 16px; min-height: 100%; } .dashboard-hero { position: relative; overflow: hidden; - padding: 20px; + padding: 22px; border: 1px solid rgba(22, 119, 255, 0.38); border-radius: 18px; background: @@ -69,6 +69,10 @@ gap: 16px; } +.dashboard-hero-copy { + min-width: 0; +} + .dashboard-eyebrow { display: flex; align-items: center; @@ -84,11 +88,48 @@ .dashboard-title { margin: 0; color: #f5faff; - font-size: 30px; + font-size: 32px; font-weight: 700; line-height: 1.1; } +.dashboard-command-summary { + display: inline-flex; + align-items: center; + max-width: 760px; + gap: 10px; + margin-top: 14px; + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--posture-color, #69b1ff) 45%, transparent); + border-radius: 999px; + background: + linear-gradient(90deg, color-mix(in srgb, var(--posture-color, #69b1ff) 18%, transparent), rgba(6, 21, 40, 0.48)), + rgba(6, 21, 40, 0.46); + box-shadow: 0 0 22px color-mix(in srgb, var(--posture-color, #69b1ff) 16%, transparent); +} + +.dashboard-command-state { + flex: none; + padding: 4px 9px; + border-radius: 999px; + color: #061528; + font-family: 'JetBrains Mono', Consolas, monospace; + font-size: 11px; + font-weight: 900; + letter-spacing: 0.08em; + background: var(--posture-color, #69b1ff); + box-shadow: 0 0 16px color-mix(in srgb, var(--posture-color, #69b1ff) 42%, transparent); +} + +.dashboard-command-copy { + overflow: hidden; + color: rgba(245, 250, 255, 0.86); + font-size: 13px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + .dashboard-hero-actions { position: relative; z-index: 2; @@ -155,6 +196,60 @@ background: rgba(250, 173, 20, 0.08); } +.dashboard-command-strip { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: 1.4fr repeat(4, minmax(150px, 1fr)); + gap: 10px; + margin-top: 18px; +} + +.dashboard-command-pill { + position: relative; + overflow: hidden; + display: grid; + gap: 6px; + padding: 11px 12px; + border: 1px solid color-mix(in srgb, var(--signal-color, #69b1ff) 35%, rgba(255, 255, 255, 0.08)); + border-radius: 14px; + background: + linear-gradient(135deg, color-mix(in srgb, var(--signal-color, #69b1ff) 14%, transparent), rgba(255, 255, 255, 0.035)), + rgba(6, 21, 40, 0.62); +} + +.dashboard-command-pill::before { + position: absolute; + inset: 0 auto 0 0; + width: 3px; + content: ''; + background: var(--signal-color, #69b1ff); + box-shadow: 0 0 18px var(--signal-color, #69b1ff); +} + +.dashboard-command-pill span { + color: rgba(214, 234, 255, 0.54); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.dashboard-command-pill strong { + overflow: hidden; + color: #f5faff; + font-family: 'JetBrains Mono', Consolas, monospace; + font-size: 18px; + line-height: 1; + text-overflow: ellipsis; + text-shadow: 0 0 16px color-mix(in srgb, var(--signal-color, #69b1ff) 26%, transparent); + white-space: nowrap; +} + +.dashboard-command-pill.emphasis strong { + font-size: 22px; +} + .dashboard-grid { display: grid; gap: 14px; @@ -208,6 +303,7 @@ .dashboard-card, .dashboard-metric-card { + position: relative; border: 1px solid rgba(22, 119, 255, 0.22); border-radius: 16px; background: @@ -240,12 +336,25 @@ } .dashboard-risk-body { + position: relative; display: grid; grid-template-columns: minmax(170px, 0.85fr) minmax(180px, 1fr); gap: 12px; align-items: center; } +.dashboard-risk-body::before { + position: absolute; + inset: 8px auto auto 8px; + width: 74px; + height: 74px; + pointer-events: none; + content: ''; + border-radius: 50%; + background: radial-gradient(circle, color-mix(in srgb, var(--risk-color, #69b1ff) 22%, transparent), transparent 68%); + filter: blur(2px); +} + .dashboard-risk-gauge { display: flex; align-items: center; @@ -260,12 +369,31 @@ gap: 8px; } +.dashboard-risk-value-row { + display: flex; + align-items: flex-end; + gap: 10px; +} + .dashboard-risk-value { - color: #f5faff; + color: var(--risk-color, #f5faff); font-size: 54px; font-weight: 800; line-height: 1; - text-shadow: 0 0 24px rgba(22, 119, 255, 0.42); + text-shadow: 0 0 24px color-mix(in srgb, var(--risk-color, #69b1ff) 44%, transparent); +} + +.dashboard-risk-level { + margin-bottom: 7px; + padding: 5px 9px; + border: 1px solid color-mix(in srgb, var(--risk-color, #69b1ff) 52%, transparent); + border-radius: 999px; + color: var(--risk-color, #69b1ff); + font-family: 'JetBrains Mono', Consolas, monospace; + font-size: 11px; + font-weight: 900; + letter-spacing: 0.1em; + background: color-mix(in srgb, var(--risk-color, #69b1ff) 13%, transparent); } .dashboard-risk-label { @@ -275,6 +403,42 @@ text-transform: uppercase; } +.dashboard-risk-summary { + color: rgba(245, 250, 255, 0.76); + font-size: 13px; + line-height: 1.35; +} + +.dashboard-risk-band { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 4px; + margin: 2px 0 4px; +} + +.dashboard-risk-band span { + overflow: hidden; + padding: 5px 4px; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 7px; + color: rgba(255, 255, 255, 0.35); + font-family: 'JetBrains Mono', Consolas, monospace; + font-size: 9px; + font-weight: 800; + letter-spacing: 0.05em; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + background: rgba(255, 255, 255, 0.03); +} + +.dashboard-risk-band span.active { + border-color: color-mix(in srgb, var(--risk-color, #69b1ff) 54%, transparent); + color: #f5faff; + background: color-mix(in srgb, var(--risk-color, #69b1ff) 20%, transparent); + box-shadow: 0 0 14px color-mix(in srgb, var(--risk-color, #69b1ff) 20%, transparent); +} + .dashboard-risk-meta { display: grid; gap: 8px; @@ -303,6 +467,17 @@ overflow: hidden; } +.dashboard-metric-card.priority { + border-color: color-mix(in srgb, var(--metric-color, #1677ff) 42%, rgba(255, 255, 255, 0.08)); + background: + linear-gradient(135deg, color-mix(in srgb, var(--metric-color, #1677ff) 16%, transparent), rgba(13, 20, 33, 0.94) 48%), + rgba(20, 20, 20, 0.92); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.06), + 0 18px 38px rgba(0, 0, 0, 0.22), + 0 0 22px color-mix(in srgb, var(--metric-color, #1677ff) 14%, transparent); +} + .dashboard-metric-card::before { position: absolute; inset: 0 auto 0 0; @@ -322,6 +497,36 @@ font-family: 'JetBrains Mono', Consolas, monospace; } +.dashboard-metric-card.priority .ant-statistic-content { + text-shadow: 0 0 18px color-mix(in srgb, var(--metric-color, #1677ff) 28%, transparent); +} + +.dashboard-metric-category { + margin-bottom: 7px; + color: var(--metric-color, #69b1ff); + font-family: 'JetBrains Mono', Consolas, monospace; + font-size: 10px; + font-weight: 900; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.dashboard-metric-card.tone-critical { + border-color: rgba(255, 77, 79, 0.3); +} + +.dashboard-metric-card.tone-warning { + border-color: rgba(250, 140, 22, 0.3); +} + +.dashboard-metric-card.tone-success { + border-color: rgba(82, 196, 26, 0.26); +} + +.dashboard-metric-card.tone-intel { + border-color: rgba(114, 46, 209, 0.26); +} + .dashboard-metric-sub { margin-top: 8px; color: rgba(255, 255, 255, 0.48); @@ -336,6 +541,10 @@ min-height: 190px; } +.dashboard-trend-card .dashboard-panel-body { + min-height: 230px; +} + .dashboard-mini-list { display: grid; gap: 10px; @@ -545,6 +754,51 @@ white-space: nowrap; } +.dashboard-radar-wrap { + display: grid; + gap: 10px; +} + +.dashboard-radar-legend { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); + gap: 6px; +} + +.dashboard-radar-legend span { + display: grid; + grid-template-columns: 8px auto minmax(0, 1fr); + align-items: center; + gap: 6px; + min-width: 0; + padding: 5px 7px; + border: 1px solid rgba(255, 255, 255, 0.055); + border-radius: 8px; + background: rgba(255, 255, 255, 0.025); +} + +.dashboard-radar-legend i { + width: 7px; + height: 7px; + border-radius: 50%; + box-shadow: 0 0 10px currentColor; +} + +.dashboard-radar-legend strong { + color: rgba(245, 250, 255, 0.82); + font-family: 'JetBrains Mono', Consolas, monospace; + font-size: 11px; +} + +.dashboard-radar-legend em { + overflow: hidden; + color: rgba(255, 255, 255, 0.38); + font-size: 11px; + font-style: normal; + text-overflow: ellipsis; + white-space: nowrap; +} + .dashboard-mitre-matrix { display: grid; gap: 6px; @@ -655,26 +909,78 @@ gap: 10px; } +.dashboard-event-stream::before { + position: absolute; + top: 14px; + bottom: 14px; + left: 72px; + width: 1px; + content: ''; + background: linear-gradient(to bottom, transparent, rgba(105, 177, 255, 0.42), transparent); +} + .dashboard-event { position: relative; display: grid; - gap: 5px; - padding: 11px 12px 11px 16px; - border: 1px solid rgba(255, 255, 255, 0.08); + grid-template-columns: 62px minmax(0, 1fr); + gap: 8px; + align-items: center; + padding: 12px; + border: 1px solid color-mix(in srgb, var(--event-color, #1677ff) 22%, rgba(255, 255, 255, 0.08)); border-radius: 12px; - background: rgba(255, 255, 255, 0.035); + background: + linear-gradient(90deg, color-mix(in srgb, var(--event-color, #1677ff) 10%, transparent), rgba(255, 255, 255, 0.035)), + rgba(255, 255, 255, 0.035); } .dashboard-event::before { position: absolute; - top: 14px; - bottom: 14px; + top: 50%; + left: 67px; + width: 10px; + height: 10px; + content: ''; + border: 2px solid rgba(6, 21, 40, 0.94); + border-radius: 999px; + background: var(--event-color, #1677ff); + box-shadow: 0 0 16px var(--event-color, #1677ff); + transform: translateY(-50%); +} + +.dashboard-event::after { + position: absolute; + top: 12px; + bottom: 12px; left: 0; width: 3px; content: ''; border-radius: 999px; - background: var(--event-color, #1677ff); - box-shadow: 0 0 14px var(--event-color, #1677ff); + background: var(--severity-color, #ff4d4f); + opacity: 0.72; +} + +.dashboard-event-time { + display: grid; + gap: 4px; + justify-items: end; + padding-right: 14px; + font-family: 'JetBrains Mono', Consolas, monospace; +} + +.dashboard-event-time span { + color: #f5faff; + font-size: 13px; + font-weight: 800; +} + +.dashboard-event-time small { + color: rgba(255, 255, 255, 0.42); + font-size: 10px; + text-transform: uppercase; +} + +.dashboard-event-body { + min-width: 0; } .dashboard-event-title { @@ -691,6 +997,10 @@ white-space: nowrap; } +.dashboard-event-title .ant-badge-status-text { + color: rgba(245, 250, 255, 0.7); +} + .dashboard-event-meta { display: flex; flex-wrap: wrap; @@ -699,6 +1009,12 @@ font-size: 12px; } +.dashboard-event-meta span:first-child { + color: var(--event-color, #69b1ff); + font-family: 'JetBrains Mono', Consolas, monospace; + font-weight: 800; +} + @media (max-width: 1380px) { .dashboard-posture-grid { grid-template-columns: minmax(300px, 0.9fr) minmax(520px, 1.8fr); diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 9760251..53da91b 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useMemo, useState, type ReactNode} from 'react' +import {useCallback, useEffect, useMemo, useState, type CSSProperties, type ReactNode} from 'react' import {Area, Gauge, Radar as RadarChart} from '@ant-design/charts' import {Alert, Badge, Button, Card, Progress, Segmented, Skeleton, Space, Statistic, Tag, Tooltip, Typography} from 'antd' import {InfoCircleOutlined, ReloadOutlined} from '@ant-design/icons' @@ -16,6 +16,16 @@ const windowOptions: { label: string; value: DashboardWindow }[] = [ ] const chartTheme = 'classicDark' as const const lucideIconProps = { size: 16, strokeWidth: 2 } +const riskBands = ['STABLE', 'GUARDED', 'ELEVATED', 'CRITICAL'] as const + +type RiskBand = typeof riskBands[number] +type MetricTone = 'critical' | 'warning' | 'signal' | 'success' | 'intel' | 'neutral' + +interface RiskPostureLevel { + band: RiskBand + color: string + summary: string +} function hasValues(data: DashboardLabelValue[]) { return data.some((item) => item.value > 0) @@ -53,6 +63,83 @@ function severityColor(severity: string) { return severityColors[severity] || '#1677ff' } +function getRiskPostureLevel(value: number | null | undefined): RiskPostureLevel { + const score = value ?? 0 + if (score >= 80) { + return { band: 'CRITICAL', color: '#ff4d4f', summary: 'Critical pressure requires command attention.' } + } + if (score >= 55) { + return { band: 'ELEVATED', color: '#fa8c16', summary: 'Elevated risk pressure across active signals.' } + } + if (score >= 35) { + return { band: 'GUARDED', color: '#fadb14', summary: 'Guarded posture with monitored signal pressure.' } + } + return { band: 'STABLE', color: '#52c41a', summary: 'Stable posture with no dominant pressure.' } +} + +function commandNarrative(data: DashboardOverview | null) { + if (!data) return 'Waiting for telemetry handshake.' + const {summary} = data + const posture = getRiskPostureLevel(summary.active_risk_index) + const leadSignal = summary.open_critical_cases > 0 + ? `${formatNumber(summary.open_critical_cases)} critical case${summary.open_critical_cases === 1 ? '' : 's'} open` + : summary.critical_high_alerts > 0 + ? `${formatNumber(summary.critical_high_alerts)} critical/high alert${summary.critical_high_alerts === 1 ? '' : 's'} observed` + : 'critical signal quiet' + const automation = summary.failed_playbooks > 0 + ? `${formatNumber(summary.failed_playbooks)} failed playbook${summary.failed_playbooks === 1 ? '' : 's'}` + : `${formatPercent(summary.automation_success_rate)} automation success` + return `${posture.band} posture · ${leadSignal} · ${automation}` +} + +function automationTone(summary: DashboardOverview['summary'] | undefined): MetricTone { + if (!summary) return 'neutral' + return summary.failed_playbooks > 0 ? 'critical' : 'success' +} + +function eventTimeParts(timestamp: string) { + const value = new Date(timestamp) + return { + time: value.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + date: value.toLocaleDateString([], { month: 'short', day: '2-digit' }), + } +} + +function compactMitreTacticLabel(label: string) { + const normalized = label.trim() + const labels: Record = { + Reconnaissance: 'Recon', + 'Resource Development': 'Resource', + 'Initial Access': 'Initial', + Execution: 'Exec', + Persistence: 'Persist', + 'Privilege Escalation': 'Priv Esc', + 'Defense Evasion': 'Defense', + 'Credential Access': 'Cred', + Discovery: 'Discover', + 'Lateral Movement': 'Lateral', + Collection: 'Collect', + 'Command and Control': 'C2', + 'Command & Control': 'C2', + Exfiltration: 'Exfil', + Impact: 'Impact', + } + if (labels[normalized]) return labels[normalized] + if (normalized.length <= 10) return normalized + return normalized + .replace(/\band\b/gi, '&') + .split(/\s+/) + .map((part) => part.slice(0, 4)) + .slice(0, 2) + .join(' ') +} + +function eventAccentColor(item: DashboardHighlight) { + if (item.kind === 'case') return statusColor(item.status) + if (item.status === 'Resolved' || item.status === 'Closed') return '#52c41a' + return '#b37feb' +} + function generatedAtLabel(value: string | undefined) { if (!value) return 'Waiting for telemetry' return `Last refresh ${new Date(value).toLocaleString()}` @@ -88,6 +175,9 @@ function MetricCard({ tooltip, accent = '#1677ff', sub, + category, + tone = 'signal', + priority = false, }: { title: string value: ReactNode @@ -95,6 +185,9 @@ function MetricCard({ tooltip?: string accent?: string sub?: ReactNode + category?: string + tone?: MetricTone + priority?: boolean }) { const statisticTitle = ( @@ -108,7 +201,8 @@ function MetricCard({ ) return ( - + + {category &&
{category}
} {sub &&
{sub}
}
@@ -122,6 +216,8 @@ function MeanTimeCard({ title, metric, tooltip, accent }: { title: string; metri value={formatDuration(metric.seconds)} tooltip={tooltip} accent={accent} + category="Response Clock" + tone="signal" sub={`${metric.sample_count} valid sample${metric.sample_count === 1 ? '' : 's'}`} /> ) @@ -130,29 +226,30 @@ function MeanTimeCard({ title, metric, tooltip, accent }: { title: string; metri function RiskPostureCard({ data, loading }: { data: DashboardOverview | null; loading: boolean }) { const riskIndex = data?.summary.active_risk_index ?? 0 const clampedRiskIndex = Math.min(100, Math.max(0, riskIndex)) + const riskLevel = getRiskPostureLevel(riskIndex) const gaugeConfig = useMemo(() => ({ data: { percent: clampedRiskIndex / 100, - thresholds: [0.55, 0.8, 1], + thresholds: [0.35, 0.55, 0.8, 1], }, height: 190, theme: chartTheme, scale: { color: { - range: ['#52c41a', '#fadb14', '#fa8c16'], + range: ['#52c41a', '#13c2c2', '#fa8c16', '#ff4d4f'], }, }, style: { arcLineWidth: 9, pointerLineWidth: 3, pointerLineCap: 'round', - pointerStroke: '#69b1ff', + pointerStroke: riskLevel.color, pinR: 7, pinFill: '#061528', - pinStroke: '#69b1ff', + pinStroke: riskLevel.color, textContent: () => '', }, - }), [clampedRiskIndex]) + }), [clampedRiskIndex, riskLevel.color]) return ( ) : ( -
+
-
{riskIndex}
+
+
{riskIndex}
+ {riskLevel.band} +
weighted pressure / 100
+
{riskLevel.summary}
+
+ {riskBands.map((band) => ( + {band} + ))} +
Open cases{formatNumber(data?.summary.open_cases)}
Critical cases{formatNumber(data?.summary.open_critical_cases)}
@@ -198,7 +304,7 @@ function statusColor(status: string) { } function signalColor(index: number) { - return ['#69b1ff', '#13c2c2', '#b37feb', '#f759ab', '#faad14', '#52c41a'][index % 6] + return ['#69b1ff', '#13c2c2', '#b37feb', '#597ef7', '#36cfc9', '#9254de'][index % 6] } function ChartPanel({ title, children, compact = false, className = '' }: { title: ReactNode; children: ReactNode; compact?: boolean; className?: string }) { @@ -214,7 +320,7 @@ function AlertTrend({ data }: { data: DashboardLabelValue[] }) { data, xField: 'label', yField: 'value', - height: 150, + height: 190, autoFit: true, smooth: true, colorField: 'value', @@ -226,7 +332,7 @@ function AlertTrend({ data }: { data: DashboardLabelValue[] }) { } function SeverityPressure({ data }: { data: DashboardLabelValue[] }) { - const visibleData = data.filter((item) => item.label !== 'Info') + const visibleData = data.filter((item) => !['Info', 'Informational'].includes(item.label)) const maxValue = Math.max(...visibleData.map((item) => item.value), 0) const total = visibleData.reduce((sum, item) => sum + item.value, 0) @@ -358,12 +464,17 @@ function ThreatKeywordCloud({ data }: { data: DashboardKeyword[] }) { function SecurityDomainRadar({ data }: { data: DashboardLabelValue[] }) { const visibleData = data.filter((item) => item.value > 0) if (!hasValues(visibleData)) return - const maxValue = Math.max(...visibleData.map((item) => item.value), 1) + const radarData = visibleData.map((item) => ({ + ...item, + fullLabel: item.label, + label: compactMitreTacticLabel(item.label), + })) + const maxValue = Math.max(...radarData.map((item) => item.value), 1) const config = { - data: visibleData, + data: radarData, xField: 'label', yField: 'value', - height: 260, + height: 220, autoFit: true, theme: chartTheme, coordinateType: 'polar' as const, @@ -395,7 +506,20 @@ function SecurityDomainRadar({ data }: { data: DashboardLabelValue[] }) { }, } - return + return ( +
+ +
+ {radarData.map((item, index) => ( + + + {item.label} + {item.fullLabel} + + ))} +
+
+ ) } function MitreSeverityMatrix({ data }: { data: DashboardMitreSeverityCell[] }) { @@ -514,21 +638,43 @@ function HighlightStream({ highlights }: { highlights: DashboardHighlight[] }) { return (
- {highlights.map((item) => ( -
-
- {item.title || item.readable_id} - + {highlights.map((item) => { + const timestamp = eventTimeParts(item.timestamp) + const eventAccent = eventAccentColor(item) + return ( +
+
+ {timestamp.time} + {timestamp.date} +
+
+
+ {item.title || item.readable_id} + +
+
+ {item.kind.toUpperCase()} + {item.readable_id} + {item.status || 'Unknown'} + {item.subtitle} +
+
-
- {item.kind.toUpperCase()} - {item.readable_id} - {item.status || 'Unknown'} - {item.subtitle} - {new Date(item.timestamp).toLocaleString()} -
-
- ))} + ) + })} +
+ ) +} + +function CommandSignalPill({ label, value, color, emphasis = false }: { label: string; value: ReactNode; color: string; emphasis?: boolean }) { + return ( +
+ {label} + {value}
) } @@ -558,15 +704,20 @@ export default function Dashboard() { }, [loadOverview, selectedWindow]) const summary = data?.summary + const riskLevel = getRiskPostureLevel(summary?.active_risk_index) return (
-
+
Cyber Command Center

Security Posture Dashboard

+
+ {riskLevel.band} + {commandNarrative(data)} +
{error && } @@ -586,18 +737,25 @@ export default function Dashboard() {
+
+ + + + + +
- - - - - - + + + + + +
- Alert Telemetry Trend} compact> + Alert Telemetry Trend} className="dashboard-trend-card"> {loading && !data ? : } Automation State}> @@ -650,8 +808,8 @@ export default function Dashboard() { Threat Keyword Cloud} className="dashboard-keyword-card"> {loading && !data ? : } - Security Domain Radar}> - {loading && !data ? : } + MITRE Tactic Radar}> + {loading && !data ? : } MITRE Severity Matrix} className="dashboard-mitre-card"> {loading && !data ? : }