fix(ui): route palette drops + design-time remove through live API on active topologies
When topoStatus is active/degraded, editor.updateDecky enqueues into
the mutator queue and returns {kind:'enqueued'}. The palette-drop
handler then short-circuits on that and never updates local state, so
a service dragged onto a deployed decky just vanishes — what ANTI saw
as 'no way to APPLY'.
Same gap on the design-time 'REMOVE SERVICE' button in the Inspector's
service detail panel: enqueue + no local update = chip stays.
Both now route through liveAddService / liveRemoveService when the
topology is active, hitting POST/DELETE /topologies/{id}/deckies/{name}/services
directly and patching local state from the response. Pending
topologies still queue through the mutator (correct: no live
containers to mutate).
Hoisted serviceRegistry / liveAddService / liveRemoveService above
the palette-drop callback so the deps array doesn't trip the const
TDZ at render time.
This commit is contained in:
@@ -109,6 +109,29 @@ const MazeNET: React.FC = () => {
|
||||
setTimeout(() => setActionErr(null), 4000);
|
||||
}, []);
|
||||
|
||||
/* ── Live service mutation (W3 endpoints) — hoisted above palette
|
||||
drop so onPaletteDrop's deps can reference it without hitting the
|
||||
const TDZ. Optimistic local update; SSE forwarder reconciles
|
||||
cross-tab. */
|
||||
const serviceRegistry = useServiceRegistry();
|
||||
|
||||
const liveAddService = useCallback(async (nodeName: string, slug: string) => {
|
||||
const { data } = await axios.post<{ services: string[] }>(
|
||||
`/topologies/${encodeURIComponent(topologyId)}/deckies/${encodeURIComponent(nodeName)}/services`,
|
||||
{ name: slug },
|
||||
);
|
||||
setNodes((p) => p.map((x) => x.kind === 'decky' && x.name === nodeName
|
||||
? { ...x, services: data.services } : x));
|
||||
}, [topologyId]);
|
||||
|
||||
const liveRemoveService = useCallback(async (nodeName: string, slug: string) => {
|
||||
const { data } = await axios.delete<{ services: string[] }>(
|
||||
`/topologies/${encodeURIComponent(topologyId)}/deckies/${encodeURIComponent(nodeName)}/services/${encodeURIComponent(slug)}`,
|
||||
);
|
||||
setNodes((p) => p.map((x) => x.kind === 'decky' && x.name === nodeName
|
||||
? { ...x, services: data.services } : x));
|
||||
}, [topologyId]);
|
||||
|
||||
/* ── Palette drop — create LANs / deckies / services via REST ─── */
|
||||
const onPaletteDrop = useCallback(
|
||||
async (drag: PaletteDrag, world: { x: number; y: number }, overNetId: string | null, overNodeId: string | null) => {
|
||||
@@ -213,6 +236,21 @@ const MazeNET: React.FC = () => {
|
||||
const target = nodes.find((n) => n.id === overNodeId);
|
||||
if (!target || target.kind !== 'decky') return;
|
||||
if (target.services.includes(drag.slug)) return;
|
||||
// For active/degraded topologies, route through the live W3
|
||||
// endpoint — the design-time mutator queue would silently
|
||||
// enqueue and the dropped chip would never visibly land
|
||||
// (resulting in the "no way to APPLY" feedback). liveAddService
|
||||
// returns the post-mutation services list and patches local
|
||||
// state so the chip appears immediately.
|
||||
const live = topoStatus === 'active' || topoStatus === 'degraded';
|
||||
if (live) {
|
||||
try {
|
||||
await liveAddService(target.name, drag.slug);
|
||||
} catch (err) {
|
||||
flashErr(err, 'add service failed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const nextServices = [...target.services, drag.slug];
|
||||
try {
|
||||
const r = await editor.updateDecky(topologyId, overNodeId, target.name, { services: nextServices });
|
||||
@@ -225,7 +263,7 @@ const MazeNET: React.FC = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[api, archetypes, editor, flashErr, nets, nodes, topologyId],
|
||||
[api, archetypes, editor, flashErr, nets, nodes, topologyId, topoStatus, liveAddService],
|
||||
);
|
||||
|
||||
/* ── Cross-net reparent via node drag (detach + attach edge) ─── */
|
||||
@@ -403,6 +441,20 @@ const MazeNET: React.FC = () => {
|
||||
const removeServiceFromNode = async (id: string, slug: string) => {
|
||||
const n = nodes.find((x) => x.id === id);
|
||||
if (!n || n.kind !== 'decky' || !n.services.includes(slug)) return;
|
||||
// Same routing rule as the palette drop: active/degraded topologies
|
||||
// hit the live W3 endpoint so the chip disappears immediately and
|
||||
// the container stops; pending topologies queue through the
|
||||
// design-time mutator.
|
||||
const live = topoStatus === 'active' || topoStatus === 'degraded';
|
||||
if (live) {
|
||||
try {
|
||||
await liveRemoveService(n.name, slug);
|
||||
setSelection(null);
|
||||
} catch (err) {
|
||||
flashErr(err, 'remove service failed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const nextServices = n.services.filter((s) => s !== slug);
|
||||
try {
|
||||
const r = await editor.updateDecky(topologyId, id, n.name, { services: nextServices });
|
||||
@@ -429,33 +481,6 @@ const MazeNET: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/* Live service add/remove — talks to the W3 endpoints directly,
|
||||
bypassing the design-time mutation queue. Used when topology
|
||||
status is active/degraded; the Inspector switches between this
|
||||
and the design-time path based on the topologyStatus prop.
|
||||
|
||||
Optimistic local update is fine: the W3 endpoint returns the
|
||||
post-mutation services list, and the SSE forwarder (commit C-sse)
|
||||
reconciles cross-tab. */
|
||||
const serviceRegistry = useServiceRegistry();
|
||||
|
||||
const liveAddService = useCallback(async (nodeName: string, slug: string) => {
|
||||
const { data } = await axios.post<{ services: string[] }>(
|
||||
`/topologies/${encodeURIComponent(topologyId)}/deckies/${encodeURIComponent(nodeName)}/services`,
|
||||
{ name: slug },
|
||||
);
|
||||
setNodes((p) => p.map((x) => x.kind === 'decky' && x.name === nodeName
|
||||
? { ...x, services: data.services } : x));
|
||||
}, [topologyId]);
|
||||
|
||||
const liveRemoveService = useCallback(async (nodeName: string, slug: string) => {
|
||||
const { data } = await axios.delete<{ services: string[] }>(
|
||||
`/topologies/${encodeURIComponent(topologyId)}/deckies/${encodeURIComponent(nodeName)}/services/${encodeURIComponent(slug)}`,
|
||||
);
|
||||
setNodes((p) => p.map((x) => x.kind === 'decky' && x.name === nodeName
|
||||
? { ...x, services: data.services } : x));
|
||||
}, [topologyId]);
|
||||
|
||||
/* Force-mutate is a no-op against a pending topology (no live containers).
|
||||
* Keep the menu item disabled for now; real hook lands with live-editing polish. */
|
||||
const forceMutate = (_id: string) => {
|
||||
|
||||
Reference in New Issue
Block a user