Files
cloudron-box/dashboard/src/views/BackupTargetsView.vue
T
2025-08-06 13:53:06 +02:00

221 lines
6.1 KiB
Vue

<script setup>
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, useTemplateRef } from 'vue';
import { Button, Menu, ButtonGroup, TableView, InputDialog } from '@cloudron/pankow';
import Section from '../components/Section.vue';
import StateLED from '../components/StateLED.vue';
import BackupScheduleDialog from '../components/BackupScheduleDialog.vue';
import BackupTargetDialog from '../components/BackupTargetDialog.vue';
import BackupTargetsModel from '../models/BackupTargetsModel.js';
import ProfileModel from '../models/ProfileModel.js';
const profileModel = ProfileModel.create();
const backupTargetsModel = BackupTargetsModel.create();
const inputDialog = useTemplateRef('inputDialog');
const profile = ref({});
const targets = ref([]);
const busy = ref(false);
const columns = {
status: {
width: '30px',
},
name: {
label: 'Name',
sort: true,
},
provider: {
label: 'Provider',
sort: true,
},
format: {
label: 'Format',
sort: false,
hideMobile: true,
},
actions: {}
};
const backupTargetDialog = useTemplateRef('backupTargetDialog');
function onAddOrEdit(target = null) {
backupTargetDialog.value.open(target);
}
const backupScheduleDialog = useTemplateRef('backupScheduleDialog');
function onEditSchedule(target) {
backupScheduleDialog.value.open(target);
}
async function onMakePrimaryTarget(target) {
// TODO translate
const yes = await inputDialog.value.confirm({
title: '',
message: 'Make this target the primary backup destination?',
confirmLabel: t('main.dialog.yes'),
confirmStyle: 'primary',
rejectLabel: t('main.dialog.cancel'),
rejectStyle: 'secondary',
});
if (!yes) return;
const [error] = await backupTargetsModel.setPrimary(target.id);
if (error) console.error(error);
await refresh();
}
async function onRemoveTarget(target) {
// TODO translate
const yes = await inputDialog.value.confirm({
title: 'Really remove this backup target?',
message: 'This will also remove any backups linked to this target',
confirmLabel: t('main.dialog.yes'),
confirmStyle: 'danger',
rejectLabel: t('main.dialog.cancel'),
rejectStyle: 'secondary',
});
if (!yes) return;
const [error] = await backupTargetsModel.del(target.id);
if (error) console.error(error);
await refresh();
}
async function onRemount(target) {
target.status.busy = true;
const [error] = await backupTargetsModel.remount(target.id);
if (error) return console.error(error);
const [statusError, status] = await backupTargetsModel.status(target.id);
if (statusError) console.error(statusError);
target.status.state = status.state === 'active' ? 'success' : 'danger';
target.status.busy = false;
}
async function refresh() {
busy.value = true;
const [error, result] = await backupTargetsModel.list();
if (error) return console.error(error);
for (const target of result) {
target.status = { busy: true, state: '', message: '' };
}
for (const target of result) {
const [error, status] = await backupTargetsModel.status(target.id);
if (error) {
console.error(error);
continue;
}
target.status.state = status.state === 'active' ? 'success' : 'danger';
target.status.busy = false;
}
targets.value = result;
busy.value = false;
}
const actionMenuModel = ref([]);
const actionMenuElement = useTemplateRef('actionMenuElement');
function onActionMenu(target, event) {
// TODO translate
actionMenuModel.value = [{
icon: 'fa-solid fa-sync-alt',
label: t('backups.location.remount'),
visible: target.provider === 'sshfs' || target.provider === 'cifs' || target.provider === 'nfs' || target.provider === 'ext4' || target.provider === 'xfs',
action: onRemount.bind(null, target),
}, {
separator: true,
visible: target.provider === 'sshfs' || target.provider === 'cifs' || target.provider === 'nfs' || target.provider === 'ext4' || target.provider === 'xfs',
}, {
icon: 'fa-solid fa-clock',
label: 'Schedule and Retention',
action: onEditSchedule.bind(null, target),
}, {
icon: 'fa-solid fa-crown',
label: 'Set as Primary',
disabled: target.primary,
action: onMakePrimaryTarget.bind(null, target),
}, {
icon: 'fa-solid fa-pencil-alt',
label: 'Edit',
}, {
separator: true
}, {
icon: 'fa-solid fa-trash',
label: 'Remove',
disabled: target.primary,
action: onRemoveTarget.bind(null, target),
}];
actionMenuElement.value.open(event, event.currentTarget);
}
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" />
<BackupTargetDialog ref="backupTargetDialog" @success="refresh()"/>
<BackupScheduleDialog ref="backupScheduleDialog" @success="refresh()"/>
<!-- TODO translate -->
<Section title="Backup Storage">
<template #header-buttons>
<Button @click="onAddOrEdit()" icon="fa-solid fa-plus"> Add Storage</Button>
</template>
<p>TODO Explain what backup targets are and what primary/secondary is</p>
<TableView :columns="columns" :model="targets" :busy="busy">
<template #status="target">
<div style="text-align: center;" :title="target.status.message">
<StateLED :busy="target.status.busy" :state="target.status.state"/>
</div>
</template>
<template #name="target">
{{ target.name }}
</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" />
</div>
</template>
</TableView>
</Section>
</div>
</template>