Migrate first parts of backups view to vue
This commit is contained in:
@@ -4,6 +4,7 @@ import { onMounted, ref } from 'vue';
|
||||
import { Notification } from 'pankow';
|
||||
import AppsView from './views/AppsView.vue';
|
||||
import AppstoreView from './views/AppstoreView.vue';
|
||||
import BackupsView from './views/BackupsView.vue';
|
||||
import DomainsView from './views/DomainsView.vue';
|
||||
import EventlogView from './views/EventlogView.vue';
|
||||
import NetworkView from './views/NetworkView.vue';
|
||||
@@ -17,6 +18,7 @@ import VolumesView from './views/VolumesView.vue';
|
||||
const VIEWS = {
|
||||
APPS: 'apps',
|
||||
APPSTORE: 'appstore',
|
||||
BACKUPS: 'backups',
|
||||
DOMAINS: 'domains',
|
||||
EVENTLOG: 'eventlog',
|
||||
NETWORK: 'network',
|
||||
@@ -37,6 +39,8 @@ function onHashChange() {
|
||||
view.value = VIEWS.APPS;
|
||||
} else if (v.indexOf(VIEWS.APPSTORE) === 0) {
|
||||
view.value = VIEWS.APPSTORE;
|
||||
} else if (v === VIEWS.BACKUPS) {
|
||||
view.value = VIEWS.BACKUPS;
|
||||
} else if (v === VIEWS.DOMAINS) {
|
||||
view.value = VIEWS.DOMAINS;
|
||||
} else if (v === VIEWS.EVENTLOG) {
|
||||
@@ -81,6 +85,7 @@ onMounted(async () => {
|
||||
|
||||
<AppsView v-if="view === VIEWS.APPS" />
|
||||
<AppstoreView v-else-if="view === VIEWS.APPSTORE" />
|
||||
<BackupsView v-else-if="view === VIEWS.BACKUPS" />
|
||||
<DomainsView v-else-if="view === VIEWS.DOMAINS" />
|
||||
<EventlogView v-else-if="view === VIEWS.EVENTLOG" />
|
||||
<NetworkView v-else-if="view === VIEWS.NETWORK" />
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup>
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, useTemplateRef, computed } from 'vue';
|
||||
import { Button, InputDialog, Dialog, FormGroup, TableView } from 'pankow';
|
||||
import { prettyLongDate } from 'pankow/utils';
|
||||
import { TASK_TYPES } from '../constants.js';
|
||||
import Section from '../components/Section.vue';
|
||||
import BackupsModel from '../models/BackupsModel.js';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
|
||||
const backupsModel = BackupsModel.create();
|
||||
const appsModel = AppsModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
|
||||
const columns = {
|
||||
archived: {},
|
||||
packageVersion: { label: t('backups.listing.version'), sort: true },
|
||||
creationTime: { label: t('main.table.date'), sort: true },
|
||||
content: { label: t('backups.listing.contents'), sort: false },
|
||||
actions: {}
|
||||
};
|
||||
|
||||
const busy = ref(true);
|
||||
const backups = ref([]);
|
||||
const taskLogsMenu = ref([]);
|
||||
const backupTask = ref({});
|
||||
|
||||
async function waitForTask() {
|
||||
if (!backupTask.value.id) return;
|
||||
|
||||
const [error, result] = await tasksModel.get(backupTask.value.id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
backupTask.value = result;
|
||||
|
||||
// task done, refresh menu
|
||||
if (!result.active) return await refreshTasks();
|
||||
|
||||
setTimeout(waitForTask, 2000);
|
||||
}
|
||||
|
||||
async function refreshTasks() {
|
||||
const [error, result] = await tasksModel.getByType(TASK_TYPES.TASK_BACKUP);
|
||||
if (error) return console.error(error);
|
||||
|
||||
backupTask.value = result[0] || {};
|
||||
|
||||
// limit to last 10
|
||||
taskLogsMenu.value = result.slice(0,10).map(t => {
|
||||
return {
|
||||
icon: 'fa-solid ' + ((!t.active && t.success) ? 'status-active fa-check-circle' : (t.active ? 'fa-circle-notch fa-spin' : 'status-error fa-times-circle')),
|
||||
label: prettyLongDate(t.ts),
|
||||
action: () => { window.open(`/logs.html?taskId=${t.id}`); }
|
||||
};
|
||||
});
|
||||
|
||||
// if last task is currently active, start polling
|
||||
if (backupTask.value.active) waitForTask();
|
||||
}
|
||||
|
||||
async function refreshBackups() {
|
||||
const [error, result] = await backupsModel.list();
|
||||
if (error) return console.error(error);
|
||||
|
||||
// add contents property
|
||||
const appsById = {};
|
||||
const appsByFqdn = {};
|
||||
|
||||
const [appsError, apps] = await appsModel.list();
|
||||
if (appsError) console.error(error);
|
||||
|
||||
(apps || []).forEach(function (app) {
|
||||
appsById[app.id] = app;
|
||||
appsByFqdn[app.fqdn] = app;
|
||||
});
|
||||
|
||||
result.forEach(function (backup) {
|
||||
backup.contents = []; // { id, label, fqdn }
|
||||
backup.dependsOn.forEach(function (appBackupId) {
|
||||
const match = appBackupId.match(/app_(.*?)_.*/); // *? means non-greedy
|
||||
if (!match) return; // for example, 'mail'
|
||||
const app = appsById[match[1]];
|
||||
if (app) {
|
||||
backup.contents.push({
|
||||
id: app.id,
|
||||
label: app.label,
|
||||
fqdn: app.fqdn
|
||||
});
|
||||
} else {
|
||||
backup.contents.push({
|
||||
id: match[1],
|
||||
label: null,
|
||||
fqdn: null
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
backups.value = result;
|
||||
}
|
||||
onMounted(async () => {
|
||||
await refreshBackups();
|
||||
await refreshTasks();
|
||||
busy.value = false;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Section :title="$t('backups.listing.title')">
|
||||
<template #header-buttons>
|
||||
<Button tool icon="fas fa-align-left" v-tooltip="$t('settings.updates.showLogsAction')" :menu="taskLogsMenu" :disabled="!taskLogsMenu.length"/>
|
||||
</template>
|
||||
|
||||
<TableView :columns="columns" :model="backups" :busy="busy">
|
||||
<template #archived="slotProps">
|
||||
<i class="fas fa-archive" v-show="slotProps.preserveSecs === -1" v-tooltip="$t('backups.listing.tooltipPreservedBackup')"></i>
|
||||
</template>
|
||||
|
||||
<template #creationTime="slotProps">{{ prettyLongDate(slotProps.creationTime) }}</template>
|
||||
|
||||
<template #content="slotProps">
|
||||
<span v-if="slotProps.contents.length">{{ $t('backups.listing.appCount', { appCount: slotProps.contents.length }) }}</span>
|
||||
<span v-else>{{ $t('backups.listing.noApps') }}</span>
|
||||
</template>
|
||||
|
||||
<template #actions="slotProps">
|
||||
<div class="table-actions">
|
||||
<Button tool secondary small icon="fa-solid fa-pencil-alt" v-tooltip="$t('backups.listing.tooltipEditBackup')" @click="onEdit(slotProps)"></Button>
|
||||
<Button tool secondary small icon="fa-solid fa-file-alt" v-tooltip="$t('backups.listing.tooltipDownloadBackupConfig')" @click="onDownloadConfig(slotProps)"></Button>
|
||||
</div>
|
||||
</template>
|
||||
</TableView>
|
||||
</Section>
|
||||
</template>
|
||||
@@ -0,0 +1,263 @@
|
||||
<script setup>
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, useTemplateRef, computed } from 'vue';
|
||||
import { Button, InputDialog, Dialog, FormGroup, MultiSelect } from 'pankow';
|
||||
import { prettyLongDate } from 'pankow/utils';
|
||||
import Section from '../components/Section.vue';
|
||||
import { TASK_TYPES } from '../constants.js';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
import BackupsModel from '../models/BackupsModel.js';
|
||||
|
||||
const props = defineProps({
|
||||
profile: Object
|
||||
});
|
||||
|
||||
const backupsModel = BackupsModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
|
||||
const backupRetentions = [
|
||||
{ name: '2 days', id: { keepWithinSecs: 2 * 24 * 60 * 60 }},
|
||||
{ name: '1 week', id: { keepWithinSecs: 7 * 24 * 60 * 60 }}, // default
|
||||
{ name: '1 month', id: { keepWithinSecs: 30 * 24 * 60 * 60 }},
|
||||
{ name: '3 months', id: { keepWithinSecs: 3 * 30 * 24 * 60 * 60 }},
|
||||
{ name: '2 daily, 4 weekly', id: { keepDaily: 2, keepWeekly: 4 }},
|
||||
{ name: '3 daily, 4 weekly, 6 monthly', id: { keepDaily: 3, keepWeekly: 4, keepMonthly: 6 }},
|
||||
{ name: '7 daily, 4 weekly, 12 monthly', id: { keepDaily: 7, keepWeekly: 4, keepMonthly: 12 }},
|
||||
{ name: 'Forever', id: { keepWithinSecs: -1 }}
|
||||
];
|
||||
|
||||
// values correspond to cron days
|
||||
const cronDays = [
|
||||
{ name: 'Sunday', id: 0 },
|
||||
{ name: 'Monday', id: 1 },
|
||||
{ name: 'Tuesday', id: 2 },
|
||||
{ name: 'Wednesday', id: 3 },
|
||||
{ name: 'Thursday', id: 4 },
|
||||
{ name: 'Friday', id: 5 },
|
||||
{ name: 'Saturday', id: 6 },
|
||||
];
|
||||
|
||||
// generates 24h time sets (instead of american 12h) to avoid having to translate everything to locales eg. 12:00
|
||||
const cronHours = Array.from({ length: 24 }).map(function (v, i) { return { name: (i < 10 ? '0' : '') + i + ':00', id: i }; });
|
||||
|
||||
function prettyBackupSchedule(pattern) {
|
||||
if (!pattern) return '';
|
||||
|
||||
const tmp = pattern.split(' ');
|
||||
const hours = tmp[2].split(',');
|
||||
const days = tmp[5].split(',');
|
||||
|
||||
let prettyDay;
|
||||
if (days.length === 7 || days[0] === '*') {
|
||||
prettyDay = 'Everyday';
|
||||
} else {
|
||||
prettyDay = days.map(function (day) { return cronDays[parseInt(day, 10)].name.substr(0, 3); }).join(',');
|
||||
}
|
||||
|
||||
const prettyHour = hours.map(function (hour) { return cronHours[parseInt(hour, 10)].name; }).join(',');
|
||||
|
||||
return prettyDay + ' at ' + prettyHour;
|
||||
};
|
||||
|
||||
function prettyBackupRetention(retention) {
|
||||
const retentionString = JSON.stringify(retention);
|
||||
const tmp = backupRetentions.find(function (p) { return JSON.stringify(p.id) === retentionString; });
|
||||
return tmp ? tmp.name : '';
|
||||
};
|
||||
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
const taskLogsMenu = ref([]);
|
||||
const policy = ref({
|
||||
schedule: '',
|
||||
retention: ''
|
||||
});
|
||||
const cleanupBusy = ref(false);
|
||||
const cleanupTask = ref({});
|
||||
|
||||
async function onCleanup() {
|
||||
const yes = await inputDialog.value.confirm({
|
||||
// title: t('backups.cleanupBackups.title'),
|
||||
message: t('backups.cleanupBackups.description'),
|
||||
modal: true,
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: t('main.dialog.yes'),
|
||||
rejectLabel: t('main.dialog.no')
|
||||
});
|
||||
|
||||
if (!yes) return;
|
||||
|
||||
const [error] = await backupsModel.cleanup();
|
||||
if (error) return console.error(error);
|
||||
|
||||
await refreshTasks();
|
||||
}
|
||||
|
||||
const configureBusy = ref(false);
|
||||
const configureError = ref('');
|
||||
const configureDialog = useTemplateRef('configureDialog');
|
||||
const configureDays = ref([]);
|
||||
const configureHours = ref([]);
|
||||
const configureRetention = ref('');
|
||||
const isConfigureValid = computed(() => {
|
||||
return configureDays.value.length && configureHours.value.length;
|
||||
});
|
||||
|
||||
function onConfigure() {
|
||||
configureBusy.value = false;
|
||||
configureError.value = false;
|
||||
|
||||
const currentRetentionString = JSON.stringify(policy.value.retention);
|
||||
let selectedRetention = backupRetentions.find(function (x) { return JSON.stringify(x.id) === currentRetentionString; });
|
||||
if (!selectedRetention) selectedRetention = backupRetentions[0];
|
||||
configureRetention.value = selectedRetention.id;
|
||||
|
||||
const tmp = policy.value.schedule.split(' ');
|
||||
const hours = tmp[2].split(',');
|
||||
const days = tmp[5].split(',');
|
||||
if (days[0] === '*') configureDays.value = [ ...cronDays ];
|
||||
else configureDays.value = days.map(function (day) { return cronDays[parseInt(day, 10)]; });
|
||||
|
||||
configureHours.value = hours.map(function (hour) { return cronHours[parseInt(hour, 10)]; });
|
||||
|
||||
configureDialog.value.open();
|
||||
}
|
||||
|
||||
async function onSubmitConfigure() {
|
||||
if (!isConfigureValid.value) return;
|
||||
|
||||
configureBusy.value = true;
|
||||
|
||||
let daysPattern;
|
||||
if (configureDays.value.length === 7) daysPattern = '*';
|
||||
else daysPattern = configureDays.value.map(function (d) { return d.id; });
|
||||
|
||||
let hoursPattern;
|
||||
if (configureHours.value.length === 24) hoursPattern = '*';
|
||||
else hoursPattern = configureHours.value.map(function (d) { return d.id; });
|
||||
|
||||
const policy = {
|
||||
retention: configureRetention.value,
|
||||
schedule: `00 00 ${hoursPattern} * * ${daysPattern}`
|
||||
};
|
||||
|
||||
const [error] = await backupsModel.setPolicy(policy);
|
||||
if (error) {
|
||||
configureBusy.value = false;
|
||||
configureError.value = error.body ? error.body.message : 'Internal error';
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
configureDialog.value.close();
|
||||
await refreshPolicy();
|
||||
configureBusy.value = false;
|
||||
}
|
||||
|
||||
async function waitForTask() {
|
||||
if (!cleanupTask.value.id) return;
|
||||
|
||||
const [error, result] = await tasksModel.get(cleanupTask.value.id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
cleanupTask.value = result;
|
||||
|
||||
// task done, refresh menu
|
||||
if (!result.active) return await refreshTasks();
|
||||
|
||||
setTimeout(waitForTask, 2000);
|
||||
}
|
||||
|
||||
async function refreshTasks() {
|
||||
const [error, result] = await tasksModel.getByType(TASK_TYPES.TASK_CLEAN_BACKUPS);
|
||||
if (error) return console.error(error);
|
||||
|
||||
cleanupTask.value = result[0] || {};
|
||||
|
||||
// limit to last 10
|
||||
taskLogsMenu.value = result.slice(0,10).map(t => {
|
||||
return {
|
||||
icon: 'fa-solid ' + ((!t.active && t.success) ? 'status-active fa-check-circle' : (t.active ? 'fa-circle-notch fa-spin' : 'status-error fa-times-circle')),
|
||||
label: prettyLongDate(t.ts),
|
||||
action: () => { window.open(`/logs.html?taskId=${t.id}`); }
|
||||
};
|
||||
});
|
||||
|
||||
// if last task is currently active, start polling
|
||||
if (cleanupTask.value.active) waitForTask();
|
||||
}
|
||||
|
||||
async function refreshPolicy() {
|
||||
const [error, result] = await backupsModel.getPolicy();
|
||||
if (error) return console.error(error);
|
||||
|
||||
policy.value = result;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
refreshTasks();
|
||||
refreshPolicy();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputDialog ref="inputDialog" />
|
||||
<Dialog ref="configureDialog"
|
||||
:title="$t('backups.configureBackupSchedule.title')"
|
||||
reject-style="secondary"
|
||||
:reject-label="configureBusy ? null : $t('main.dialog.cancel')"
|
||||
:confirm-label="$t('main.dialog.save')"
|
||||
:confirm-busy="configureBusy"
|
||||
:confirm-active="isConfigureValid"
|
||||
@confirm="onSubmitConfigure()"
|
||||
>
|
||||
<p class="has-error text-center" v-show="configureError">{{ configureError }}</p>
|
||||
|
||||
<form @submit.prevent="onSubmitConfigure()" autocomplete="off">
|
||||
<fieldset>
|
||||
<FormGroup>
|
||||
<label for="daysInput">{{ $t('backups.configureBackupSchedule.schedule') }}</label>
|
||||
<p v-html="$t('backups.configureBackupSchedule.scheduleDescription')"></p>
|
||||
|
||||
<div style="display: flex;">
|
||||
<div>{{ $t('backups.configureBackupSchedule.days') }}: <MultiSelect id="daysInput" v-model="configureDays" :options="cronDays" option-label="name"></MultiSelect></div>
|
||||
<div>{{ $t('backups.configureBackupSchedule.hours') }}: <MultiSelect v-model="configureHours" :options="cronHours" option-label="name"></MultiSelect></div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="retentionInput">{{ $t('backups.configureBackupSchedule.retentionPolicy') }}</label>
|
||||
<select id="retentionInput" v-model="configureRetention">
|
||||
<option v-for="elem in backupRetentions" :key="elem.id" :value="elem.id">{{ elem.name }}</option>
|
||||
</select>
|
||||
</FormGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
<Section :title="$t('backups.schedule.title')">
|
||||
<template #header-buttons>
|
||||
<Button tool icon="fas fa-align-left" v-tooltip="$t('settings.updates.showLogsAction')" :menu="taskLogsMenu" :disabled="!taskLogsMenu.length"/>
|
||||
</template>
|
||||
|
||||
<p v-html="$t('backups.schedule.description')"></p>
|
||||
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.schedule.schedule') }}</div>
|
||||
<div class="info-value">{{ prettyBackupSchedule(policy.schedule) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.schedule.retentionPolicy') }}</div>
|
||||
<div class="info-value">{{ prettyBackupRetention(policy.retention) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO maybe track the cleanup -->
|
||||
|
||||
<Button @click="onCleanup()" :disabled="cleanupBusy" :loading="cleanupBusy">{{ $t('backups.listing.cleanupBackups') }}</Button>
|
||||
<Button v-show="props.profile.isAtLeastOwner" @click="onConfigure()">{{ $t('backups.schedule.configure') }}</Button>
|
||||
</Section>
|
||||
</template>
|
||||
@@ -8,7 +8,7 @@ import Section from '../components/Section.vue';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
import DomainsModel from '../models/DomainsModel.js';
|
||||
|
||||
const taskModel = TasksModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
const domainsModel = DomainsModel.create();
|
||||
|
||||
const lastTask = ref({});
|
||||
@@ -17,7 +17,7 @@ const taskLogsMenu = ref([]);
|
||||
async function waitForLastTask() {
|
||||
if (!lastTask.value.id) return;
|
||||
|
||||
const [error, result] = await taskModel.get(lastTask.value.id);
|
||||
const [error, result] = await tasksModel.get(lastTask.value.id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
lastTask.value = result;
|
||||
@@ -29,7 +29,7 @@ async function waitForLastTask() {
|
||||
}
|
||||
|
||||
async function refreshTasks() {
|
||||
const [error, result] = await taskModel.getByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS);
|
||||
const [error, result] = await tasksModel.getByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS);
|
||||
if (error) return console.error(error);
|
||||
|
||||
lastTask.value = result[0] || {};
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
import { fetcher } from 'pankow';
|
||||
|
||||
function create() {
|
||||
const accessToken = localStorage.token;
|
||||
const origin = import.meta.env.VITE_API_ORIGIN || window.location.origin;
|
||||
|
||||
return {
|
||||
async list() {
|
||||
const page = 1;
|
||||
const per_page = 1000;
|
||||
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${origin}/api/v1/backups`, { page, per_page, access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null, result.body.backups];
|
||||
},
|
||||
async getConfig() {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${origin}/api/v1/backups/config`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null, result.body];
|
||||
},
|
||||
async getPolicy() {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${origin}/api/v1/backups/policy`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null, result.body.policy];
|
||||
},
|
||||
async setPolicy(policy) {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.post(`${origin}/api/v1/backups/policy`, policy, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null];
|
||||
},
|
||||
async cleanup() {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.post(`${origin}/api/v1/backups/cleanup`, {}, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 202) return [error || result];
|
||||
return [null, result.body.taskId];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
create,
|
||||
};
|
||||
@@ -29,9 +29,12 @@ function create() {
|
||||
return [];
|
||||
}
|
||||
|
||||
result.body.isAtLeastAdmin = [ ROLES.OWNER, ROLES.ADMIN ].indexOf(result.body.role) !== -1;
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
|
||||
return result.body;
|
||||
result.body.isAtLeastAdmin = [ ROLES.OWNER, ROLES.ADMIN ].indexOf(result.body.role) !== -1;
|
||||
result.body.isAtLeastOwner = [ ROLES.OWNER ].indexOf(result.body.role) !== -1;
|
||||
|
||||
return [null, result.body];
|
||||
},
|
||||
async setPassword(password, newPassword) {
|
||||
let error, result;
|
||||
|
||||
@@ -169,14 +169,17 @@ function toggleView() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
profile.value = await profileModel.get();
|
||||
let [error, result] = await profileModel.get();
|
||||
if (error) return console.error(error);
|
||||
|
||||
profile.value = result;
|
||||
|
||||
await refreshApps();
|
||||
|
||||
const [error, domains] = await domainsModel.list();
|
||||
[error, result] = await domainsModel.list();
|
||||
if (error) return console.error(error);
|
||||
|
||||
domainFilterOptions.value = domainFilterOptions.value.concat(domains.map(d => { d.id = d.domain; return d; }));
|
||||
domainFilterOptions.value = domainFilterOptions.value.concat(result.map(d => { d.id = d.domain; return d; }));
|
||||
domainFilter.value = domainFilterOptions.value[0].id;
|
||||
|
||||
stateFilter.value = stateFilterOptions[0].id;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import { Button } from 'pankow';
|
||||
import Section from '../components/Section.vue';
|
||||
import BackupSchedule from '../components/BackupSchedule.vue';
|
||||
import BackupList from '../components/BackupList.vue';
|
||||
import BackupsModel from '../models/BackupsModel.js';
|
||||
import ProfileModel from '../models/ProfileModel.js';
|
||||
|
||||
const profileModel = ProfileModel.create();
|
||||
const backupsModel = BackupsModel.create();
|
||||
|
||||
function mountlike(provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
|
||||
}
|
||||
|
||||
function s3like(provider) {
|
||||
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|
||||
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces' || provider === 'hetzner-objectstorage'
|
||||
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|
||||
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
|
||||
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|
||||
|| provider === 'contabo-objectstorage';
|
||||
}
|
||||
|
||||
const profile = ref({});
|
||||
const manualBackupApps = ref([]);
|
||||
const config = ref({});
|
||||
const mountOptions = ref({});
|
||||
const mountStatus = ref({});
|
||||
const remountBusy = ref(false);
|
||||
|
||||
function onConfigure() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function onRemount() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const [error, result] = await backupsModel.getConfig();
|
||||
if (error) return console.error(error);
|
||||
|
||||
config.value = result;
|
||||
mountOptions.value = result.mountOptions || {};
|
||||
|
||||
// $scope.backupConfig = backupConfig;
|
||||
// $scope.mountStatus = null;
|
||||
|
||||
// if (!$scope.mountlike($scope.backupConfig.provider)) return;
|
||||
|
||||
// Client.getBackupMountStatus(function (error, mountStatus) {
|
||||
// if (error) return console.error(error);
|
||||
|
||||
// $scope.mountStatus = mountStatus;
|
||||
// });
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [error, result] = await profileModel.get();
|
||||
if (error) return console.error(error);
|
||||
|
||||
profile.value = result;
|
||||
|
||||
await refresh();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<Section :title="$t('backups.title')">
|
||||
<p>{{ $t('backups.location.description') }}
|
||||
<span v-show="manualBackupApps.length">
|
||||
{{ $t('backups.location.disabledList') }}
|
||||
<span v-for="app in manualBackupApps" :key="app.id">
|
||||
<a :href="`/#/app/${app.id}/backups`">{{ app.label || app.fqdn }}</a>,
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p v-show="config.provider === 'noop'" class="text-danger" v-html="marked.parse($t('backups.check.noop'))"></p>
|
||||
<p v-show="config.provider === 'filesystem'" class="text-danger" v-html="marked.parse($t('backups.check.sameDisk'))"></p>
|
||||
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.location.provider') }}</div>
|
||||
<div class="info-value">{{ config.provider }}</div>
|
||||
</div>
|
||||
<div class="info-row" v-show="config.provider !== 'noop'">
|
||||
<div class="info-label">{{ $t('backups.location.location') }}</div>
|
||||
<div class="info-value">
|
||||
<span v-show="config.provider === 'filesystem'">{{ config.backupFolder }}</span>
|
||||
<span v-show="mountlike(config.provider)">
|
||||
<i class="fa fa-circle" :style="{ color: mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" v-show="mountStatus" v-tooltip="mountStatus.message"></i>
|
||||
<span v-show="config.provider === 'disk' || config.provider === 'filesystem' || config.provider === 'ext4' || config.provider === 'xfs' || config.provider === 'mountpoint'">{{ mountOptions.diskPath || config.mountPoint }}{{ (config.prefix ? '/' : '') + config.prefix }}</span>
|
||||
<span v-show="config.provider === 'cifs' || config.provider === 'nfs' || config.provider === 'sshfs'">{{ mountOptions.host }}:{{ mountOptions.remoteDir }}{{ (config.prefix ? '/' : '') + config.prefix }}</span>
|
||||
</span>
|
||||
|
||||
<span v-show="config.provider !== 's3' && config.provider !== 'minio' && (s3like(config.provider) || provider === 'gcs')">{{ config.bucket + (config.prefix ? '/' : '') + config.prefix }}</span>
|
||||
<span v-show="config.provider === 's3'">{{ config.region + ' ' + config.bucket + (config.prefix ? '/' : '') + config.prefix }}</span>
|
||||
<span v-show="config.provider === 'minio'">{{ config.endpoint + ' ' + config.bucket + (config.prefix ? '/' : '') + config.prefix }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-row" v-show="config.endpoint && config.provider !== 'minio'">
|
||||
<div class="info-label">{{ $t('backups.location.endpoint') }}</div>
|
||||
<div class="info-value">{{ config.endpoint || config.region }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.location.format') }}</div>
|
||||
<div class="info-value">{{ config.format }} <i class="fas fa-lock" v-show="config.password" ></i></div>
|
||||
</div>
|
||||
|
||||
<Button v-show="profile.isAtLeastOwner" @click="onConfigure()">{{ $t('backups.location.configure') }}</Button>
|
||||
<Button v-show="profile.isAtLeastOwner && mountlike(config.provider)" :disabled="remountBusy" :loading="remountBusy" @click="onRemount()">{{ $t('backups.location.remount') }}</Button>
|
||||
</Section>
|
||||
|
||||
<BackupSchedule :profile="profile"/>
|
||||
<BackupList />
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,6 +36,12 @@ async function onSelectLanguage(lang) {
|
||||
// TODO dynamically change lange instead of reloading
|
||||
}
|
||||
|
||||
async function refreshProfile() {
|
||||
const [error, result] = await profileModel.get();
|
||||
if (error) return console.error(error);
|
||||
|
||||
user.value = result;
|
||||
}
|
||||
|
||||
// Profile edits
|
||||
async function onChangeDisplayName(currentDisplayName) {
|
||||
@@ -53,7 +59,7 @@ async function onChangeDisplayName(currentDisplayName) {
|
||||
const error = await profileModel.setDisplayName(displayName);
|
||||
if (error) return console.error('Failed to set displayName', error);
|
||||
|
||||
user.value = await profileModel.get();
|
||||
await refreshProfile();
|
||||
}
|
||||
|
||||
async function onChangeEmail(currentEmail) {
|
||||
@@ -72,7 +78,7 @@ async function onChangeEmail(currentEmail) {
|
||||
const error = await profileModel.setEmail(result[0], result[1]);
|
||||
if (error) return console.error('Failed to set email', error);
|
||||
|
||||
user.value = await profileModel.get();
|
||||
await refreshProfile();
|
||||
}
|
||||
|
||||
async function onChangeFallbackEmail(currentFallbackEmail) {
|
||||
@@ -91,7 +97,7 @@ async function onChangeFallbackEmail(currentFallbackEmail) {
|
||||
const error = await profileModel.setFallbackEmail(result[0], result[1]);
|
||||
if (error) return console.error('Failed to set fallback email', error);
|
||||
|
||||
user.value = await profileModel.get();
|
||||
await refreshProfile();
|
||||
}
|
||||
|
||||
const avatarFileInput = useTemplateRef('avatarFileInput');
|
||||
@@ -173,7 +179,7 @@ async function onOpenTwoFASetupDialog() {
|
||||
async function onTwoFAEnable() {
|
||||
const [error] = await profileModel.enableTwoFA(twoFATotpToken.value);
|
||||
if (error) return twoFAEnableError.value = error.body ? error.body.message : 'Internal error';
|
||||
user.value = await profileModel.get();
|
||||
await refreshProfile();
|
||||
|
||||
twoFADialog.value.close();
|
||||
}
|
||||
@@ -194,13 +200,13 @@ async function onTwoFADisable() {
|
||||
const [error] = await profileModel.disableTwoFA(password);
|
||||
if (error) return onTwoFADisable();
|
||||
|
||||
user.value = await profileModel.get();
|
||||
await refreshProfile();
|
||||
}
|
||||
|
||||
|
||||
// Init
|
||||
onMounted(async () => {
|
||||
user.value = await profileModel.get();
|
||||
await refreshProfile();
|
||||
|
||||
let [error, result] = await cloudronModel.languages();
|
||||
languages.value = result.map(l => {
|
||||
|
||||
Reference in New Issue
Block a user