Add backup listing

This commit is contained in:
Johannes Zellner
2025-02-05 15:47:46 +01:00
parent 42f493b2c3
commit 86c4045073
6 changed files with 173 additions and 15 deletions
+122 -14
View File
@@ -4,21 +4,28 @@ import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, useTemplateRef, computed } from 'vue';
import { Button, InputDialog, Dialog, FormGroup, TableView } from 'pankow';
import { ref, onMounted, useTemplateRef } from 'vue';
import { Button, ProgressBar, TableView, Dialog } from 'pankow';
import { prettyLongDate } from 'pankow/utils';
import { TASK_TYPES } from '../constants.js';
import { TASK_TYPES, SECRET_PLACEHOLDER } from '../constants.js';
import Section from '../components/Section.vue';
import BackupsModel from '../models/BackupsModel.js';
import AppsModel from '../models/AppsModel.js';
import TasksModel from '../models/TasksModel.js';
import DashboardModel from '../models/DashboardModel.js';
import { download } from '../utils.js';
const props = defineProps({
config: Object
});
const backupsModel = BackupsModel.create();
const appsModel = AppsModel.create();
const tasksModel = TasksModel.create();
const dashboardModel = DashboardModel.create();
const columns = {
archived: {},
preserveSecs: {}, // archived
packageVersion: { label: t('backups.listing.version'), sort: true },
creationTime: { label: t('main.table.date'), sort: true },
content: { label: t('backups.listing.contents'), sort: false },
@@ -28,15 +35,15 @@ const columns = {
const busy = ref(true);
const backups = ref([]);
const taskLogsMenu = ref([]);
const backupTask = ref({});
const lastTask = ref({});
async function waitForTask() {
if (!backupTask.value.id) return;
if (!lastTask.value.id) return;
const [error, result] = await tasksModel.get(backupTask.value.id);
const [error, result] = await tasksModel.get(lastTask.value.id);
if (error) return console.error(error);
backupTask.value = result;
lastTask.value = result;
// task done, refresh menu
if (!result.active) return await refreshTasks();
@@ -48,7 +55,7 @@ async function refreshTasks() {
const [error, result] = await tasksModel.getByType(TASK_TYPES.TASK_BACKUP);
if (error) return console.error(error);
backupTask.value = result[0] || {};
lastTask.value = result[0] || {};
// limit to last 10
taskLogsMenu.value = result.slice(0,10).map(t => {
@@ -60,7 +67,7 @@ async function refreshTasks() {
});
// if last task is currently active, start polling
if (backupTask.value.active) waitForTask();
if (lastTask.value.active) waitForTask();
}
async function refreshBackups() {
@@ -103,6 +110,58 @@ async function refreshBackups() {
backups.value = result;
}
async function onStartBackup() {
const [error] = await backupsModel.create();
if (error) {
if (error.status === 409 && error.message.indexOf('full_backup') !== -1) {
// TODO
// $scope.createBackup.errorMessage = 'Backup already in progress. Please retry later.';
} else if (error.statusCode === 409) {
// TODO
// $scope.createBackup.errorMessage = 'App task is currently in progress. Please retry later.';
}
return console.error(error);
}
await refreshTasks();
}
async function onStopBackup() {
const [error] = await tasksModel.stop(lastTask.value.id);
if (error) return console.error(error);
}
async function onDownloadConfig(backup) {
const [error, result] = await dashboardModel.getConfig();
if (error) return console.error(error);
// secrets and tokens already come with placeholder characters we remove them
const tmp = {
remotePath: backup.remotePath,
encrypted: !!props.config.password // we add this just to help the import UI
};
Object.keys(props.config).forEach((k) => {
if (props.config[k] !== SECRET_PLACEHOLDER) tmp[k] = props.config[k];
});
const filename = `${result.adminFqdn}-backup-config-${(new Date(backup.creationTime)).toISOString().split('T')[0]}.json`;
download(filename, JSON.stringify(tmp, null, 4));
}
function onEdit(backup) {
console.log('edit', backup);
}
const infoDialog = useTemplateRef('infoDialog');
const infoBackup = ref({ contents: [] });
function onInfo(backup) {
infoBackup.value = backup;
infoDialog.value.open();
}
onMounted(async () => {
await refreshBackups();
await refreshTasks();
@@ -112,13 +171,51 @@ onMounted(async () => {
</script>
<template>
<Dialog ref="infoDialog"
:title="$t('backups.backupDetails.title')"
:reject-label="$t('main.dialog.cancel')"
>
<div class="info-row">
<div class="info-label">{{ $t('backups.backupDetails.id') }}</div>
<div class="info-value">{{ infoBackup.id }}</div>
</div>
<div class="info-row">
<div class="info-label">{{ $t('backups.backupEdit.label') }}</div>
<div class="info-value">{{ infoBackup.label }}</div>
</div>
<div class="info-row">
<div class="info-label">{{ $t('backups.backupEdit.remotePath') }}</div>
<div class="info-value">{{ infoBackup.remotePath }}</div>
</div>
<div class="info-row">
<div class="info-label">{{ $t('backups.backupDetails.date') }}</div>
<div class="info-value">{{ prettyLongDate(infoBackup.creationTime) }}</div>
</div>
<div class="info-row">
<div class="info-label">{{ $t('backups.backupDetails.version') }}</div>
<div class="info-value">{{ infoBackup.packageVersion }}</div>
</div>
<div class="info-row">
<div class="info-label">{{ $t('backups.backupDetails.format') }}</div>
<div class="info-value">{{ infoBackup.format }}</div>
</div>
<br/>
<p class="text-muted">{{ $t('backups.backupDetails.list', { appCount: infoBackup.contents.length }) }}:</p>
<span v-for="content in infoBackup.contents" :key="content.id">
<a v-if="content.fqdn" ng-href="/#/app/{{content.id}}/backups">{{ content.label || content.fqdn }}</a>
<a v-else :href="`/#/eventlog?search=${content.id}`">{{ content.id }}</a>
</span>
</Dialog>
<Section :title="$t('backups.listing.title')">
<template #header-buttons>
<Button tool icon="fas fa-align-left" v-tooltip="$t('settings.updates.showLogsAction')" :menu="taskLogsMenu" :disabled="!taskLogsMenu.length"/>
</template>
<TableView :columns="columns" :model="backups" :busy="busy">
<template #archived="slotProps">
<TableView :columns="columns" :model="backups" :busy="busy" @row-click="onInfo">
<template #preserveSecs="slotProps">
<i class="fas fa-archive" v-show="slotProps.preserveSecs === -1" v-tooltip="$t('backups.listing.tooltipPreservedBackup')"></i>
</template>
@@ -131,10 +228,21 @@ onMounted(async () => {
<template #actions="slotProps">
<div class="table-actions">
<Button tool secondary small icon="fa-solid fa-pencil-alt" v-tooltip="$t('backups.listing.tooltipEditBackup')" @click="onEdit(slotProps)"></Button>
<Button tool secondary small icon="fa-solid fa-file-alt" v-tooltip="$t('backups.listing.tooltipDownloadBackupConfig')" @click="onDownloadConfig(slotProps)"></Button>
<Button tool secondary small icon="fa-solid fa-pencil-alt" v-tooltip="$t('backups.listing.tooltipEditBackup')" @click.stop="onEdit(slotProps)"></Button>
<Button tool secondary small icon="fa-solid fa-file-alt" v-tooltip="$t('backups.listing.tooltipDownloadBackupConfig')" @click.stop="onDownloadConfig(slotProps)"></Button>
</div>
</template>
</TableView>
<div v-if="lastTask.active">
<ProgressBar :value="lastTask.percent" />
<div>{{ lastTask.message }}</div>
</div>
<!-- TODO show last task error message if any -->
<br/>
<Button danger @click="onStopBackup()" v-if="lastTask.active">{{ $t('backups.listing.stopTask') }}</Button>
<Button @click="onStartBackup()" v-else>{{ $t('backups.listing.backupNow') }}</Button>
</Section>
</template>