feat(export): respect connection query timeout in table export
This commit is contained in:
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");
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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) —
|
||||
|
||||
Reference in new issue
Block a user