Create separate views for backup targets and archives
This commit is contained in:
+22
-7
@@ -21,7 +21,8 @@ import AppsView from './views/AppsView.vue';
|
||||
import AppConfigureView from './views/AppConfigureView.vue';
|
||||
import AppearanceView from './views/AppearanceView.vue';
|
||||
import AppstoreView from './views/AppstoreView.vue';
|
||||
import BackupsView from './views/BackupsView.vue';
|
||||
import BackupTargetsView from './views/BackupTargetsView.vue';
|
||||
import BackupAppArchivesView from './views/BackupAppArchivesView.vue';
|
||||
import CloudronAccountView from './views/CloudronAccountView.vue';
|
||||
import DomainsView from './views/DomainsView.vue';
|
||||
import EmailDomainView from './views/EmailDomainView.vue';
|
||||
@@ -49,7 +50,8 @@ const VIEWS = {
|
||||
APPEARANCE: 'appearance',
|
||||
APPS: 'apps',
|
||||
APPSTORE: 'appstore',
|
||||
BACKUPS: 'backups',
|
||||
BACKUP_TARGETS: 'backup-targets',
|
||||
BACKUP_APP_ARCHIVES: 'backup-app-archives',
|
||||
CLOUDRON_ACCOUNT: 'cloudron-account',
|
||||
DOMAINS: 'domains',
|
||||
EMAIL_DOMAIN: 'email-domain',
|
||||
@@ -155,7 +157,8 @@ function onHashChange() {
|
||||
|
||||
activeSidebarItem.value = v;
|
||||
|
||||
if (activeSidebarItem.value.indexOf('email') === 0) activeSidebarGroup.value = 'email';
|
||||
if (activeSidebarItem.value.indexOf('backup') === 0) activeSidebarGroup.value = 'backup';
|
||||
else if (activeSidebarItem.value.indexOf('email') === 0) activeSidebarGroup.value = 'email';
|
||||
else if (activeSidebarItem.value.indexOf('system') === 0) activeSidebarGroup.value = 'system';
|
||||
else if (activeSidebarItem.value.indexOf('user-directory') === 0) activeSidebarGroup.value = 'user-directory';
|
||||
else activeSidebarGroup.value = '';
|
||||
@@ -168,8 +171,10 @@ function onHashChange() {
|
||||
view.value = VIEWS.APP;
|
||||
} else if (v === VIEWS.APPEARANCE && profile.value.isAtLeastAdmin) {
|
||||
view.value = VIEWS.APPEARANCE;
|
||||
} else if (v === VIEWS.BACKUPS && profile.value.isAtLeastAdmin) {
|
||||
view.value = VIEWS.BACKUPS;
|
||||
} else if (v === VIEWS.BACKUP_TARGETS && profile.value.isAtLeastAdmin) {
|
||||
view.value = VIEWS.BACKUP_TARGETS;
|
||||
} else if (v === VIEWS.BACKUP_APP_ARCHIVES && profile.value.isAtLeastAdmin) {
|
||||
view.value = VIEWS.BACKUP_APP_ARCHIVES;
|
||||
} else if (v === VIEWS.CLOUDRON_ACCOUNT && profile.value.isAtLeastOwner) {
|
||||
view.value = VIEWS.CLOUDRON_ACCOUNT;
|
||||
} else if (v === VIEWS.DOMAINS && profile.value.isAtLeastAdmin) {
|
||||
@@ -297,8 +302,17 @@ onMounted(async () => {
|
||||
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'appstore' }" v-show="profile.isAtLeastAdmin" href="#/appstore" @click="onSidebarClose()"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ $t('appstore.title') }}</a>
|
||||
<hr/>
|
||||
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'appearance' }" v-show="profile.isAtLeastAdmin" href="#/appearance" @click="onSidebarClose()"><i class="fa fa-pen-ruler fa-fw"></i> {{ $t('branding.title') }}</a>
|
||||
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'backups' }" v-show="profile.isAtLeastAdmin" href="#/backups" @click="onSidebarClose()"><i class="fa fa-archive fa-fw"></i> {{ $t('backups.title') }}</a>
|
||||
|
||||
<div class="sidebar-item" v-show="profile.isAtLeastAdmin" @click="onToggleGroup('backup')"><i class="fa fa-archive fa-fw"></i> {{ $t('backups.title') }} <i class="collapse fa-solid fa-angle-right" :class="{ expanded: activeSidebarGroup === 'backup' }" style="margin-left: 6px;"></i></div>
|
||||
<Transition name="sidebar-item-group-animation">
|
||||
<div class="sidebar-item-group" v-if="activeSidebarGroup === 'backup'">
|
||||
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'backup-targets' }" href="#/backup-targets" @click="onSidebarClose()"><i class="fa fa-fw fa-hard-drive"></i> Targets</a>
|
||||
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'backup-app-archives' }" href="#/backup-app-archives" @click="onSidebarClose()"><i class="fa fa-fw fa-grip"></i> App Archives</a>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'domains' }" v-show="profile.isAtLeastAdmin" href="#/domains" @click="onSidebarClose()"><i class="fa fa-globe fa-fw"></i> {{ $t('domains.title') }}</a>
|
||||
|
||||
<div class="sidebar-item" v-show="profile.isAtLeastMailManager" @click="onToggleGroup('email')"><i class="fa fa-envelope fa-fw"></i> {{ $t('emails.title') }} <i class="collapse fa-solid fa-angle-right" :class="{ expanded: activeSidebarGroup === 'email' }" style="margin-left: 6px;"></i></div>
|
||||
<Transition name="sidebar-item-group-animation">
|
||||
<div class="sidebar-item-group" v-if="activeSidebarGroup === 'email'">
|
||||
@@ -345,7 +359,8 @@ onMounted(async () => {
|
||||
<AppConfigureView v-else-if="view === VIEWS.APP" />
|
||||
<AppearanceView v-else-if="view === VIEWS.APPEARANCE" />
|
||||
<AppstoreView v-else-if="view === VIEWS.APPSTORE" />
|
||||
<BackupsView v-else-if="view === VIEWS.BACKUPS" />
|
||||
<BackupTargetsView v-else-if="view === VIEWS.BACKUP_TARGETS" />
|
||||
<BackupAppArchivesView v-else-if="view === VIEWS.BACKUP_APP_ARCHIVES" />
|
||||
<CloudronAccountView v-else-if="view === VIEWS.CLOUDRON_ACCOUNT" />
|
||||
<DomainsView v-else-if="view === VIEWS.DOMAINS" />
|
||||
<EmailDomainView v-else-if="view === VIEWS.EMAIL_DOMAIN" />
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user