Assessor_2.0/src/features/context-layer/ContextHeatmap.tsx

45 lines
2.8 KiB
TypeScript

import { useId, useMemo } from 'react';
import { useApp } from '../../app/AppProvider';
import type { AssessmentContextResponse } from '../../api/assessor-api-contract';
import { contextHeatmap, contextHeatmapFindingCount, contextSeverityLabels } from '../../assessment-context/context-heatmap';
import { useAssessmentContext } from './use-assessment-context';
import './context-heatmap.css';
/** Existing scoped Context request is shared with Work and the Finding Reader. */
export function ClientContextHeatmap({ onOpenCluster }: { onOpenCluster(clusterId: string): void }) {
const app = useApp();
const state = useAssessmentContext(Boolean(app.dataset), app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds, app.dataset);
if (state.status === 'off' || state.status === 'unavailable') return null;
if (state.status === 'loading') return <section className="context-heatmap" aria-label="Context heatmap" aria-busy="true">
<h2>Context heatmap</h2><p role="status">Loading Context…</p>
</section>;
if (state.status === 'error') return <section className="context-heatmap" aria-label="Context heatmap">
<h2>Context heatmap</h2><p role="alert">Context could not be loaded. Reopen the client to retry.</p>
</section>;
return <ContextHeatmap context={state.context} onOpenCluster={onOpenCluster} />;
}
export function ContextHeatmap({ context, onOpenCluster }: { context: AssessmentContextResponse; onOpenCluster(clusterId: string): void }) {
const id = useId();
const cells = useMemo(() => contextHeatmap(context), [context]);
const findingCount = useMemo(() => contextHeatmapFindingCount(context), [context]);
return <section className="context-heatmap" aria-labelledby={`${id}-title`}>
<header className="context-heatmap__header">
<h2 id={`${id}-title`}>Context heatmap</h2>
<span className="context-heatmap__total" aria-label="Unique findings represented by the heatmap">Findings: {findingCount.toLocaleString('en-US')}</span>
</header>
<div className="context-heatmap__grid" aria-label="Context clusters">
{cells.map(({ cluster: item, reference, hasEvidence, scannerDeviations, gpoDeviations }) => <button
key={item.clusterId} type="button" className="context-heatmap__cell"
data-cluster={item.clusterId} data-severity={hasEvidence ? reference.severity : 'neutral'}
aria-label={`Open ${item.label.en} findings in Work (${scannerDeviations + gpoDeviations})`}
onClick={() => onOpenCluster(item.clusterId)}>
<span className="context-heatmap__category">Base Severity · {contextSeverityLabels[reference.severity]}</span>
<strong>{item.label.en}</strong>
<span className="context-heatmap__amount">{(scannerDeviations + gpoDeviations).toLocaleString('en-US')} {scannerDeviations + gpoDeviations === 1 ? 'finding' : 'findings'}</span>
</button>)}
</div>
</section>;
}