Create separate views for backup targets and archives

This commit is contained in:
Johannes Zellner
2025-07-31 11:51:45 +02:00
parent ae3a34287a
commit b40248a1d5
3 changed files with 168 additions and 17 deletions
@@ -0,0 +1,133 @@
<script setup>
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, useTemplateRef } from 'vue';
import { Button, ButtonGroup, TableView, InputDialog } from '@cloudron/pankow';
import { prettyLongDate } from '@cloudron/pankow/utils';
import { API_ORIGIN, SECRET_PLACEHOLDER } from '../constants.js';
import AppRestoreDialog from '../components/AppRestoreDialog.vue';
import Section from '../components/Section.vue';
import ArchivesModel from '../models/ArchivesModel.js';
import { download } from '../utils.js';
const archivesModel = ArchivesModel.create();
const columns = {
icon: {}, // archived
location: {
label: t('app.location.location'),
sort: true
},
info: {
label: t('backups.archives.info'),
sort: false,
hideMobile: true,
},
creationTime: {
label: t('main.table.date'),
sort: true,
hideMobile: true,
},
actions: {}
};
const busy = ref(true);
const archives = ref([]);
async function refreshArchives() {
const [error, result] = await archivesModel.list();
if (error) return console.error(error);
// ensure we use the full api oprigin
result.forEach(a => {
a.iconUrl = API_ORIGIN + a.iconUrl;
});
archives.value = result;
}
const inputDialog = useTemplateRef('inputDialog');
async function onRemove(archive) {
const yes = await inputDialog.value.confirm({
title: t('backups.deleteArchiveDialog.title', { appTitle: archive.appConfig?.manifest?.title, fqdn: archive.appConfig?.fqdn }),
message: t('backups.deleteArchiveDialog.description'),
confirmStyle: 'danger',
confirmLabel: t('backups.deleteArchive.deleteAction'),
rejectLabel: t('main.dialog.cancel')
});
if (!yes) return;
const [error] = await archivesModel.remove(archive.id);
if (error) return console.error(error);
await refreshArchives();
}
const restoreDialog = useTemplateRef('restoreDialog');
async function onRestore(archive) {
restoreDialog.value.open(archive);
}
async function onDownloadConfig(archive) {
// secrets and tokens already come with placeholder characters we remove them
// TODO fill tmp from target config
const tmp = {
remotePath: archive.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 = `${archive.appConfig.fqdn}-archive-config-${(new Date(archive.creationTime)).toISOString().split('T')[0]}.json`;
download(filename, JSON.stringify(tmp, null, 4));
}
onMounted(async () => {
await refreshArchives();
busy.value = false;
});
</script>
<template>
<div class="content">
<Section :title="$t('backups.archives.title')">
<InputDialog ref="inputDialog"/>
<AppRestoreDialog ref="restoreDialog"/>
<p v-html="$t('backups.archive.description')"></p>
<TableView :columns="columns" :model="archives" :busy="busy">
<template #icon="archive">
<img :src="archive.iconUrl || 'img/appicon_fallback.png'" v-fallback-image="API_ORIGIN + '/img/appicon_fallback.png'" height="24" width="24"/>
</template>
<!-- for pre-8.2 backups, appConfig can be null -->
<template #location="archive">{{ archive.appConfig ? archive.appConfig.fqdn : '-' }}</template>
<template #info="archive">
<span v-tooltip="`${archive.manifest.id}@${archive.manifest.version}`">{{ archive.manifest.title }}</span>
</template>
<template #creationTime="archive">{{ prettyLongDate(archive.creationTime) }}</template>
<template #actions="archive">
<div class="table-actions">
<ButtonGroup>
<Button tool secondary small icon="fa-solid fa-history" v-tooltip="'Restore from Archive'" @click.stop="onRestore(archive)"></Button>
<Button tool secondary small icon="fa-solid fa-file-alt" v-tooltip="$t('backups.listing.tooltipDownloadBackupConfig')" @click.stop="onDownloadConfig(archive)"></Button>
</ButtonGroup>
<Button tool danger small icon="fa-solid fa-trash-alt" v-tooltip="'Delete Archive'" @click.stop="onRemove(archive)"></Button>
</div>
</template>
</TableView>
</Section>
</div>
</template>
@@ -8,19 +8,21 @@ import StateLED from '../components/StateLED.vue';
import BackupDialog from '../components/BackupDialog.vue';
import BackupSchedule from '../components/BackupSchedule.vue';
import BackupList from '../components/BackupList.vue';
import AppArchive from '../components/AppArchive.vue';
import BackupsModel from '../models/BackupsModel.js';
import BackupTargetsModel from '../models/BackupTargetsModel.js';
import ProfileModel from '../models/ProfileModel.js';
import { mountlike, s3like } from '../utils.js';
const profileModel = ProfileModel.create();
const backupsModel = BackupsModel.create();
const backupTargetsModel = BackupTargetsModel.create();
const profile = ref({});
const manualBackupApps = ref([]);
const config = ref({});
const mountOptions = ref({});
const mountStatus = ref({});
const targets = ref([]);
const backupDialog = useTemplateRef('backupDialog');
function onConfigure() {
@@ -48,19 +50,21 @@ async function onRemount() {
}
async function refresh() {
let [error, result] = await backupsModel.getConfig();
const [error, result] = await backupTargetsModel.list();
if (error) return console.error(error);
config.value = result;
mountOptions.value = result.mountOptions || {};
mountStatus.value = {};
targets.value = result;
// mountOptions.value = result.mountOptions || {};
// mountStatus.value = {};
if (!mountlike(config.value.provider)) return;
// if (!mountlike(config.value.provider)) return;
[error, result] = await backupsModel.mountStatus();
if (error) return console.error(error);
// [error, result] = await backupsModel.mountStatus();
// if (error) return console.error(error);
mountStatus.value = result;
// mountStatus.value = result;
console.log(targets.value)
}
onMounted(async () => {
@@ -127,6 +131,5 @@ onMounted(async () => {
<BackupSchedule :profile="profile"/>
<BackupList :config="config"/>
<AppArchive :config="config"/>
</div>
</template>