Public source validation / validate (push) Failing after 3m8s
48 lines
5.4 KiB
TypeScript
48 lines
5.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { copy } from './copy';
|
|
import { plural, presentComponent, presentReason } from './presentation';
|
|
import { formatDateTime } from './locale';
|
|
import { backupPresentation, refreshSystemStatus, systemStateLabel as label, useSystemStatus } from './systemStatus';
|
|
|
|
function Badge({ state }: { state: string }) {
|
|
const ready = state === 'healthy';
|
|
return <span className={'status-badge status-badge--' + (ready ? 'ready' : 'unknown')}><span className="status-icon" aria-hidden="true">{ready ? '✓' : '?'}</span>{label(state)}</span>;
|
|
}
|
|
|
|
function age(seconds?: number): string {
|
|
if (seconds == null || !Number.isFinite(seconds)) return copy.systemStatus.notAvailable;
|
|
if (seconds < 60) return `${Math.max(0, Math.round(seconds))} ${copy.systemStatus.seconds}`;
|
|
if (seconds < 3600) return `${Math.round(seconds / 60)} ${copy.systemStatus.minutes}`;
|
|
if (seconds < 86400) { const hours = Math.round(seconds / 3600); return `${hours} ${plural(hours, copy.systemStatus.hourAgo, copy.systemStatus.hoursAgo)}`; }
|
|
const days = Math.round(seconds / 86400);
|
|
return `${days} ${plural(days, copy.systemStatus.dayAgo, copy.systemStatus.daysAgo)}`;
|
|
}
|
|
|
|
export function SystemStatusPage() {
|
|
const { state, status } = useSystemStatus();
|
|
const [backupAction, setBackupAction] = useState<'idle' | 'creating' | 'created' | 'error'>('idle');
|
|
if (state === 'loading') return <section className="state-page" role="status"><span className="loader" aria-hidden="true" /><h1>{copy.systemStatus.loading}</h1></section>;
|
|
if (state === 'unauthorized') return <section className="state-page" role="alert"><h1>{copy.systemStatus.unauthorized}</h1><p>{copy.systemStatus.unauthorizedDetail}</p></section>;
|
|
if (state === 'forbidden') return <section className="state-page" role="alert"><h1>{copy.systemStatus.forbidden}</h1><p>{copy.systemStatus.forbiddenDetail}</p></section>;
|
|
if (state === 'error' || !status) return <section className="state-page" role="alert"><h1>{copy.systemStatus.errorTitle}</h1><p>{copy.systemStatus.errorDetail}</p></section>;
|
|
const release = status.release;
|
|
const backup = backupPresentation(status.backup);
|
|
const createBackup = async () => {
|
|
setBackupAction('creating');
|
|
try {
|
|
const response = await fetch('/api/v1/system/backups', { method: 'POST' });
|
|
if (!response.ok) throw new Error('backup create failed');
|
|
setBackupAction('created');
|
|
refreshSystemStatus();
|
|
} catch {
|
|
setBackupAction('error');
|
|
}
|
|
};
|
|
return <>
|
|
<header className="page-intro"><p className="eyebrow">{copy.systemStatus.eyebrow}</p><h1>{copy.systemStatus.title}</h1><p className="intro">{copy.systemStatus.intro}</p></header>
|
|
<section className="card system-status-summary" aria-labelledby="system-status-summary-title"><div className="card-heading"><div><p className="card-kicker">{copy.systemStatus.current}</p><h2 id="system-status-summary-title">{label(status.overallState)}</h2></div><Badge state={status.overallState} /></div><p className="card-copy">{copy.systemStatus.version}: {release?.version || status.version} · {copy.systemStatus.commit}: {release?.commit || copy.systemStatus.notAvailable} · {copy.systemStatus.migration}: {release?.migrationVersion || copy.systemStatus.notAvailable}</p><p className="card-copy">{copy.systemStatus.built}: {release?.builtAt ? formatDateTime(release.builtAt) : copy.systemStatus.notAvailable} · {copy.systemStatus.generated}: {formatDateTime(status.generatedAt)}</p></section>
|
|
<section className="card" aria-labelledby="system-components-title"><div className="card-heading"><div><p className="card-kicker">{copy.systemStatus.components}</p><h2 id="system-components-title">{copy.systemStatus.componentTitle}</h2></div></div><ul className="inventory-list">{status.components.map((component) => <li key={component.id}><span><strong>{presentComponent(component.id)}</strong><small>{presentReason(component.reason)}</small></span><Badge state={component.state} /></li>)}</ul></section>
|
|
<section className="card-grid"><article className="card" aria-labelledby="backup-status-title"><p className="card-kicker">{copy.systemStatus.backup}</p><h2 id="backup-status-title"><Badge state={backup.state} /></h2><p className="card-copy">{presentReason(backup.reason)} · {age(backup.ageSeconds)}{backup.verifiedAt ? ` · ${copy.systemStatus.verified}: ${formatDateTime(backup.verifiedAt)}` : ''}</p><p className="card-copy">{copy.systemStatus.backupFreshness}</p><button className="button button--secondary" type="button" disabled={backupAction === 'creating'} onClick={() => void createBackup()}>{backupAction === 'creating' ? copy.systemStatus.creatingBackup : copy.systemStatus.createBackup}</button>{backupAction === 'created' && <p className="card-copy" role="status">{copy.systemStatus.backupCreated}</p>}{backupAction === 'error' && <p className="card-copy" role="alert">{copy.systemStatus.backupCreateFailed}</p>}</article><article className="card" aria-labelledby="source-lag-title"><p className="card-kicker">{copy.systemStatus.sourceLag}</p><h2 id="source-lag-title">{status.sourceLag.length} {plural(status.sourceLag.length, copy.systemStatus.source, copy.systemStatus.sources)}</h2>{status.sourceLag.length === 0 ? <p className="card-copy">{copy.systemStatus.noSources}</p> : <ul className="compact-list">{status.sourceLag.map((source) => <li key={source.sourceId}><strong>{presentComponent(source.sourceId)}</strong>: {label(source.state)} · {age(source.ageSeconds)}</li>)}</ul>}</article></section>
|
|
</>;
|
|
}
|