14 files changed
+497
-43
No files matched your search
@@ -223,6 +223,21 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("SqlFileExecutionDialog retries", () => {
|
||||
it("shows byte-based progress while SQL executes and only completes on a terminal event", async () => {
|
||||
await mountReadyDialog();
|
||||
const executionGate = deferred();
|
||||
mocks.executeSqlFiles.mockImplementationOnce(async (request: { executionId: string }) => {
|
||||
mocks.progressHandler?.(progress(request.executionId, "running", { statementIndex: 500, successCount: 499, bytesRead: 1024, totalBytes: 4096, phase: "executing" }));
|
||||
await executionGate.promise;
|
||||
mocks.progressHandler?.(progress(request.executionId, "done", { bytesRead: 4096, totalBytes: 4096 }));
|
||||
});
|
||||
findButton("sqlFile.execute").click();
|
||||
await vi.waitFor(() => expect(root!.querySelector('[role="progressbar"]')?.getAttribute("aria-valuenow")).toBe("25"));
|
||||
expect(root!.textContent).toContain("sqlFile.progressPhase.executing");
|
||||
executionGate.resolve();
|
||||
await vi.waitFor(() => expect(root!.querySelector('[role="progressbar"]')?.getAttribute("aria-valuenow")).toBe("100"));
|
||||
});
|
||||
|
||||
it("does not auto-select the first connection for an unassociated external file", async () => {
|
||||
root = document.createElement("div");
|
||||
document.body.append(root);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { fetchSqlFileTargetOptions } from "@/composables/useDatabaseOptions";
|
||||
import { requiresSqlFileTargetDatabaseSelection, supportsConnectionLevelDatabaseBootstrap } from "@/lib/connection/connectionLevelDatabaseBootstrap";
|
||||
import { cancelSqlFileExecution, executeSqlFiles, inspectSqlFileTables, listenSqlFileProgress, previewSqlFile, type SqlFilePreview, type SqlFileProgress, type SqlFileStatus, type SqlFileTable } from "@/lib/backend/api";
|
||||
import { buildDisplayFileNames, tooltipText as computeTooltipText } from "./sqlFilePreviewLabel";
|
||||
import SqlFileProgressIndicator from "./SqlFileProgressIndicator.vue";
|
||||
import { useExportTracker, type ExportTask } from "@/composables/useExportTracker";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { Check, CheckSquare, ChevronRight, FileCode, FolderOpen, Loader2, Play, Square, X } from "@lucide/vue";
|
||||
@@ -222,14 +223,6 @@ const statusIcon = computed(() => {
|
||||
return FileCode;
|
||||
});
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!progress.value) return 0;
|
||||
if (terminalStatus.value === "done") return 100;
|
||||
const attempted = progress.value.successCount + progress.value.failureCount;
|
||||
const current = Math.max(progress.value.statementIndex, attempted);
|
||||
if (current <= 0) return running.value ? 8 : 0;
|
||||
return Math.min(95, Math.max(8, Math.round((attempted / current) * 100)));
|
||||
});
|
||||
const sqlFileFailures = computed(() => activeExecutionTask.value?.sqlFileFailures ?? []);
|
||||
const sqlFileFailureCount = computed(() => sqlFileFailures.value.length + (activeExecutionTask.value?.sqlFileFailuresOmitted ?? 0));
|
||||
const unlistedTerminalError = computed(() => {
|
||||
@@ -826,9 +819,7 @@ watch(
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-muted rounded-full h-2 overflow-hidden">
|
||||
<div class="h-full rounded-full transition-[width] duration-300" :class="terminalStatus === 'error' ? 'bg-destructive' : terminalStatus === 'cancelled' ? 'bg-yellow-500' : 'bg-primary'" :style="{ width: `${progressPercent}%` }" />
|
||||
</div>
|
||||
<SqlFileProgressIndicator :status="terminalStatus" :bytes-read="progress?.bytesRead" :total-bytes="progress?.totalBytes" :phase="progress?.phase" />
|
||||
|
||||
<div v-if="running && previews.length > 1 && currentFileIndex >= 0" class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<FileCode class="w-3.5 h-3.5 shrink-0" />
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { SqlFileProgress } from "@/lib/backend/tauri";
|
||||
import { formatSqlFileBytes, sqlFileProgressPercent } from "@/lib/sql/sqlFileProgress";
|
||||
|
||||
const props = defineProps<{
|
||||
status: string;
|
||||
bytesRead?: number;
|
||||
totalBytes?: number;
|
||||
phase?: SqlFileProgress["phase"];
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
const normalizedStatus = computed(() => props.status.toLowerCase());
|
||||
const active = computed(() => !["done", "error", "cancelled", "idle"].includes(normalizedStatus.value));
|
||||
const percent = computed(() => sqlFileProgressPercent(props.status, props.bytesRead, props.totalBytes));
|
||||
const phaseLabel = computed(() => {
|
||||
if (!active.value) return t("sqlFile.byteProgress");
|
||||
const phase = props.phase ?? (normalizedStatus.value === "started" ? "preparing" : "executing");
|
||||
return t(`sqlFile.progressPhase.${phase}`);
|
||||
});
|
||||
const byteLabel = computed(() => {
|
||||
if (props.bytesRead === undefined || !Number.isFinite(props.bytesRead)) return "";
|
||||
const read = formatSqlFileBytes(props.bytesRead);
|
||||
return props.totalBytes === undefined || !Number.isFinite(props.totalBytes) ? t("sqlFile.bytesRead", { read }) : t("sqlFile.bytesReadOfTotal", { read, total: formatSqlFileBytes(props.totalBytes) });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-w-0 space-y-1.5 text-xs" data-testid="sql-file-progress" :title="t('sqlFile.byteProgressHint')">
|
||||
<div class="flex items-center justify-between gap-2 text-muted-foreground">
|
||||
<span class="truncate">{{ phaseLabel }}</span>
|
||||
<span v-if="percent !== null" class="shrink-0 tabular-nums">{{ percent }}%</span>
|
||||
</div>
|
||||
<div role="progressbar" :aria-label="t('sqlFile.byteProgress')" :aria-valuenow="percent ?? undefined" :aria-valuemin="0" :aria-valuemax="100" class="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div v-if="percent === null && active" class="sql-file-progress-indeterminate h-full rounded-full bg-primary" />
|
||||
<div v-else class="h-full rounded-full transition-[width] duration-300" :class="normalizedStatus === 'error' ? 'bg-destructive' : normalizedStatus === 'cancelled' ? 'bg-yellow-500' : normalizedStatus === 'done' ? 'bg-green-500' : 'bg-primary'" :style="{ width: `${percent ?? 0}%` }" />
|
||||
</div>
|
||||
<div v-if="byteLabel" class="break-words text-muted-foreground tabular-nums">{{ byteLabel }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sql-file-progress-indeterminate {
|
||||
width: 40%;
|
||||
animation: sql-file-progress-slide 1.15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes sql-file-progress-slide {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(250%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user