feat(export): respect connection query timeout in table export

This commit is contained in:
zipg authored and GitHub committed 2026-09-03 17:52:15 +08:00
1 parent aa637b2d90
commit 291890f67e
8 files changed
+148 -7

No files matched your search

@@ -3,12 +3,13 @@ import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Loader2, CheckCircle2, XCircle, AlertCircle, FolderOpen, Minimize2, X } from "@lucide/vue";
import { Loader2, CheckCircle2, XCircle, AlertCircle, FolderOpen, Minimize2, Wrench, X } from "@lucide/vue";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import * as api from "@/lib/backend/api";
import { translateBackendError } from "@/i18n/backend-errors";
import { formatDataTransferDuration } from "@/composables/useExportTracker";
import { isQueryTimeoutErrorMessage } from "@/lib/sql/queryError";
const { t } = useI18n();
const { toast } = useToast();
@@ -38,11 +39,13 @@ const props = defineProps<{
finishedAt?: number;
disableCancel?: boolean;
canMinimize?: boolean;
connectionId?: string;
}>();
const emit = defineEmits<{
cancel: [];
minimize: [];
changeQueryTimeout: [];
"update:open": [value: boolean];
}>();
@@ -59,6 +62,7 @@ const displayName = computed(() => {
});
const isActive = computed(() => props.status === "Running" || props.status === "Writing");
const isFinished = computed(() => props.status === "Done" || props.status === "Error" || props.status === "Cancelled");
const canChangeQueryTimeout = computed(() => props.status === "Error" && !!props.connectionId && !!props.errorMessage && isQueryTimeoutErrorMessage(props.errorMessage));
const canRevealFile = computed(() => props.status === "Done" && !!props.filePath && isTauriRuntime());
const progressPercent = computed(() => {
if (!props.totalRows || props.totalRows <= 0) return 0;
@@ -163,6 +167,10 @@ async function revealExportFile() {
</Button>
</template>
<template v-else-if="isFinished">
<Button v-if="canChangeQueryTimeout" variant="outline" size="sm" @click="emit('changeQueryTimeout')">
<Wrench class="mr-1 h-3.5 w-3.5" />
{{ t("editor.changeQueryTimeout") }}
</Button>
<Button v-if="canRevealFile" size="sm" :disabled="isRevealing" @click="revealExportFile">
<Loader2 v-if="isRevealing" class="mr-1 h-3.5 w-3.5 animate-spin" />
<FolderOpen v-else class="mr-1 h-3.5 w-3.5" />
@@ -80,3 +80,29 @@ describe("ExportProgressDialog display name", () => {
expect(document.body.textContent).toContain("audit_log (.csv)");
});
});
describe("ExportProgressDialog query timeout action", () => {
it("offers connection settings for a query timeout", async () => {
i18n.global.locale.value = "en";
const onChangeQueryTimeout = vi.fn();
await mountDialog({
...baseProps,
status: "Error",
errorMessage: "Agent RPC error (-1): dm.jdbc.driver.DMException: 请求执行超时",
connectionId: "dm-1",
onChangeQueryTimeout,
});
const button = [...document.body.querySelectorAll("button")].find((element) => element.textContent?.includes("Change query timeout"));
expect(button).toBeTruthy();
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onChangeQueryTimeout).toHaveBeenCalledOnce();
});
it("does not offer timeout settings for unrelated export errors", async () => {
i18n.global.locale.value = "en";
await mountDialog({ ...baseProps, status: "Error", errorMessage: "Permission denied", connectionId: "dm-1" });
expect(document.body.textContent).not.toContain("Change query timeout");
});
});
+18 -1
View File
@@ -568,6 +568,7 @@ const emit = defineEmits<{
"update:whereInput": [value: string];
"update:orderByInput": [value: string];
"local-column-filters-change": [value: Record<string, string[]>];
changeQueryTimeout: [connectionId: string];
}>();
const autoRefresh = useDataGridAutoRefresh({
@@ -8155,6 +8156,12 @@ async function cancelActiveExport() {
await exportCancelHandler.value?.();
}
function changeExportQueryTimeout() {
if (!props.connectionId) return;
exportProgressDialog.value = false;
emit("changeQueryTimeout", props.connectionId);
}
const userFacingSql = ref("");
let userFacingSqlGeneration = 0;
@@ -14857,7 +14864,17 @@ function openGridSnapshot() {
/>
<ImagePreviewDialog v-if="imagePreviewMounted" v-model:open="imagePreviewOpen" :src="imagePreviewSrc" :title="imagePreviewTitle" />
<component v-if="previewDialogOpen && previewDialogConfig" :is="previewDialogConfig.component" v-model:open="previewDialogOpen" v-bind="previewDialogConfig.props" />
<ExportProgressDialog v-if="exportProgressDialogMounted" v-model:open="exportProgressDialog" v-bind="exportProgressState" :disable-cancel="!exportCancelHandler" :can-minimize="exportCanMinimize" @cancel="cancelActiveExport" @minimize="exportProgressDialog = false" />
<ExportProgressDialog
v-if="exportProgressDialogMounted"
v-model:open="exportProgressDialog"
v-bind="exportProgressState"
:connection-id="connectionId"
:disable-cancel="!exportCancelHandler"
:can-minimize="exportCanMinimize"
@cancel="cancelActiveExport"
@minimize="exportProgressDialog = false"
@change-query-timeout="changeExportQueryTimeout"
/>
</div>
</template>
@@ -1831,6 +1831,7 @@ defineExpose({
@reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', sql, searchText, whereInput, orderBy, limit, offset, intent)"
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', offset, limit, whereInput, orderBy)"
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', column, columnIndex, direction, whereInput, mode)"
@change-query-timeout="(connectionId: string) => emit('openConnectionSettings', connectionId, 'advanced')"
>
<template #result-toolbar-leading="{ compact }">
<QueryResultViewSwitcher
@@ -2204,6 +2205,7 @@ defineExpose({
@reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', sql, searchText, whereInput, orderBy, limit, offset, intent)"
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', offset, limit, whereInput, orderBy)"
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', column, columnIndex, direction, whereInput, mode)"
@change-query-timeout="(connectionId: string) => emit('openConnectionSettings', connectionId, 'advanced')"
>
<template v-if="activeTab.result && isQueryExecutionErrorResult(activeTab.result)" #error-actions="{ errorMessage }">
<QueryErrorActions
+1
View File
@@ -44,6 +44,7 @@ describe("isQueryTimeoutErrorMessage", () => {
expect(isQueryTimeoutErrorMessage("Query timed out after 30 seconds")).toBe(true);
expect(isQueryTimeoutErrorMessage("查询超时 (60s),请检查数据库连接是否正常")).toBe(true);
expect(isQueryTimeoutErrorMessage("查詢逾時 (60s),請檢查資料庫連線是否正常")).toBe(true);
expect(isQueryTimeoutErrorMessage("Agent RPC error (-1): dm.jdbc.driver.DMException: 请求执行超时")).toBe(true);
});
it("detects statement timeout messages", () => {
+1 -1
View File
@@ -22,7 +22,7 @@ export function isQueryTimeoutErrorMessage(message: string, backendError?: Backe
return QUERY_TIMEOUT_STAGES.has(stage);
}
const lower = message.toLowerCase();
if (lower.includes("query timed out") || lower.includes("查询超时") || lower.includes("查詢逾時")) return true;
if (lower.includes("query timed out") || lower.includes("查询超时") || lower.includes("查詢逾時") || lower.includes("请求执行超时") || lower.includes("請求執行逾時")) return true;
// Agent RPC client-side timeout (tokio::time::timeout in agent_driver.rs). This is the
// fallback when JDBC setQueryTimeout never fires (unsupported/unresponsive driver), so the
// backend already treats it as a query timeout (is_agent_rpc_timeout_error in query.rs) —