85 lines
5.8 KiB
TypeScript
85 lines
5.8 KiB
TypeScript
import { useCallback, useEffect, useState, type FormEvent } from 'react';
|
|
import { AssessorApiClient } from '../../api/assessor-api-client';
|
|
import { useEnginePreferences } from '../../app/engine-preferences';
|
|
import { useBackendWorkActivity } from './use-backend-work-activity';
|
|
import { statusPollApi } from './status-poll-client';
|
|
import type { RegulatoryCorpusStatus } from '../regulatory-corpus-management';
|
|
import { TriangleIcon } from '../../components/TriangleIcon';
|
|
|
|
const api = new AssessorApiClient();
|
|
const MAXIMUM_PDF_BYTES = 32 * 1024 * 1024;
|
|
|
|
/** Data Hub ISO corpus tile; a switched-off Compliance Engine renders nothing and requests nothing. */
|
|
export function IsoCorpusLibrary() {
|
|
return useEnginePreferences().compliance ? <IsoCorpusLibraryPanel/> : null;
|
|
}
|
|
|
|
function IsoCorpusLibraryPanel() {
|
|
const [status, setStatus] = useState<RegulatoryCorpusStatus | null>(null);
|
|
const [iso27001, setIso27001] = useState<File | null>(null);
|
|
const [iso27002, setIso27002] = useState<File | null>(null);
|
|
const [token, setToken] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const load = useCallback(async (poll = false) => {
|
|
try { setStatus(await (poll ? statusPollApi : api).getIsoCorpusStatus()); setError(null); }
|
|
catch (reason) { setError(reason instanceof Error ? reason.message : 'ISO corpus status could not be loaded.'); }
|
|
}, []);
|
|
useEffect(() => { void load(); }, [load]);
|
|
const job = status?.job?.status;
|
|
useBackendWorkActivity('Embedding ISO corpus', job === 'running' ? 'running' : job === 'failed' ? 'failed' : job === 'succeeded' ? 'finished' : 'idle');
|
|
useEffect(() => {
|
|
if (status?.job?.status !== 'running') return;
|
|
const interval = setInterval(() => { void load(true); }, 2_000);
|
|
return () => clearInterval(interval);
|
|
}, [load, status?.job?.status]);
|
|
|
|
const submit = async (event: FormEvent) => {
|
|
event.preventDefault();
|
|
const form = event.currentTarget as HTMLFormElement;
|
|
if (!iso27001 || !iso27002 || !token) return;
|
|
if (iso27001.size > MAXIMUM_PDF_BYTES || iso27002.size > MAXIMUM_PDF_BYTES) {
|
|
setError('Each PDF must be no larger than 32 MiB.'); return;
|
|
}
|
|
setSubmitting(true); setError(null);
|
|
try {
|
|
setStatus(await api.submitIsoCorpus(iso27001, iso27002, token));
|
|
setIso27001(null); setIso27002(null); setToken('');
|
|
form.reset();
|
|
} catch (reason) { setError(reason instanceof Error ? reason.message : 'ISO corpus upload failed.'); }
|
|
finally { setSubmitting(false); }
|
|
};
|
|
|
|
const running = status?.job?.status === 'running';
|
|
return <details className="tenant-scan-history baseline-catalog iso-corpus-catalog">
|
|
<summary className="tenant-section-bar">
|
|
<span className="tenant-history-meta baseline-title"><b>{status?.active ? '1' : '0'}</b><strong>ISO Control Corpus</strong></span>
|
|
<span className="tenant-history-meta"><span>{running ? status.job?.phase === 'embedding' && status.job.totalChunks
|
|
? `Embedding ${status.job.completedChunks ?? 0} / ${status.job.totalChunks}`
|
|
: `Processing: ${status.job?.phase}` : status?.active
|
|
? status.indexStatus === 'incomplete' ? 'Rebuild required' : status.indexStatus === 'unavailable' ? 'Index unavailable' : 'Ready'
|
|
: 'Not indexed'}</span><TriangleIcon direction="right"/></span>
|
|
</summary>
|
|
<div className="iso-corpus-content">
|
|
{status?.active && <div className="iso-corpus-summary" role="status">
|
|
<span><strong>Active version</strong> {status.active.corpusVersion}</span>
|
|
<span><strong>Controls and sections</strong> {status.active.documentCount.toLocaleString('en-US')} · {status.active.chunkCount.toLocaleString('en-US')} indexed chunks</span>
|
|
<span><strong>Embedding model</strong> {status.active.embeddingModel}</span>
|
|
{status.active.sources.map(source => <span key={source.standard}><strong>{source.standard}</strong> {source.pageCount} pages · SHA-256 {source.sha256.slice(0, 12)}…</span>)}
|
|
</div>}
|
|
{status?.indexStatus === 'incomplete' && <div className="datahub-alert" role="alert">The index is incomplete. Upload the same PDFs to rebuild it.</div>}
|
|
{status?.job?.status === 'failed' && <div className="datahub-alert" role="alert">{status.job.error}</div>}
|
|
{status?.job?.status === 'succeeded' && <div className="tenant-save-feedback success" role="status"><strong>ISO corpus indexed and active.</strong></div>}
|
|
{!status?.configured && status?.disabledReason && <div className="datahub-alert" role="alert">{status.disabledReason}</div>}
|
|
<form className="iso-corpus-form" onSubmit={event => void submit(event)}>
|
|
<label><span>DIN EN ISO/IEC 27001:2024-01 PDF</span><input className="input" type="file" accept=".pdf,application/pdf" required disabled={submitting || running} onChange={event => setIso27001(event.target.files?.[0] ?? null)}/></label>
|
|
<label><span>DIN EN ISO/IEC 27002:2024-01 PDF</span><input className="input" type="file" accept=".pdf,application/pdf" required disabled={submitting || running} onChange={event => setIso27002(event.target.files?.[0] ?? null)}/></label>
|
|
<label><span>ISO upload token</span><input className="input" type="password" autoComplete="off" value={token} required disabled={submitting || running} onChange={event => setToken(event.target.value)}/></label>
|
|
<button className="btn datahub-maintenance-action" type="submit" disabled={!status?.configured || !iso27001 || !iso27002 || !token || submitting || running}>{submitting ? 'Uploading …' : running ? 'Embedding …' : 'Upload and embed PDFs'}</button>
|
|
</form>
|
|
<p className="iso-corpus-note">The server keeps the generated corpus privately for rebuilding the index. Original PDFs are removed after parsing.</p>
|
|
{error && <div className="datahub-alert" role="alert">{error}</div>}
|
|
</div>
|
|
</details>;
|
|
}
|