fix(export): keep MySQL database export snapshots alive
This commit is contained in:
17 files changed
+290
-35
No files matched your search
@@ -68,7 +68,10 @@ const progressValue = (task: ExportTask) => {
|
||||
};
|
||||
|
||||
const taskTitle = (task: ExportTask) => {
|
||||
if (task.kind === "database-export") return t("exportProgress.databaseExportTitle", { name: task.tableName });
|
||||
if (task.kind === "database-export") {
|
||||
const key = task.databaseExportSource === "scheduled" ? "exportProgress.databaseBackupTitle" : "exportProgress.databaseExportTitle";
|
||||
return t(key, { name: task.tableName });
|
||||
}
|
||||
if (task.kind === "sql-file") return t("exportProgress.sqlFileTitle", { name: task.tableName });
|
||||
if (task.kind === "data-transfer") return t("exportProgress.dataTransferTitle", { name: task.tableName });
|
||||
return `${task.tableName}.${task.format}`;
|
||||
@@ -113,6 +116,18 @@ const elapsedText = (task: ExportTask) => {
|
||||
return t("exportProgress.elapsed", { duration: formatDataTransferDuration(finishedAt - task.startedAt) });
|
||||
};
|
||||
|
||||
const databaseObjectText = (task: ExportTask) => {
|
||||
if (task.kind !== "database-export" || !isActive(task.status) || !task.currentObject) return "";
|
||||
if (task.preparing || !task.totalObjects) {
|
||||
return t("databaseExport.preparingObject", { object: task.currentObject });
|
||||
}
|
||||
return t("databaseExport.currentTable", {
|
||||
table: task.currentObject,
|
||||
current: (task.objectIndex ?? 0).toLocaleString(),
|
||||
total: task.totalObjects.toLocaleString(),
|
||||
});
|
||||
};
|
||||
|
||||
const statusIcon = (task: ExportTask) => {
|
||||
if (isActive(task.status)) {
|
||||
if (task.kind === "database-export") return DatabaseBackup;
|
||||
@@ -172,7 +187,7 @@ function failureDetailCount(task: ExportTask) {
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="relative h-8 w-8" :title="triggerTitle" :class="{ 'bg-destructive/10 text-destructive hover:bg-destructive/15': failedCount > 0, 'bg-accent text-primary': failedCount === 0 && hasActive }">
|
||||
<FileDown class="h-4 w-4" />
|
||||
<span v-if="failedCount > 0" class="absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-bold leading-none text-destructive-foreground"> ! </span>
|
||||
<span v-if="failedCount > 0" class="absolute right-0.5 top-0.5 h-2.5 w-2.5 rounded-full bg-destructive ring-2 ring-background" />
|
||||
<span v-else-if="hasActive" class="absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium leading-none text-primary-foreground">
|
||||
{{ activeCount > 9 ? "9+" : activeCount }}
|
||||
</span>
|
||||
@@ -207,6 +222,10 @@ function failureDetailCount(task: ExportTask) {
|
||||
<div class="h-full bg-green-500 rounded-full" style="width: 100%" />
|
||||
</div>
|
||||
|
||||
<div v-if="databaseObjectText(task)" class="min-w-0 truncate text-muted-foreground" :title="task.currentObject">
|
||||
{{ databaseObjectText(task) }}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 text-muted-foreground">
|
||||
<span class="break-words tabular-nums">{{ rowsText(task) }}</span>
|
||||
<span v-if="task.kind !== 'data-transfer' && task.startedAt !== undefined" class="ml-1 tabular-nums">{{ elapsedText(task) }}</span>
|
||||
|
||||
@@ -67,6 +67,96 @@ async function mountPopover() {
|
||||
}
|
||||
|
||||
describe("ExportProgressPopover task duration", () => {
|
||||
it("distinguishes manual exports from scheduled backups and uses a compact failure dot", async () => {
|
||||
const tracker = useExportTracker();
|
||||
tracker.addDatabaseExportTask("manual-export", "app", "/tmp/app.sql");
|
||||
tracker.addDatabaseExportTask("scheduled-backup", "Nightly", "/tmp/backups", "scheduled");
|
||||
tracker.updateDatabaseExportTask("manual-export", {
|
||||
exportId: "manual-export",
|
||||
currentObject: "",
|
||||
objectIndex: 0,
|
||||
totalObjects: 0,
|
||||
rowsExported: 0,
|
||||
totalRows: null,
|
||||
status: "Error",
|
||||
error: "export failed",
|
||||
});
|
||||
|
||||
await mountPopover();
|
||||
|
||||
expect(document.body.textContent).toContain("Database export: app");
|
||||
expect(document.body.textContent).toContain("Database backup: Nightly");
|
||||
const failureDot = document.body.querySelector<HTMLSpanElement>("button > span");
|
||||
expect(failureDot?.classList.contains("h-2.5")).toBe(true);
|
||||
expect(failureDot?.classList.contains("w-2.5")).toBe(true);
|
||||
expect(failureDot?.classList.contains("right-0.5")).toBe(true);
|
||||
expect(failureDot?.classList.contains("top-0.5")).toBe(true);
|
||||
expect(failureDot?.textContent?.trim()).toBe("");
|
||||
});
|
||||
|
||||
it("shows the current database export object without replacing the task title", async () => {
|
||||
const tracker = useExportTracker();
|
||||
const task = tracker.addDatabaseExportTask("database-export", "demo_2000_tables", "/tmp/demo.sql");
|
||||
tracker.updateDatabaseExportTask(task.exportId, {
|
||||
exportId: task.exportId,
|
||||
currentObject: "t_0123_with_a_long_descriptive_name",
|
||||
objectIndex: 123,
|
||||
totalObjects: 2000,
|
||||
rowsExported: 456,
|
||||
totalRows: null,
|
||||
status: "Running",
|
||||
error: null,
|
||||
preparing: false,
|
||||
});
|
||||
|
||||
await mountPopover();
|
||||
|
||||
expect(document.body.textContent).toContain("Database export: demo_2000_tables");
|
||||
expect(document.body.textContent).toContain("Current: t_0123_with_a_long_descriptive_name (123/2,000)");
|
||||
expect(document.body.querySelector('[title="t_0123_with_a_long_descriptive_name"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows the current object while database export metadata is being prepared", async () => {
|
||||
const tracker = useExportTracker();
|
||||
const task = tracker.addDatabaseExportTask("preparing-export", "demo", "/tmp/demo.sql");
|
||||
tracker.updateDatabaseExportTask(task.exportId, {
|
||||
exportId: task.exportId,
|
||||
currentObject: "t_0001",
|
||||
objectIndex: 0,
|
||||
totalObjects: 0,
|
||||
rowsExported: 0,
|
||||
totalRows: null,
|
||||
status: "Running",
|
||||
error: null,
|
||||
preparing: true,
|
||||
});
|
||||
|
||||
await mountPopover();
|
||||
|
||||
expect(document.body.textContent).toContain("Preparing: t_0001");
|
||||
});
|
||||
|
||||
it.each(["Done", "Error", "Cancelled"] as const)("hides stale database object text after the task reaches %s", async (status) => {
|
||||
const tracker = useExportTracker();
|
||||
const task = tracker.addDatabaseExportTask(`terminal-${status}`, "Nightly", "/tmp/backups", "scheduled");
|
||||
tracker.updateDatabaseExportTask(task.exportId, {
|
||||
exportId: task.exportId,
|
||||
currentObject: "Nightly",
|
||||
objectIndex: 2,
|
||||
totalObjects: status === "Error" ? 0 : 2,
|
||||
rowsExported: 0,
|
||||
totalRows: null,
|
||||
status,
|
||||
error: status === "Error" ? "backup failed" : null,
|
||||
preparing: status === "Error",
|
||||
});
|
||||
|
||||
await mountPopover();
|
||||
|
||||
expect(document.body.textContent).not.toContain("Preparing: Nightly");
|
||||
expect(document.body.textContent).not.toContain("Current: Nightly");
|
||||
});
|
||||
|
||||
it("shows frozen transfer elapsed time and live duration for table tasks", async () => {
|
||||
const tracker = useExportTracker();
|
||||
now = 1_000;
|
||||
|
||||
Reference in new issue
Block a user