feat(ui): schema-driven ServiceConfigForm in Fleet & MazeNET inspectors

ServiceConfigForm.tsx fetches /topologies/services/{slug}/schema and renders
typed inputs (string/password/int/bool/textarea/enum) with reveal toggles for
secrets. SAVE persists via PUT (no restart); APPLY persists + force-recreates
the service container after a confirm dialog (matches the forwards_l3 pattern).

Mounts:
- DeckyFleet DeckyCard: clicking a service tag toggles the form below the
  EXPOSED row, gated on liveServicesEnabled (admin + non-swarm).
- MazeNET Inspector: renders the form above REMOVE SERVICE when a service
  is selected on a non-observed decky.

UI test plan is manual — no jsdom test infra in decnet_web yet.
This commit is contained in:
2026-04-29 11:41:43 -04:00
parent 75b1ce3a31
commit bd7f2dfaed
5 changed files with 374 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ import {
} from '../../icons';
import type { Net, MazeNode, Edge } from './types';
import { DEFAULT_SERVICES } from './data';
import ServiceConfigForm from '../ServiceConfigForm';
export type Selection =
| { type: 'net'; id: string }
@@ -18,6 +19,9 @@ interface Props {
nets: Net[];
nodes: MazeNode[];
edges: Edge[];
/** Topology ID (MazeNET-only) — required for the schema-driven service
* config form to hit the per-topology REST path. Omit for fleet. */
topologyId?: string;
topologyStatus?: string;
onClose?: () => void;
onDeleteNet?: (id: string) => void;
@@ -46,7 +50,7 @@ interface Props {
}
const Inspector: React.FC<Props> = ({
selection, nets, nodes, edges, topologyStatus, onClose,
selection, nets, nodes, edges, topologyId, topologyStatus, onClose,
onDeleteNet, onDeleteNode, onDeleteEdge, onRemoveService,
onLiveAddService, onLiveRemoveService, availableServices = [],
onToggleGateway,
@@ -440,6 +444,18 @@ const Inspector: React.FC<Props> = ({
<div className="k">SUBNET</div>
<div className="v">{serviceParentNet?.label ?? '—'}</div>
</div>
{topologyId && serviceParent && serviceParent.kind !== 'observed' && (
<ServiceConfigForm
key={`${serviceParent.name}:${serviceSel.id}`}
deckyName={serviceParent.name}
serviceSlug={serviceSel.id}
topologyId={topologyId}
currentConfig={
((serviceParent.decky_config as { service_config?: Record<string, Record<string, unknown>> } | undefined)
?.service_config?.[serviceSel.id]) ?? {}
}
/>
)}
{onRemoveService && serviceParent && serviceParent.kind !== 'observed' && (
<button
type="button"

View File

@@ -892,6 +892,7 @@ const MazeNET: React.FC = () => {
nets={nets}
nodes={nodes}
edges={edges}
topologyId={topologyId || undefined}
topologyStatus={topoStatus}
onClose={() => setInspectorOpen(false)}
onDeleteNet={removeNet}