feat(compliance): add 'View in Reporting' link for 2.3.x Ivanti metrics
In ComplianceDetailPanel, active metrics with a metric_id starting with '2.3' and an Ivanti_Vulnerability_ID in extra_json now surface the ID prominently alongside a 'View in Reporting →' button. Clicking navigates directly to the Reporting page. onNavigate prop threaded through App → CompliancePage → ComplianceDetailPanel → MetricRow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1044,7 +1044,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Page content */}
|
{/* Page content */}
|
||||||
{currentPage === 'reporting' && <ReportingPage filterDate={calendarFilter} filterEXC={reportingExcFilter} />}
|
{currentPage === 'reporting' && <ReportingPage filterDate={calendarFilter} filterEXC={reportingExcFilter} />}
|
||||||
{currentPage === 'compliance' && <CompliancePage />}
|
{currentPage === 'compliance' && <CompliancePage onNavigate={setCurrentPage} />}
|
||||||
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
||||||
{currentPage === 'exports' && <ExportsPage />}
|
{currentPage === 'exports' && <ExportsPage />}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function MetricChip({ metricId, category, status }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded }) {
|
export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded, onNavigate }) {
|
||||||
const [detail, setDetail] = useState(null);
|
const [detail, setDetail] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -158,7 +158,7 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded }
|
|||||||
{activeMetrics.length > 0 && (
|
{activeMetrics.length > 0 && (
|
||||||
<Section title="Failing Metrics" icon={<Shield style={{ width: '14px', height: '14px' }} />}>
|
<Section title="Failing Metrics" icon={<Shield style={{ width: '14px', height: '14px' }} />}>
|
||||||
{activeMetrics.map(m => (
|
{activeMetrics.map(m => (
|
||||||
<MetricRow key={m.metric_id} metric={m} />
|
<MetricRow key={m.metric_id} metric={m} onNavigate={onNavigate} />
|
||||||
))}
|
))}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
@@ -288,10 +288,14 @@ function Section({ title, icon, children, muted, grow }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetricRow({ metric, resolved }) {
|
function MetricRow({ metric, resolved, onNavigate }) {
|
||||||
const color = resolved ? '#475569' : categoryColor(metric.category);
|
const color = resolved ? '#475569' : categoryColor(metric.category);
|
||||||
const extra = metric.extra || {};
|
const extra = metric.extra || {};
|
||||||
|
|
||||||
|
const ivantiId = (!resolved && metric.metric_id?.startsWith('2.3'))
|
||||||
|
? (extra['Ivanti_Vulnerability_ID'] || null)
|
||||||
|
: null;
|
||||||
|
|
||||||
// Surface the most useful extra fields per metric type
|
// Surface the most useful extra fields per metric type
|
||||||
const highlights = [];
|
const highlights = [];
|
||||||
if (extra['CVEs_Associated']) highlights.push({ label: 'CVEs', value: extra['CVEs_Associated'] });
|
if (extra['CVEs_Associated']) highlights.push({ label: 'CVEs', value: extra['CVEs_Associated'] });
|
||||||
@@ -317,10 +321,38 @@ function MetricRow({ metric, resolved }) {
|
|||||||
{resolved && <span style={{ fontSize: '0.68rem', color: '#334155', fontFamily: 'monospace' }}>resolved {metric.resolved_on || ''}</span>}
|
{resolved && <span style={{ fontSize: '0.68rem', color: '#334155', fontFamily: 'monospace' }}>resolved {metric.resolved_on || ''}</span>}
|
||||||
</div>
|
</div>
|
||||||
{metric.metric_desc && (
|
{metric.metric_desc && (
|
||||||
<div style={{ fontSize: '0.72rem', color: '#475569', marginBottom: highlights.length ? '0.4rem' : 0, lineHeight: 1.4 }}>
|
<div style={{ fontSize: '0.72rem', color: '#475569', marginBottom: (highlights.length || ivantiId) ? '0.4rem' : 0, lineHeight: 1.4 }}>
|
||||||
{metric.metric_desc.length > 100 ? metric.metric_desc.slice(0, 100) + '…' : metric.metric_desc}
|
{metric.metric_desc.length > 100 ? metric.metric_desc.slice(0, 100) + '…' : metric.metric_desc}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{ivantiId && (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: highlights.length ? '0.25rem' : 0 }}>
|
||||||
|
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', minWidth: 0 }}>
|
||||||
|
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', flexShrink: 0 }}>Ivanti ID</span>
|
||||||
|
<span style={{ fontSize: '0.68rem', color: '#94A3B8', fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ivantiId}</span>
|
||||||
|
</div>
|
||||||
|
{onNavigate && (
|
||||||
|
<button
|
||||||
|
onClick={e => { e.stopPropagation(); onNavigate('reporting'); }}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0, marginLeft: '0.5rem',
|
||||||
|
background: 'rgba(14,165,233,0.1)',
|
||||||
|
border: '1px solid rgba(14,165,233,0.4)',
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
color: '#0EA5E9',
|
||||||
|
fontSize: '0.65rem', fontFamily: 'monospace',
|
||||||
|
padding: '0.2rem 0.5rem',
|
||||||
|
cursor: 'pointer', whiteSpace: 'nowrap',
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.18)'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.7)'; }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.1)'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.4)'; }}
|
||||||
|
>
|
||||||
|
View in Reporting →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{highlights.map(h => (
|
{highlights.map(h => (
|
||||||
<div key={h.label} style={{ display: 'flex', gap: '0.4rem', marginTop: '0.25rem' }}>
|
<div key={h.label} style={{ display: 'flex', gap: '0.4rem', marginTop: '0.25rem' }}>
|
||||||
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', minWidth: '48px' }}>{h.label}</span>
|
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', minWidth: '48px' }}>{h.label}</span>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ function SeenBadge({ count }) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Main Page
|
// Main Page
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
export default function CompliancePage() {
|
export default function CompliancePage({ onNavigate }) {
|
||||||
const { canWrite } = useAuth();
|
const { canWrite } = useAuth();
|
||||||
|
|
||||||
const [activeTeam, setActiveTeam] = useState('STEAM');
|
const [activeTeam, setActiveTeam] = useState('STEAM');
|
||||||
@@ -424,6 +424,7 @@ export default function CompliancePage() {
|
|||||||
hostname={selectedHost}
|
hostname={selectedHost}
|
||||||
onClose={() => setSelectedHost(null)}
|
onClose={() => setSelectedHost(null)}
|
||||||
onNoteAdded={refresh}
|
onNoteAdded={refresh}
|
||||||
|
onNavigate={onNavigate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user