Implement update dialog

This commit is contained in:
Johannes Zellner
2025-06-30 17:35:01 +02:00
parent a0d96d5a74
commit 8e18a5fb4c
2 changed files with 135 additions and 17 deletions
+125 -17
View File
@@ -1,15 +1,18 @@
<script setup>
import { ref, onMounted, useTemplateRef } from 'vue';
import { Button, Dialog, ProgressBar, Radiobutton, MultiSelect } from 'pankow';
import { ref, onMounted, useTemplateRef, computed } from 'vue';
import { marked } from 'marked';
import { Button, Dialog, ProgressBar, Radiobutton, MultiSelect, Checkbox } from 'pankow';
import { prettyLongDate } from 'pankow/utils';
import { TASK_TYPES } from '../constants.js';
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';
const appsModel = AppsModel.create();
const tasksModel = TasksModel.create();
const updaterModel = UpdaterModel.create();
const dashboardModel = DashboardModel.create();
@@ -47,15 +50,32 @@ function prettyAutoUpdateSchedule(pattern) {
}
}
const updateDialog = useTemplateRef('updateDialog');
const taskLogsMenu = ref([]);
const apps = ref([]);
const version = ref('');
const ubuntuVersion = ref('');
const currentPattern = ref('');
const percent = ref(0);
const message = ref('');
const updateBusy = ref(false);
const updateError = ref({});
const checkingBusy = ref(false);
const pendingUpdate = ref(null);
const skipBackup = ref(false);
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);
@@ -76,6 +96,13 @@ async function refreshAutoupdatePattern() {
configurePattern.value = result.pattern;
}
async function refreshApps() {
const [error, result] = await appsModel.list();
if (error) return console.error(error);
apps.value = result;
}
async function refreshInfo() {
const [error, result] = await updaterModel.getBoxUpdate();
if (error) return console.error(error);
@@ -128,8 +155,66 @@ async function onSubmitConfigure() {
configureDialog.value.close();
}
function onShowUpdate() {
async function onShowUpdate() {
skipBackup.value = false;
await refreshApps();
updateDialog.value.open();
}
const lastTask = ref({});
async function waitForTask() {
if (!lastTask.value.id) return;
const [error, result] = await tasksModel.get(lastTask.value.id);
if (error) return console.error(error);
lastTask.value = result;
// task done, refresh menu
if (!result.active) {
refreshInfo();
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] || {};
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}`); }
};
});
// if last task is currently active, start polling
if (lastTask.value.active) waitForTask();
}
async function onSubmitUpdate() {
updateError.value = {};
updateBusy.value = true;
percent.value = 0;
message.value = '';
const [error] = await updaterModel.update(skipBackup.value);
updateBusy.value = false;
if (error) {
updateError.value.generic = error.message || 'Internal error';
return;
}
await refreshTasks();
updateDialog.value.close();
}
async function onCheck() {
@@ -142,19 +227,6 @@ async function onCheck() {
checkingBusy.value = false;
}
async function refreshTasks() {
const [error, result] = await tasksModel.getByType(TASK_TYPES.TASK_UPDATE);
if (error) return console.error(error);
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}`); }
};
});
}
onMounted(async () => {
const [error, result] = await dashboardModel.config();
if (error) return console.error(error);
@@ -171,6 +243,42 @@ onMounted(async () => {
<template>
<div>
<Dialog ref="updateDialog"
:title="$t('settings.updateDialog.title') + ` v${pendingUpdate ? pendingUpdate.version : ''}`"
:confirm-label="$t('settings.updateDialog.updateAction')"
:confirm-active="canUpdate"
:confirm-busy="updateBusy"
:confirm-style="pendingUpdate && pendingUpdate.unstable ? 'danger' : 'primary'"
:reject-label="updateBusy ? null : $t('main.dialog.cancel')"
reject-style="secondary"
@confirm="onSubmitUpdate()"
>
<div v-if="pendingUpdate">
<div v-if="canUpdate">
<p class="text-danger" v-if="pendingUpdate.unstable">{{ $t('settings.updateDialog.unstableWarning') }}</p>
<p>{{ $t('settings.updateDialog.changes') }}:</p>
<ul>
<li v-for="change in pendingUpdate.changelog" :key="change">{{ change }}</li>
<li v-for="change in pendingUpdate.changelog" :key="change" v-html="marked.parse(change)"></li>
</ul>
<br/>
<p v-if="updateError.generic" class="error-label">{{ updateError.generic }}</p>
<Checkbox v-model="skipBackup" :label="$t('settings.updateDialog.skipBackupCheckbox')"/>
</div>
<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>
</div>
</Dialog>
<Dialog ref="configureDialog"
:title="$t('settings.updateScheduleDialog.title')"
:confirm-label="$t('main.dialog.save')"