mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
some UI tweaking before testing in production lol
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// 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 { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import CardFrame from '../../components/CardFrame/index.jsx';
|
||||
import { importAdminConfigurationFile, updateConfiguration } from '../api.js';
|
||||
import SchemaConfigurationForm from './SchemaConfigurationForm.jsx';
|
||||
@@ -22,6 +22,8 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
|
||||
const [notice, setNotice] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [validationErrors, setValidationErrors] = useState([]);
|
||||
const editorRef = useRef(null);
|
||||
const toolbarRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(clone(serverValue));
|
||||
@@ -31,6 +33,35 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
|
||||
setValidationErrors([]);
|
||||
}, [revision, serverValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
const toolbar = toolbarRef.current;
|
||||
if (!editor || !toolbar) return undefined;
|
||||
|
||||
/*
|
||||
The action toolbar can wrap differently at each viewport width, so a
|
||||
fixed CSS offset would eventually let section headings overlap it. Feed
|
||||
its real rendered height into one local CSS variable instead; every
|
||||
sticky configuration CardFrame can then meet the toolbar exactly.
|
||||
*/
|
||||
const updateStickyOffset = () => {
|
||||
editor.style.setProperty('--configuration-sticky-top', `${toolbar.offsetHeight}px`);
|
||||
};
|
||||
updateStickyOffset();
|
||||
|
||||
const observer = typeof ResizeObserver === 'function'
|
||||
? new ResizeObserver(updateStickyOffset)
|
||||
: null;
|
||||
observer?.observe(toolbar);
|
||||
window.addEventListener('resize', updateStickyOffset);
|
||||
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
window.removeEventListener('resize', updateStickyOffset);
|
||||
editor.style.removeProperty('--configuration-sticky-top');
|
||||
};
|
||||
}, [serverValue, schema]);
|
||||
|
||||
const dirty = useMemo(
|
||||
() => JSON.stringify(draft) !== JSON.stringify(serverValue) || Object.keys(secretOperations).length > 0,
|
||||
[draft, secretOperations, serverValue],
|
||||
@@ -104,21 +135,22 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
|
||||
}
|
||||
|
||||
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">
|
||||
<p className="text-xs text-slate-400">Saving applies the complete revision immediately and reloads each affected service.</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. */}
|
||||
<div className="flex flex-wrap gap-0.5">
|
||||
<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 || busy} onClick={save}>{saving ? 'Applying…' : 'Save configuration'}</button>
|
||||
<div ref={editorRef} className="configuration-editor">
|
||||
<CardFrame title="Configuration" meta={`revision ${revision}`} clipOverflow={false} bodyClassName="p-0.5">
|
||||
<div ref={toolbarRef} 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">Saving applies the complete revision immediately and reloads each affected service.</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. */}
|
||||
<div className="flex flex-wrap gap-0.5">
|
||||
<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 || busy} onClick={save}>{saving ? 'Applying…' : 'Save configuration'}</button>
|
||||
</div>
|
||||
</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
|
||||
@@ -168,6 +200,7 @@ export default function ConfigurationEditor({ snapshot, socket, runSensitive, on
|
||||
secretOperations={secretOperations}
|
||||
setSecretOperation={setSecretOperation}
|
||||
/>
|
||||
</CardFrame>
|
||||
</CardFrame>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ function ConfigurationFieldTemplate({
|
||||
<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 htmlFor={id} className="configuration-key-label">
|
||||
{label}{required ? <span className="ml-0.25 text-sky-300">*</span> : null}
|
||||
</label>
|
||||
</div>
|
||||
@@ -215,7 +215,9 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
|
||||
title={title}
|
||||
color={layer.color}
|
||||
clipOverflow={false}
|
||||
stickyHeader
|
||||
className={`configuration-card${topLevel ? ' configuration-top-level-card' : ''}`}
|
||||
headerClassName="configuration-card-header"
|
||||
bodyClassName="configuration-card-body"
|
||||
>
|
||||
{description ? <div className="configuration-branch-description">{description}</div> : null}
|
||||
@@ -235,7 +237,9 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
|
||||
title={`Item ${index + 1}`}
|
||||
color={layer.color}
|
||||
clipOverflow={false}
|
||||
stickyHeader
|
||||
className="configuration-card configuration-array-item"
|
||||
headerClassName="configuration-card-header"
|
||||
bodyClassName="configuration-card-body"
|
||||
>
|
||||
{hasToolbar ? (
|
||||
@@ -274,7 +278,9 @@ function ConfigurationArrayTemplate({ canAdd, disabled, fieldPathId, items, onAd
|
||||
meta={`${items.length} ${items.length === 1 ? 'item' : 'items'}`}
|
||||
color={layer.color}
|
||||
clipOverflow={false}
|
||||
stickyHeader
|
||||
className={`configuration-card configuration-array${topLevel ? ' configuration-top-level-card' : ''}`}
|
||||
headerClassName="configuration-card-header"
|
||||
bodyClassName="configuration-card-body"
|
||||
>
|
||||
{schema.description ? <div className="configuration-branch-description">{schema.description}</div> : null}
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.configuration-card-header {
|
||||
/* The editor toolbar owns the top edge of the viewport. Structural title
|
||||
bars use its measured height so sticky CardFrames remain visible directly
|
||||
beneath the actions instead of covering them at narrow widths. */
|
||||
top: var(--configuration-sticky-top, 0px);
|
||||
}
|
||||
|
||||
.configuration-card .configuration-card {
|
||||
/* Indent the next structural boundary itself, not every row owned by the
|
||||
parent. Since nested CardFrames repeat this rule, the offset accumulates
|
||||
@@ -48,6 +55,12 @@
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.configuration-key-label {
|
||||
/* Keys are the primary identifiers on YAML-like rows, so they must be more
|
||||
prominent than the explanatory copy in the adjacent value column. */
|
||||
@apply text-base font-semibold leading-snug text-slate-100;
|
||||
}
|
||||
|
||||
.configuration-root-description,
|
||||
.configuration-branch-description,
|
||||
.configuration-item-description,
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function CardFrame({
|
||||
actions = null,
|
||||
color = null,
|
||||
hideHeader = false,
|
||||
stickyHeader = false,
|
||||
className = '',
|
||||
headerClassName = '',
|
||||
bodyClassName = '',
|
||||
@@ -93,6 +94,11 @@ export default function CardFrame({
|
||||
<header
|
||||
className={cx(
|
||||
'flex items-center justify-between gap-0.5 border-b border-neutral-500/50 bg-slate-800 px-0.5 py-0.5',
|
||||
// Sticky headings are opt-in because many CardFrames are short or
|
||||
// live inside independently scrolling panes. Keeping the behavior
|
||||
// on the shared component gives long cards a consistent title bar
|
||||
// without changing the layout of existing callers.
|
||||
stickyHeader && 'sticky top-0 z-10',
|
||||
// 'flex items-center justify-between gap-0.5 border-b border-neutral-500/50 bg-gradient-to-r from-neutral-800 via-neutral-700 to-neutral-600 px-0.5 py-0.5',
|
||||
headerClassName,
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user