|
| 1 | +<script setup lang="ts"> |
| 2 | +import { computed, onMounted, ref } from "vue"; |
| 3 | +import type { Column } from "../../../services/filter_registry.ts"; |
| 4 | +import LoadingSpinner from "../base/LoadingSpinner.vue"; |
| 5 | +import dateHelper from "../../../utils/date_helper.ts"; |
| 6 | +import { useSettingsStore } from "../../../services/stores/settings_store.ts"; |
| 7 | +import { useToastStore } from "../../../services/stores/toast_store.ts"; |
| 8 | +import { usePermissions } from "../../../utils/use_permissions.ts"; |
| 9 | +import { useConfirm } from "primevue/useconfirm"; |
| 10 | +import type { BackupInfo } from "../../../models/dataio_models.ts"; |
| 11 | +import { useAuthStore } from "../../../services/stores/auth_store.ts"; |
| 12 | +
|
| 13 | +const settingsStore = useSettingsStore(); |
| 14 | +const toastStore = useToastStore(); |
| 15 | +const authStore = useAuthStore(); |
| 16 | +
|
| 17 | +const { hasPermission } = usePermissions(); |
| 18 | +const confirm = useConfirm(); |
| 19 | +
|
| 20 | +const backups = ref<BackupInfo[]>([]); |
| 21 | +const loading = ref(false); |
| 22 | +
|
| 23 | +onMounted(async () => { |
| 24 | + await getData(); |
| 25 | +}); |
| 26 | +
|
| 27 | +async function getData() { |
| 28 | + try { |
| 29 | + loading.value = true; |
| 30 | + const response = await settingsStore.getBackups(); |
| 31 | + backups.value = response.data.backups || []; |
| 32 | + } catch (e) { |
| 33 | + toastStore.errorResponseToast(e); |
| 34 | + } finally { |
| 35 | + loading.value = false; |
| 36 | + } |
| 37 | +} |
| 38 | +
|
| 39 | +function refresh() { |
| 40 | + getData(); |
| 41 | +} |
| 42 | +
|
| 43 | +defineExpose({ refresh }); |
| 44 | +
|
| 45 | +const activeColumns = computed<Column[]>(() => [ |
| 46 | + { field: "name", header: "Name" }, |
| 47 | + { field: "metadata.app_version", header: "App ver.", hideOnMobile: true }, |
| 48 | + { field: "metadata.db_version", header: "DB ver.", hideOnMobile: true }, |
| 49 | + { field: "metadata.backup_size", header: "Size" }, |
| 50 | + { field: "metadata.created_at", header: "Created" }, |
| 51 | +]); |
| 52 | +
|
| 53 | +function formatBytes(bytes: number): string { |
| 54 | + if (bytes === 0) return "0 Bytes"; |
| 55 | + const k = 1024; |
| 56 | + const sizes = ["Bytes", "KB", "MB", "GB"]; |
| 57 | + const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| 58 | + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; |
| 59 | +} |
| 60 | +
|
| 61 | +function getNestedValue(obj: any, path: string): any { |
| 62 | + return path.split(".").reduce((current, key) => current?.[key], obj); |
| 63 | +} |
| 64 | +
|
| 65 | +async function restoreConfirmation(backupName: string) { |
| 66 | + confirm.require({ |
| 67 | + header: "Restore database?", |
| 68 | + message: `This will restore the database from backup: "${backupName}". All current data will be replaced. This action cannot be undone.`, |
| 69 | + icon: "pi pi-exclamation-triangle", |
| 70 | + acceptLabel: "Restore", |
| 71 | + rejectLabel: "Cancel", |
| 72 | + acceptClass: "p-button-danger", |
| 73 | + rejectClass: "p-button-text", |
| 74 | + accept: () => restoreBackup(backupName), |
| 75 | + }); |
| 76 | +} |
| 77 | +
|
| 78 | +async function restoreBackup(backupName: string) { |
| 79 | + if (!hasPermission("manage_data")) { |
| 80 | + toastStore.createInfoToast( |
| 81 | + "Access denied", |
| 82 | + "You don't have permission to perform this action.", |
| 83 | + ); |
| 84 | + return; |
| 85 | + } |
| 86 | +
|
| 87 | + try { |
| 88 | + loading.value = true; |
| 89 | + await settingsStore.restoreFromDatabaseDump(backupName); |
| 90 | + toastStore.successResponseToast({ |
| 91 | + title: "Success", |
| 92 | + message: "Database restored successfully", |
| 93 | + }); |
| 94 | + authStore.logout(); |
| 95 | + } catch (error) { |
| 96 | + toastStore.errorResponseToast(error); |
| 97 | + } finally { |
| 98 | + loading.value = false; |
| 99 | + } |
| 100 | +} |
| 101 | +
|
| 102 | +async function downloadBackup(backupName: string) { |
| 103 | + if (!hasPermission("manage_data")) { |
| 104 | + toastStore.createInfoToast( |
| 105 | + "Access denied", |
| 106 | + "You don't have permission to perform this action.", |
| 107 | + ); |
| 108 | + return; |
| 109 | + } |
| 110 | +
|
| 111 | + try { |
| 112 | + loading.value = true; |
| 113 | + await settingsStore.downloadBackup(backupName); |
| 114 | + toastStore.createInfoToast( |
| 115 | + "Success", |
| 116 | + "Backup downloaded successfully.", |
| 117 | + ); |
| 118 | + } catch (error) { |
| 119 | + toastStore.errorResponseToast(error); |
| 120 | + } finally { |
| 121 | + loading.value = false; |
| 122 | + } |
| 123 | +} |
| 124 | +</script> |
| 125 | + |
| 126 | +<template> |
| 127 | + <div class="w-full flex flex-row gap-2 justify-content-center"> |
| 128 | + <DataTable |
| 129 | + data-key="name" |
| 130 | + class="w-full enhanced-table" |
| 131 | + :loading="loading" |
| 132 | + :value="backups" |
| 133 | + scrollable |
| 134 | + scroll-height="50vh" |
| 135 | + column-resize-mode="fit" |
| 136 | + scroll-direction="both" |
| 137 | + > |
| 138 | + <template #empty> |
| 139 | + <div style="padding: 10px">No backups found.</div> |
| 140 | + </template> |
| 141 | + <template #loading> |
| 142 | + <LoadingSpinner /> |
| 143 | + </template> |
| 144 | + <Column header="Actions"> |
| 145 | + <template #body="{ data }"> |
| 146 | + <div class="flex flex-row align-items-center gap-2"> |
| 147 | + <i |
| 148 | + v-if="hasPermission('manage_data')" |
| 149 | + v-tooltip.top="'Download'" |
| 150 | + class="pi pi-download hover-icon" |
| 151 | + style="font-size: 0.875rem" |
| 152 | + @click="downloadBackup(data?.name)" |
| 153 | + /> |
| 154 | + <i |
| 155 | + v-if="hasPermission('manage_data')" |
| 156 | + v-tooltip.top="'Restore'" |
| 157 | + class="pi pi-refresh hover-icon" |
| 158 | + style="font-size: 0.875rem; color: var(--p-orange-300)" |
| 159 | + @click="restoreConfirmation(data?.name)" |
| 160 | + /> |
| 161 | + </div> |
| 162 | + </template> |
| 163 | + </Column> |
| 164 | + <Column |
| 165 | + v-for="col of activeColumns" |
| 166 | + :key="col.field" |
| 167 | + :header="col.header" |
| 168 | + :field="col.field" |
| 169 | + :header-class="col.hideOnMobile ? 'mobile-hide ' : ''" |
| 170 | + :body-class="col.hideOnMobile ? 'mobile-hide ' : ''" |
| 171 | + > |
| 172 | + <template #body="{ data }"> |
| 173 | + <template v-if="col.field === 'metadata.created_at'"> |
| 174 | + {{ dateHelper.formatDate(data.metadata.created_at, true) }} |
| 175 | + </template> |
| 176 | + <template v-else-if="col.field === 'metadata.backup_size'"> |
| 177 | + {{ formatBytes(data.metadata.backup_size) }} |
| 178 | + </template> |
| 179 | + <template v-else> |
| 180 | + {{ getNestedValue(data, col.field) }} |
| 181 | + </template> |
| 182 | + </template> |
| 183 | + </Column> |
| 184 | + </DataTable> |
| 185 | + </div> |
| 186 | +</template> |
| 187 | + |
| 188 | +<style scoped></style> |
0 commit comments