Files
cloudron-box/dashboard/src/views/AppConfigureView.vue

442 lines
16 KiB
Vue
Raw Normal View History

2025-02-20 10:54:43 +01:00
<script setup>
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, onBeforeUnmount, useTemplateRef } from 'vue';
import { Button, ButtonGroup, SingleSelect, InputDialog, ProgressBar } from '@cloudron/pankow';
2025-04-23 15:32:42 +02:00
import PostInstallDialog from '../components/PostInstallDialog.vue';
import SftpInfoDialog from '../components/SftpInfoDialog.vue';
import Access from '../components/app/Access.vue';
2025-03-01 19:22:11 +01:00
import Backups from '../components/app/Backups.vue';
2025-02-22 11:13:15 +01:00
import Cron from '../components/app/Cron.vue';
2025-03-01 19:22:11 +01:00
import Display from '../components/app/Display.vue';
import Email from '../components/app/Email.vue';
import Eventlog from '../components/app/Eventlog.vue';
2025-03-07 11:54:43 +01:00
import Graphs from '../components/app/Graphs.vue';
2025-03-01 19:22:11 +01:00
import Info from '../components/app/Info.vue';
2025-03-03 12:45:40 +01:00
import Location from '../components/app/Location.vue';
2025-03-07 12:08:19 +01:00
import Proxy from '../components/app/Proxy.vue';
2025-02-25 19:04:58 +01:00
import Resources from '../components/app/Resources.vue';
2025-02-22 18:31:21 +01:00
import Repair from '../components/app/Repair.vue';
2025-03-01 19:22:11 +01:00
import Security from '../components/app/Security.vue';
2025-03-06 23:52:41 +01:00
import Services from '../components/app/Services.vue';
2025-03-06 16:40:52 +01:00
import Storage from '../components/app/Storage.vue';
import Uninstall from '../components/app/Uninstall.vue';
2025-03-01 19:22:11 +01:00
import Updates from '../components/app/Updates.vue';
2025-02-20 10:54:43 +01:00
import AppsModel from '../models/AppsModel.js';
2025-03-03 12:45:40 +01:00
import TasksModel from '../models/TasksModel.js';
2025-03-03 11:22:56 +01:00
import { API_ORIGIN, APP_TYPES, ISTATES, RSTATES, HSTATES } from '../constants.js';
2025-02-20 10:54:43 +01:00
const appsModel = AppsModel.create();
2025-03-03 12:45:40 +01:00
const tasksModel = TasksModel.create();
2025-02-22 18:38:00 +01:00
const installationStateLabel = AppsModel.installationStateLabel;
2025-02-20 16:12:36 +01:00
2025-02-25 19:04:58 +01:00
const busy = ref(true);
2025-02-20 10:54:43 +01:00
const id = ref('');
const app = ref(null);
2025-05-27 14:57:33 +02:00
const currentView = ref('');
2025-02-20 10:54:43 +01:00
const link = ref('');
const infoMenu = ref([]);
const hasLocalStorage = ref(false);
const hasOptionalServices = ref(false);
const hasEmail = ref(false);
2025-03-03 12:45:40 +01:00
const busyStopTask = ref(false);
2025-04-23 15:32:42 +02:00
const postInstallDialog = useTemplateRef('postInstallDialog');
const sftpInfoDialog = useTemplateRef('sftpInfoDialog');
const inputDialog = useTemplateRef('inputDialog');
2025-02-20 10:54:43 +01:00
function onSetView(newView) {
if (!isViewEnabled(newView, app.value.error?.details.installationState)) {
if (!currentView.value) {
currentView.value = 'info';
window.location.hash = `/app/${id.value}/info`;
}
return;
}
2025-05-27 14:57:33 +02:00
currentView.value = newView;
window.location.hash = `/app/${id.value}/${newView}`;
}
2025-02-21 14:07:07 +01:00
let refreshTimer = null;
2025-02-20 10:54:43 +01:00
async function refresh() {
const [error, result] = await appsModel.get(id.value);
if (error) {
if (error.status === 403) return window.location.hash = '/';
2025-03-31 14:42:10 +02:00
else if (error.status === 404) return window.location.hash = '/apps';
return console.error(error);
}
// prevent users who have no acces to
if (result.accessLevel !== 'admin' && result.accessLevel !== 'operator') return window.location.hash = '/';
2025-02-20 10:54:43 +01:00
app.value = result;
2025-02-26 20:30:33 +01:00
link.value = (result.installationState !== ISTATES.INSTALLED || result.health !== HSTATES.HEALTHY || result.runState === RSTATES.STOPPED) ? '' : `https://${result.fqdn}`;
2025-02-20 10:54:43 +01:00
hasLocalStorage.value = result.manifest && result.manifest.addons && result.manifest.addons.localstorage;
hasOptionalServices.value = result.manifest && result.manifest.addons && (result.manifest.addons.turn?.optional || result.manifest.addons.redis?.optional);
hasEmail.value = result.manifest && result.manifest.addons && (result.manifest.addons.sendmail || result.manifest.addons.recvmail);
2025-02-20 10:54:43 +01:00
infoMenu.value = [];
2025-02-20 10:54:43 +01:00
infoMenu.value.push({
label: t('app.docsAction'),
disabled: !result.manifest?.documentationUrl,
2025-04-22 17:27:10 +02:00
href: result.manifest.documentationUrl,
target: '_blank',
2025-02-20 10:54:43 +01:00
});
if (result.manifest?.postInstallMessage) {
infoMenu.value.push({
label: t('app.firstTimeSetupAction'),
2025-04-23 15:32:42 +02:00
action: () => postInstallDialog.value.open(app.value),
2025-02-20 10:54:43 +01:00
});
}
if (result.manifest?.configurePath) {
infoMenu.value.push({
label: t('app.adminPageAction'),
2025-04-22 17:27:10 +02:00
href: link.value + result.manifest.configurePath,
target: '_blank',
2025-02-20 10:54:43 +01:00
});
}
if (result.manifest?.addons?.localstorage?.ftp) {
infoMenu.value.push({
label: t('app.sftpInfoAction'),
action: () => sftpInfoDialog.value.open(app.value),
2025-02-20 10:54:43 +01:00
});
}
infoMenu.value.push({ separator: true });
infoMenu.value.push({
label: t('app.forumUrlAction'),
disabled: !result.manifest?.forumUrl,
2025-04-22 17:27:10 +02:00
href: result.manifest.forumUrl,
target: '_blank',
2025-02-20 10:54:43 +01:00
});
infoMenu.value.push({ separator: true });
infoMenu.value.push({
label: t('app.projectWebsiteAction'),
disabled: !result.manifest?.website,
2025-04-22 17:27:10 +02:00
href: result.manifest.website,
target: '_blank',
2025-02-20 10:54:43 +01:00
});
refreshTimer = setTimeout(refresh, 2000);
2025-02-20 10:54:43 +01:00
}
function isViewEnabled(view, errorState) {
if (!errorState) return true;
if (view === 'info' || view === 'uninstall' || view === 'eventlog') return true;
if (errorState === ISTATES.PENDING_INSTALL) return view === 'repair';
if (errorState === ISTATES.PENDING_UNINSTALL) return false;
if (errorState === ISTATES.PENDING_CLONE) return false;
if (view === 'display'
|| view === 'backups'
|| view === 'access'
|| view === 'proxy'
|| view === 'graphs'
|| view === 'security'
|| view === 'cron'
) return true;
if (view === 'location') {
return errorState === ISTATES.PENDING_LOCATION_CHANGE;
} else if (view === 'repair') {
return errorState === ISTATES.PENDING_RESTART || errorState === ISTATES.PENDING_CONFIGURE || ISTATES.PENDING_INSTALL || ISTATES.PENDING_DEBUG;
} else if (view === 'resources') {
return errorState === ISTATES.PENDING_RESIZE || errorState === ISTATES.PENDING_RECREATE_CONTAINER;
} else if (view === 'storage') {
return errorState === ISTATES.PENDING_DATA_DIR_MIGRATION || errorState === ISTATES.PENDING_RECREATE_CONTAINER;
} else if (view === 'services') {
return errorState === ISTATES.PENDING_SERVICES_CHANGE;
} else if (view === 'email') {
return errorState === ISTATES.PENDING_SERVICES_CHANGE;
} else if (view === 'updates') {
return errorState === ISTATES.PENDING_UPDATE;
}
return false;
}
2025-03-03 12:45:40 +01:00
async function onStopAppTask() {
if (!app.value.taskId) return;
busyStopTask.value = true;
const [error] = await tasksModel.stop(app.value.taskId);
if (error) console.error(error);
busyStopTask.value = false;
}
2025-05-27 14:57:33 +02:00
const views = ref([]);
function hashChange() {
const tmp = window.location.hash.slice('#/app/'.length);
if (!tmp) return;
const parts = tmp.split('/');
if (parts.length !== 2) return;
onSetView(parts[1] || 'info');
}
2025-02-20 10:54:43 +01:00
onMounted(async () => {
const tmp = window.location.hash.slice('#/app/'.length);
if (!tmp) return;
const parts = tmp.split('/');
if (parts.length !== 2) return;
id.value = parts[0];
await refresh();
if (!app.value) return;
2025-02-20 16:12:36 +01:00
let hasServices = false;
if (app.value.manifest.addons.turn && app.value.manifest.addons.turn.optional) hasServices = true;
if (app.value.manifest.addons.redis && app.value.manifest.addons.redis.optional) hasServices = true;
views.value.push({ enabled: true, id: 'info', display: t('app.infoTabTitle'), });
views.value.push({ enabled: true, id: 'display', display: t('app.displayTabTitle'), });
if (app.value.accessLevel === 'admin') views.value.push({ enabled: true, id: 'location', display: t('app.locationTabTitle'), });
if (app.value.type === APP_TYPES.PROXIED) views.value.push({ enabled: true, id: 'proxy', display: 'Proxy', });
if (app.value.accessLevel === 'admin') views.value.push({ enabled: true, id: 'access', display: t('app.accessControlTabTitle'), });
if (app.value.type !== APP_TYPES.PROXIED) views.value.push({ enabled: true, id: 'resources', display: t('app.resourcesTabTitle'), });
if (app.value.type !== APP_TYPES.PROXIED && hasServices) views.value.push({ enabled: true, id: 'services', display: t('app.servicesTabTitle'), });
if (app.value.accessLevel === 'admin' && app.value.type !== APP_TYPES.PROXIED) views.value.push({ enabled: true, id: 'storage', display: t('app.storageTabTitle'), });
if (app.value.type !== APP_TYPES.PROXIED) views.value.push({ enabled: true, id: 'graphs', display: t('app.graphsTabTitle'), });
views.value.push({ enabled: true, id: 'security', display: t('app.securityTabTitle'), });
if (app.value.accessLevel === 'admin' && hasEmail.value && app.value.type !== APP_TYPES.PROXIED) views.value.push({ enabled: true, id: 'email', display: t('app.emailTabTitle'), });
if (app.value.type !== APP_TYPES.PROXIED) views.value.push({ enabled: true, id: 'cron', display: t('app.cronTabTitle'), });
views.value.push({ enabled: true, id: 'updates', display: t('app.updatesTabTitle'), });
if (app.value.type !== APP_TYPES.PROXIED) views.value.push({ enabled: true, id: 'backups', display: t('app.backupsTabTitle'), });
views.value.push({ enabled: true, id: 'repair', display: t('app.repairTabTitle'), });
views.value.push({ enabled: true, id: 'eventlog', display: t('app.eventlogTabTitle'), });
if (app.value.accessLevel === 'admin') views.value.push({ enabled: true, id: 'uninstall', display: t('app.uninstallTabTitle'), });
2025-05-27 14:57:33 +02:00
hashChange();
window.addEventListener('hashchange', hashChange);
2025-02-25 19:04:58 +01:00
busy.value = false;
2025-02-20 10:54:43 +01:00
});
2025-02-22 11:13:15 +01:00
onBeforeUnmount(() => {
if (refreshTimer) clearTimeout(refreshTimer);
window.removeEventListener('hashchange', hashChange);
});
2025-02-20 10:54:43 +01:00
</script>
<template>
<div class="configure-outer">
2025-04-23 15:32:42 +02:00
<PostInstallDialog ref="postInstallDialog"/>
<SftpInfoDialog ref="sftpInfoDialog"/>
<InputDialog ref="inputDialog"/>
2025-04-23 15:32:42 +02:00
2025-02-25 19:04:58 +01:00
<div class="configure-inner" v-if="!busy">
<div class="titlebar">
<div style="display: flex; flex-grow: 1; overflow: hidden;">
2025-03-01 11:44:38 +01:00
<img :src="app.iconUrl" v-fallback-image="API_ORIGIN + '/img/appicon_fallback.png'" style="height: 64px; width: 64px; margin-right: 10px;"/>
2025-02-22 18:38:00 +01:00
<h2>
<a class="applink" :href="link || null" target="_blank">{{ app.label || app.fqdn }}</a>
2025-08-10 18:28:17 +02:00
<div class="statelabel" v-if="app.error">{{ installationStateLabel(app) }} - {{ app.error.message }}</div>
<div class="statelabel" v-else>{{ installationStateLabel(app) }} {{ app.message ? ' - ' + app.message : '' }}</div>
<ProgressBar v-if="app.progress" :busy="true" slim :value="app.progress" :show-label="false" style="margin-top: 10px"/>
2025-02-22 18:38:00 +01:00
</h2>
</div>
2025-05-27 14:57:33 +02:00
<div class="titlebar-toolbar">
<SingleSelect class="pankow-no-desktop" v-model="currentView" :options="views" optionKey="id" optionLabel="display" @select="onSetView"/>
2025-05-27 14:57:33 +02:00
<div style="display: flex; gap: 10px">
<Button v-if="app.taskId" danger tool plain icon="fa-solid fa-xmark" v-tooltip="'Cancel Task'" :loading="busyStopTask" :disabled="busyStopTask" @click="onStopAppTask()"/>
<ButtonGroup>
<Button secondary tool :href="`/logs.html?appId=${app.id}`" target="_blank" v-tooltip="$t('app.logsActionTooltip')" icon="fa-solid fa-align-left" />
<Button secondary tool v-if="app.type !== APP_TYPES.PROXIED" :href="`/terminal.html?id=${app.id}`" target="_blank" v-tooltip="$t('app.terminalActionTooltip')" icon="fa fa-terminal" />
<Button secondary tool v-if="hasLocalStorage" :href="`/filemanager.html#/home/app/${app.id}`" target="_blank" v-tooltip="$t('app.filemanagerActionTooltip')" icon="fas fa-folder" />
</ButtonGroup>
<Button secondary tool icon="fa-solid fa-book" v-tooltip="$t('app.docsActionTooltip')" :menu="infoMenu" />
</div>
</div>
2025-02-20 10:54:43 +01:00
</div>
2025-03-03 12:45:40 +01:00
<div class="configure-body">
2025-05-27 14:57:33 +02:00
<div class="configure-menu pankow-no-mobile">
<div v-for="view in views" :key="view.id" class="configure-menu-item" :active="currentView === view.id ? true : null" :disabled="isViewEnabled(view.id, app.error?.details.installationState) ? null : true">
<a v-if="isViewEnabled(view.id, app.error?.details.installationState)" :href="`/#/app/${app.id}/${view.id}`">{{ view.display }}</a>
<span v-else>{{ view.display }}</span>
</div>
</div>
<div class="configure-content">
<Transition name="slide-fade" mode="out-in">
2025-05-27 14:57:33 +02:00
<Info v-if="currentView === 'info'" :app="app"/>
<Display v-else-if="currentView === 'display'" :app="app"/>
<Location v-else-if="currentView === 'location'" :app="app"/>
<Proxy v-else-if="currentView === 'proxy'" :app="app"/>
<Access v-else-if="currentView === 'access'" :app="app"/>
<Resources v-else-if="currentView === 'resources'" :app="app"/>
<Services v-else-if="currentView === 'services'" :app="app"/>
<Storage v-else-if="currentView === 'storage'" :app="app"/>
<Graphs v-else-if="currentView === 'graphs'" :app="app"/>
<Security v-else-if="currentView === 'security'" :app="app"/>
<Email v-else-if="currentView === 'email'" :app="app"/>
<Cron v-else-if="currentView === 'cron'" :app="app"/>
<Updates v-else-if="currentView === 'updates'" :app="app"/>
<Backups v-else-if="currentView === 'backups'" :app="app"/>
<Repair v-else-if="currentView === 'repair'" :app="app"/>
<Eventlog v-else-if="currentView === 'eventlog'" :app="app"/>
<Uninstall v-else-if="currentView === 'uninstall'" :app="app"/>
</Transition>
</div>
2025-02-20 10:54:43 +01:00
</div>
</div>
</div>
</template>
2025-02-21 23:20:02 +01:00
<style scoped>
2025-02-22 16:29:28 +01:00
.applink {
display: flex;
2025-02-22 18:38:00 +01:00
align-items: center;
2025-02-22 16:29:28 +01:00
color: var(--pankow-text-color);
}
2025-05-27 14:57:33 +02:00
.titlebar {
display: flex;
gap: 10px;
justify-content: space-between;
}
2025-02-22 18:38:00 +01:00
.titlebar h2 {
overflow: hidden;
2025-03-03 12:45:40 +01:00
flex-grow: 1;
2025-02-22 18:38:00 +01:00
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
2025-02-22 18:38:00 +01:00
}
2025-05-27 14:57:33 +02:00
.titlebar-toolbar {
display: flex;
gap: 10px;
align-items: center;
justify-content: space-between;
}
@media (max-width: 576px) {
.titlebar-toolbar {
flex-grow: 1;
}
}
2025-02-22 18:38:00 +01:00
.statelabel {
font-size: 12px;
margin-top: 6px;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
2025-02-22 18:38:00 +01:00
}
2025-02-22 16:29:28 +01:00
.applink:not([href]) {
cursor: not-allowed;
}
.configure-outer {
width: 100%;
margin: auto;
2025-03-23 17:35:35 +01:00
margin-top: 0;
}
.configure-inner {
width: 100%;
max-width: 1200px;
margin: auto;
padding: 0 15px;
}
2025-03-03 12:45:40 +01:00
.apptask-progress {
position: relative;
width: 100%;
height: 5px;
text-align: center;
border-radius: 10px;
color: var(--pankow-text-color);
margin-top: 4px;
2025-03-03 12:45:40 +01:00
}
.apptask-progress-filled {
background-color: var(--pankow-color-primary);
position: relative;
top: 0;
left: 0;
height: 100%;
border-radius: 10px;
z-index: 1;
background-image: linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);
background-size: 40px 40px;
animation: apptask-progress-bar-stripes 1s linear infinite;
transition: width 300ms;
}
@keyframes apptask-progress-bar-stripes {
from {
background-position: 40px 0;
}
to {
background-position: 0 0;
}
}
.configure-body {
2025-03-03 12:45:40 +01:00
margin-top: 20px;
margin-bottom: 10px;
display: flex;
}
.configure-menu-item {
display: block;
white-space: nowrap;
cursor: pointer;
2025-02-22 16:29:28 +01:00
padding: 4px 60px 4px 4px;
}
.configure-menu-item > a,
.configure-menu-item > span {
display: block;
color: var(--pankow-text-color);
}
.configure-menu-item[active] > a,
.configure-menu-item[active] > span {
color: var(--pankow-color-primary-active);
font-weight: bold;
}
.configure-menu-item:hover > a,
.configure-menu-item:hover > span {
color: var(--pankow-color-primary-hover);
}
.configure-menu-item[disabled] > a,
.configure-menu-item[disabled] > span {
color: gray;
cursor: not-allowed;
}
.configure-content {
flex-grow: 1;
}
2025-02-21 23:20:02 +01:00
</style>