// 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'}
{replacing ? ( setOperation({ action: 'replace', value: event.target.value })} /> ) : null}
); } 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 (

{title}

{items.length} {items.length === 1 ? 'item' : 'items'}
{schema.description ?
{schema.description}
: null}
{items.length ?
{items}
:

No items configured.

} {canAdd ? ( ) : null}
); } export default function SchemaConfigurationForm({ schema, value, onChange, configuredSecrets, secretOperations, setSecretOperation }) { const uiSchema = useMemo(() => buildUiSchema(schema), [schema]); const widgets = useMemo(() => ({ SecretWidget }), []); const templates = useMemo(() => ({ ArrayFieldItemTemplate: ConfigurationArrayItemTemplate, ArrayFieldTemplate: ConfigurationArrayTemplate, FieldTemplate: ConfigurationFieldTemplate, ObjectFieldTemplate: ConfigurationObjectTemplate, }), []); const formContext = useMemo(() => ({ configuredSecrets, secretOperations, setSecretOperation, }), [configuredSecrets, secretOperations, setSecretOperation]); return (
onChange(formData)} > {/* Saving is owned by the sticky revision-aware toolbar above the form. */} <>
); }