feat(backup): support table filters and MySQL compatibility
This commit is contained in:
1 parent
09c3f7f44c
commit
7abac0959f
15 files changed
+386
-32
No files matched your search
@@ -13,7 +13,7 @@ import * as api from "@/lib/backend/api";
|
||||
import { useScheduledDatabaseBackups } from "@/composables/useScheduledDatabaseBackups";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
|
||||
import { nextDatabaseBackupRunAt, supportsScheduledDatabaseBackup, type DatabaseBackupFile, type DatabaseBackupRun, type DatabaseBackupSchedule } from "@/lib/backup/scheduledDatabaseBackup";
|
||||
import { nextDatabaseBackupRunAt, normalizeDatabaseBackupTablePatterns, supportsScheduledDatabaseBackup, type DatabaseBackupFile, type DatabaseBackupRun, type DatabaseBackupSchedule } from "@/lib/backup/scheduledDatabaseBackup";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
@@ -32,6 +32,7 @@ const saving = ref(false);
|
||||
const databaseOptions = ref<string[]>([]);
|
||||
const allDatabases = ref(true);
|
||||
const selectedDatabases = ref<string[]>([]);
|
||||
const tablePatternsInput = ref("");
|
||||
const expandedRunIds = reactive(new Set<string>());
|
||||
|
||||
const sqlConnections = computed(() => connectionStore.connections.filter((connection) => supportsScheduledDatabaseBackup(connection.db_type)));
|
||||
@@ -54,6 +55,8 @@ function newScheduleDraft(connectionId = sqlConnections.value[0]?.id ?? ""): Dat
|
||||
enabled: true,
|
||||
connectionId,
|
||||
databases: [],
|
||||
tableFilterMode: "all",
|
||||
tablePatterns: [],
|
||||
destinationDirectory: "",
|
||||
frequency: "daily",
|
||||
intervalHours: 6,
|
||||
@@ -76,7 +79,8 @@ const draft = ref<DatabaseBackupSchedule>(newScheduleDraft());
|
||||
const canSave = computed(() => {
|
||||
const hasContent = draft.value.includeStructure || draft.value.includeData || draft.value.includeObjects;
|
||||
const hasDatabaseScope = allDatabases.value || selectedDatabases.value.length > 0;
|
||||
return !!draft.value.name.trim() && !!draft.value.connectionId && !!draft.value.destinationDirectory.trim() && hasContent && hasDatabaseScope && !saving.value && !loadingDatabases.value;
|
||||
const hasTableScope = draft.value.tableFilterMode === "all" || normalizeDatabaseBackupTablePatterns(tablePatternsInput.value).length > 0;
|
||||
return !!draft.value.name.trim() && !!draft.value.connectionId && !!draft.value.destinationDirectory.trim() && hasContent && hasDatabaseScope && hasTableScope && !saving.value && !loadingDatabases.value;
|
||||
});
|
||||
const nextRunPreview = computed(() => nextDatabaseBackupRunAt(draft.value, new Date()));
|
||||
|
||||
@@ -104,6 +108,12 @@ function databaseScopeLabel(schedule: DatabaseBackupSchedule): string {
|
||||
return t("databaseBackup.databaseCount", { count: schedule.databases.length });
|
||||
}
|
||||
|
||||
function tableScopeLabel(schedule: DatabaseBackupSchedule): string {
|
||||
if (schedule.tableFilterMode === "include") return t("databaseBackup.includedTablePatterns", { count: schedule.tablePatterns.length });
|
||||
if (schedule.tableFilterMode === "exclude") return t("databaseBackup.excludedTablePatterns", { count: schedule.tablePatterns.length });
|
||||
return "";
|
||||
}
|
||||
|
||||
function runStatusLabel(status: DatabaseBackupRun["status"]): string {
|
||||
return t(`databaseBackup.status.${status}`);
|
||||
}
|
||||
@@ -130,6 +140,9 @@ async function loadDatabases(connectionId: string, preserveSelection: boolean) {
|
||||
if (!preserveSelection) {
|
||||
selectedDatabases.value = [];
|
||||
allDatabases.value = true;
|
||||
draft.value.tableFilterMode = "all";
|
||||
draft.value.tablePatterns = [];
|
||||
tablePatternsInput.value = "";
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast(error?.message || String(error), 5000);
|
||||
@@ -143,15 +156,17 @@ async function openCreateSchedule() {
|
||||
draft.value = newScheduleDraft();
|
||||
allDatabases.value = true;
|
||||
selectedDatabases.value = [];
|
||||
tablePatternsInput.value = "";
|
||||
scheduleDialogOpen.value = true;
|
||||
await loadDatabases(draft.value.connectionId, false);
|
||||
}
|
||||
|
||||
async function openEditSchedule(schedule: DatabaseBackupSchedule) {
|
||||
editingScheduleId.value = schedule.id;
|
||||
draft.value = { ...schedule, databases: [...schedule.databases] };
|
||||
draft.value = { ...schedule, databases: [...schedule.databases], tablePatterns: [...schedule.tablePatterns] };
|
||||
allDatabases.value = schedule.databases.length === 0;
|
||||
selectedDatabases.value = [...schedule.databases];
|
||||
tablePatternsInput.value = schedule.tablePatterns.join(", ");
|
||||
scheduleDialogOpen.value = true;
|
||||
await loadDatabases(schedule.connectionId, true);
|
||||
}
|
||||
@@ -181,6 +196,7 @@ async function submitSchedule() {
|
||||
saveSchedule({
|
||||
...draft.value,
|
||||
databases: allDatabases.value ? [] : [...selectedDatabases.value],
|
||||
tablePatterns: draft.value.tableFilterMode === "all" ? [] : normalizeDatabaseBackupTablePatterns(tablePatternsInput.value),
|
||||
});
|
||||
scheduleDialogOpen.value = false;
|
||||
toast(t(editingScheduleId.value ? "databaseBackup.scheduleUpdated" : "databaseBackup.scheduleCreated"), 2500);
|
||||
@@ -286,6 +302,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
|
||||
<div class="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>{{ frequencyLabel(schedule) }}</span>
|
||||
<span>{{ databaseScopeLabel(schedule) }}</span>
|
||||
<span v-if="schedule.tableFilterMode !== 'all'">{{ tableScopeLabel(schedule) }}</span>
|
||||
<span>{{ t("databaseBackup.nextRun", { time: formatDate(schedule.nextRunAt) }) }}</span>
|
||||
<span>{{ t("databaseBackup.keepRuns", { count: schedule.retentionCount }) }}</span>
|
||||
</div>
|
||||
@@ -413,6 +430,27 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="grid gap-4 sm:grid-cols-[minmax(0,200px)_minmax(0,1fr)]">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.tableScope") }}</Label>
|
||||
<Select v-model="draft.tableFilterMode">
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{{ t("databaseBackup.allTables") }}</SelectItem>
|
||||
<SelectItem value="include">{{ t("databaseBackup.includeTables") }}</SelectItem>
|
||||
<SelectItem value="exclude">{{ t("databaseBackup.excludeTables") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="draft.tableFilterMode !== 'all'" class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.tablePatterns") }}</Label>
|
||||
<Input v-model="tablePatternsInput" :placeholder="t('databaseBackup.tablePatternsPlaceholder')" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="draft.tableFilterMode !== 'all'" class="text-xs text-muted-foreground">{{ t("databaseBackup.tablePatternsHint") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("databaseBackup.frequency") }}</Label>
|
||||
|
||||
Reference in new issue
Block a user