105 lines
5.4 KiB
TypeScript
105 lines
5.4 KiB
TypeScript
import { useApp } from '../../app/AppProvider';
|
|
import { useEnginePreferences } from '../../app/engine-preferences';
|
|
import type { FindingRecord } from '../../domain/finding';
|
|
import { gpoWorkSettingIdentity } from '../results-browser/work-record-lookup';
|
|
import type { DynamicFacet } from '../search-filter/dynamic-facets';
|
|
import { GPO_SOURCE_ID, type TimeslotChange, type TimeslotChangeEntry, type TimeslotComparisonReport } from '../../comparison/timeslot-comparison-report';
|
|
import { usePublishedComparison, type PublishedComparison } from './use-timeslot-comparison';
|
|
|
|
/** Work facet of the Comparison Engine; present only while a comparison of the open timeslot B is loaded. */
|
|
export const COMPARISON_CHANGE_FACET_ID = 'comparison:change';
|
|
const SOURCE_SCAN_FACET_ID = 'provenance:source-scan';
|
|
|
|
/** Facet order: better first, then worse, then neutral and unclear states. */
|
|
export const CHANGE_LABELS: Array<{ change: TimeslotChange; label: string }> = [
|
|
{ change: 'resolved', label: 'Resolved' },
|
|
{ change: 'new', label: 'New' },
|
|
{ change: 'regressed', label: 'Regressed' },
|
|
{ change: 'value-changed', label: 'Value changed' },
|
|
{ change: 'unchanged', label: 'Unchanged' },
|
|
{ change: 'unverified', label: 'Unverified, still open' },
|
|
{ change: 'not-applicable', label: 'Not applicable' },
|
|
{ change: 'not-comparable', label: 'Not comparable' }
|
|
];
|
|
|
|
export interface ComparisonWorkFacetIndex {
|
|
facets: DynamicFacet[];
|
|
/** The change of a Work record of timeslot B, if the comparison names one. */
|
|
changeOf(record: FindingRecord): TimeslotChange | undefined;
|
|
/** The full comparison entry behind that change, for the Finding Reader. */
|
|
entryOf(record: FindingRecord): TimeslotChangeEntry | undefined;
|
|
comparison: PublishedComparison;
|
|
}
|
|
|
|
const gpoKey = (scope: unknown, identity: unknown) => JSON.stringify([scope, identity]);
|
|
|
|
/**
|
|
* Maps the report onto Work records of timeslot B: scanner records by occurrence (a current record, or the
|
|
* B counterpart of an A deviation), GPO settings by scope and technical identity, like Result DB drill-downs.
|
|
* A deviation of A without a B record has no Work row in B and is therefore not offered.
|
|
*/
|
|
function entryIndex(report: TimeslotComparisonReport) {
|
|
const byOccurrence = new Map<string, TimeslotChangeEntry>();
|
|
const bySetting = new Map<string, TimeslotChangeEntry>();
|
|
for (const entry of report.changes) {
|
|
if (entry.sourceId === GPO_SOURCE_ID) {
|
|
if (entry.identityKey && (entry.side === 'target' || entry.counterpartIds.length)) bySetting.set(entry.identityKey, entry);
|
|
continue;
|
|
}
|
|
for (const id of entry.side === 'target' ? entry.recordIds : entry.counterpartIds) byOccurrence.set(id, entry);
|
|
}
|
|
return (record: FindingRecord): TimeslotChangeEntry | undefined => {
|
|
if (record.findingOccurrenceId) return byOccurrence.get(record.findingOccurrenceId);
|
|
const identity = gpoWorkSettingIdentity(record.id);
|
|
return identity ? bySetting.get(gpoKey(identity.scope, identity.settingId)) : undefined;
|
|
};
|
|
}
|
|
|
|
/** Changes that have at least one Work record in timeslot B, so the Compare tab can open them in Work. */
|
|
export function changesWithWorkRecords(report: TimeslotComparisonReport): Set<TimeslotChange> {
|
|
return new Set(report.changes.filter(entry => entry.side === 'target' || entry.counterpartIds.length).map(entry => entry.change));
|
|
}
|
|
|
|
const indexes = new WeakMap<PublishedComparison, ComparisonWorkFacetIndex>();
|
|
export function comparisonWorkFacetIndex(comparison: PublishedComparison): ComparisonWorkFacetIndex {
|
|
const existing = indexes.get(comparison);
|
|
if (existing) return existing;
|
|
const entryOf = entryIndex(comparison.report);
|
|
const changeOf = (record: FindingRecord) => entryOf(record)?.change;
|
|
const date = new Date(comparison.baseline.date);
|
|
const label = Number.isNaN(date.getTime()) ? comparison.baseline.date
|
|
: date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
|
|
const index: ComparisonWorkFacetIndex = {
|
|
comparison,
|
|
changeOf,
|
|
entryOf,
|
|
facets: [{
|
|
id: COMPARISON_CHANGE_FACET_ID,
|
|
label: 'Change',
|
|
source: 'derived',
|
|
kind: 'category',
|
|
description: `Comparison Engine: change of each finding and GPO setting against timeslot A (${label}). It does not change results.`,
|
|
values: CHANGE_LABELS.map(item => ({ value: item.change, label: item.label, count: 0 })),
|
|
valuesFor: record => { const change = changeOf(record); return change ? [change] : []; }
|
|
}]
|
|
};
|
|
indexes.set(comparison, index);
|
|
return index;
|
|
}
|
|
|
|
/** Adds the `Change` facet directly before the permanent final `Source: Scan` control. */
|
|
export function withComparisonFacets(facets: DynamicFacet[], index: ComparisonWorkFacetIndex | undefined): DynamicFacet[] {
|
|
if (!index) return facets;
|
|
const sourceScan = facets.findIndex(facet => facet.id === SOURCE_SCAN_FACET_ID);
|
|
return sourceScan < 0 ? [...facets, ...index.facets]
|
|
: [...facets.slice(0, sourceScan), ...index.facets, ...facets.slice(sourceScan)];
|
|
}
|
|
|
|
/** The facet index for Work, or undefined while the engine is off or no comparison of the open timeslot is loaded. */
|
|
export function useComparisonWorkFacets(): ComparisonWorkFacetIndex | undefined {
|
|
const app = useApp();
|
|
const enabled = useEnginePreferences().comparison;
|
|
const comparison = usePublishedComparison(app.targetProjectId, app.activeAssessmentIds ?? []);
|
|
return enabled && comparison ? comparisonWorkFacetIndex(comparison) : undefined;
|
|
}
|