import { memo, type CSSProperties, type KeyboardEvent } from 'react'; import type { FindingRecord } from '../../domain/finding'; import { normalizeSeverity } from '../../profiles/purple-knight/presentation'; import { outcomeForStatusValue, toolIdForRecord } from '../../importers/status-normalization'; import { resultStatusFieldFor } from '../../features/linked-results/result-message'; import { highlightText } from '../HighlightText/highlight-text'; import { BookmarkIcon } from '../BookmarkIcon'; import { cisBenchmarkCategories } from '../../profiles/cis-cat/cis-benchmark-hierarchy'; export interface TileBadge { label: string; value: string; text: string; severity?: string; } export function tileBadges(record: FindingRecord, fallbackToolId?: string): TileBadge[] { const requested = [ { label: 'Status', pattern: /^(result|status|evaluation)$/i }, { label: 'Category', pattern: /^category$/i }, { label: 'Severity', pattern: /^(severity|criticality|priority)$/i }, { label: 'Weight', pattern: /^weight$/i }, { label: 'Score', pattern: /^score$/i } ]; const badges = requested.flatMap(({ label, pattern }) => { const field = label === 'Status' ? resultStatusFieldFor(record, fallbackToolId) : record.fields.find(candidate => { if (!candidate.value) return false; return pattern.test(candidate.label.trim()); }); if (!field) return []; const text = /^(Weight|Score)$/.test(label) ? `${label}: ${field.value}` : field.value; return [{ label, value: field.value, text, ...(label === 'Severity' ? { severity: normalizeSeverity(field.value) } : {}) }]; }); const cisBadges = cisBenchmarkCategories(record, fallbackToolId).map(value => ({ label: 'CIS Category', value, text: value })); const orderedBadges = [ ...badges.filter(badge => badge.label === 'Status' || badge.label === 'Category'), ...cisBadges, ...badges.filter(badge => badge.label !== 'Status' && badge.label !== 'Category') ]; return orderedBadges.filter((badge, index) => !orderedBadges.slice(0, index).some(previous => previous.value.trim().toLocaleLowerCase('en-US') === badge.value.trim().toLocaleLowerCase('en-US'))); } export function compactTileBadges(badges: TileBadge[], limit?: number): TileBadge[] { if (!limit || badges.length <= limit) return badges; const cisBadges = badges.filter(badge => badge.label === 'CIS Category'); const preferred = cisBadges.length ? [badges.find(badge => badge.label === 'Status'), badges.find(badge => badge.label === 'Weight'), cisBadges.at(-1)] : [badges.find(badge => badge.label === 'Status'), badges.find(badge => badge.label === 'Category'), badges.find(badge => badge.label === 'Severity')]; const selected = preferred.filter((badge, index, candidates): badge is TileBadge => Boolean(badge) && candidates.indexOf(badge) === index); for (const badge of badges) { if (selected.length >= limit - 1) break; if (!selected.includes(badge)) selected.push(badge); } const hidden = badges.length - selected.length; return [...selected, { label: 'More', value: String(hidden), text: `+${hidden}` }]; } interface Props { record: FindingRecord; title: string; bookmarked: boolean; resultColor?: string; /** Werkzeug-ID des Datasets; entscheidet ueber die werkzeuggebundene Statusnormalisierung. */ toolId?: string; search?: string; badgeLimit?: number; onOpen(): void; onBookmark(): void; onResults?(): void; } function TileCardComponent({ record, title, bookmarked, resultColor, toolId, search, badgeLimit, onOpen, onBookmark, onResults }: Props) { const severityField = record.fields.find(field => /^(severity|criticality|priority)$/i.test(field.label) && field.value); const severity = normalizeSeverity(severityField?.value); const recordToolId = toolIdForRecord(record, toolId); const badges = tileBadges(record, recordToolId); const visibleBadges = compactTileBadges(badges, badgeLimit); const style = resultColor ? ({ '--result-color': resultColor } as CSSProperties) : undefined; const activate = (event: KeyboardEvent) => { if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return; event.preventDefault(); onOpen(); }; return (
); } export const TileCard = memo(TileCardComponent, (previous, next) => previous.record === next.record && previous.title === next.title && previous.bookmarked === next.bookmarked && previous.resultColor === next.resultColor && previous.toolId === next.toolId && previous.search === next.search && previous.badgeLimit === next.badgeLimit && Boolean(previous.onResults) === Boolean(next.onResults) );