14 files changed
+497
-43
No files matched your search
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Loader2, Check, CheckCircle2, XCircle, AlertCircle, X, FileDown, DatabaseBackup, FileCode2, ArrowRightLeft, Layers3, GitCompareArrows, ChevronRight, FolderOpen, Copy } from "@lucide/vue";
|
||||
import { formatDataTransferDuration, useExportTracker, type ExportTask } from "@/composables/useExportTracker";
|
||||
import { dataTransferFailureCopyText, sqlFileFailureCopyText } from "@/components/export/failureDetailCopyText";
|
||||
import SqlFileProgressIndicator from "@/components/sql-file/SqlFileProgressIndicator.vue";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
@@ -65,12 +66,6 @@ const progressValue = (task: ExportTask) => {
|
||||
if (!task.totalObjects || task.totalObjects <= 0) return 0;
|
||||
return Math.min(100, Math.round(((task.objectIndex ?? 0) / task.totalObjects) * 100));
|
||||
}
|
||||
if (task.kind === "sql-file") {
|
||||
const total = task.totalRows ?? task.statementIndex ?? 0;
|
||||
if (task.status === "Done") return 100;
|
||||
if (total <= 0) return 0;
|
||||
return Math.min(95, Math.round((task.rowsExported / total) * 100));
|
||||
}
|
||||
if (task.kind === "data-transfer") {
|
||||
if (!task.totalTables || task.totalTables <= 0) return 0;
|
||||
if (task.status === "Done") return 100;
|
||||
@@ -358,7 +353,8 @@ function openTask(task: ExportTask): void {
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div v-if="isActive(task.status)" class="w-full bg-muted rounded-full h-1.5 overflow-hidden">
|
||||
<SqlFileProgressIndicator v-if="task.kind === 'sql-file'" :status="task.status" :bytes-read="task.bytesRead" :total-bytes="task.totalBytes" :phase="task.sqlFilePhase" />
|
||||
<div v-else-if="isActive(task.status)" class="w-full bg-muted rounded-full h-1.5 overflow-hidden">
|
||||
<div
|
||||
v-if="
|
||||
task.totalRows ||
|
||||
@@ -384,6 +380,7 @@ function openTask(task: ExportTask): void {
|
||||
|
||||
<div class="min-w-0 text-muted-foreground">
|
||||
<span class="break-words tabular-nums">{{ rowsText(task) }}</span>
|
||||
<span v-if="task.kind === 'sql-file' && task.elapsedMs !== undefined" class="ml-1 tabular-nums">{{ t("exportProgress.elapsed", { duration: formatDataTransferDuration(task.elapsedMs) }) }}</span>
|
||||
<span v-if="task.kind !== 'data-transfer' && task.startedAt !== undefined" class="ml-1 tabular-nums">{{ elapsedText(task) }}</span>
|
||||
<span v-if="taskStatusText(task)" class="ml-1 font-medium text-primary">{{ taskStatusText(task) }}</span>
|
||||
<span v-if="hasUnlistedTaskError(task)" class="mt-1 block whitespace-normal break-words text-destructive" :title="translateBackendError(t, task.errorMessage!)">
|
||||
|
||||
@@ -94,6 +94,61 @@ describe("failure detail copy text", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("SQL file byte progress", () => {
|
||||
it("keeps legacy progress indeterminate instead of using successful statements as the total", async () => {
|
||||
const tracker = useExportTracker();
|
||||
const task = tracker.addSqlFileTask("legacy-sql-progress", "large.sql", "/tmp/large.sql");
|
||||
tracker.updateSqlFileTask(task.exportId, {
|
||||
executionId: task.exportId,
|
||||
status: "running",
|
||||
statementIndex: 772205,
|
||||
successCount: 772205,
|
||||
failureCount: 0,
|
||||
affectedRows: 0,
|
||||
elapsedMs: 1000,
|
||||
statementSummary: "INSERT INTO users VALUES (1)",
|
||||
});
|
||||
await mountPopover();
|
||||
expect(document.querySelector('[data-testid="sql-file-progress"] [role="progressbar"]')?.hasAttribute("aria-valuenow")).toBe(false);
|
||||
expect(task.totalRows).toBeNull();
|
||||
});
|
||||
|
||||
it.each(["done", "error", "cancelled"] as const)("shows byte progress and preserves it on %s", async (status) => {
|
||||
const tracker = useExportTracker();
|
||||
const task = tracker.addSqlFileTask(`byte-progress-${status}`, "large.sql", "/tmp/large.sql");
|
||||
const progress = {
|
||||
executionId: task.exportId,
|
||||
status: "running" as const,
|
||||
statementIndex: 772205,
|
||||
successCount: 772205,
|
||||
failureCount: 0,
|
||||
affectedRows: 0,
|
||||
elapsedMs: 1000,
|
||||
statementSummary: "INSERT INTO users VALUES (1)",
|
||||
bytesRead: 1024,
|
||||
totalBytes: 4096,
|
||||
phase: "executing" as const,
|
||||
};
|
||||
tracker.updateSqlFileTask(task.exportId, progress);
|
||||
await mountPopover();
|
||||
const indicator = document.querySelector('[data-testid="sql-file-progress"]')!;
|
||||
expect(indicator.textContent).toContain("Executing SQL");
|
||||
expect(indicator.textContent).toContain("Read 1.0 KB / 4.0 KB");
|
||||
expect(indicator.querySelector('[role="progressbar"]')?.getAttribute("aria-valuenow")).toBe("25");
|
||||
|
||||
tracker.updateSqlFileTask(task.exportId, { ...progress, status: "statementDone", fileIndex: 1, statementIndex: 2, successCount: 2, bytesRead: 2048 });
|
||||
await nextTick();
|
||||
expect(indicator.querySelector('[role="progressbar"]')?.getAttribute("aria-valuenow")).toBe("50");
|
||||
|
||||
tracker.updateSqlFileTask(task.exportId, { ...progress, status, bytesRead: undefined, totalBytes: undefined, phase: undefined });
|
||||
await nextTick();
|
||||
expect(indicator.querySelector('[role="progressbar"]')?.getAttribute("aria-valuenow")).toBe(status === "done" ? "100" : "50");
|
||||
expect(task.bytesRead).toBe(2048);
|
||||
expect(task.totalBytes).toBe(4096);
|
||||
expect(task.elapsedMs).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExportProgressPopover task duration", () => {
|
||||
it("distinguishes manual exports from scheduled backups and uses a compact failure dot", async () => {
|
||||
const tracker = useExportTracker();
|
||||
|
||||
Reference in new issue
Block a user