Assessor_2.0/src/features/project-import/GpoStatusRow.tsx

63 lines
4.6 KiB
TypeScript

import type { GpoBackupStatusRecord } from '../../api/assessor-api-contract';
import type { GpoEvaluationUpdate } from '../../domain/policy-analysis-overview';
import { GpoFolderImport } from './GpoFolderImport';
import { missingGpoSourceMessage, unavailableGpoStorageMessage } from './gpo-source-recovery';
const labels: Record<GpoBackupStatusRecord['state'], string> = {
queued: 'Queued', running: 'Processing', succeeded: 'Ready', partial: 'Limited analysis',
'dead-letter': 'Failed', 'recovery-required': 'Recovery pending',
'upload-required': 'Upload required', 'storage-unavailable': 'Storage unavailable'
};
/** Names every retained input that changed, so the cause stays visible before re-evaluating. */
export function gpoReevaluationCause(updates: readonly GpoEvaluationUpdate[]): string {
const reasons = new Set(updates.flatMap(update => update.reasons ?? ['engine-version']));
const causes = [
...(reasons.has('engine-version')
? [`Engine ${[...new Set(updates.map(update => `${update.previousVersion} → ${update.currentVersion}`))].join(', ')}`]
: []),
...(reasons.has('baseline-analysis') ? ['Updated Microsoft baseline analysis'] : [])
];
return causes.join(' · ');
}
export function GpoStatusRow({ projectId, record, imported, busy, onEvaluate, onReprocess, onRetry, onDelete }: {
projectId: string;
record: GpoBackupStatusRecord; imported: string; busy: boolean;
onEvaluate(): void; onReprocess(): void; onRetry(): void; onDelete(): void;
}) {
const pending = ['queued', 'running'].includes(record.jobStatus ?? record.state);
const sourceBlocked = record.state === 'upload-required' || record.state === 'storage-unavailable';
const failed = ['dead-letter', 'recovery-required'].includes(record.state) || sourceBlocked;
// Reprocessing rebuilds what parsing retained and therefore precedes re-evaluation.
const analysis = record.analysisUpdate;
const outdated = Boolean(record.evaluationUpdates?.length) || Boolean(analysis);
const cause = analysis
? `Analyzer ${analysis.previousAnalyzerVersion} → ${analysis.currentAnalyzerVersion}`
: gpoReevaluationCause(record.evaluationUpdates ?? []);
return <div className="tenant-report-row tenant-report-gpo" role="row">
<span className="tenant-gpo-source-marker" aria-label="Policy analysis source">G</span>
<span className="tenant-report-copy"><b>GPO Backup</b>
<small>{record.fileName} · {record.gpoCount.toLocaleString('en-US')} GPO{record.gpoCount === 1 ? '' : 's'}
{Boolean(record.notLinkedGpoCount) && <span className="gpo-status-row__not-linked" title="No enabled link to the domain or an OU in the backup report; these GPOs apply to no device."> · {record.notLinkedGpoCount!.toLocaleString('en-US')} not linked</span>}</small>
{sourceBlocked && <small role="status">{record.state === 'upload-required' ? missingGpoSourceMessage : unavailableGpoStorageMessage}</small>}
{sourceBlocked && outdated && !analysis && <small role="status">Re-evaluation required · {cause}. Retained inputs can still be re-evaluated.</small>}
{outdated && !sourceBlocked && <small role="status">{analysis ? 'Reprocessing required' : 'Re-evaluation required'} · {cause}. Saved results are historical. {analysis ? 'Reprocess the stored source' : 'Re-evaluate retained inputs'}; no file upload is required for this update.</small>}
</span>
<span className="tenant-report-imported">{imported}</span>
<b className="tenant-report-status" data-status={outdated && !pending && !failed ? 're-evaluation-required' : record.state}>
{outdated && !pending && !failed ? analysis ? 'Reprocessing required' : 'Re-evaluation required' : labels[record.state]}
</b>
<span className="tenant-gpo-row-actions">
{record.state === 'upload-required' && <GpoFolderImport projectId={projectId} assessmentId={record.assessmentId}
restoreArtifactId={record.artifactId} showStatus={false} disabled={busy}
label="Upload original folder" archiveLabel="Upload original ZIP"/>}
{analysis
? !sourceBlocked && <button className="btn compact" type="button" disabled={busy || pending || failed} onClick={onReprocess}>Reprocess</button>
: outdated && <button className="btn compact" type="button" disabled={busy || pending || (failed && !sourceBlocked)} onClick={onEvaluate}>Re-evaluate</button>}
{record.state === 'dead-letter' && <button className="btn compact" type="button" disabled={busy} onClick={onRetry}>Retry</button>}
<button className="btn compact danger" type="button" disabled={busy || pending} onClick={onDelete}>Delete</button>
</span>
</div>;
}