Replace backup target listing with a more detailed list view to track progress
This commit is contained in:
@@ -5,17 +5,21 @@ const i18n = useI18n();
|
|||||||
const t = i18n.t;
|
const t = i18n.t;
|
||||||
|
|
||||||
import { ref, onMounted, useTemplateRef } from 'vue';
|
import { ref, onMounted, useTemplateRef } from 'vue';
|
||||||
import { Button, Menu, TableView, InputDialog, SingleSelect } from '@cloudron/pankow';
|
import { Button, Menu, ProgressBar, InputDialog, SingleSelect } from '@cloudron/pankow';
|
||||||
|
import { prettyLongDate } from '@cloudron/pankow/utils';
|
||||||
import Section from '../components/Section.vue';
|
import Section from '../components/Section.vue';
|
||||||
import SettingsItem from '../components/SettingsItem.vue';
|
import SettingsItem from '../components/SettingsItem.vue';
|
||||||
import StateLED from '../components/StateLED.vue';
|
import StateLED from '../components/StateLED.vue';
|
||||||
import BackupScheduleDialog from '../components/BackupScheduleDialog.vue';
|
import BackupScheduleDialog from '../components/BackupScheduleDialog.vue';
|
||||||
import BackupTargetAddDialog from '../components/BackupTargetAddDialog.vue';
|
import BackupTargetAddDialog from '../components/BackupTargetAddDialog.vue';
|
||||||
import BackupTargetEditDialog from '../components/BackupTargetEditDialog.vue';
|
import BackupTargetEditDialog from '../components/BackupTargetEditDialog.vue';
|
||||||
|
import { TASK_TYPES } from '../constants.js';
|
||||||
import BackupTargetsModel from '../models/BackupTargetsModel.js';
|
import BackupTargetsModel from '../models/BackupTargetsModel.js';
|
||||||
import ProfileModel from '../models/ProfileModel.js';
|
import ProfileModel from '../models/ProfileModel.js';
|
||||||
|
import TasksModel from '../models/TasksModel.js';
|
||||||
|
|
||||||
const profileModel = ProfileModel.create();
|
const profileModel = ProfileModel.create();
|
||||||
|
const tasksModel = TasksModel.create();
|
||||||
const backupTargetsModel = BackupTargetsModel.create();
|
const backupTargetsModel = BackupTargetsModel.create();
|
||||||
|
|
||||||
const inputDialog = useTemplateRef('inputDialog');
|
const inputDialog = useTemplateRef('inputDialog');
|
||||||
@@ -24,27 +28,6 @@ const profile = ref({});
|
|||||||
const targets = ref([]);
|
const targets = ref([]);
|
||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
|
|
||||||
const columns = {
|
|
||||||
status: {
|
|
||||||
width: '30px',
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
label: t('backups.configureBackupStorage.name'),
|
|
||||||
sort: true,
|
|
||||||
},
|
|
||||||
provider: {
|
|
||||||
label: t('backups.configureBackupStorage.provider'),
|
|
||||||
sort: true,
|
|
||||||
hideMobile: true,
|
|
||||||
},
|
|
||||||
format: {
|
|
||||||
label: t('backups.configureBackupStorage.format'),
|
|
||||||
sort: true,
|
|
||||||
hideMobile: true,
|
|
||||||
},
|
|
||||||
actions: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const backupTargetAddDialog = useTemplateRef('backupTargetAddDialog');
|
const backupTargetAddDialog = useTemplateRef('backupTargetAddDialog');
|
||||||
function onAdd() {
|
function onAdd() {
|
||||||
backupTargetAddDialog.value.open();
|
backupTargetAddDialog.value.open();
|
||||||
@@ -91,18 +74,41 @@ async function onRemount(target) {
|
|||||||
target.status.busy = false;
|
target.status.busy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onStartBackup(target) {
|
||||||
|
const [error, result] = await backupTargetsModel.createBackup(target.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);
|
||||||
|
|
||||||
|
target.task = task;
|
||||||
|
|
||||||
|
setTimeout(waitForTargetTask.bind(null,target), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
const actionMenuModel = ref([]);
|
const actionMenuModel = ref([]);
|
||||||
|
|
||||||
const actionMenuElement = useTemplateRef('actionMenuElement');
|
const actionMenuElement = useTemplateRef('actionMenuElement');
|
||||||
function onActionMenu(target, event) {
|
function onActionMenu(target, event) {
|
||||||
actionMenuModel.value = [{
|
actionMenuModel.value = [{
|
||||||
|
icon: 'fa-solid fa-plus',
|
||||||
|
label: t('backups.listing.backupNow'),
|
||||||
|
action: onStartBackup.bind(null, target),
|
||||||
|
}, {
|
||||||
icon: 'fa-solid fa-sync-alt',
|
icon: 'fa-solid fa-sync-alt',
|
||||||
label: t('backups.location.remount'),
|
label: t('backups.location.remount'),
|
||||||
visible: target.provider === 'sshfs' || target.provider === 'cifs' || target.provider === 'nfs' || target.provider === 'ext4' || target.provider === 'xfs',
|
visible: target.provider === 'sshfs' || target.provider === 'cifs' || target.provider === 'nfs' || target.provider === 'ext4' || target.provider === 'xfs',
|
||||||
action: onRemount.bind(null, target),
|
action: onRemount.bind(null, target),
|
||||||
}, {
|
}, {
|
||||||
separator: true,
|
separator: true,
|
||||||
visible: target.provider === 'sshfs' || target.provider === 'cifs' || target.provider === 'nfs' || target.provider === 'ext4' || target.provider === 'xfs',
|
|
||||||
}, {
|
}, {
|
||||||
icon: 'fa-solid fa-clock',
|
icon: 'fa-solid fa-clock',
|
||||||
label: t('backups.schedule.title'),
|
label: t('backups.schedule.title'),
|
||||||
@@ -139,6 +145,19 @@ async function onPrimaryTargetChanged(value) {
|
|||||||
primaryTargetChangeBusy.value = false;
|
primaryTargetChangeBusy.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForTargetTask(target) {
|
||||||
|
const [error, result] = await tasksModel.get(target.task.id);
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
setTimeout(waitForTargetTask.bind(null, target), 2000);
|
||||||
|
} else if (result.active) {
|
||||||
|
target.task = result;
|
||||||
|
setTimeout(waitForTargetTask.bind(null, target), 2000);
|
||||||
|
} else {
|
||||||
|
target.task = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
busy.value = true;
|
busy.value = true;
|
||||||
|
|
||||||
@@ -158,6 +177,16 @@ async function refresh() {
|
|||||||
|
|
||||||
target.status.state = status.state === 'active' ? 'success' : 'danger';
|
target.status.state = status.state === 'active' ? 'success' : 'danger';
|
||||||
target.status.busy = false;
|
target.status.busy = false;
|
||||||
|
|
||||||
|
const [taskError, tasks] = await tasksModel.getByType(TASK_TYPES.TASK_FULL_BACKUP_PREFIX + target.id);
|
||||||
|
if (taskError) {
|
||||||
|
console.error(error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.task = tasks[0] || null;
|
||||||
|
|
||||||
|
if (target.task && target.task.active) waitForTargetTask(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryTargetId.value = result.find(t => t.primary)?.id;
|
primaryTargetId.value = result.find(t => t.primary)?.id;
|
||||||
@@ -190,31 +219,29 @@ onMounted(async () => {
|
|||||||
<Button @click="onAdd()" icon="fa-solid fa-plus"> {{ $t('main.action.add') }}</Button>
|
<Button @click="onAdd()" icon="fa-solid fa-plus"> {{ $t('main.action.add') }}</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TableView :columns="columns" :model="targets" :busy="busy">
|
<div>
|
||||||
<template #status="target">
|
<ProgressBar mode="indeterminate" v-if="busy" slim :show-label="false" />
|
||||||
<div style="text-align: center;" :title="target.status.message">
|
<div class="backup-target" v-for="target in targets" :key="target.id">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px">
|
||||||
<StateLED :busy="target.status.busy" :state="target.status.state"/>
|
<StateLED :busy="target.status.busy" :state="target.status.state"/>
|
||||||
|
<div class="backup-target-details">
|
||||||
|
<div><b style="font-size: 16px">{{ target.name }}</b> <span v-if="target.primary" class="text-muted">- used for updates</span></div>
|
||||||
|
<div>
|
||||||
|
{{ $t('backups.configureBackupStorage.provider') }}: <b>{{ target.provider }}</b> - {{ $t('backups.configureBackupStorage.format') }}: <b>{{ target.format }}</b> <i v-if="target.encrypted" class="fa-solid fa-lock"></i></div>
|
||||||
|
<div class="backup-target-task">
|
||||||
|
<div v-if="target.task && target.task.success">Last backup: <b>{{ prettyLongDate(target.task.ts) }}</b></div>
|
||||||
|
<div v-if="target.task && target.task.error">Error: <b>{{ target.task.error }}</b></div>
|
||||||
|
<div v-else-if="target.task && target.task.running">
|
||||||
|
<ProgressBar :value="target.task.percent" /> {{ target.task.message }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</div>
|
||||||
<template #name="target">
|
</div>
|
||||||
{{ target.name }} <span v-if="target.primary" class="text-muted">- used for updates</span>
|
<div style="display: flex; align-items: center;">
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #provider="target">
|
|
||||||
{{ target.provider }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #format="target">
|
|
||||||
{{ target.format }} <i v-if="target.encrypted" class="fa-solid fa-lock"></i>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions="target">
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<Button tool plain secondary @click.capture="onActionMenu(target, $event)" icon="fa-solid fa-ellipsis" />
|
<Button tool plain secondary @click.capture="onActionMenu(target, $event)" icon="fa-solid fa-ellipsis" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</TableView>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section :title="$t('backup.updateTarget.title')">
|
<Section :title="$t('backup.updateTarget.title')">
|
||||||
@@ -230,3 +257,30 @@ onMounted(async () => {
|
|||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.backup-target {
|
||||||
|
display: flex;
|
||||||
|
border-radius: var(--pankow-border-radius);
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-target:hover {
|
||||||
|
background-color: var(--pankow-color-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-target-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-target-action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user