// Schema-Generated Configuration Form
// Purpose: Renders the server-provided JSON Schema as an ordered, indented tree resembling the configuration's YAML structure.
// Scope: Defines one generic RJSF presentation; it never names or special-cases an individual service or setting.
import { useMemo } from 'react';
import Form from '@rjsf/core';
import validator from '@rjsf/validator-ajv8';
function buildUiSchema(schema, path = '') {
/*
`writeOnly` is standard JSON Schema metadata and is the sole reason a field
needs a special widget. The walk records its dotted path for the existing
server secret-operation protocol; it contains no service or field names.
*/
if (!schema || typeof schema !== 'object') return {};
if (schema.writeOnly === true) {
return {
'ui:widget': 'SecretWidget',
'ui:options': { secretPath: path },
};
}
if (schema.type !== 'object' || !schema.properties) return {};
return Object.fromEntries(Object.entries(schema.properties).map(([key, childSchema]) => [
key,
buildUiSchema(childSchema, path ? `${path}.${key}` : key),
]));
}
function SecretWidget({ id, disabled, readonly, options, registry }) {
const secretPath = options.secretPath;
const context = registry.formContext || {};
const operation = context.secretOperations?.[secretPath];
const configured = Boolean(context.configuredSecrets?.[secretPath]);
const replacing = operation?.action === 'replace';
const clearing = operation?.action === 'clear';
const unavailable = disabled || readonly;
function setOperation(nextOperation) {
if (!unavailable) context.setSecretOperation?.(secretPath, nextOperation);
}
return (
{/* Secret actions stay beside their status. A wide configuration card
must not turn related controls into a trip across the screen. */}
{clearing ? 'Will be cleared when saved' : replacing ? 'Replacement pending' : configured ? 'Configured' : 'Not configured'}
);
}
function ConfigurationFieldTemplate({
children,
classNames,
description,
errors,
help,
hidden,
id,
label,
required,
schema,
style,
}) {
if (hidden) return
{children}
;
const containerField = schema?.type === 'object' || schema?.type === 'array';
if (containerField) {
// Objects and arrays own their visible boundaries and headings in the
// templates below. Keeping this wrapper structural avoids duplicated
// titles while retaining RJSF's useful field-type classes for layout.
return
{children}{errors}{help}
;
}
return (
{/* Boolean widgets deliberately hide their internal duplicate label, so
every scalar can use this same key column and preserve YAML order. */}
{/* Descriptions belong with the editable value rather than inside the
narrow key column. This keeps long operational guidance readable
without weakening the YAML-like key/value alignment. */}
{description ?
{description}
: null}
{children}
{errors}
{help}
);
}
function ConfigurationObjectTemplate({ description, fieldPathId, properties, title }) {
const visibleProperties = properties.filter((property) => !property.hidden);
const propertyLines = visibleProperties.map((property) => property.content);
if (fieldPathId.path.length === 0) {
// The outer configuration card already names the root document. Rendering
// its properties directly makes their schema order read like YAML lines.
// The root description remains outside that stack so its introductory
// text does not accidentally become another configuration section.
return (
{description ?
{description}
: null}
{propertyLines}
);
}
if (typeof fieldPathId.path.at(-1) === 'number') {
// Array items receive their numbered heading and action row from the array
// item template. Rendering only their description and ordered property
// lines prevents redundant boxes while preserving the item's own guidance.
return (
{description ?
{description}
: null}
{propertyLines}
);
}
return (
{title}
{description ?
{description}
: null}
{propertyLines}
);
}
function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, index }) {
const unavailable = buttonsProps.disabled || buttonsProps.readonly;
return (
{hasToolbar ? (
// Text controls are intentionally kept immediately after the item
// number. RJSF's default Bootstrap toolbox pushes empty glyphicon
// buttons to the far edge, which is both unclear and hard to reach.
Item {index + 1}
{(buttonsProps.hasMoveUp || buttonsProps.hasMoveDown) ? (
) : null}
{(buttonsProps.hasMoveUp || buttonsProps.hasMoveDown) ? (
) : null}
{buttonsProps.hasCopy ? (
) : null}
{buttonsProps.hasRemove ? (
) : null}
) : null}
{children}
);
}
function ConfigurationArrayTemplate({ canAdd, disabled, items, onAddClick, readonly, schema, title }) {
return (