legacy importer in admin page aswell as setup.

This commit is contained in:
legop3
2026-09-14 18:10:45 -04:00
parent 91e0cc3886
commit c56a8b1000
23 changed files with 351 additions and 86 deletions
@@ -3,7 +3,7 @@
// Scope: Edits and saves one complete configuration document as one immutable revision.
import { useEffect, useMemo, useState } from 'react';
import CardFrame from '../../components/CardFrame/index.jsx';
import { updateConfiguration } from '../api.js';
import { importAdminConfigurationFile, updateConfiguration } from '../api.js';
import SchemaConfigurationForm from './SchemaConfigurationForm.jsx';
function clone(value) {
@@ -17,6 +17,9 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
const [draft, setDraft] = useState(() => clone(serverValue));
const [secretOperations, setSecretOperations] = useState({});
const [saving, setSaving] = useState(false);
const [importing, setImporting] = useState(false);
const [configurationFile, setConfigurationFile] = useState(null);
const [notice, setNotice] = useState('');
const [error, setError] = useState('');
const [validationErrors, setValidationErrors] = useState([]);
@@ -24,6 +27,7 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
setDraft(clone(serverValue));
setSecretOperations({});
setError('');
setConfigurationFile(null);
setValidationErrors([]);
}, [revision, serverValue]);
@@ -31,6 +35,7 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
() => JSON.stringify(draft) !== JSON.stringify(serverValue) || Object.keys(secretOperations).length > 0,
[draft, secretOperations, serverValue],
);
const busy = saving || importing;
if (!serverValue || !schema) {
return <p className="surface p-1 text-sm text-red-200">The configuration document is unavailable.</p>;
@@ -62,6 +67,42 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
}
}
async function importSelectedConfiguration() {
if (!configurationFile) return;
/*
Import replaces the complete draft and applies it immediately, so the
confirmation names both consequences before reading or transmitting the
operator-selected file. Administrator accounts remain owned by the
initialized database and are never imported from this screen.
*/
const confirmed = window.confirm(
'Replace the current configuration with this YAML file and apply it now? Any unsaved edits will be discarded. Administrator accounts in the file will be ignored.',
);
if (!confirmed) return;
setImporting(true);
setError('');
setNotice('');
setValidationErrors([]);
try {
const yamlText = await configurationFile.text();
const response = await runSensitive(() => importAdminConfigurationFile(socket, {
fileName: configurationFile.name,
yaml: yamlText,
expectedRevision: revision,
}));
onSnapshot(response.snapshot);
setNotice(response.ignoredAdministratorCount > 0
? `Configuration imported and applied. ${response.ignoredAdministratorCount} administrator entr${response.ignoredAdministratorCount === 1 ? 'y was' : 'ies were'} ignored.`
: 'Configuration imported and applied.');
} catch (importError) {
setError(importError.message);
setValidationErrors(importError.validationErrors || []);
} finally {
setImporting(false);
}
}
return (
<CardFrame title="Configuration" meta={`revision ${revision}`} clipOverflow={false} bodyClassName="p-0.5">
<div className="configuration-toolbar sticky top-0 z-20 mb-0.5 space-y-0.5 border border-neutral-500/60 bg-neutral-900/95 p-0.5 backdrop-blur">
@@ -70,14 +111,33 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
editor may use a wide canvas, but width is never used to separate a
control from the content that explains it. */}
<div className="flex flex-wrap gap-0.5">
<button type="button" className="button-dark" disabled={saving} onClick={onReload}>Reload</button>
<button type="button" className="button-dark" disabled={!dirty || saving} onClick={() => {
<button type="button" className="button-dark" disabled={busy} onClick={onReload}>Reload</button>
<button type="button" className="button-dark" disabled={!dirty || busy} onClick={() => {
setDraft(clone(serverValue));
setSecretOperations({});
}}>Reset</button>
<button type="button" className="button-dark" disabled={!dirty || saving} onClick={save}>{saving ? 'Applying…' : 'Save configuration'}</button>
<button type="button" className="button-dark" disabled={!dirty || busy} onClick={save}>{saving ? 'Applying…' : 'Save configuration'}</button>
</div>
</div>
<CardFrame title="Import legacy YAML" bodyClassName="space-y-0.5 p-1 text-sm" clipOverflow={false}>
<p className="text-sm text-slate-300">Replace this configuration from an explicitly selected legacy file. Unknown old settings and administrator accounts are ignored; current settings are validated and applied immediately.</p>
{/* Keep the picker and its action beside each other at the start of the
card. The configuration canvas can be wide, but this local action
should never be separated from the file it operates on. */}
<div className="flex max-w-3xl flex-col gap-0.5 md:flex-row">
<input
key={revision}
className="field-input min-w-0 flex-1"
type="file"
accept=".yaml,.yml,text/yaml"
disabled={busy}
onChange={(event) => setConfigurationFile(event.target.files?.[0] || null)}
/>
<button type="button" className="button-dark" disabled={busy || !configurationFile} onClick={importSelectedConfiguration}>
{importing ? 'Importing…' : 'Import selected YAML'}
</button>
</div>
</CardFrame>
{snapshot.configurationApplication?.services?.some((service) => service.status === 'failed') ? (
<div className="mb-0.5 border border-amber-500/60 bg-amber-950/40 p-1 text-xs text-amber-100">
<p className="font-semibold">Configuration was saved, but some services could not reload</p>
@@ -89,6 +149,7 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
</div>
) : null}
{error ? <p className="border border-red-500/60 bg-red-950/40 p-1 text-xs text-red-100">{error}</p> : null}
{notice ? <p className="border border-emerald-500/60 bg-emerald-950/40 p-1 text-sm text-emerald-100">{notice}</p> : null}
{validationErrors.length ? (
<div className="border border-red-500/60 bg-red-950/40 p-1 text-xs text-red-100">
<p className="font-semibold">Configuration could not be saved</p>