233 lines
7.0 KiB
Vue
233 lines
7.0 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, TableView, InputDialog, SingleSelect } from '@cloudron/pankow';
|
|
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 BackupTargetAddDialog from '../components/BackupTargetAddDialog.vue';
|
|
import BackupTargetEditDialog from '../components/BackupTargetEditDialog.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: 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');
|
|
function onAdd() {
|
|
backupTargetAddDialog.value.open();
|
|
}
|
|
|
|
const backupTargetEditDialog = useTemplateRef('backupTargetEditDialog');
|
|
function onEdit(target) {
|
|
backupTargetEditDialog.value.open(target);
|
|
}
|
|
|
|
const backupScheduleDialog = useTemplateRef('backupScheduleDialog');
|
|
function onEditSchedule(target) {
|
|
backupScheduleDialog.value.open(target);
|
|
}
|
|
|
|
async function onRemoveTarget(target) {
|
|
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 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;
|
|
}
|
|
|
|
const actionMenuModel = ref([]);
|
|
|
|
const actionMenuElement = useTemplateRef('actionMenuElement');
|
|
function onActionMenu(target, event) {
|
|
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: t('backups.schedule.title'),
|
|
action: onEditSchedule.bind(null, target),
|
|
}, {
|
|
icon: 'fa-solid fa-pencil-alt',
|
|
label: t('main.dialog.edit'),
|
|
action: onEdit.bind(null, target),
|
|
}, {
|
|
separator: true
|
|
}, {
|
|
icon: 'fa-solid fa-trash',
|
|
label: t('volumes.removeVolumeDialog.removeAction'),
|
|
disabled: target.primary,
|
|
action: onRemoveTarget.bind(null, target),
|
|
}];
|
|
|
|
actionMenuElement.value.open(event, event.currentTarget);
|
|
}
|
|
|
|
const primaryTargetId = ref('');
|
|
const primaryTargetChangeBusy = ref(false);
|
|
|
|
async function onPrimaryTargetChanged(value) {
|
|
primaryTargetChangeBusy.value = true;
|
|
|
|
const [error] = await backupTargetsModel.setPrimary(value);
|
|
if (error) return console.error(error);
|
|
|
|
// update the list to be in sync without flickering
|
|
targets.value.forEach(t => t.primary = t.id === value);
|
|
|
|
primaryTargetId.value = value;
|
|
primaryTargetChangeBusy.value = 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;
|
|
}
|
|
|
|
primaryTargetId.value = result.find(t => t.primary)?.id;
|
|
|
|
targets.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" />
|
|
<BackupTargetAddDialog ref="backupTargetAddDialog" @success="refresh()"/>
|
|
<BackupTargetEditDialog ref="backupTargetEditDialog" @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>
|
|
|
|
<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 }} <span v-if="target.primary" class="text-muted">- used for updates</span>
|
|
</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>
|
|
|
|
<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="primaryTargetChangeBusy" v-model="primaryTargetId" :searchThreshold="10" :options="targets" option-key="id" option-label="name" @select="onPrimaryTargetChanged" />
|
|
</div>
|
|
</SettingsItem>
|
|
</Section>
|
|
</div>
|
|
</template>
|