mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
slorp
This commit is contained in:
@@ -19,6 +19,7 @@ import AdministratorAccounts from './components/AdministratorAccounts.jsx';
|
||||
import AdminOverview from './components/AdminOverview.jsx';
|
||||
import ConfigurationEditor from './components/ConfigurationEditor.jsx';
|
||||
import PasswordConfirmationDialog from './components/PasswordConfirmationDialog.jsx';
|
||||
import './styles.css';
|
||||
|
||||
const TOP_LEVEL_SECTIONS = [
|
||||
{ key: 'overview', label: 'Overview' },
|
||||
|
||||
@@ -63,9 +63,9 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<CardFrame className="sticky top-0 z-20 bg-neutral-900/95 backdrop-blur" title="Configuration" meta={`revision ${revision}`} bodyClassName="space-y-0.5 p-0.5">
|
||||
<p className="text-[0.7rem] text-slate-400">Saved changes apply after an application restart.</p>
|
||||
<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">
|
||||
<p className="text-xs text-slate-400">Saved changes apply after an application restart.</p>
|
||||
{/* 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. */}
|
||||
@@ -77,7 +77,7 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
|
||||
}}>Reset</button>
|
||||
<button type="button" className="button-dark" disabled={!dirty || saving} onClick={save}>{saving ? 'Saving…' : 'Save configuration'}</button>
|
||||
</div>
|
||||
</CardFrame>
|
||||
</div>
|
||||
{error ? <p className="border border-red-500/60 bg-red-950/40 p-1 text-xs text-red-100">{error}</p> : null}
|
||||
{validationErrors.length ? (
|
||||
<div className="border border-red-500/60 bg-red-950/40 p-1 text-xs text-red-100">
|
||||
@@ -97,6 +97,6 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
|
||||
secretOperations={secretOperations}
|
||||
setSecretOperation={setSecretOperation}
|
||||
/>
|
||||
</div>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Schema-Generated Configuration Form
|
||||
// Purpose: Renders the server-provided JSON Schema in the same cards, surfaces, fields, and buttons as the rest of MultiRover.
|
||||
// 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';
|
||||
import CardFrame from '../../components/CardFrame/index.jsx';
|
||||
|
||||
function buildUiSchema(schema, path = '') {
|
||||
/*
|
||||
@@ -41,7 +40,7 @@ function SecretWidget({ id, disabled, readonly, options, registry }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="surface space-y-0.5 p-0.5">
|
||||
<div className="configuration-secret space-y-0.5">
|
||||
{/* Secret actions stay beside their status. A wide configuration card
|
||||
must not turn related controls into a trip across the screen. */}
|
||||
<div className="flex flex-wrap items-center gap-0.5">
|
||||
@@ -77,7 +76,6 @@ function ConfigurationFieldTemplate({
|
||||
children,
|
||||
classNames,
|
||||
description,
|
||||
displayLabel,
|
||||
errors,
|
||||
help,
|
||||
hidden,
|
||||
@@ -98,59 +96,48 @@ function ConfigurationFieldTemplate({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${classNames || ''} configuration-field surface p-1`} style={style}>
|
||||
{displayLabel ? (
|
||||
<label htmlFor={id} className="block text-xs font-semibold text-slate-100">
|
||||
<div className={`${classNames || ''} configuration-line`} style={style}>
|
||||
<div className="configuration-key">
|
||||
{/* Boolean widgets deliberately hide their internal duplicate label, so
|
||||
every scalar can use this same key column and preserve YAML order. */}
|
||||
<label htmlFor={id} className="text-xs font-semibold text-slate-100">
|
||||
{label}{required ? <span className="ml-0.25 text-sky-300">*</span> : null}
|
||||
</label>
|
||||
) : null}
|
||||
{displayLabel && description ? <div className="mt-0.25">{description}</div> : null}
|
||||
<div className={displayLabel ? 'mt-0.5' : ''}>{children}</div>
|
||||
{errors}
|
||||
{help}
|
||||
{description ? <div>{description}</div> : null}
|
||||
</div>
|
||||
<div className="configuration-value">
|
||||
{children}
|
||||
{errors}
|
||||
{help}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigurationObjectTemplate({ description, fieldPathId, properties, title }) {
|
||||
const visibleProperties = properties.filter((property) => !property.hidden);
|
||||
const propertyGrid = (
|
||||
<div className="configuration-property-grid grid gap-0.5 md:grid-cols-2 xl:grid-cols-3">
|
||||
{/* RJSF gives each property content its own keyed field wrapper. Rendering
|
||||
it directly preserves field-object and field-array on the grid child,
|
||||
allowing containers to span the row without another frontend schema. */}
|
||||
{visibleProperties.map((property) => property.content)}
|
||||
</div>
|
||||
);
|
||||
const propertyLines = visibleProperties.map((property) => property.content);
|
||||
|
||||
if (fieldPathId.path.length === 0) {
|
||||
// The editor toolbar already identifies the root document. The root is a
|
||||
// simple ordered stack so every service-owned top-level object receives
|
||||
// the full page width before arranging its own fields responsively.
|
||||
return <div className="space-y-1">{visibleProperties.map((property) => property.content)}</div>;
|
||||
// The outer configuration card already names the root document. Rendering
|
||||
// its properties directly makes their schema order read like YAML lines.
|
||||
return <div className="configuration-tree">{propertyLines}</div>;
|
||||
}
|
||||
|
||||
if (typeof fieldPathId.path.at(-1) === 'number') {
|
||||
// Array items receive their numbered heading and action row from the array
|
||||
// item template. Rendering only their property grid prevents redundant
|
||||
// item template. Rendering only their ordered property lines prevents redundant
|
||||
// nested boxes such as "Item 1" followed by another anonymous object box.
|
||||
return propertyGrid;
|
||||
}
|
||||
|
||||
if (fieldPathId.path.length === 1) {
|
||||
return (
|
||||
<CardFrame title={title} clipOverflow={false} bodyClassName="space-y-0.5 p-0.5">
|
||||
{description ? <div className="px-0.5 text-xs text-slate-400">{description}</div> : null}
|
||||
{propertyGrid}
|
||||
</CardFrame>
|
||||
);
|
||||
return <div>{propertyLines}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="configuration-object surface border border-neutral-500/60 p-0.5">
|
||||
<h3 className="mb-0.5 text-sm font-semibold text-slate-100">{title}</h3>
|
||||
{description ? <div className="mb-0.5 text-xs text-slate-400">{description}</div> : null}
|
||||
{propertyGrid}
|
||||
<section className="configuration-branch">
|
||||
<header className="configuration-branch-heading">
|
||||
<h3>{title}</h3>
|
||||
{description ? <div>{description}</div> : null}
|
||||
</header>
|
||||
<div className="configuration-children">{propertyLines}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -159,13 +146,13 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
|
||||
const unavailable = buttonsProps.disabled || buttonsProps.readonly;
|
||||
|
||||
return (
|
||||
<article className="surface-muted space-y-0.5 border border-neutral-500/60 p-0.5">
|
||||
<article className="configuration-branch configuration-array-item">
|
||||
{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.
|
||||
<header className="flex flex-wrap items-center gap-0.5">
|
||||
<span className="mr-0.5 text-xs font-semibold text-slate-100">Item {index + 1}</span>
|
||||
<header className="configuration-item-heading">
|
||||
<span className="configuration-item-title">Item {index + 1}</span>
|
||||
{(buttonsProps.hasMoveUp || buttonsProps.hasMoveDown) ? (
|
||||
<button type="button" className="button-dark text-xs" disabled={unavailable || !buttonsProps.hasMoveUp} onClick={buttonsProps.onMoveUpItem}>Move up</button>
|
||||
) : null}
|
||||
@@ -180,23 +167,25 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
|
||||
) : null}
|
||||
</header>
|
||||
) : null}
|
||||
{children}
|
||||
<div className="configuration-children">{children}</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigurationArrayTemplate({ canAdd, disabled, items, onAddClick, readonly, schema, title }) {
|
||||
return (
|
||||
<section className="configuration-array surface space-y-0.5 border border-neutral-500/60 p-0.5">
|
||||
<header className="flex flex-wrap items-center gap-0.5">
|
||||
<h3 className="text-sm font-semibold text-slate-100">{title}</h3>
|
||||
<section className="configuration-branch configuration-array">
|
||||
<header className="configuration-branch-heading configuration-array-heading">
|
||||
<h3>{title}</h3>
|
||||
<span className="text-[0.7rem] text-slate-400">{items.length} {items.length === 1 ? 'item' : 'items'}</span>
|
||||
</header>
|
||||
{schema.description ? <div className="text-xs text-slate-400">{schema.description}</div> : null}
|
||||
{items.length ? <div className="space-y-0.5">{items}</div> : <p className="text-xs text-slate-500">No items configured.</p>}
|
||||
{canAdd ? (
|
||||
<button type="button" className="button-dark text-xs" disabled={disabled || readonly} onClick={onAddClick}>Add item</button>
|
||||
) : null}
|
||||
{schema.description ? <div className="configuration-branch-description">{schema.description}</div> : null}
|
||||
<div className="configuration-children">
|
||||
{items.length ? <div className="space-y-0.5">{items}</div> : <p className="text-xs text-slate-500">No items configured.</p>}
|
||||
{canAdd ? (
|
||||
<button type="button" className="button-dark mt-0.5 text-xs" disabled={disabled || readonly} onClick={onAddClick}>Add item</button>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/* Administration Application Styles
|
||||
* Purpose: Styles the schema-generated configuration tree used only by /admin.
|
||||
* Scope: Keeps route-specific hierarchy and RJSF integration out of the shared global stylesheet.
|
||||
*/
|
||||
|
||||
.configuration-tree {
|
||||
/* A single ordered stack is the visual equivalent of YAML's top-to-bottom
|
||||
document order. Separation comes from hierarchy, not unrelated tiles. */
|
||||
@apply space-y-0.5;
|
||||
}
|
||||
|
||||
.configuration-branch {
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.configuration-branch-heading,
|
||||
.configuration-item-heading {
|
||||
/* Headings and their controls use normal left-to-right flow. Deliberately
|
||||
omitting space-between prevents wide pages from marooning actions. */
|
||||
@apply flex flex-wrap items-center gap-0.5 rounded-md bg-slate-800 px-0.5 py-0.5;
|
||||
}
|
||||
|
||||
.configuration-branch-heading h3,
|
||||
.configuration-item-title {
|
||||
@apply text-sm font-semibold text-slate-100;
|
||||
}
|
||||
|
||||
.configuration-branch-heading > div,
|
||||
.configuration-branch-description {
|
||||
@apply text-[0.7rem] text-slate-400;
|
||||
}
|
||||
|
||||
.configuration-children {
|
||||
/* Every container contributes exactly one indentation and one guide. Deep
|
||||
descendants therefore explain their ancestry without calculating depth or
|
||||
encoding service paths in frontend code. */
|
||||
@apply ml-1 space-y-0.5 border-l border-sky-700/70 py-0.5 pl-1;
|
||||
}
|
||||
|
||||
.configuration-line {
|
||||
/* Each scalar is one logical YAML-like line. The bounded value column starts
|
||||
immediately after its key instead of stretching to the viewport edge. */
|
||||
@apply grid min-w-0 grid-cols-1 items-start gap-0.5 rounded-md bg-neutral-800 px-0.5 py-0.5 sm:grid-cols-[minmax(9rem,16rem)_minmax(12rem,40rem)];
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.configuration-key,
|
||||
.configuration-value {
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.configuration-key .field-description,
|
||||
.configuration-key .help-block,
|
||||
.configuration-branch-description {
|
||||
@apply block text-[0.7rem] leading-tight text-slate-500;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.configuration-value input:not([type='checkbox']),
|
||||
.configuration-value select,
|
||||
.configuration-value textarea {
|
||||
@apply w-full rounded-md border border-neutral-600 bg-neutral-700 px-0.5 py-0.5 text-white placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-sky-500;
|
||||
}
|
||||
|
||||
.configuration-value input[type='checkbox'] {
|
||||
@apply h-4 w-4 align-middle accent-sky-500;
|
||||
}
|
||||
|
||||
.configuration-value .checkbox label {
|
||||
/* RJSF hides its duplicate boolean label because the key column already
|
||||
names the setting; this rule keeps the remaining checkbox compact. */
|
||||
@apply flex min-h-7 items-center text-slate-100;
|
||||
}
|
||||
|
||||
.configuration-value .error-detail {
|
||||
@apply mt-0.5 text-xs text-red-300;
|
||||
}
|
||||
|
||||
.configuration-value .help-block {
|
||||
@apply mt-0.5 block text-[0.7rem] text-slate-500;
|
||||
}
|
||||
|
||||
.configuration-secret {
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.configuration-array-item + .configuration-array-item {
|
||||
@apply mt-0.5;
|
||||
}
|
||||
@@ -66,36 +66,4 @@ body::-webkit-scrollbar,
|
||||
@apply px-0.5 py-0.5 text-sm font-medium text-white transition-colors bg-rose-600 hover:bg-rose-500 rounded-md;
|
||||
}
|
||||
|
||||
/* RJSF still owns values, validation, and schema traversal, while the
|
||||
generic React templates own layout. Only native form-control appearance
|
||||
remains here, shared uniformly by every present and future setting. */
|
||||
.configuration-schema-form input:not([type='checkbox']),
|
||||
.configuration-schema-form select,
|
||||
.configuration-schema-form textarea {
|
||||
@apply w-full rounded-md border border-neutral-600 bg-neutral-700 px-0.5 py-0.5 text-white placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-sky-500;
|
||||
}
|
||||
|
||||
.configuration-schema-form input[type='checkbox'] {
|
||||
@apply mr-0.5 h-4 w-4 align-middle accent-sky-500;
|
||||
}
|
||||
|
||||
.configuration-schema-form .checkbox label {
|
||||
@apply flex cursor-pointer items-center gap-0.5 text-sm font-semibold text-slate-100;
|
||||
}
|
||||
|
||||
.configuration-schema-form .field-description,
|
||||
.configuration-schema-form .help-block {
|
||||
@apply mt-0.5 block text-[0.7rem] text-slate-500;
|
||||
}
|
||||
|
||||
.configuration-schema-form .error-detail {
|
||||
@apply mt-0.5 text-xs text-red-300;
|
||||
}
|
||||
|
||||
/* Container fields need the complete row in responsive property grids.
|
||||
Primitive fields remain free to share columns where their parent has room. */
|
||||
.configuration-property-grid > .field-object,
|
||||
.configuration-property-grid > .field-array {
|
||||
@apply col-span-full;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user