feat(timeout): support global connection and query timeout settings

This commit is contained in:
zipg authored and GitHub committed 2026-08-09 13:57:42 +08:00
1 parent 5c343ab416
commit 72ff141f02
26 files changed
+588 -29

No files matched your search

@@ -26,7 +26,7 @@ import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSum
import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/lib/connection/sshConfigHosts";
import { canPersistConnectionTestResult, connectionEditDraftSyncAction } from "./connectionEditDraftSync";
import { REDIS_SCAN_PAGE_SIZE_DEFAULT, REDIS_SCAN_PAGE_SIZE_MIN, REDIS_SCAN_PAGE_SIZE_MAX, REDIS_SCAN_PAGE_SIZE_OPTIONS } from "@/lib/redis/redisKeyPattern";
import { useSettingsStore } from "@/stores/settingsStore";
import { normalizeGlobalConnectTimeoutSecs, normalizeGlobalQueryTimeoutSecs, useSettingsStore } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/backend/api";
@@ -184,6 +184,8 @@ type ConnectionTestState = ConnectionTestResult & { ok: boolean };
const { t } = useI18n();
const { toast } = useToast();
const settingsStore = useSettingsStore();
const editGlobalConnectTimeoutSecs = ref(settingsStore.editorSettings.globalConnectTimeoutSecs);
const editGlobalQueryTimeoutSecs = ref(settingsStore.editorSettings.globalQueryTimeoutSecs);
const open = defineModel<boolean>("open", { default: false });
const isDesktop = isTauriRuntime();
@@ -271,8 +273,10 @@ const defaultForm = (): ConnectionForm => ({
database: undefined,
color: "",
transport_layers: [],
connect_timeout_secs: 10,
query_timeout_secs: 30,
connect_timeout_secs: settingsStore.editorSettings.globalConnectTimeoutSecs,
connect_timeout_inherit: true,
query_timeout_secs: settingsStore.editorSettings.globalQueryTimeoutSecs,
query_timeout_inherit: true,
idle_timeout_secs: 60,
keepalive_interval_secs: 30,
ssl: false,
@@ -2291,6 +2295,8 @@ watch(
([config, isOpen]) => {
const syncAction = connectionEditDraftSyncAction(config?.id ?? null, isOpen, editingId.value);
if (syncAction === "preserve") return;
editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs;
if (syncAction === "hydrate" && config) {
clearSavedDatabaseInfo();
const legacyConfig = config as LegacyConnectionConfig;
@@ -2314,8 +2320,10 @@ watch(
database: config.database,
color: config.color || "",
transport_layers: transportLayersForConfig(legacyConfig),
connect_timeout_secs: config.connect_timeout_secs || 10,
query_timeout_secs: config.query_timeout_secs ?? 30,
connect_timeout_secs: config.connect_timeout_inherit === true ? settingsStore.editorSettings.globalConnectTimeoutSecs : config.connect_timeout_secs || 10,
connect_timeout_inherit: config.connect_timeout_inherit === true,
query_timeout_secs: config.query_timeout_inherit === true ? settingsStore.editorSettings.globalQueryTimeoutSecs : (config.query_timeout_secs ?? 30),
query_timeout_inherit: config.query_timeout_inherit === true,
idle_timeout_secs: config.idle_timeout_secs ?? 60,
keepalive_interval_secs: config.keepalive_interval_secs ?? 30,
ssl: config.ssl || false,
@@ -3551,9 +3559,9 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
config.connection_string = undefined;
}
const connectTimeout = Number(config.connect_timeout_secs);
config.connect_timeout_secs = Number.isFinite(connectTimeout) && connectTimeout > 0 ? connectTimeout : 10;
config.connect_timeout_secs = config.connect_timeout_inherit === true ? normalizeGlobalConnectTimeoutSecs(editGlobalConnectTimeoutSecs.value) : Number.isFinite(connectTimeout) && connectTimeout > 0 ? connectTimeout : 10;
const queryTimeout = Number(config.query_timeout_secs);
config.query_timeout_secs = Number.isFinite(queryTimeout) && queryTimeout >= 0 ? queryTimeout : 30;
config.query_timeout_secs = config.query_timeout_inherit === true ? normalizeGlobalQueryTimeoutSecs(editGlobalQueryTimeoutSecs.value) : Number.isFinite(queryTimeout) && queryTimeout >= 0 ? queryTimeout : 30;
const idleTimeout = Number(config.idle_timeout_secs);
config.idle_timeout_secs = Number.isFinite(idleTimeout) && idleTimeout >= 0 ? idleTimeout : 60;
const keepaliveInterval = Number(config.keepalive_interval_secs);
@@ -4501,6 +4509,8 @@ function openJdbcDriverManagerFromError() {
function resetForm() {
editingId.value = null;
form.value = defaultForm();
editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs;
selectedTransportLayerId.value = null;
draggedTransportLayerId.value = null;
selectedType.value = "mysql";
@@ -4815,6 +4825,25 @@ function validateTransportLayers(config: LegacyConnectionConfig) {
});
}
async function persistGlobalTimeoutDrafts() {
const nextConnect = normalizeGlobalConnectTimeoutSecs(editGlobalConnectTimeoutSecs.value);
const nextQuery = normalizeGlobalQueryTimeoutSecs(editGlobalQueryTimeoutSecs.value);
editGlobalConnectTimeoutSecs.value = nextConnect;
editGlobalQueryTimeoutSecs.value = nextQuery;
const connectChanged = nextConnect !== settingsStore.editorSettings.globalConnectTimeoutSecs;
const queryChanged = nextQuery !== settingsStore.editorSettings.globalQueryTimeoutSecs;
if (!connectChanged && !queryChanged) return;
settingsStore.updateEditorSettings({
globalConnectTimeoutSecs: nextConnect,
globalQueryTimeoutSecs: nextQuery,
});
await settingsStore.persistEditorSettings();
await store.applyGlobalTimeouts({
connectTimeoutSecs: connectChanged ? nextConnect : undefined,
queryTimeoutSecs: queryChanged ? nextQuery : undefined,
});
}
async function save() {
if (!ensureConnectionHostResolvedFromUrl()) return;
if (isSaving.value) return;
@@ -4825,12 +4854,14 @@ async function save() {
const updated = withSavedDatabaseInfo(connectionConfigForSubmit(editingId.value), databaseInfoForSave);
await ensureRequiredAgentDriverInstalled(updated);
await ensureRequiredGaussdbMJdbcRuntime(updated);
await persistGlobalTimeoutDrafts();
await store.updateConnection(updated);
store.stopEditing();
} else {
const config = withSavedDatabaseInfo(connectionConfigForSubmit(draftTestConnectionId.value), databaseInfoForSave);
await ensureRequiredAgentDriverInstalled(config);
await ensureRequiredGaussdbMJdbcRuntime(config);
await persistGlobalTimeoutDrafts();
await store.addConnection(config);
draftTestConnectionId.value = uuid();
if (config.db_type === "jdbc") {
@@ -7402,11 +7433,33 @@ function openExternalUrl(url: string) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.connectTimeout") }}</Label>
<Input v-model.number="form.connect_timeout_secs" type="number" min="1" max="300" step="1" class="col-span-3" />
<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.connect_timeout_inherit === true ? 'border-primary/60 bg-background' : 'border-border bg-muted/30 text-muted-foreground'">
<input id="connect-timeout-global" v-model="form.connect_timeout_inherit" type="radio" name="connect-timeout-scope" :value="true" class="h-3.5 w-3.5 shrink-0 accent-primary" />
<label for="connect-timeout-global" class="min-w-0 flex-1 cursor-pointer truncate text-xs" :title="t('connection.useGlobalQueryTimeout')">{{ t("connection.useGlobalQueryTimeout") }}</label>
<Input v-model.number="editGlobalConnectTimeoutSecs" type="number" min="1" max="300" step="1" class="col-span-2 h-7 w-full shrink-0 sm:col-span-1 sm:w-20" :disabled="form.connect_timeout_inherit !== true" />
</div>
<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.connect_timeout_inherit !== true ? 'border-primary/60 bg-background' : 'border-border bg-muted/30 text-muted-foreground'">
<input id="connect-timeout-connection" v-model="form.connect_timeout_inherit" type="radio" name="connect-timeout-scope" :value="false" class="h-3.5 w-3.5 shrink-0 accent-primary" />
<label for="connect-timeout-connection" class="min-w-0 flex-1 cursor-pointer truncate text-xs" :title="t('connection.useConnectionQueryTimeout')">{{ t("connection.useConnectionQueryTimeout") }}</label>
<Input v-model.number="form.connect_timeout_secs" type="number" min="1" max="300" step="1" class="col-span-2 h-7 w-full shrink-0 sm:col-span-1 sm:w-20" :disabled="form.connect_timeout_inherit === true" />
</div>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.queryTimeout") }}</Label>
<Input v-model.number="form.query_timeout_secs" type="number" min="0" max="300" step="1" class="col-span-3" />
<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'">
<input id="query-timeout-global" v-model="form.query_timeout_inherit" type="radio" name="query-timeout-scope" :value="true" class="h-3.5 w-3.5 shrink-0 accent-primary" />
<label for="query-timeout-global" class="min-w-0 flex-1 cursor-pointer truncate text-xs" :title="t('connection.useGlobalQueryTimeout')">{{ t("connection.useGlobalQueryTimeout") }}</label>
<Input v-model.number="editGlobalQueryTimeoutSecs" type="number" min="0" max="300" step="1" class="col-span-2 h-7 w-full shrink-0 sm:col-span-1 sm:w-20" :disabled="form.query_timeout_inherit !== true" />
</div>
<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'">
<input id="query-timeout-connection" v-model="form.query_timeout_inherit" type="radio" name="query-timeout-scope" :value="false" class="h-3.5 w-3.5 shrink-0 accent-primary" />
<label for="query-timeout-connection" class="min-w-0 flex-1 cursor-pointer truncate text-xs" :title="t('connection.useConnectionQueryTimeout')">{{ t("connection.useConnectionQueryTimeout") }}</label>
<Input v-model.number="form.query_timeout_secs" type="number" min="0" max="300" step="1" class="col-span-2 h-7 w-full shrink-0 sm:col-span-1 sm:w-20" :disabled="form.query_timeout_inherit === true" />
</div>
</div>
</div>
<div v-show="form.db_type === 'mongodb'" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.idleTimeout") }}</Label>
@@ -272,6 +272,8 @@ const editCustomThemes = ref<CustomTheme[]>([...settingsStore.editorSettings.cus
const editActiveCustomThemeId = ref(settingsStore.editorSettings.activeCustomThemeId);
const showThemeCustomizer = ref(false);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editGlobalConnectTimeoutSecs = ref(settingsStore.editorSettings.globalConnectTimeoutSecs);
const editGlobalQueryTimeoutSecs = ref(settingsStore.editorSettings.globalQueryTimeoutSecs);
const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker);
const editShowStatementRunButtons = ref(settingsStore.editorSettings.showStatementRunButtons);
const editShowCurrentStatementFrame = ref(settingsStore.editorSettings.showCurrentStatementFrame);
@@ -441,6 +443,8 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
customThemes: editCustomThemes.value,
activeCustomThemeId: editActiveCustomThemeId.value,
executeMode: editExecuteMode.value,
globalConnectTimeoutSecs: editGlobalConnectTimeoutSecs.value,
globalQueryTimeoutSecs: editGlobalQueryTimeoutSecs.value,
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
showStatementRunButtons: editShowStatementRunButtons.value,
showCurrentStatementFrame: editShowCurrentStatementFrame.value,
@@ -709,6 +713,8 @@ function syncEditorSettingsDraftFromStore() {
editCustomThemes.value = [...settingsStore.editorSettings.customThemes];
editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId;
editExecuteMode.value = settingsStore.editorSettings.executeMode;
editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs;
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons;
editShowCurrentStatementFrame.value = settingsStore.editorSettings.showCurrentStatementFrame;
@@ -860,6 +866,8 @@ function hasChanges(): boolean {
async function persistSettings() {
if (hasApplyBlocker.value) return;
const editorSettingsPatch = editorSettingsPatchFromDraft(currentEditorSettingsDraft(), editEditorSettingsBase.value);
const globalConnectTimeoutChanged = editorSettingsPatch.globalConnectTimeoutSecs !== undefined;
const globalQueryTimeoutChanged = editorSettingsPatch.globalQueryTimeoutSecs !== undefined;
const sidebarObjectDisplayChanged = editorSettingsPatch.sidebarObjectDisplay !== undefined && editorSettingsPatch.sidebarObjectDisplay !== settingsStore.editorSettings.sidebarObjectDisplay;
const sidebarTablePageSizeChanged = editSidebarTablePageSize.value !== (settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE);
if (Object.keys(editorSettingsPatch).length > 0) {
@@ -867,6 +875,12 @@ async function persistSettings() {
await settingsStore.persistEditorSettings();
editEditorSettingsBase.value = editorSettingsDraftFromSettings(settingsStore.editorSettings);
}
if (globalConnectTimeoutChanged || globalQueryTimeoutChanged) {
await connectionStore.applyGlobalTimeouts({
connectTimeoutSecs: globalConnectTimeoutChanged ? settingsStore.editorSettings.globalConnectTimeoutSecs : undefined,
queryTimeoutSecs: globalQueryTimeoutChanged ? settingsStore.editorSettings.globalQueryTimeoutSecs : undefined,
});
}
await settingsStore.updateDesktopSettings({
show_tray_icon: editShowTrayIcon.value,
quit_on_close: editQuitOnClose.value,
@@ -913,6 +927,8 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editFontFamily.value = DEFAULT_EDITOR_SETTINGS.fontFamily;
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editGlobalConnectTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs;
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame;
@@ -1006,6 +1022,8 @@ function resetAllDefaults() {
editCustomThemes.value = [...DEFAULT_EDITOR_SETTINGS.customThemes];
editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editGlobalConnectTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs;
editGlobalQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs;
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame;
@@ -3628,6 +3646,26 @@ onUnmounted(() => {
</Select>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-global-connect-timeout">{{ t("settings.globalConnectTimeout") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.globalConnectTimeoutDescription") }}
</p>
</div>
<Input id="editor-global-connect-timeout" v-model.number="editGlobalConnectTimeoutSecs" type="number" min="1" max="300" step="1" class="w-24" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-global-query-timeout">{{ t("settings.globalQueryTimeout") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.globalQueryTimeoutDescription") }}
</p>
</div>
<Input id="editor-global-query-timeout" v-model.number="editGlobalQueryTimeoutSecs" type="number" min="0" max="300" step="1" class="w-24" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-show-execution-target-picker">{{ t("settings.showExecutionTargetPicker") }}</Label>
@@ -2867,7 +2867,7 @@ async function lastPage() {
const countTarget = await buildCurrentCountTarget();
const sql = countTarget?.sql;
if (!sql) return;
const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", sql, countTarget.schema, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId)));
const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", sql, countTarget.schema, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId), settingsStore.editorSettings.globalQueryTimeoutSecs));
const total = Number(result.rows?.[0]?.[0] ?? 0);
if (!Number.isFinite(total) || total < 0) return;
manualTotalRowCount.value = total;
@@ -2915,7 +2915,7 @@ async function calculateTotalRowCount() {
if (!props.connectionId) return;
const countTarget = await buildCurrentCountTarget();
if (!countTarget?.sql) return;
const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", countTarget.sql, countTarget.schema, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId)));
const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", countTarget.sql, countTarget.schema, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId), settingsStore.editorSettings.globalQueryTimeoutSecs));
const total = Number(result.rows?.[0]?.[0] ?? 0);
if (Number.isFinite(total) && total >= 0) {
manualTotalRowCount.value = total;
@@ -2317,7 +2317,7 @@ async function applyChanges() {
try {
const result = hasSqliteTypeChange.value
? await api.applySqliteTableStructureChange(props.connectionId, props.database, structureChangeOptions(), sqliteSchemaRevision.value!)
: await api.executeBatch(props.connectionId, props.database, pendingStatements.value, props.schema, queryTimeoutSecsForConnection(connection));
: await api.executeBatch(props.connectionId, props.database, pendingStatements.value, props.schema, queryTimeoutSecsForConnection(connection, settingsStore.editorSettings.globalQueryTimeoutSecs));
await recordStructureHistory(sql, startedAt, true, result);
if (!isCreateMode.value && props.tableName) {
invalidateTableMetadataCache({ connectionId: props.connectionId, database: props.database, schema: metadataSchema.value, tableName: props.tableName });