Files
cloudron-box/dashboard/src/Index.vue

410 lines
17 KiB
Vue
Raw Normal View History

2025-02-03 19:14:01 +01:00
<script setup>
2024-11-01 14:16:09 +01:00
import { onMounted, ref, useTemplateRef } from 'vue';
2025-03-23 14:29:42 +01:00
import { Notification, fetcher, SideBar } from 'pankow';
2025-03-17 16:53:53 +01:00
import { API_ORIGIN, TOKEN_TYPES } from './constants.js';
import { redirectIfNeeded } from './utils.js';
2025-03-16 11:12:49 +01:00
import ProfileModel from './models/ProfileModel.js';
import ProvisionModel from './models/ProvisionModel.js';
2025-03-16 11:12:49 +01:00
import DashboardModel from './models/DashboardModel.js';
2025-04-07 15:48:43 +02:00
import BrandingModel from './models/BrandingModel.js';
2025-03-16 11:12:49 +01:00
import Headerbar from './components/Headerbar.vue';
import OfflineOverlay from './components/OfflineOverlay.vue';
2025-01-21 17:08:09 +01:00
import AppsView from './views/AppsView.vue';
2025-02-20 10:54:43 +01:00
import AppConfigureView from './views/AppConfigureView.vue';
2025-01-21 17:08:09 +01:00
import AppstoreView from './views/AppstoreView.vue';
import BackupsView from './views/BackupsView.vue';
2025-02-10 18:42:02 +01:00
import BrandingView from './views/BrandingView.vue';
import DomainsView from './views/DomainsView.vue';
import EmailDomainView from './views/EmailDomainView.vue';
2025-04-16 17:59:03 +02:00
import EmailMailboxesView from './views/EmailMailboxesView.vue';
import EmailMailinglistsView from './views/EmailMailinglistsView.vue';
import EmailSettingsView from './views/EmailSettingsView.vue';
import EmailEventlogView from './views/EmailEventlogView.vue';
import EmailStatusView from './views/EmailStatusView.vue';
2025-01-25 17:09:53 +01:00
import EventlogView from './views/EventlogView.vue';
2025-01-22 14:46:31 +01:00
import NetworkView from './views/NetworkView.vue';
2025-01-21 17:08:09 +01:00
import ProfileView from './views/ProfileView.vue';
import ServicesView from './views/ServicesView.vue';
2025-01-23 18:36:30 +01:00
import SettingsView from './views/SettingsView.vue';
2025-01-21 17:08:09 +01:00
import SupportView from './views/SupportView.vue';
2025-02-17 11:18:57 +01:00
import SystemView from './views/SystemView.vue';
2025-01-21 17:08:09 +01:00
import UserDirectoryView from './views/UserDirectoryView.vue';
2025-02-11 18:50:10 +01:00
import UsersView from './views/UsersView.vue';
2025-01-21 17:08:09 +01:00
import VolumesView from './views/VolumesView.vue';
2024-11-01 14:16:09 +01:00
const VIEWS = {
2024-12-29 00:36:48 +01:00
APPS: 'apps',
2025-02-20 10:54:43 +01:00
APP: 'app',
2025-01-05 22:47:50 +01:00
APPSTORE: 'appstore',
BACKUPS: 'backups',
2025-02-10 18:42:02 +01:00
BRANDING: 'branding',
DOMAINS: 'domains',
EMAIL_DOMAIN: 'email-domain',
2025-04-16 17:59:03 +02:00
EMAIL_MAILBOXES: 'email-mailboxes',
EMAIL_MAILINGLISTS: 'email-mailinglists',
EMAIL_SETTINGS: 'email-settings',
EMAIL_EVENTLOG: 'email-eventlog',
EMAIL_STATUS: 'email-status',
2025-01-25 17:09:53 +01:00
EVENTLOG: 'eventlog',
2025-01-22 14:46:31 +01:00
NETWORK: 'network',
2025-01-14 10:27:27 +01:00
PROFILE: 'profile',
2025-01-21 16:54:56 +01:00
SERVICES: 'services',
2025-01-23 18:36:30 +01:00
SETTINGS: 'settings',
SUPPORT: 'support',
2025-02-17 11:18:57 +01:00
SYSTEM: 'system',
USER_DIRECTORY: 'user-directory',
2025-02-11 18:50:10 +01:00
USERS: 'users',
2024-12-26 12:19:48 +01:00
VOLUMES: 'volumes',
2024-11-01 14:16:09 +01:00
};
const offlineOverlay = useTemplateRef('offlineOverlay');
function onOnline() {
ready.value = true;
}
fetcher.globalOptions.errorHook = (error) => {
// network error, request killed by browser
if (error instanceof TypeError) {
ready.value = false;
return offlineOverlay.value.open();
}
2025-03-25 18:05:29 +01:00
// re-login will make the code get a new token
if (error.status === 401) return profileModel.logout();
if (error.status === 500 || error.status === 501) {
// actual internal server error, most likely a bug or timeout log to console only to not alert the user
if (!ready.value) {
console.error(error);
console.log('------\nCloudron Internal Error\n\nIf you see this, please send a mail with above log to support@cloudron.io\n------\n');
}
}
2025-03-22 20:53:40 +01:00
if (error.status >= 502) {
// This means the box service is not reachable. We just show offline banner for now
ready.value = false;
return offlineOverlay.value.open();
}
};
2025-03-16 11:12:49 +01:00
const dashboardModel = DashboardModel.create();
const profileModel = ProfileModel.create();
const provisionModel = ProvisionModel.create();
2025-03-16 11:12:49 +01:00
2025-03-23 14:29:42 +01:00
const sidebar = useTemplateRef('sidebar');
const ready = ref(false);
2025-02-03 19:14:01 +01:00
const view = ref('');
2025-03-16 11:12:49 +01:00
const profile = ref({});
const subscription = ref({
plan: {},
});
const config = ref({});
2025-04-07 15:48:43 +02:00
const avatarUrl = ref('');
2025-01-03 15:06:41 +01:00
2025-03-23 14:29:42 +01:00
function onSidebarClose() {
sidebar.value.close();
}
const activeSidebarItem = ref('');
const activeSidebarGroup = ref('');
function onToggleGroup(group) {
activeSidebarGroup.value = activeSidebarGroup.value === group ? '' : group;
}
2025-02-03 19:14:01 +01:00
function onHashChange() {
const v = location.hash.slice(2);
2024-11-01 14:16:09 +01:00
2025-03-23 14:29:42 +01:00
activeSidebarItem.value = v;
if (activeSidebarItem.value.indexOf('email') === 0) activeSidebarGroup.value = 'email';
else activeSidebarGroup.value = '';
2025-02-03 19:14:01 +01:00
if (v === VIEWS.APPS) {
view.value = VIEWS.APPS;
} else if (v.indexOf(VIEWS.APPSTORE) === 0 && profile.value.isAtLeastAdmin) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.APPSTORE;
} else if (v.indexOf(VIEWS.APP+'/') === 0) { // this checks permissions within the view as we may have an app operator
2025-02-20 10:54:43 +01:00
view.value = VIEWS.APP;
} else if (v === VIEWS.BACKUPS && profile.value.isAtLeastAdmin) {
view.value = VIEWS.BACKUPS;
} else if (v === VIEWS.BRANDING && profile.value.isAtLeastAdmin) {
2025-02-10 18:42:02 +01:00
view.value = VIEWS.BRANDING;
} else if (v === VIEWS.DOMAINS && profile.value.isAtLeastAdmin) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.DOMAINS;
2025-04-16 17:59:03 +02:00
} else if (v === VIEWS.EMAIL_DOMAIN && profile.value.isAtLeastMailManager) {
view.value = VIEWS.EMAIL_DOMAIN;
2025-04-16 17:59:03 +02:00
} else if (v === VIEWS.EMAIL_MAILBOXES && profile.value.isAtLeastMailManager) {
view.value = VIEWS.EMAIL_MAILBOXES;
} else if (v === VIEWS.EMAIL_MAILINGLISTS && profile.value.isAtLeastMailManager) {
view.value = VIEWS.EMAIL_MAILINGLISTS;
} else if (v === VIEWS.EMAIL_SETTINGS && profile.value.isAtLeastMailManager) {
view.value = VIEWS.EMAIL_SETTINGS;
} else if (v === VIEWS.EMAIL_EVENTLOG && profile.value.isAtLeastMailManager) {
view.value = VIEWS.EMAIL_EVENTLOG;
} else if (v === VIEWS.EMAIL_STATUS && profile.value.isAtLeastMailManager) {
view.value = VIEWS.EMAIL_STATUS;
} else if (v === VIEWS.EVENTLOG && profile.value.isAtLeastAdmin) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.EVENTLOG;
} else if (v === VIEWS.NETWORK && profile.value.isAtLeastAdmin) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.NETWORK;
2025-03-25 18:05:29 +01:00
} else if (v.indexOf(VIEWS.PROFILE) === 0) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.PROFILE;
} else if (v === VIEWS.SERVICES && profile.value.isAtLeastAdmin) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.SERVICES;
} else if (v === VIEWS.SETTINGS && profile.value.isAtLeastAdmin) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.SETTINGS;
} else if (v === VIEWS.SUPPORT && profile.value.isAtLeastOwner) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.SUPPORT;
} else if (v === VIEWS.SYSTEM && profile.value.isAtLeastAdmin) {
2025-02-17 11:18:57 +01:00
view.value = VIEWS.SYSTEM;
} else if (v === VIEWS.USER_DIRECTORY && profile.value.isAtLeastAdmin) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.USER_DIRECTORY;
} else if (v === VIEWS.USERS && profile.value.isAtLeastUserManager) {
2025-02-11 18:50:10 +01:00
view.value = VIEWS.USERS;
} else if (v === VIEWS.VOLUMES && profile.value.isAtLeastAdmin) {
2025-02-03 19:14:01 +01:00
view.value = VIEWS.VOLUMES;
} else {
2025-03-16 11:12:49 +01:00
window.location.hash = '/' + VIEWS.APPS;
2025-02-03 19:14:01 +01:00
}
}
2024-11-01 14:16:09 +01:00
2025-04-07 15:48:43 +02:00
BrandingModel.onChange(BrandingModel.KEYS.NAME, (value) => {
window.document.title = value;
config.value.cloudronName = value;
});
BrandingModel.onChange(BrandingModel.KEYS.AVATAR, (value) => {
avatarUrl.value = value;
document.getElementById('favicon').href = value;
});
ProfileModel.onChange(ProfileModel.KEYS.AVATAR, (value) => {
profile.value.avatarUrl = value;
});
2025-02-03 19:14:01 +01:00
onMounted(async () => {
let [error, result] = await provisionModel.status();
if (error) return console.error(error);
if (redirectIfNeeded(result, 'dashboard')) return; // redirected to some other view...
2025-02-03 19:14:01 +01:00
if (!localStorage.token) {
2025-03-17 16:53:53 +01:00
localStorage.setItem('redirectToHash', window.location.hash);
// start oidc flow
window.location.href = `${API_ORIGIN}/openid/auth?client_id=` + (API_ORIGIN ? TOKEN_TYPES.ID_DEVELOPMENT : TOKEN_TYPES.ID_WEBADMIN) + '&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
2025-02-03 19:14:01 +01:00
return;
2024-11-01 14:16:09 +01:00
}
2025-02-03 19:14:01 +01:00
[error, result] = await profileModel.get();
2025-03-16 11:12:49 +01:00
if (error) return console.error(error);
profile.value = result;
[error, result] = await dashboardModel.config();
if (error) return console.error(error);
config.value = result;
2025-04-07 15:48:43 +02:00
avatarUrl.value = `https://${config.value.adminFqdn}/api/v1/cloudron/avatar`;
2025-03-16 11:12:49 +01:00
window.document.title = result.cloudronName;
document.getElementById('favicon').href = `${API_ORIGIN}/api/v1/cloudron/avatar?ts=${Date.now()}`;
2025-03-25 18:05:29 +01:00
if (config.value.mandatory2FA && !profile.value.twoFactorAuthenticationEnabled) window.location.hash = `/${VIEWS.PROFILE}?setup2fa`;
window.addEventListener('hashchange', onHashChange);
onHashChange();
ready.value = true;
2025-02-03 19:14:01 +01:00
});
2024-11-01 14:16:09 +01:00
</script>
<template>
2025-03-16 11:12:49 +01:00
<div style="overflow: hidden; height: 100%;">
<Notification />
<OfflineOverlay ref="offlineOverlay" @online="onOnline()"/>
<Transition name="pankow-animation-pop-up">
<div v-if="ready" style="display: flex; flex-direction: row; overflow: hidden; height: 100%;">
2025-03-23 14:29:42 +01:00
<!-- <Sidebar :profile="profile" :config="config"/> -->
<SideBar v-if="profile.isAtLeastUserManager" ref="sidebar">
<a href="#/" class="sidebar-logo" @click="onSidebarClose()">
2025-04-07 15:48:43 +02:00
<img :src="avatarUrl" width="40" height="40"/> {{ config.cloudronName || 'Cloudron' }}
2025-03-23 14:29:42 +01:00
</a>
<div class="sidebar-list">
<!-- TODO arrow indicator for submenu -->
2025-03-23 14:29:42 +01:00
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'apps' }" href="#/apps" @click="onSidebarClose()"><i class="fa fa-grip fa-fw"></i> {{ $t('apps.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'appstore' }" v-show="profile.isAtLeastAdmin" href="#/appstore" @click="onSidebarClose()"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ $t('appstore.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'users' }" v-show="profile.isAtLeastUserManager" href="#/users" @click="onSidebarClose()"><i class="fa fa-users fa-fw"></i> {{ $t('main.navbar.users') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'backups' }" v-show="profile.isAtLeastAdmin" href="#/backups" @click="onSidebarClose()"><i class="fa fa-archive fa-fw"></i> {{ $t('backups.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'branding' }" v-show="profile.isAtLeastAdmin" href="#/branding" @click="onSidebarClose()"><i class="fa fa-passport fa-fw"></i> {{ $t('branding.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'domains' }" v-show="profile.isAtLeastAdmin" href="#/domains" @click="onSidebarClose()"><i class="fa fa-globe fa-fw"></i> {{ $t('domains.title') }}</a>
2025-04-08 14:23:54 +02:00
<div class="sidebar-item" v-show="profile.isAtLeastMailManager" @click="onToggleGroup('email')"><i class="fa fa-envelope fa-fw"></i> {{ $t('emails.title') }}</div>
2025-03-23 14:29:42 +01:00
<Transition name="sidebar-item-group-animation">
<div class="sidebar-item-group" v-if="activeSidebarGroup === 'email'">
<!-- TODO separate the views -->
2025-04-16 17:59:03 +02:00
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'email-domain' }" href="#/email-domain" @click="onSidebarClose()"><i class="fa fa-fw fa-wrench"></i> Domains</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'email-mailboxes' }" href="#/email-mailboxes" @click="onSidebarClose()"><i class="fa fa-fw fa-inbox"></i> {{ $t('email.incoming.mailboxes.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'email-mailinglists' }" href="#/email-mailinglists" @click="onSidebarClose()"><i class="fa fa-fw-solid fa-envelopes-bulk"></i> {{ $t('email.incoming.mailinglists.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'email-eventlog' }" href="#/email-eventlog" @click="onSidebarClose()"><i class="fa fa-fw fa-list-alt"></i> Logs</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'email-status' }" href="#/email-status" @click="onSidebarClose()"><i class="fa fa-fw fa-clipboard-check"></i> Status</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'email-settings' }" href="#/email-settings" @click="onSidebarClose()"><i class="fa fa-fw fa-cog"></i> {{ $t('settings.title') }}</a>
2025-03-23 14:29:42 +01:00
</div>
</Transition>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'eventlog' }" v-show="profile.isAtLeastAdmin" href="#/eventlog" @click="onSidebarClose()"><i class="fa fa-list-alt fa-fw"></i> {{ $t('eventlog.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'network' }" v-show="profile.isAtLeastAdmin" href="#/network" @click="onSidebarClose()"><i class="fas fa-network-wired fa-fw"></i> {{ $t('network.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'services' }" v-show="profile.isAtLeastAdmin" href="#/services" @click="onSidebarClose()"><i class="fa fa-cogs fa-fw"></i> {{ $t('services.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'settings' }" v-show="profile.isAtLeastAdmin" href="#/settings" @click="onSidebarClose()"><i class="fa fa-wrench fa-fw"></i> {{ $t('settings.title') }}</a>
<!-- TODO split this in 3, settings, external ldap connector, directory server -->
2025-03-23 14:29:42 +01:00
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'user-directory' }" v-show="profile.isAtLeastAdmin" href="#/user-directory" @click="onSidebarClose()"><i class="fa fa-users-gear fa-fw"></i> {{ $t('users.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'volumes' }" v-show="profile.isAtLeastAdmin" href="#/volumes" @click="onSidebarClose()"><i class="fa fa-hdd fa-fw"></i> {{ $t('volumes.title') }}</a>
<a class="sidebar-item" :class="{ active: activeSidebarItem === 'system' }" v-show="profile.isAtLeastAdmin" href="#/system" @click="onSidebarClose()"><i class="fa fa-chart-area fa-fw"></i> {{ $t('system.title') }}</a>
</div>
</SideBar>
<div style="flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; height: 100%;">
<Headerbar :config="config" :subscription="subscription" :profile="profile"/>
<div style="display: flex; justify-content: center; overflow: auto; flex-grow: 1; padding: 0 6px; position: relative;">
<Transition name="slide-fade">
<AppsView v-if="view === VIEWS.APPS" />
<AppConfigureView v-else-if="view === VIEWS.APP" />
<AppstoreView v-else-if="view === VIEWS.APPSTORE" />
<BackupsView v-else-if="view === VIEWS.BACKUPS" />
<BrandingView v-else-if="view === VIEWS.BRANDING" />
<DomainsView v-else-if="view === VIEWS.DOMAINS" />
<EmailDomainView v-else-if="view === VIEWS.EMAIL_DOMAIN" />
2025-04-16 17:59:03 +02:00
<EmailMailboxesView v-else-if="view === VIEWS.EMAIL_MAILBOXES" />
<EmailMailinglistsView v-else-if="view === VIEWS.EMAIL_MAILINGLISTS" />
<EmailSettingsView v-else-if="view === VIEWS.EMAIL_SETTINGS" />
<EmailEventlogView v-else-if="view === VIEWS.EMAIL_EVENTLOG" />
<EmailStatusView v-else-if="view === VIEWS.EMAIL_STATUS" />
<EventlogView v-else-if="view === VIEWS.EVENTLOG" />
<NetworkView v-else-if="view === VIEWS.NETWORK" />
<ProfileView v-else-if="view === VIEWS.PROFILE" />
<ServicesView v-else-if="view === VIEWS.SERVICES" />
<SettingsView v-else-if="view === VIEWS.SETTINGS" />
<SupportView v-else-if="view === VIEWS.SUPPORT" />
<SystemView v-else-if="view === VIEWS.SYSTEM" />
<UserDirectoryView v-else-if="view === VIEWS.USER_DIRECTORY" />
<UsersView v-else-if="view === VIEWS.USERS" />
<VolumesView v-else-if="view === VIEWS.VOLUMES" />
</Transition>
</div>
2025-03-16 11:12:49 +01:00
</div>
</div>
</Transition>
2025-03-16 11:12:49 +01:00
</div>
</template>
2025-03-23 14:29:42 +01:00
<style scoped>
.pankow-sidebar {
background-color: var(--navbar-background);
border-right: 1px solid var(--pankow-color-background-hover);
padding: 22px 10px 10px 10px;
margin-right: 20px;
}
.sidebar-logo img {
margin-right: 10px;
border-radius: var(--pankow-border-radius);
}
.sidebar-logo,
.sidebar-logo:hover {
display: flex;
align-items: center;
color: var(--pankow-text-color);
text-decoration: none;
padding-left: 10px;
}
.sidebar-list {
overflow: auto;
padding-top: 20px;
scrollbar-color: transparent transparent;
scrollbar-width: thin;
}
.sidebar-list:hover {
scrollbar-color: var(--color-neutral-border) transparent;
}
.sidebar-item {
display: block;
color: var(--pankow-text-color);
border-radius: 3px;
padding: 10px 15px;
2025-03-26 16:46:32 +01:00
white-space: nowrap;
2025-04-08 14:23:54 +02:00
cursor: pointer;
2025-03-23 14:29:42 +01:00
}
.sidebar-item i {
opacity: 0.5;
margin-right: 10px;
}
.sidebar-item.active {
color: var(--pankow-color-primary);
text-decoration: none;
font-weight: bold;
}
.sidebar-item:hover {
background-color: #e9ecef;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
.sidebar-item:hover {
background-color: var(--card-background);
}
}
.sidebar-item.active i ,
.sidebar-item:hover i {
opacity: 1;
}
.sidebar-item-group {
padding-left: 20px;
height: auto;
overflow: hidden;
/* we need height to auto so we animate max-height. needs to be bigger than we need */
max-height: 300px;
}
.sidebar-item-group-animation-enter-active,
.sidebar-item-group-animation-leave-active {
transition: all 0.2s linear;
}
.sidebar-item-group-animation-leave-to,
.sidebar-item-group-animation-enter-from {
transform: translateX(-100px);
opacity: 0;
max-height: 0;
}
.slide-fade-enter-active {
transition: all 0.1s ease-out;
}
.slide-fade-leave-active {
transition: all 0.1s ease-out;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
2025-03-23 14:29:42 +01:00
</style>