63 lines
4.6 KiB
TypeScript
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>;
|
|
}
|