import { For, Show, createEffect, createSignal, onCleanup, type Component } from "solid-js"; import { runBackendAction, setBackendWeight, type BackendAction } from "../api/admin"; import Modal from "./Modal"; import { applyConfiguredWeight } from "../stores/state"; type Props = { maglevd: string; frontend: string; pool: string; backend: string; state: string; // Current configured weight. Used to seed the weight dialog's // number input so the operator sees the existing value and only // has to change the digits that matter. configuredWeight: number; }; // MenuAction is what the user clicks. It maps 1:1 onto BackendAction // plus a virtual "weight" entry that opens the weight form rather // than firing an immediate lifecycle RPC. type MenuAction = BackendAction | "weight"; type MenuItem = { label: string; action: MenuAction; }; // Available items per state. "weight" is always present (operators // can adjust the configured weight in any state including paused // and disabled — the effective weight won't change until the state // lets it, but the config value is still meaningful). A removed // backend has no actionable operations, so the kebab is suppressed // entirely by returning an empty list. function itemsForState(state: string): MenuItem[] { const weightItem: MenuItem = { label: "set weight…", action: "weight" }; switch (state) { case "up": case "down": case "unknown": return [ weightItem, { label: "pause", action: "pause" }, { label: "disable", action: "disable" }, ]; case "paused": return [ weightItem, { label: "resume", action: "resume" }, { label: "disable", action: "disable" }, ]; case "disabled": return [weightItem, { label: "enable", action: "enable" }]; default: return []; } } // consequenceText returns the "this will…" description shown in the // confirmation dialog for each lifecycle action. Spelling it out in // plain English makes the live-traffic impact unmistakable before // the operator commits. function consequenceText(action: BackendAction): string { switch (action) { case "pause": return "This will stop health checks and set the weight to 0, but existing flows to this backend are kept. New traffic will be rerouted to other backends."; case "resume": return "This will restart health checks. The backend re-enters the 'unknown' state and will start receiving traffic once it probes up."; case "disable": return "This will stop health checks, set the weight to 0, AND flush VPP's flow table for this backend. Active sessions will be dropped immediately."; case "enable": return "This will restart health checks on a previously disabled backend. It re-enters the 'unknown' state and will start receiving traffic once it probes up."; } } const BackendActionsMenu: Component = (props) => { const [open, setOpen] = createSignal(false); const [dialog, setDialog] = createSignal(); const [busy, setBusy] = createSignal(false); const [error, setError] = createSignal(); // Weight-dialog form state. Seeded from the current configured // weight each time the dialog opens so re-opening after a change // shows the new value. const [weightInput, setWeightInput] = createSignal(props.configuredWeight); const [flushInput, setFlushInput] = createSignal(false); let wrapRef: HTMLDivElement | undefined; const items = () => itemsForState(props.state); // Close the popover on outside click or Escape while it's open. // The dialog has its own Escape handler; this effect only runs // when the kebab popover (not the dialog) is visible. createEffect(() => { if (!open()) return; const onMouseDown = (e: MouseEvent) => { if (!wrapRef) return; if (!wrapRef.contains(e.target as Node)) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onMouseDown); document.addEventListener("keydown", onKey); onCleanup(() => { document.removeEventListener("mousedown", onMouseDown); document.removeEventListener("keydown", onKey); }); }); const openDialog = (action: MenuAction) => { setOpen(false); setError(undefined); if (action === "weight") { // Seed form from current value on each open. setWeightInput(props.configuredWeight); setFlushInput(false); } setDialog(action); }; const closeDialog = () => { if (busy()) return; // don't yank the modal out from under an in-flight call setDialog(undefined); setError(undefined); }; const commit = async () => { const action = dialog(); if (!action) return; setBusy(true); setError(undefined); try { if (action === "weight") { const w = weightInput(); if (!Number.isFinite(w) || w < 0 || w > 100) { throw new Error("weight must be an integer in [0, 100]"); } const newWeight = Math.floor(w); await setBackendWeight( props.maglevd, props.frontend, props.pool, props.backend, newWeight, flushInput(), ); // Mirror the server-side mutation into our local store so // the new weight (and any resulting effective-weight // recompute) is visible instantly, without waiting for // the next 30s refresh tick. applyConfiguredWeight(props.maglevd, props.frontend, props.pool, props.backend, newWeight); } else { await runBackendAction(props.maglevd, props.backend, action); } setDialog(undefined); } catch (err) { setError(`${err}`); } finally { setBusy(false); } }; return ( 0}>
{(action) => ( {action() === "weight" ? (

{props.backend} in pool {props.pool} of frontend{" "} {props.frontend}

VPP's flow table is left alone. Existing sessions keep reaching this backend until they finish.

} >

VPP's flow table will be cleared for this backend. Active sessions will be dropped immediately.

) : (

{consequenceText(action() as BackendAction)}

)}

{error()}

)}
); }; function dialogTitle(action: MenuAction, backend: string): string { switch (action) { case "weight": return `Set weight — ${backend}`; case "pause": return `Pause ${backend}?`; case "resume": return `Resume ${backend}?`; case "disable": return `Disable ${backend}?`; case "enable": return `Enable ${backend}?`; } } export default BackendActionsMenu;