// Complete Configuration Editor // Purpose: Connects the one hierarchical configuration form to revision, secret, validation, and save behavior. // 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 { importAdminConfigurationFile, updateConfiguration } from '../api.js'; import SchemaConfigurationForm from './SchemaConfigurationForm.jsx'; function clone(value) { return JSON.parse(JSON.stringify(value)); } export default function ConfigurationEditor({ snapshot, socket, runSensitive, onSnapshot, onReload }) { const serverValue = snapshot?.configuration?.config; const schema = snapshot?.configuration?.schema; const revision = snapshot?.configuration?.revision; 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([]); useEffect(() => { setDraft(clone(serverValue)); setSecretOperations({}); setError(''); setConfigurationFile(null); setValidationErrors([]); }, [revision, serverValue]); const dirty = useMemo( () => JSON.stringify(draft) !== JSON.stringify(serverValue) || Object.keys(secretOperations).length > 0, [draft, secretOperations, serverValue], ); const busy = saving || importing; if (!serverValue || !schema) { return
The configuration document is unavailable.
; } const setSecretOperation = (path, operation) => setSecretOperations((current) => { const next = { ...current }; if (operation) next[path] = operation; else delete next[path]; return next; }); async function save() { setSaving(true); setError(''); setValidationErrors([]); try { const response = await runSensitive(() => updateConfiguration(socket, { value: draft, expectedRevision: revision, secretOperations, })); onSnapshot(response.snapshot); } catch (saveError) { setError(saveError.message); setValidationErrors(saveError.validationErrors || []); } finally { setSaving(false); } } 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 (Saving applies the complete revision immediately and reloads each affected service.
{/* All document actions stay together at the start of the toolbar. The editor may use a wide canvas, but width is never used to separate a control from the content that explains it. */}Replace this configuration from an explicitly selected legacy file. Unknown old settings and administrator accounts are ignored; current settings are validated and applied immediately.
{/* 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. */}Configuration was saved, but some services could not reload
{error}
: null} {notice ?{notice}
: null} {validationErrors.length ? (Configuration could not be saved