-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathConfigBackups.vue
More file actions
120 lines (110 loc) · 3.69 KB
/
Copy pathConfigBackups.vue
File metadata and controls
120 lines (110 loc) · 3.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<template>
<div v-if="!loading">
<div v-if="backups.length > 0">
<p class="text-muted mb-3">
{{ count }} {{ count === 1 ? 'backup' : 'backups' }} ·
{{ formatBytes(totalSizeBytes) }} total
</p>
<BTable :items="backups" :fields="fields">
<template #cell(size_bytes)="data">
{{ formatBytes(data.item.size_bytes) }}
</template>
<template #cell(created_at)="data">
{{ formatDate(data.item.created_at) }}
</template>
<template #cell(actions)="data">
<BButton
variant="danger"
size="sm"
:disabled="isDeleting"
@click="deleteBackup(data.item)"
>
Delete
</BButton>
</template>
</BTable>
</div>
<BAlert v-else variant="info" :model-value="true">
No backup files found. Backups are created automatically before database migrations.
</BAlert>
</div>
<div v-else class="text-center">
<BSpinner style="width: 10rem; height: 10rem" variant="info" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import log from 'loglevel';
import { makeURL } from '@/js/utils';
import { toast } from '@/js/toast';
import { useConfirm } from '@/composables/useConfirm';
import type { BackupFile, BackupsResponse } from '@/types/api/backup';
const { confirm } = useConfirm();
const loading = ref(true);
const isDeleting = ref(false);
const backups = ref<BackupFile[]>([]);
const count = ref(0);
const totalSizeBytes = ref(0);
const fields = [
{ key: 'filename', label: 'Filename' },
{ key: 'size_bytes', label: 'Size', sortable: true },
{ key: 'created_at', label: 'Date Created', sortable: true },
{ key: 'actions', label: '' },
];
async function fetchBackups(): Promise<void> {
try {
const response = await fetch(makeURL('/api/v1/admin/db-backups'));
if (response.ok) {
const data: BackupsResponse = await response.json();
backups.value = data.backups;
count.value = data.count;
totalSizeBytes.value = data.total_size_bytes;
} else {
log.error('Unable to fetch backup files');
}
} catch (err) {
log.error('Error fetching backup files:', err);
}
}
async function deleteBackup(backup: BackupFile): Promise<void> {
const confirmed = await confirm(
`Are you sure you want to permanently delete "${backup.filename}"? This cannot be undone.`,
{ title: 'Delete Backup', okVariant: 'danger', okTitle: 'Delete' }
);
if (!confirmed) return;
isDeleting.value = true;
try {
const response = await fetch(
makeURL(`/api/v1/admin/db-backups?timestamp=${backup.created_at}`),
{ method: 'DELETE' }
);
if (response.ok) {
toast.success('Backup deleted');
await fetchBackups();
} else {
const body = await response.json();
toast.error(`Failed to delete backup: ${body.message}`);
}
} catch (err) {
log.error('Error deleting backup:', err);
toast.error('Failed to delete backup');
} finally {
isDeleting.value = false;
}
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
function formatDate(unixTimestamp: number): string {
const d = new Date(unixTimestamp * 1000);
const pad = (n: number) => String(n).padStart(2, '0');
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
onMounted(async () => {
await fetchBackups();
loading.value = false;
});
</script>