Assessor_2.0/src/features/sidebar/SidebarSecurityDomains.tsx

136 lines
5.8 KiB
TypeScript

import { useMemo } from 'react';
import { useApp } from '../../app/AppProvider';
import type { DatasetSheet, ImportedDataset } from '../../domain/dataset';
import type { ImportProfile } from '../../domain/import-profile';
import type { Review } from '../../domain/review';
import { measureNavigationStep } from '../../app/navigation-diagnostics';
import { SECURITY_DOMAIN_FACET_ID, securityDomainCatalog } from '../dashboard/security-domains';
import { type DynamicFacetFilters } from '../search-filter/dynamic-facets';
import { selectWorkFacets } from '../search-filter/work-facets';
import type { ContextClusterIndex } from '../context-layer/context-cluster-facet';
import { useContextClusterIndex } from '../context-layer/use-context-cluster-index';
import { businessImpactControls, BUSINESS_IMPACT_FACET_ID } from '../search-filter/business-impact-controls';
import { useEnginePreferences } from '../../app/engine-preferences';
interface SecurityDomainOptions {
sheet: DatasetSheet | undefined;
dataset: ImportedDataset | null;
activeProfile: ImportProfile;
reviews: Record<string, Review>;
statusFilter: string;
facetFilters: DynamicFacetFilters;
pathFindingIds?: readonly string[];
search: string;
actionOnly: boolean;
contextClusters?: ContextClusterIndex;
}
export interface SidebarSecurityDomain {
id: string;
label: string;
count: number;
}
export interface SidebarSecurityDomainSummary {
facetId: typeof SECURITY_DOMAIN_FACET_ID;
domains: SidebarSecurityDomain[];
}
/** Uses the exact shared Client Info facet, ordering and self-excluding counts. */
export function summarizeSecurityDomains(options: SecurityDomainOptions): SidebarSecurityDomainSummary | undefined {
const { sheet, dataset, activeProfile, reviews, statusFilter, facetFilters, pathFindingIds, search, actionOnly, contextClusters } = options;
if (!sheet) return undefined;
const facets = selectWorkFacets(sheet, {
dataset, activeProfile, reviews, statusFilter, facetFilters, pathFindingIds, search, actionOnly, contextClusters
});
const domainFacet = facets.find(facet => facet.id === SECURITY_DOMAIN_FACET_ID);
if (!domainFacet) return undefined;
const counts = new Map(domainFacet.values.map(value => [value.value, value.count]));
const domains: SidebarSecurityDomain[] = securityDomainCatalog.map(domain => ({
id: domain.id,
label: domain.label,
count: counts.get(domain.id) ?? 0
}));
return { facetId: SECURITY_DOMAIN_FACET_ID, domains };
}
export function toggleSecurityDomainSelection(selected: string[], domainId: string): string[] {
return selected.includes(domainId) ? [] : [domainId];
}
export function SidebarSecurityDomains({ sheet }: { sheet: DatasetSheet | undefined }) {
const {
dataset,
activeProfile,
reviews,
statusFilter,
facetFilters,
pathFindingIds,
search,
actionOnly,
setFacetFilter,
setStatusFilter
} = useApp();
const contextClusters = useContextClusterIndex();
const engines = useEnginePreferences();
const impactCounts = useMemo(() => {
if (!engines.report || !engines.context || !contextClusters) return undefined;
const facet = selectWorkFacets(sheet, { dataset, activeProfile, reviews, statusFilter: 'deviation', facetFilters,
pathFindingIds, search, actionOnly, contextClusters }).find(item => item.id === BUSINESS_IMPACT_FACET_ID);
if (!facet) return undefined;
const counts = new Map(facet.values.map(value => [value.value, value.count]));
return businessImpactControls.map(theme => ({ ...theme, count: counts.get(theme.id) ?? 0 }));
}, [engines.report, engines.context, contextClusters, sheet, dataset, activeProfile, reviews, facetFilters, pathFindingIds, search, actionOnly]);
const summary = useMemo(() => measureNavigationStep('security-domain-summary', () => summarizeSecurityDomains({
sheet,
dataset,
activeProfile,
reviews,
statusFilter,
facetFilters,
pathFindingIds,
search,
actionOnly,
contextClusters
})), [actionOnly, activeProfile, contextClusters, dataset, facetFilters, pathFindingIds, reviews, search, sheet, statusFilter]);
if (!summary) return null;
const selected = facetFilters[summary.facetId]?.values ?? [];
return (
<section className="sidebar-domain-rack" aria-labelledby="sidebar-domain-heading">
<h2 id="sidebar-domain-heading">Security Domains</h2>
{summary.domains.map(domain => {
const active = selected.includes(domain.id);
return <button
aria-label={`${domain.label}: ${domain.count.toLocaleString('en-US')}`}
aria-pressed={active}
className="sidebar-domain-button"
key={domain.id}
type="button"
onClick={() => {
const next = toggleSecurityDomainSelection(selected, domain.id);
setFacetFilter(summary.facetId, next.length ? { values: next } : null);
}}
>
<span>{domain.label}</span>
<strong>{domain.count.toLocaleString('en-US')}</strong>
</button>;
})}
{impactCounts && <section className="sidebar-business-impact" aria-labelledby="sidebar-business-impact-heading">
<h2 id="sidebar-business-impact-heading">Possible Business Impact</h2>
{impactCounts.map(theme => {
const active = facetFilters[BUSINESS_IMPACT_FACET_ID]?.values?.includes(theme.id) ?? false;
return <button type="button" className="sidebar-domain-button" key={theme.id} aria-pressed={active}
aria-label={`${theme.label}: ${theme.count.toLocaleString('en-US')}`} disabled={!active && theme.count === 0}
onClick={() => {
setStatusFilter('deviation');
setFacetFilter(BUSINESS_IMPACT_FACET_ID, active ? null : { values: [theme.id] });
}}><span>{theme.label}</span><strong>{theme.count.toLocaleString('en-US')}</strong></button>;
})}
</section>}
</section>
);
}