From bd7f2dfaedf588ce54fee0dee9b2a54245c2b86e Mon Sep 17 00:00:00 2001 From: anti Date: Wed, 29 Apr 2026 11:41:43 -0400 Subject: [PATCH] feat(ui): schema-driven ServiceConfigForm in Fleet & MazeNET inspectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- decnet_web/src/components/DeckyFleet.css | 55 ++++ decnet_web/src/components/DeckyFleet.tsx | 28 +- .../src/components/MazeNET/Inspector.tsx | 18 +- decnet_web/src/components/MazeNET/MazeNET.tsx | 1 + .../src/components/ServiceConfigForm.tsx | 274 ++++++++++++++++++ 5 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 decnet_web/src/components/ServiceConfigForm.tsx diff --git a/decnet_web/src/components/DeckyFleet.css b/decnet_web/src/components/DeckyFleet.css index 374dd0b6..2a839453 100644 --- a/decnet_web/src/components/DeckyFleet.css +++ b/decnet_web/src/components/DeckyFleet.css @@ -127,6 +127,61 @@ } .decky-hits { font-variant-numeric: tabular-nums; } +/* Schema-driven per-service config form (shared with MazeNET Inspector). */ +.service-config-form { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 8px; + padding: 10px; + border: 1px dashed var(--border); + border-radius: 2px; +} +.svc-cfg-row { display: flex; flex-direction: column; gap: 4px; } +.svc-cfg-label { + font-size: 0.62rem; + letter-spacing: 1px; + text-transform: uppercase; + opacity: 0.7; +} +.svc-cfg-secret-tag { font-size: 0.55rem; opacity: 0.6; letter-spacing: 1px; } +.svc-cfg-input { + flex: 1; + font-size: 0.72rem; + padding: 4px 6px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border); + color: inherit; + font-family: inherit; +} +.svc-cfg-input:focus { outline: 1px solid var(--violet); } +.svc-cfg-pw-wrap { display: flex; gap: 6px; align-items: stretch; } +.svc-cfg-help { font-size: 0.62rem; opacity: 0.55; } +.svc-cfg-actions { + display: flex; + gap: 6px; + align-items: center; + justify-content: flex-end; + margin-top: 4px; +} +.svc-cfg-dirty-tag { + font-size: 0.6rem; + letter-spacing: 1px; + color: var(--violet); + margin-right: auto; +} +.svc-cfg-toggle-btn { + background: transparent; + border: none; + color: inherit; + cursor: pointer; + font-size: 0.62rem; + letter-spacing: 1px; + opacity: 0.7; + padding: 0; +} +.svc-cfg-toggle-btn:hover { opacity: 1; } + /* Status dots */ .status-dot { display: inline-block; diff --git a/decnet_web/src/components/DeckyFleet.tsx b/decnet_web/src/components/DeckyFleet.tsx index 21d9dd39..9e41a03d 100644 --- a/decnet_web/src/components/DeckyFleet.tsx +++ b/decnet_web/src/components/DeckyFleet.tsx @@ -8,6 +8,7 @@ import { ARCHETYPES as FALLBACK_ARCHETYPES, DEFAULT_SERVICES } from './MazeNET/d import { useToast } from './Toasts/useToast'; import Modal from './Modal/Modal'; import { useServiceRegistry } from '../hooks/useServiceRegistry'; +import ServiceConfigForm from './ServiceConfigForm'; import './DeckyFleet.css'; // ─── Types ──────────────────────────────────────────────────────────────── @@ -157,6 +158,7 @@ const DeckyCard: React.FC = ({ const [addSlug, setAddSlug] = useState(''); const [busy, setBusy] = useState(null); const [opError, setOpError] = useState(null); + const [openCfgSvc, setOpenCfgSvc] = useState(null); const removeService = async (slug: string) => { setOpError(null); @@ -269,7 +271,21 @@ const DeckyCard: React.FC = ({
{decky.services.map((s) => ( - {s} + {liveServicesEnabled ? ( + + ) : ( + {s} + )} {liveServicesEnabled && (
diff --git a/decnet_web/src/components/MazeNET/Inspector.tsx b/decnet_web/src/components/MazeNET/Inspector.tsx index 8022c5b0..de721a2b 100644 --- a/decnet_web/src/components/MazeNET/Inspector.tsx +++ b/decnet_web/src/components/MazeNET/Inspector.tsx @@ -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 = ({ - 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 = ({
SUBNET
{serviceParentNet?.label ?? '—'}
+ {topologyId && serviceParent && serviceParent.kind !== 'observed' && ( + > } | undefined) + ?.service_config?.[serviceSel.id]) ?? {} + } + /> + )} {onRemoveService && serviceParent && serviceParent.kind !== 'observed' && (