136 lines
5.8 KiB
TypeScript
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>
|
|
);
|
|
}
|