c5f97e8bb0
in some old instances, we had "00 00 * * *" (note double space and only 5 components).
374 lines
12 KiB
Vue
374 lines
12 KiB
Vue
<script setup>
|
||
|
||
import { useI18n } from 'vue-i18n';
|
||
const i18n = useI18n();
|
||
const t = i18n.t;
|
||
|
||
import { ref, onMounted, useTemplateRef, computed } from 'vue';
|
||
import { marked } from 'marked';
|
||
import { Button, FormGroup, Dialog, ProgressBar, Radiobutton, MultiSelect, Checkbox, InputDialog } from '@cloudron/pankow';
|
||
import { prettyLongDate } from '@cloudron/pankow/utils';
|
||
import { TASK_TYPES, ISTATES } from '../constants.js';
|
||
import Section from '../components/Section.vue';
|
||
import SettingsItem from '../components/SettingsItem.vue';
|
||
import AppsModel from '../models/AppsModel.js';
|
||
import UpdaterModel from '../models/UpdaterModel.js';
|
||
import TasksModel from '../models/TasksModel.js';
|
||
import DashboardModel from '../models/DashboardModel.js';
|
||
import { cronDays, cronHours, prettySchedule, parseSchedule } from '../utils.js';
|
||
|
||
const appsModel = AppsModel.create();
|
||
const tasksModel = TasksModel.create();
|
||
const updaterModel = UpdaterModel.create();
|
||
const dashboardModel = DashboardModel.create();
|
||
|
||
const inputDialog = useTemplateRef('inputDialog');
|
||
const updateDialog = useTemplateRef('updateDialog');
|
||
|
||
const ready = ref(false);
|
||
const taskLogsMenu = ref([]);
|
||
const apps = ref([]);
|
||
const version = ref('');
|
||
const ubuntuVersion = ref('');
|
||
const currentPattern = ref('');
|
||
const updateBusy = ref(false);
|
||
const updateError = ref({});
|
||
const stopError = ref({});
|
||
const updateCheckError = ref({});
|
||
const checkingBusy = ref(false);
|
||
const pendingUpdate = ref(null);
|
||
const skipBackup = ref(false);
|
||
const lastTask = ref({});
|
||
|
||
const canUpdate = computed(() => {
|
||
return apps.value.every(function (app) {
|
||
return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED);
|
||
});
|
||
});
|
||
|
||
const inProgressApps = computed(() => {
|
||
return apps.value.filter(function (app) {
|
||
return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED;
|
||
});
|
||
});
|
||
|
||
const configureDialog = useTemplateRef('configureDialog');
|
||
const configureBusy = ref(false);
|
||
const configureError = ref('');
|
||
const configureType = ref('');
|
||
const configurePattern = ref('');
|
||
const configureDays = ref([]);
|
||
const configureHours = ref([]);
|
||
|
||
async function refreshAutoupdatePattern() {
|
||
const [error, result] = await updaterModel.getAutoupdatePattern();
|
||
if (error) return console.error(error);
|
||
|
||
currentPattern.value = result.pattern;
|
||
configurePattern.value = result.pattern;
|
||
}
|
||
|
||
async function refreshApps() {
|
||
const [error, result] = await appsModel.list();
|
||
if (error) return console.error(error);
|
||
|
||
apps.value = result;
|
||
}
|
||
|
||
async function refreshPendingUpdateInfo() {
|
||
const [error, result] = await updaterModel.getBoxUpdate();
|
||
if (error) return console.error(error);
|
||
|
||
updateError.value = {};
|
||
stopError.value = {};
|
||
updateCheckError.value = {};
|
||
|
||
pendingUpdate.value = result || null;
|
||
}
|
||
|
||
function onShowConfigure() {
|
||
if (currentPattern.value === 'never') {
|
||
configureType.value = 'never';
|
||
} else {
|
||
configureType.value = 'pattern';
|
||
const result = parseSchedule(currentPattern.value);
|
||
configureDays.value = result.days; // Array of cronDays.id
|
||
configureHours.value = result.hours; // Array of cronHours.id
|
||
}
|
||
|
||
configureDialog.value.open();
|
||
}
|
||
|
||
async function onSubmitConfigure() {
|
||
let pattern = 'never';
|
||
if (configureType.value === 'pattern') {
|
||
let daysPattern;
|
||
if (configureDays.value.length === 7) daysPattern = '*';
|
||
else daysPattern = configureDays.value.join(',');
|
||
|
||
let hoursPattern;
|
||
if (configureHours.value.length === 24) hoursPattern = '*';
|
||
else hoursPattern = configureHours.value.join(',');
|
||
|
||
pattern ='00 00 ' + hoursPattern + ' * * ' + daysPattern;
|
||
}
|
||
|
||
configureBusy.value = true;
|
||
const [error] = await updaterModel.setAutoupdatePattern(pattern);
|
||
if (error) {
|
||
configureError.value = error.body ? error.body.message : 'Internal error';
|
||
configureBusy.value = false;
|
||
return console.error(error);
|
||
}
|
||
|
||
await refreshAutoupdatePattern();
|
||
|
||
configureBusy.value = false;
|
||
configureDialog.value.close();
|
||
}
|
||
|
||
async function onShowUpdate() {
|
||
skipBackup.value = false;
|
||
await refreshApps();
|
||
updateDialog.value.open();
|
||
}
|
||
|
||
async function waitForTask() {
|
||
if (!lastTask.value.id) return;
|
||
|
||
const [error, result] = await tasksModel.get(lastTask.value.id);
|
||
|
||
if (error) {
|
||
setTimeout(waitForTask, 2000);
|
||
return console.error(error);
|
||
}
|
||
|
||
lastTask.value = result;
|
||
|
||
// task done, refresh menu
|
||
if (!result.active) {
|
||
refreshPendingUpdateInfo();
|
||
refreshTasks();
|
||
return;
|
||
}
|
||
|
||
setTimeout(waitForTask, 2000);
|
||
}
|
||
|
||
async function refreshTasks() {
|
||
const [error, result] = await tasksModel.getByType(TASK_TYPES.TASK_UPDATE);
|
||
if (error) return console.error(error);
|
||
|
||
lastTask.value = result[0] || {};
|
||
if (result.length && !result[0].active && !result[0].success) updateError.value.generic = result[0].error.message;
|
||
|
||
taskLogsMenu.value = result.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}`); }
|
||
};
|
||
});
|
||
|
||
updateBusy.value = lastTask.value.active;
|
||
|
||
// if last task is currently active, start polling
|
||
if (lastTask.value.active) waitForTask();
|
||
}
|
||
|
||
async function onSubmitUpdate() {
|
||
updateError.value = {};
|
||
updateBusy.value = true;
|
||
|
||
const [error] = await updaterModel.update(skipBackup.value);
|
||
if (error) {
|
||
updateBusy.value = false;
|
||
|
||
updateDialog.value.close();
|
||
|
||
inputDialog.value.info({
|
||
title: t('notifications.settings.cloudronUpdateFailed'),
|
||
message: error.body ? error.body.message : 'Internal error. Please try again.',
|
||
confirmLabel: t('main.dialog.close'),
|
||
confirmStyle: 'secondary'
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
await refreshTasks();
|
||
|
||
updateDialog.value.close();
|
||
}
|
||
|
||
async function onCheck() {
|
||
checkingBusy.value = true;
|
||
updateCheckError.value = {};
|
||
|
||
const [error] = await updaterModel.checkBoxUpdate();
|
||
if (error) {
|
||
updateCheckError.value.generic = error.body?.message || 'Internal error';
|
||
checkingBusy.value = false;
|
||
return;
|
||
}
|
||
|
||
await refreshPendingUpdateInfo();
|
||
checkingBusy.value = false;
|
||
}
|
||
|
||
async function onStop() {
|
||
const [error] = await tasksModel.stop(lastTask.value.id);
|
||
if (error) {
|
||
if (error.statusCode === 409) {
|
||
stopError.value.generic = 'No update is currently in progress';
|
||
} else {
|
||
console.error(error);
|
||
stopError.value.generic = error.message || 'Internal error';
|
||
}
|
||
return;
|
||
}
|
||
|
||
updateBusy.value = false;
|
||
}
|
||
|
||
onMounted(async () => {
|
||
const [error, result] = await dashboardModel.config();
|
||
if (error) return console.error(error);
|
||
|
||
version.value = result.version;
|
||
ubuntuVersion.value = result.ubuntuVersion;
|
||
|
||
await refreshPendingUpdateInfo();
|
||
await refreshAutoupdatePattern();
|
||
await refreshTasks();
|
||
|
||
ready.value = true;
|
||
});
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<InputDialog ref="inputDialog"/>
|
||
|
||
<Dialog ref="updateDialog"
|
||
:title="$t('settings.updateDialog.title')"
|
||
:confirm-label="$t('settings.updateDialog.updateAction')"
|
||
:confirm-active="canUpdate"
|
||
:confirm-busy="updateBusy"
|
||
:confirm-style="pendingUpdate?.unstable ? 'danger' : 'primary'"
|
||
:reject-label="$t('main.dialog.cancel')"
|
||
:reject-active="!updateBusy"
|
||
reject-style="secondary"
|
||
@confirm="onSubmitUpdate()"
|
||
>
|
||
<div v-if="pendingUpdate && canUpdate">
|
||
<h3>{{ $t('settings.updateDialog.updateAvailable', { newVersion: `v${pendingUpdate.version}` }) }}</h3>
|
||
<p v-if="pendingUpdate.unstable" class="error-label">{{ $t('settings.updateDialog.unstableWarning') }}</p>
|
||
|
||
<div>{{ $t('settings.updateDialog.changes') }}:</div>
|
||
<div class="changelog-container">
|
||
<ul class="changelogs">
|
||
<li v-for="change in pendingUpdate.changelog" :key="change" v-html="marked.parse(change)"></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<Checkbox class="skip-backup" v-model="skipBackup" :label="$t('settings.updateDialog.skipBackupCheckbox')"/>
|
||
</div>
|
||
<!-- !canUpdate -->
|
||
<div v-else>
|
||
<p>{{ $t('settings.updateDialog.blockingApps') }}</p>
|
||
<ul>
|
||
<li v-for="app in inProgressApps" :key="app.id">{{ app.fqdn }}</li>
|
||
</ul>
|
||
<span>{{ $t('settings.updateDialog.blockingAppsInfo') }}</span>
|
||
<br/>
|
||
<br/>
|
||
</div>
|
||
</Dialog>
|
||
|
||
<Dialog ref="configureDialog"
|
||
:title="$t('settings.updateScheduleDialog.title')"
|
||
:confirm-label="$t('main.dialog.save')"
|
||
:confirm-active="configureType === 'never' ? true : (configureHours.length > 0 && configureDays.length > 0)"
|
||
:reject-label="$t('main.dialog.cancel')"
|
||
reject-style="secondary"
|
||
@confirm="onSubmitConfigure()"
|
||
>
|
||
<FormGroup>
|
||
<div description v-html="$t('settings.updateScheduleDialog.description')"></div>
|
||
|
||
<p class="has-error text-center" v-show="configureError">{{ configureError }}</p>
|
||
|
||
<Radiobutton v-model="configureType" value="never" :label="$t('settings.updateScheduleDialog.disableCheckbox')" />
|
||
<Radiobutton v-model="configureType" value="pattern" :label="$t('settings.updateScheduleDialog.enableCheckbox')"/>
|
||
|
||
<div v-show="configureType === 'pattern'" style="display: flex; gap: 10px; align-items: center; margin: 10px 0px 0px 25px">
|
||
<div>{{ $t('settings.updateScheduleDialog.days') }}: <MultiSelect v-model="configureDays" :options="cronDays" option-label="name" option-key="id"/></div>
|
||
<div>{{ $t('settings.updateScheduleDialog.hours') }}: <MultiSelect v-model="configureHours" :options="cronHours" option-label="name" option-key="id"/></div>
|
||
<div class="text-small text-danger" v-show="configureType === 'pattern' && !(configureHours.length !== 0 && configureDays.length !== 0)">{{ $t('settings.updateScheduleDialog.selectOne') }}</div>
|
||
</div>
|
||
</FormGroup>
|
||
</Dialog>
|
||
|
||
<Section :title="$t('settings.updates.title')">
|
||
<template #header-buttons>
|
||
<Button tool secondary :menu="taskLogsMenu" :disabled="taskLogsMenu.length === 0">{{ $t('main.action.logs') }}</Button>
|
||
</template>
|
||
|
||
<div v-html="$t('settings.updates.description')"></div>
|
||
<br/>
|
||
|
||
<SettingsItem v-if="ready">
|
||
<div>
|
||
<label>{{ $t('settings.updates.schedule') }}</label>
|
||
<span v-if="currentPattern !== 'never'">{{ prettySchedule(currentPattern) }}</span>
|
||
<span v-else>{{ $t('settings.updates.disabled') }}</span>
|
||
</div>
|
||
<div style="display: flex; align-items: center">
|
||
<Button tool plain @click="onShowConfigure()">{{ $t('main.dialog.edit') }}</Button>
|
||
</div>
|
||
</SettingsItem>
|
||
|
||
<SettingsItem v-if="ready">
|
||
<div>
|
||
<label>{{ $t('system.info.cloudronVersion') }}</label>
|
||
<span>{{ version }} <span v-if="!pendingUpdate">({{ $t('settings.updates.onLatest') }})</span></span>
|
||
</div>
|
||
</SettingsItem>
|
||
|
||
<ProgressBar :value="lastTask.percent" v-if="updateBusy && lastTask" :busy="true" />
|
||
<p v-if="updateBusy && lastTask">{{ lastTask.message }}</p>
|
||
|
||
<div class="error-label" v-if="stopError.generic">{{ stopError.generic }}</div>
|
||
<div class="error-label" v-if="updateCheckError.generic">{{ updateCheckError.generic }}</div>
|
||
<div class="error-label" v-if="updateError.generic">{{ updateError.generic }}</div>
|
||
|
||
<div class="button-bar" v-if="ready">
|
||
<Button :disabled="checkingBusy" :loading="checkingBusy" v-if="!updateBusy" @click="onCheck()">{{ $t('settings.updates.checkForUpdatesAction') }}</Button>
|
||
<Button danger v-if="updateBusy" @click="onStop()">{{ $t('settings.updates.stopUpdateAction') }}</Button>
|
||
<Button :danger="(pendingUpdate && pendingUpdate.unstable) ? true : undefined" :success="(pendingUpdate && !pendingUpdate.unstable) ? true : undefined" v-show="pendingUpdate && pendingUpdate.version !== version && !updateBusy" @click="onShowUpdate()">{{ $t('settings.updates.updateAvailableAction') }}</Button>
|
||
</div>
|
||
</Section>
|
||
</div>
|
||
</template>
|
||
|
||
<style>
|
||
|
||
.changelogs {
|
||
padding-left: 15px;
|
||
}
|
||
|
||
.changelogs > li > p {
|
||
margin: 0;
|
||
padding: 2px;
|
||
}
|
||
|
||
.changelog-container {
|
||
overflow: auto;
|
||
max-height: 20lh;
|
||
padding-right: 0.5rem; /* space so scrollbar doesn’t overlap text */
|
||
}
|
||
|
||
</style>
|