Files
cloudron-box/dashboard/src/components/Headerbar.vue
T

201 lines
5.8 KiB
Vue
Raw Normal View History

2025-03-16 11:12:49 +01:00
<script setup>
2025-05-06 17:24:08 +02:00
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
2025-07-01 11:44:02 +02:00
import { onMounted, onUnmounted, ref, useTemplateRef, inject } from 'vue';
2025-03-18 16:53:15 +01:00
import { marked } from 'marked';
2026-01-22 18:55:50 +01:00
import { Menu, Popover, Icon, InputDialog, Spinner } from '@cloudron/pankow';
2025-03-18 16:53:15 +01:00
import NotificationsModel from '../models/NotificationsModel.js';
2025-07-01 11:44:02 +02:00
import ServicesModel from '../models/ServicesModel.js';
2026-01-13 22:49:00 +01:00
import ProfileModel from '../models/ProfileModel.js';
2025-03-18 16:53:15 +01:00
2025-11-28 14:50:18 +01:00
defineProps(['config', 'subscription']);
const profile = inject('profile');
2025-03-16 11:12:49 +01:00
2025-05-06 17:24:08 +02:00
const helpButton = useTemplateRef('helpButton');
const helpPopover = useTemplateRef('helpPopover');
function onOpenHelp(popover, event, elem) {
popover.open(event, elem);
}
2025-07-01 11:44:02 +02:00
const servicesModel = ServicesModel.create();
2026-01-13 22:49:00 +01:00
const profileModel = ProfileModel.create();
2025-07-01 11:44:02 +02:00
2025-03-18 16:53:15 +01:00
const notificationModel = NotificationsModel.create();
const notifications = ref([]);
async function refresh() {
2026-01-22 18:55:50 +01:00
const [error, result] = await notificationModel.list(false);
if (error) return console.error(error);
2025-03-18 16:53:15 +01:00
result.forEach(n => {
n.isCollapsed = true;
n.busy = false;
});
notifications.value = result;
}
const subscriptionRequiredDialog = inject('subscriptionRequiredDialog');
function onSubscriptionRequired() {
subscriptionRequiredDialog.value.open();
}
2025-07-01 11:44:02 +02:00
const platformStatus = ref({
message: '',
state: '',
2025-03-18 16:53:15 +01:00
});
2025-07-01 11:44:02 +02:00
let platformTimeoutId = 0;
async function trackPlatformStatus() {
const [error, result] = await servicesModel.getPlatformStatus();
if (error) return console.error('Failed to get platform status.', error);
2025-07-01 11:44:02 +02:00
platformStatus.value = result;
if (result.state === 'starting') platformTimeoutId = setTimeout(trackPlatformStatus, 5000);
2025-07-01 11:44:02 +02:00
}
2025-11-28 14:50:18 +01:00
const inputDialog = useTemplateRef('inputDialog');
function onShowPlatformError() {
inputDialog.value.info({
confirmLabel: t('main.dialog.close'),
title: t('main.platform.startupFailed'),
message: platformStatus.value.message,
});
}
2025-05-06 17:24:08 +02:00
const description = marked.parse(t('support.help.description', {
2025-09-08 16:02:46 +02:00
docsLink: 'https://docs.cloudron.io',
packagingLink: 'https://docs.cloudron.io/packaging/tutorial',
forumLink: 'https://forum.cloudron.io',
apiLink: 'https://docs.cloudron.io/api.html'
2025-05-06 17:24:08 +02:00
}));
2026-01-13 22:49:00 +01:00
const avatarActions = [{//
icon: 'fa-solid fa-circle-user',
label: t('profile.title'),
action: () => { window.location.href = '#/profile'; }
}, {
separator: true,
}, {
icon: 'fa-solid fa-right-from-bracket',
label: t('main.logout'),
action: () => { profileModel.logout(); }
}];
const avatarMenu = useTemplateRef('avatarMenu');
function onAvatarClick(event) {
avatarMenu.value.open(event, event.currentTarget);
}
2025-07-01 11:44:02 +02:00
onMounted(async () => {
2025-09-25 08:25:36 +02:00
if (profile.value.isAtLeastAdmin) await refresh();
2025-07-01 11:44:02 +02:00
await trackPlatformStatus();
});
onUnmounted(() => {
clearTimeout(platformTimeoutId);
});
2025-03-16 11:12:49 +01:00
</script>
<template>
<div class="headerbar">
2025-11-28 14:50:18 +01:00
<InputDialog ref="inputDialog"/>
2026-01-13 22:49:00 +01:00
<Menu ref="avatarMenu" :model="avatarActions" />
2025-05-06 17:24:08 +02:00
<Popover ref="helpPopover" :width="'min(80%, 400px)'" :height="'min(80%, 600px)'">
<div style="padding: 10px; display: flex; flex-direction: column; overflow: hidden; height: 100%;">
<h1 class="help-title">{{ $t('support.help.title') }}</h1>
<div v-html="description"></div>
</div>
</Popover>
<div v-if="!profile.isAtLeastUserManager">
2025-03-26 11:30:45 +01:00
<a href="#/" class="headerbar-action">
<img :src="`https://${config.adminFqdn}/api/v1/cloudron/avatar`"/> {{ config.cloudronName || 'Cloudron' }}
</a>
</div>
2025-03-16 11:12:49 +01:00
<div style="flex-grow: 1;"></div>
2025-11-28 14:50:18 +01:00
<div v-if="platformStatus.state === 'starting'" class="headerbar-info">
<Spinner style="margin-right: 10px"/>{{ platformStatus.message }}
</div>
<div v-else-if="platformStatus.state === 'failed'" class="headerbar-info text-danger" style="cursor: pointer" @click="onShowPlatformError">
<Icon :icon="'fa fa-exclamation-triangle'"/> {{ $t('main.platform.startupFailed') }}
2025-07-01 11:44:02 +02:00
</div>
<!-- Warnings if subscription is expired or unpaid -->
<div v-if="profile.isAtLeastOwner && subscription.plan.id === 'expired'" class="headerbar-action subscription-expired" style="gap: 6px" @click="onSubscriptionRequired()">Subscription Expired</div>
2026-01-22 18:55:50 +01:00
<a class="headerbar-action" v-if="profile.isAtLeastAdmin" href="/#/notifications"><Icon :icon="notifications.length ? 'fas fa-bell' : 'far fa-bell'"/> {{ notifications.length > 99 ? '99+' : notifications.length }}</a>
2025-11-28 14:50:18 +01:00
<div class="headerbar-action pankow-no-mobile" v-if="profile.isAtLeastAdmin" ref="helpButton" @click="onOpenHelp(helpPopover, $event, helpButton)"><Icon icon="fa fa-question"/></div>
2025-05-06 17:24:08 +02:00
<!-- <a class="headerbar-action" v-if="profile.isAtLeastAdmin" href="#/support"><Icon icon="fa fa-question"/></a> -->
2026-01-13 22:49:00 +01:00
<a class="headerbar-action" @click.capture="onAvatarClick($event)"><img :src="profile.avatarUrl" @error="event => event.target.src = '/img/avatar-default-symbolic.svg'"/> {{ profile.username }}</a>
2025-03-16 11:12:49 +01:00
</div>
</template>
<style scoped>
.headerbar {
display: flex;
2025-03-26 11:30:45 +01:00
padding: 15px;
2025-03-16 11:12:49 +01:00
align-items: center;
gap: 15px;
}
2025-07-09 13:52:30 +02:00
.headerbar-info {
display: flex;
2025-11-28 14:50:18 +01:00
gap: 6px;
2025-07-09 13:52:30 +02:00
align-items: center;
padding: 4px 15px;
}
2025-03-26 11:30:45 +01:00
.headerbar-action {
2025-03-16 11:12:49 +01:00
display: flex;
2025-11-28 14:50:18 +01:00
gap: 6px;
2025-03-16 11:12:49 +01:00
align-items: center;
2025-03-26 11:30:45 +01:00
cursor: pointer;
color: var(--pankow-text-color);
padding: 4px 15px;
2025-03-16 11:12:49 +01:00
}
2025-03-26 11:30:45 +01:00
.headerbar-action img {
margin-right: 10px;
2025-03-26 11:30:45 +01:00
width: 40px;
height: 40px;
border-radius: var(--pankow-border-radius);
}
2025-03-26 11:30:45 +01:00
.headerbar-action:hover {
color: var(--pankow-color-primary-hover);
}
2025-05-06 17:24:08 +02:00
.help-title {
font-size: 20px;
padding-bottom: 10px;
margin: 0;
border-bottom: 1px solid var(--pankow-input-border-color);
}
.subscription-expired {
background-color: var(--pankow-color-danger);
color: white;
border-radius: 20px;
}
.subscription-expired:hover {
color: white;
background-color: var(--pankow-color-danger-hover);
}
2025-03-16 11:12:49 +01:00
</style>