dashboard: rename backupTargets to backupSites
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
<script setup>
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { Button, Menu, ProgressBar, InputDialog, SingleSelect } from '@cloudron/pankow';
|
||||
import { prettyLongDate } from '@cloudron/pankow/utils';
|
||||
import Section from '../components/Section.vue';
|
||||
import SettingsItem from '../components/SettingsItem.vue';
|
||||
import StateLED from '../components/StateLED.vue';
|
||||
import BackupScheduleDialog from '../components/BackupScheduleDialog.vue';
|
||||
import BackupSiteAddDialog from '../components/BackupSiteAddDialog.vue';
|
||||
import BackupSiteEditDialog from '../components/BackupSiteEditDialog.vue';
|
||||
import { TASK_TYPES } from '../constants.js';
|
||||
import BackupSitesModel from '../models/BackupSitesModel.js';
|
||||
import ProfileModel from '../models/ProfileModel.js';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
|
||||
const profileModel = ProfileModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
const backupSitesModels = BackupSitesModel.create();
|
||||
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
|
||||
const profile = ref({});
|
||||
const sites = ref([]);
|
||||
const busy = ref(false);
|
||||
|
||||
const backupSiteAddDialog = useTemplateRef('backupSiteAddDialog');
|
||||
function onAdd() {
|
||||
backupSiteAddDialog.value.open();
|
||||
}
|
||||
|
||||
const backupSiteEditDialog = useTemplateRef('backupSiteEditDialog');
|
||||
function onEdit(site) {
|
||||
backupSiteEditDialog.value.open(site);
|
||||
}
|
||||
|
||||
const backupScheduleDialog = useTemplateRef('backupScheduleDialog');
|
||||
function onEditSchedule(site) {
|
||||
backupScheduleDialog.value.open(site);
|
||||
}
|
||||
|
||||
async function onRemoveSite(site) {
|
||||
const yes = await inputDialog.value.confirm({
|
||||
title: t('backup.target.removeDialog.title'),
|
||||
message: t('backup.target.removeDialog.description'),
|
||||
confirmLabel: t('main.dialog.yes'),
|
||||
confirmStyle: 'danger',
|
||||
rejectLabel: t('main.dialog.cancel'),
|
||||
rejectStyle: 'secondary',
|
||||
});
|
||||
|
||||
if (!yes) return;
|
||||
|
||||
const [error] = await backupSitesModels.del(site.id);
|
||||
if (error) console.error(error);
|
||||
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function onRemount(site) {
|
||||
site.status.busy = true;
|
||||
|
||||
const [error] = await backupSitesModels.remount(site.id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
const [statusError, status] = await backupSitesModels.status(site.id);
|
||||
if (statusError) console.error(statusError);
|
||||
|
||||
site.status.state = status.state === 'active' ? 'success' : 'danger';
|
||||
site.status.busy = false;
|
||||
}
|
||||
|
||||
async function onStartBackup(site) {
|
||||
const [error, result] = await backupSitesModels.createBackup(site.id);
|
||||
if (error) {
|
||||
if (error.status === 409) {
|
||||
// TODO
|
||||
// if (error.body.message.indexOf('full_backup') !== -1) startBackupError.value = 'Backup already in progress. Please retry later.';
|
||||
// else startBackupError.value = 'App task is currently in progress. Please retry later.';
|
||||
}
|
||||
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
const [taskError, task] = await tasksModel.get(result);
|
||||
if (taskError) return console.error(taskError);
|
||||
|
||||
site.task = task;
|
||||
|
||||
setTimeout(waitForSiteTask.bind(null,site), 2000);
|
||||
}
|
||||
|
||||
const actionMenuModel = ref([]);
|
||||
|
||||
const actionMenuElement = useTemplateRef('actionMenuElement');
|
||||
function onActionMenu(site, event) {
|
||||
actionMenuModel.value = [{
|
||||
icon: 'fa-solid fa-plus',
|
||||
label: t('backups.listing.backupNow'),
|
||||
action: onStartBackup.bind(null, site),
|
||||
}, {
|
||||
icon: 'fa-solid fa-sync-alt',
|
||||
label: t('backups.location.remount'),
|
||||
visible: site.provider === 'sshfs' || site.provider === 'cifs' || site.provider === 'nfs' || site.provider === 'ext4' || site.provider === 'xfs',
|
||||
action: onRemount.bind(null, site),
|
||||
}, {
|
||||
separator: true,
|
||||
}, {
|
||||
icon: 'fa-solid fa-clock',
|
||||
label: t('backups.schedule.title'),
|
||||
action: onEditSchedule.bind(null, site),
|
||||
}, {
|
||||
icon: 'fa-solid fa-pencil-alt',
|
||||
label: t('main.dialog.edit'),
|
||||
action: onEdit.bind(null, site),
|
||||
}, {
|
||||
separator: true
|
||||
}, {
|
||||
icon: 'fa-solid fa-trash',
|
||||
label: t('volumes.removeVolumeDialog.removeAction'),
|
||||
disabled: site.primary,
|
||||
action: onRemoveSite.bind(null, site),
|
||||
}];
|
||||
|
||||
actionMenuElement.value.open(event, event.currentTarget);
|
||||
}
|
||||
|
||||
const primarySiteId = ref('');
|
||||
const primarySiteChangeBusy = ref(false);
|
||||
|
||||
async function onPrimarySiteChanged(value) {
|
||||
primarySiteChangeBusy.value = true;
|
||||
|
||||
const [error] = await backupSitesModels.setPrimary(value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
// update the list to be in sync without flickering
|
||||
sites.value.forEach(t => t.primary = t.id === value);
|
||||
|
||||
primarySiteId.value = value;
|
||||
primarySiteChangeBusy.value = false;
|
||||
}
|
||||
|
||||
async function waitForSiteTask(site) {
|
||||
const [error, result] = await tasksModel.get(site.task.id);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
setTimeout(waitForSiteTask.bind(null, site), 2000);
|
||||
} else if (result.active) {
|
||||
site.task = result;
|
||||
setTimeout(waitForSiteTask.bind(null, site), 2000);
|
||||
} else {
|
||||
site.task = result;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
busy.value = true;
|
||||
|
||||
const [error, result] = await backupSitesModels.list();
|
||||
if (error) return console.error(error);
|
||||
|
||||
for (const site of result) {
|
||||
site.status = { busy: true, state: '', message: '' };
|
||||
}
|
||||
|
||||
for (const site of result) {
|
||||
const [error, status] = await backupSitesModels.status(site.id);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
site.status.state = status.state === 'active' ? 'success' : 'danger';
|
||||
site.status.busy = false;
|
||||
|
||||
const [taskError, tasks] = await tasksModel.getByType(TASK_TYPES.TASK_FULL_BACKUP_PREFIX + site.id);
|
||||
if (taskError) {
|
||||
console.error(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
site.task = tasks[0] || null;
|
||||
|
||||
if (site.task && site.task.active) waitForSiteTask(site);
|
||||
}
|
||||
|
||||
primarySiteId.value = result.find(t => t.primary)?.id;
|
||||
|
||||
sites.value = result;
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [error, result] = await profileModel.get();
|
||||
if (error) return console.error(error);
|
||||
|
||||
profile.value = result;
|
||||
|
||||
await refresh();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<Menu ref="actionMenuElement" :model="actionMenuModel" />
|
||||
<InputDialog ref="inputDialog" />
|
||||
<BackupSiteAddDialog ref="backupSiteAddDialog" @success="refresh()"/>
|
||||
<BackupSiteEditDialog ref="backupSiteEditDialog" @success="refresh()"/>
|
||||
<BackupScheduleDialog ref="backupScheduleDialog" @success="refresh()"/>
|
||||
|
||||
<Section :title="$t('backup.target.title')">
|
||||
<template #header-buttons>
|
||||
<Button @click="onAdd()" icon="fa-solid fa-plus"> {{ $t('main.action.add') }}</Button>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<ProgressBar mode="indeterminate" v-if="busy" slim :show-label="false" />
|
||||
<div class="backup-site" v-for="site in sites" :key="site.id">
|
||||
<div style="display: flex; align-items: center; gap: 10px">
|
||||
<StateLED :busy="site.status.busy" :state="site.status.state"/>
|
||||
<div class="backup-site-details">
|
||||
<div><b style="font-size: 16px">{{ site.name }}</b> <span v-if="site.primary" class="text-muted">- used for updates</span></div>
|
||||
<div>
|
||||
{{ $t('backups.configureBackupStorage.provider') }}: <b>{{ site.provider }}</b> - {{ $t('backups.configureBackupStorage.format') }}: <b>{{ site.format }}</b> <i v-if="site.encrypted" class="fa-solid fa-lock"></i></div>
|
||||
<div class="backup-site-task">
|
||||
<div v-if="site.task && site.task.success">Last backup: <b>{{ prettyLongDate(site.task.ts) }}</b></div>
|
||||
<div v-if="site.task && site.task.error">Error: <b>{{ site.task.error }}</b></div>
|
||||
<div v-else-if="site.task && site.task.running">
|
||||
<ProgressBar :value="site.task.percent" /> {{ site.task.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<Button tool plain secondary @click.capture="onActionMenu(site, $event)" icon="fa-solid fa-ellipsis" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section :title="$t('backup.updateTarget.title')">
|
||||
<SettingsItem>
|
||||
<div>
|
||||
<label>{{ $t('backup.target.label') }}</label>
|
||||
<div>{{ $t('backup.updateTarget.description') }}</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<SingleSelect style="min-width: 160px" :disabled="primarySiteChangeBusy" v-model="primarySiteId" :searchThreshold="10" :options="sites" option-key="id" option-label="name" @select="onPrimarySiteChanged" />
|
||||
</div>
|
||||
</SettingsItem>
|
||||
</Section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.backup-site {
|
||||
display: flex;
|
||||
border-radius: var(--pankow-border-radius);
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.backup-site:hover {
|
||||
background-color: var(--pankow-color-background-hover);
|
||||
}
|
||||
|
||||
.backup-site-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.backup-site-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user