fix(desktop): keep timeout inherit flags when saving plugin connections (#9579)

* fix(desktop): keep timeout inherit flags when saving plugin connections

* fix(desktop): hide dead query timeout scope from plugin connections

query_timeout_secs only feeds the database query pipeline (dataGrid,
queryStore); plugin connections such as SSH never consume it, so the
generic Advanced-tab radio only suggested a budget the provider cannot
honor. Hide the row for plugin connections while keeping hydration and
submit untouched, and pin the round-trip contract (explicit inherit
flag, preserved scalar) plus the deliberate keep of the connect timeout
radio in the regression spec.

---------

Co-authored-by: jinpy.he <jinpy.he@kuehne-nagel.com>
This commit is contained in:
Jinpy
2026-09-19 13:31:36 +08:00
committed by GitHub
co-authored by jinpy.he
parent cf6467ab08
commit 58fd050a28
2 changed files with 75 additions and 1 deletions
@@ -4118,6 +4118,13 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi
config.color = form.value.color;
config.transport_layers = form.value.transport_layers || [];
config.connect_timeout_secs = form.value.connect_timeout_secs;
// buildPluginConnectionConfig rebuilds the config from scratch and drops
// the timeout inherit flags, so mirror the Advanced-tab radio state the
// same way the built-in branch keeps them via the form spread. Kept ahead
// of the resolvedPluginConnectTimeout override below, which intentionally
// forces connect inheritance off for providers declaring their own
// handshake timeout field.
config.connect_timeout_inherit = form.value.connect_timeout_inherit;
if (resolvedPluginConnectTimeout !== undefined) {
// A provider declaring its own connect_timeout_secs field makes it the
// single source of truth (declared default or advanced-form value): the
@@ -4129,6 +4136,7 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi
config.connect_timeout_inherit = false;
}
config.query_timeout_secs = form.value.query_timeout_secs;
config.query_timeout_inherit = form.value.query_timeout_inherit;
config.idle_timeout_secs = form.value.idle_timeout_secs;
config.keepalive_interval_secs = form.value.keepalive_interval_secs;
config.read_only = form.value.read_only;
@@ -8987,7 +8995,11 @@ function openExternalUrl(url: string) {
<p class="text-xs leading-5 text-muted-foreground">{{ t("connection.etcdGrpcMaxInboundHint") }}</p>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<!-- query_timeout_secs only feeds the database query pipeline
(dataGrid/queryStore); plugin connections like SSH never
consume it, so the generic radio would only suggest a
budget the provider cannot honor. -->
<div v-if="!isPluginConnection" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.queryTimeout") }}</Label>
<div class="col-span-3 grid grid-cols-2 gap-2">
<div class="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2 gap-y-1 rounded border px-2 py-1.5 sm:flex" :class="form.query_timeout_inherit === true ? 'border-primary/60 bg-background' : 'border-border bg-muted/30 text-muted-foreground'">
@@ -0,0 +1,62 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("ConnectionDialog plugin connection timeout inheritance", () => {
const submitFn = dialogSource.slice(dialogSource.indexOf("function connectionConfigForSubmit"));
const pluginBranch = submitFn.slice(0, submitFn.indexOf("} else {"));
it("keeps the connect timeout inherit flag when submitting plugin connections", () => {
// buildPluginConnectionConfig rebuilds the config from scratch and drops
// the inherit flags; without this mirror the store normalizes the absent
// flag back to the persisted scope and the Advanced-tab radio reverts.
expect(pluginBranch).toContain("config.connect_timeout_inherit = form.value.connect_timeout_inherit;");
});
it("keeps the query timeout inherit flag when submitting plugin connections", () => {
expect(pluginBranch).toContain("config.query_timeout_inherit = form.value.query_timeout_inherit;");
});
it("still forces connect inheritance off for providers declaring their own handshake timeout field", () => {
// The resolvedPluginConnectTimeout override must run after the generic
// mirror so a provider-declared connect_timeout_secs stays the single
// source of truth and the host RPC deadline cannot inherit the global.
const mirrorIndex = pluginBranch.indexOf("config.connect_timeout_inherit = form.value.connect_timeout_inherit;");
const overrideIndex = pluginBranch.indexOf("config.connect_timeout_inherit = false;");
expect(mirrorIndex).toBeGreaterThan(-1);
expect(overrideIndex).toBeGreaterThan(mirrorIndex);
});
it("keeps the per-connection timeout scalars when submitting plugin connections", () => {
// Toggling back to "current connection" must preserve the typed value:
// the store treats a stored scalar with inherit=false as a local override.
expect(pluginBranch).toContain("config.query_timeout_secs = form.value.query_timeout_secs;");
expect(pluginBranch).toContain("config.connect_timeout_secs = form.value.connect_timeout_secs;");
});
});
describe("ConnectionDialog plugin connection timeout scope UI", () => {
const hydrateBlock = dialogSource.slice(dialogSource.indexOf("connect_timeout_inherit: config.connect_timeout_inherit === true"));
const queryTimeoutRowStart = dialogSource.lastIndexOf("<div", dialogSource.indexOf('t("connection.queryTimeout")'));
const connectTimeoutRowStart = dialogSource.lastIndexOf("<div", dialogSource.indexOf('t("connection.connectTimeout")'));
it("hydrates the global scope only from an explicit inherit flag", () => {
// Round trip with the submit mirror above: only a stored `true` counts as
// "global", so a preserved flag reopens as global and a preserved scalar
// reopens as a local override.
expect(hydrateBlock).toContain("query_timeout_inherit: config.query_timeout_inherit === true,");
});
it("hides the generic query timeout radio from plugin connections", () => {
// query_timeout_secs only feeds the database query pipeline, which plugin
// connections (SSH, ...) never run, so the control would be dead weight.
expect(dialogSource.slice(queryTimeoutRowStart, queryTimeoutRowStart + 220)).toContain('v-if="!isPluginConnection"');
});
it("keeps the generic connect timeout radio visible for plugin connections", () => {
// Deliberate scope cut: for providers without their own handshake timeout
// field the connect radio is still effective, so it stays rendered.
expect(dialogSource.slice(connectTimeoutRowStart, connectTimeoutRowStart + 220)).not.toContain('v-if="!isPluginConnection"');
});
});