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:
22 files changed
+1282
-159
No files matched your search
@@ -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";
|
||||
@@ -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[],
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user