perf(export): 优化导出ui体验和性能

* docs(getting-started): set brew upgrade to use official homebrew cask

* fix(mysql): make admin show handling dialect aware

* fix(data-compare): create missing target tables during sync

* feat(sql): suggest join conditions from foreign keys

* fix(sidebar): use filtered order for range selection

* fix(tabs): close connection tabs on disconnect

* fix(sqlserver): execute cte queries without wrapping

* fix(opengauss): open table structure editor

* perf(export): 优化导出ui体验和性能

* perf(export): 优化导出ui体验和性能

* fix(export): normalize status casing and fix web route issues

- Add missing table export web routes (POST /export/table, SSE progress, cancel)
- Fix tauri table export Promise to wait for terminal events
- Change cancel to return Ok(()) so it doesn't get overwritten as Error
- Unify ExportStatus to PascalCase across all frontend components
- Fix http.ts table export POST body wrapping
- Fix duplicate pub mod in dbx-web routes

* fix(export): return export file for browser download in web mode

Write table export output to temp dir, register a download endpoint,
and trigger browser download from the frontend when export completes.

* fix(export): avoid table export progress races

---------

Co-authored-by: Francesco Lucarelli <francesco06lucarelli@gmail.com>
Co-authored-by: t8y2 <1156263951@qq.com>
Co-authored-by: dqn <someonegdeng@qq.com>
This commit is contained in:
authored and GitHub committed 2026-06-03 10:39:11 +08:00
1 parent bed72d2132
commit bea3b45fd8
22 files changed
+1282 -159

No files matched your search

+5
View File
@@ -188,6 +188,8 @@ export const exportTableDataCsv = forward("exportTableDataCsv");
export const exportQueryResultXlsx = forward("exportQueryResultXlsx");
export const exportQueryResultJson = forward("exportQueryResultJson");
export const exportQueryResultMarkdown = forward("exportQueryResultMarkdown");
export const startTableExport = forward("startTableExport");
export const cancelTableExport = forward("cancelTableExport");
// Redis
export const redisListDatabases = forward("redisListDatabases");
@@ -286,4 +288,7 @@ export type {
TableImportProgress,
DatabaseExportRequest,
ExportProgress,
TableExportProgress,
TableExportStatus,
TableExportRequest,
} from "./tauri";
+64
View File
@@ -49,6 +49,8 @@ import type {
TableImportProgress,
DatabaseExportRequest,
ExportProgress,
TableExportRequest,
TableExportProgress,
TableCsvExportOptions,
XlsxCellValue,
QueryPaginationExecutionPlanOptions,
@@ -1071,6 +1073,68 @@ export async function cancelDatabaseExport(exportId: string): Promise<void> {
await post("/api/export/database/cancel", { exportId });
}
// --- Table Export ---
export async function startTableExport(
request: TableExportRequest,
onProgress: (progress: TableExportProgress) => void,
): Promise<TableExportProgress> {
const { exportId } = request;
return new Promise((resolve, reject) => {
let started = false;
let settled = false;
const eventSource = new EventSource(`/api/export/table/progress/${exportId}`);
const finish = (callback: () => void) => {
if (settled) return;
settled = true;
eventSource.close();
callback();
};
eventSource.onopen = () => {
if (started) return;
started = true;
post("/api/export/table", { request }).catch((error) => {
finish(() => reject(error));
});
};
eventSource.onmessage = (event) => {
const progress: TableExportProgress = JSON.parse(event.data);
onProgress(progress);
if (progress.status === "Done" || progress.status === "Error" || progress.status === "Cancelled") {
if (progress.status === "Error") {
finish(() => reject(new Error(progress.errorMessage || "Export failed")));
} else if (progress.status === "Done") {
// Trigger browser download
downloadTableExportFile(exportId, request.format);
finish(() => resolve(progress));
} else {
finish(() => resolve(progress));
}
}
};
eventSource.onerror = () => {
finish(() => reject(new Error("Export progress connection lost")));
};
});
}
function downloadTableExportFile(exportId: string, format: string): void {
const ext = format === "csv" ? "csv" : "xlsx";
const a = document.createElement("a");
a.href = `/api/export/table/download/${exportId}`;
a.download = `table_export_${exportId}.${ext}`;
a.click();
}
export async function cancelTableExport(exportId: string): Promise<void> {
return post("/api/export/table/cancel", { exportId });
}
export async function exportQueryResultCsv(
filePath: string,
columns: string[],
+71
View File
@@ -1479,6 +1479,20 @@ export interface ExportProgress {
error: string | null;
}
// --- Table Export ---
export type TableExportStatus = "Running" | "Writing" | "Done" | "Error" | "Cancelled";
export interface TableExportRequest {
exportId: string;
connectionId: string;
database: string;
schema?: string;
tableName: string;
filePath: string;
format: "csv" | "xlsx";
columns?: string[];
}
export interface TableCsvExportOptions {
filePath: string;
connectionId: string;
@@ -1490,6 +1504,63 @@ export interface TableCsvExportOptions {
timeoutSecs?: number;
}
export interface TableExportProgress {
exportId: string;
tableName: string;
rowsExported: number;
totalRows: number | null;
status: TableExportStatus;
errorMessage?: string;
}
export async function startTableExport(
request: TableExportRequest,
onProgress: (progress: TableExportProgress) => void,
): Promise<TableExportProgress> {
let unlisten: UnlistenFn | undefined;
let settled = false;
let resolveTerminal: (progress: TableExportProgress) => void = () => {};
let rejectTerminal: (error: unknown) => void = () => {};
const terminalProgress = new Promise<TableExportProgress>((resolve, reject) => {
resolveTerminal = resolve;
rejectTerminal = reject;
});
const finish = (callback: () => void) => {
if (settled) return;
settled = true;
unlisten?.();
callback();
};
try {
unlisten = await listen<TableExportProgress>("table-export-progress", (event) => {
if (event.payload.exportId !== request.exportId) return;
onProgress(event.payload);
if (event.payload.status === "Done" || event.payload.status === "Error" || event.payload.status === "Cancelled") {
if (event.payload.status === "Error") {
finish(() => rejectTerminal(new Error(event.payload.errorMessage || "Export failed")));
} else {
finish(() => resolveTerminal(event.payload));
}
}
});
await invoke("start_table_export", { request });
return await terminalProgress;
} catch (error) {
if (!settled) {
settled = true;
unlisten?.();
}
throw error;
}
}
export async function cancelTableExport(exportId: string): Promise<void> {
return invoke("cancel_table_export", { exportId });
}
export async function exportDatabaseSql(
request: DatabaseExportRequest,
onProgress: (progress: ExportProgress) => void,