From 3028ab117622c23b9dde2469b4c7bc795a00de16 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Tue, 29 Sep 2026 22:40:01 -0500 Subject: [PATCH] fix: load and save plugin host settings in the editor and tighten settings row spacing --- plugins/web-endpoint/locales/en.json | 2 ++ .../src/frontend/HostEditorWebUiSection.tsx | 18 ++++++++---- src/ui/components/section-card.tsx | 4 ++- src/ui/sidebar/HostEditor.tsx | 22 +++++++++++--- src/ui/sidebar/HostManagerData.ts | 1 + src/ui/tests/sidebar/HostManagerData.test.ts | 29 +++++++++++++++++++ 6 files changed, 66 insertions(+), 10 deletions(-) diff --git a/plugins/web-endpoint/locales/en.json b/plugins/web-endpoint/locales/en.json index 6e633f96d..592190658 100644 --- a/plugins/web-endpoint/locales/en.json +++ b/plugins/web-endpoint/locales/en.json @@ -20,6 +20,8 @@ "webUiEndpointLabel": "Endpoint {{number}}", "webUiIntegration": "Web UI", "webUiEndpointsSection": "Endpoints", + "webUiNoEndpoints": "No web endpoints yet. Use Add endpoint to create one.", + "webUiEndpointsDisabled": "Turn on Web UI above to add endpoints.", "webUiRequirementsText": "A direct endpoint is opened by your browser, so its port must be reachable from here. An SSH tunnel is opened by Termix over this host's SSH connection instead, so the port only has to be reachable from the host.", "enableWebUi": "Enable web endpoints", "enableWebUiDesc": "Open this host's web interfaces from the sidebar.", diff --git a/plugins/web-endpoint/src/frontend/HostEditorWebUiSection.tsx b/plugins/web-endpoint/src/frontend/HostEditorWebUiSection.tsx index 65e128cca..ec7c8dafb 100644 --- a/plugins/web-endpoint/src/frontend/HostEditorWebUiSection.tsx +++ b/plugins/web-endpoint/src/frontend/HostEditorWebUiSection.tsx @@ -220,9 +220,17 @@ export function HostEditorWebUiSection({ ) : undefined } > -
- {enableWebUi && ( -
+
+ {!enableWebUi ? ( +

+ {t("hosts.webUiEndpointsDisabled")} +

+ ) : endpoints.length === 0 ? ( +

+ {t("hosts.webUiNoEndpoints")} +

+ ) : ( + <> {endpoints.map((endpoint, index) => { const rowError = webEndpointRowError(endpoint); return ( @@ -497,7 +505,7 @@ export function HostEditorWebUiSection({ ); })} - {!tunnelAvailable && endpoints.length > 0 && ( + {!tunnelAvailable && ( // Shown whenever tunnelling is unavailable, not only once a row // already asks for it: the Tunnel option renders disabled, and // a disabled control with no stated reason reads as the @@ -506,7 +514,7 @@ export function HostEditorWebUiSection({ {t("hosts.webUiAccessTunnelUnavailable")}

)} -
+ )}
diff --git a/src/ui/components/section-card.tsx b/src/ui/components/section-card.tsx index 1a0a64f8f..8f21fac99 100644 --- a/src/ui/components/section-card.tsx +++ b/src/ui/components/section-card.tsx @@ -42,7 +42,9 @@ export function SettingRow({ children: React.ReactNode; }) { return ( -
+ // In a gap-4 form stack the gap already spaces the row, so its own padding + // and divider would double up. +
{label} diff --git a/src/ui/sidebar/HostEditor.tsx b/src/ui/sidebar/HostEditor.tsx index e0c8b1e93..164dcbb83 100644 --- a/src/ui/sidebar/HostEditor.tsx +++ b/src/ui/sidebar/HostEditor.tsx @@ -153,17 +153,23 @@ export function HostEditor({ const savePluginHostSettings = async ( hostId: number, values: Record> | undefined, - ) => { - if (!values || !Number.isInteger(hostId)) return; + ): Promise>> => { + const saved: Record> = {}; + if (!values || !Number.isInteger(hostId)) return saved; for (const [pluginId, fields] of Object.entries(values)) { if (!fields || Object.keys(fields).length === 0) continue; try { - await updatePluginHostSettings(pluginId, hostId, fields); + saved[pluginId] = await updatePluginHostSettings( + pluginId, + hostId, + fields, + ); } catch { toast.error(t("settings.pluginSettingsSaveFailed")); } } + return saved; }; const handleSave = async () => { @@ -186,12 +192,20 @@ export function HostEditor({ // After the host: a new one has no id to scope settings to until it // exists. A failure here must not claim the host itself failed to save. const pluginValues = withProtocolSettings(form.pluginSettings, protocols); - await savePluginHostSettings( + const savedPluginValues = await savePluginHostSettings( Number(saved.id), host ? pluginValues : withNewHostDefaults(pluginValues ?? {}, hostSettingPlugins), ); + // The host was read back before these were written. + saved = { + ...saved, + pluginSettings: { + ...(saved.pluginSettings ?? {}), + ...savedPluginValues, + }, + }; toast.success(host ? t("hosts.hostUpdated") : t("hosts.hostCreated")); onSave(saved); diff --git a/src/ui/sidebar/HostManagerData.ts b/src/ui/sidebar/HostManagerData.ts index 0c27438d3..f4d1e16da 100644 --- a/src/ui/sidebar/HostManagerData.ts +++ b/src/ui/sidebar/HostManagerData.ts @@ -79,6 +79,7 @@ export function sshHostToHost(h: SSHHostWithStatus): Host { h.sshPort ?? (h.connectionType === "ssh" || !h.connectionType ? h.port : 22), protocolAuth: h.protocolAuth ?? {}, + pluginSettings: h.pluginSettings ?? {}, jumpHosts: (parseJson(h.jumpHosts) ?? []).map((j) => ({ hostId: String(j.hostId ?? j.hostid ?? j), })), diff --git a/src/ui/tests/sidebar/HostManagerData.test.ts b/src/ui/tests/sidebar/HostManagerData.test.ts index c63fbfda8..607fcbc5c 100644 --- a/src/ui/tests/sidebar/HostManagerData.test.ts +++ b/src/ui/tests/sidebar/HostManagerData.test.ts @@ -32,4 +32,33 @@ describe("sshHostToHost", () => { expect(host.id).toBe("-12"); expect(host.isShared).toBe(true); }); + + it("keeps every plugin's host settings so the editor can load them", () => { + const pluginSettings = { + docker: { enableDocker: true, containerRuntime: "podman" }, + "tmux-monitor": { enableTmuxMonitor: true }, + }; + const host = sshHostToHost({ + id: 3, + name: "box", + ip: "10.0.0.3", + port: 22, + username: "root", + pluginSettings, + } as unknown as SSHHostWithStatus); + + expect(host.pluginSettings).toEqual(pluginSettings); + }); + + it("defaults plugin settings to an empty map", () => { + const host = sshHostToHost({ + id: 4, + name: "bare", + ip: "10.0.0.4", + port: 22, + username: "root", + } as SSHHostWithStatus); + + expect(host.pluginSettings).toEqual({}); + }); });