Files
cloudron-box/dashboard/src/components/Headerbar.vue
2025-06-10 22:44:48 +02:00

211 lines
6.9 KiB
Vue

<script setup>
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
import { onMounted, ref, useTemplateRef, inject } from 'vue';
import { marked } from 'marked';
import { eachLimit } from 'async';
import { Button, Popover, Icon } from 'pankow';
import { prettyDate, prettyLongDate } from 'pankow/utils';
import NotificationsModel from '../models/NotificationsModel.js';
const props = defineProps(['config', 'profile', 'subscription']);
const helpButton = useTemplateRef('helpButton');
const helpPopover = useTemplateRef('helpPopover');
function onOpenHelp(popover, event, elem) {
popover.open(event, elem);
}
const notificationModel = NotificationsModel.create();
const notificationButton = useTemplateRef('notificationButton');
const notificationPopover = useTemplateRef('notificationPopover');
const notifications = ref([]);
const notificationsAllBusy = ref(false);
function onOpenNotifications(popover, event, elem) {
popover.open(event, elem);
}
async function onMarkNotificationRead(notification) {
notification.busy = true;
const [error] = await notificationModel.update(notification.id, true);
if (error) return console.error(error);
await refresh();
}
async function onMarkAllNotificationRead() {
notificationsAllBusy.value = true;
await eachLimit(notifications.value, 2, async (notification) => {
notification.busy = true;
const [error] = await notificationModel.update(notification.id, true);
if (error) return console.error(error);
});
await refresh();
notificationsAllBusy.value = false;
}
async function refresh() {
const [error, result] = await notificationModel.list();
if (error) return console.error(error);
result.forEach(n => {
n.isCollapsed = true;
n.busy = false;
});
notifications.value = result;
}
const subscriptionRequiredDialog = inject('subscriptionRequiredDialog');
function onSubscriptionRequired() {
subscriptionRequiredDialog.value.open();
}
onMounted(async () => {
if (props.profile.isAtLeastAdmin) await refresh();
});
const description = marked.parse(t('support.help.description', {
docsLink: 'https://docs.cloudron.io/?support_view',
packagingLink: 'https://docs.cloudron.io/packaging/tutorial/?support_view',
forumLink: 'https://forum.cloudron.io/'
}));
</script>
<template>
<div class="headerbar">
<Popover ref="notificationPopover" :width="'min(80%, 400px)'" :height="'min(80%, 600px)'">
<div style="padding: 10px; display: flex; flex-direction: column; overflow: hidden; height: 100%;">
<div v-if="notifications.length" style="overflow: auto; margin-bottom: 10px">
<div class="notification-item" v-for="notification in notifications" :key="notification.id">
<div class="notification-item-title" @click="notification.isCollapsed = !notification.isCollapsed">
<div>
{{ notification.title }}<br/>
<span class="notification-item-date" v-tooltip="prettyLongDate(notification.creationTime)">{{ prettyDate(notification.creationTime) }}</span>
</div>
<Button plain small tool :loading="notification.busy" :disabled="notification.busy" class="notification-read-button" @click.stop="onMarkNotificationRead(notification)">{{ $t('notifications.dismissTooltip') }}</Button>
</div>
<div class="notification-item-body" v-if="!notification.isCollapsed">
<pre v-if="notification.messageJson" style="cursor: auto">{{ JSON.stringify(notification.messageJson, null, 4) }}</pre>
<div v-else style="cursor: auto; overflow: auto;" v-html="marked.parse(notification.message)"></div>
</div>
</div>
</div>
<Button v-if="notifications.length" @click="onMarkAllNotificationRead()" :loading="notificationsAllBusy" :disabled="notificationsAllBusy">{{ $t('notifications.markAllAsRead') }}</Button>
<div v-if="notifications.length === 0" class="notification-item-empty-placeholder">
{{ $t('notifications.allCaughtUp') }}
</div>
</div>
</Popover>
<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">
<a href="#/" class="headerbar-action">
<img :src="`https://${config.adminFqdn}/api/v1/cloudron/avatar`"/> {{ config.cloudronName || 'Cloudron' }}
</a>
</div>
<div style="flex-grow: 1;"></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>
<div class="headerbar-action" style="gap: 6px" v-if="profile.isAtLeastAdmin" ref="notificationButton" @click="onOpenNotifications(notificationPopover, $event, notificationButton)"><Icon :icon="notifications.length ? 'fas fa-bell' : 'far fa-bell'"/> {{ notifications.length > 99 ? '99+' : notifications.length }}</div>
<div class="headerbar-action" style="gap: 6px" v-if="profile.isAtLeastAdmin" ref="helpButton" @click="onOpenHelp(helpPopover, $event, helpButton)"><Icon icon="fa fa-question"/></div>
<!-- <a class="headerbar-action" v-if="profile.isAtLeastAdmin" href="#/support"><Icon icon="fa fa-question"/></a> -->
<a class="headerbar-action" href="#/profile"><img :src="profile.avatarUrl" @error="event => event.target.src = '/img/avatar-default-symbolic.svg'"/> {{ profile.username }}</a>
</div>
</template>
<style scoped>
.headerbar {
display: flex;
padding: 15px;
align-items: center;
gap: 15px;
}
.headerbar-action {
display: flex;
align-items: center;
cursor: pointer;
color: var(--pankow-text-color);
padding: 4px 15px;
}
.headerbar-action img {
margin-right: 10px;
width: 40px;
height: 40px;
border-radius: var(--pankow-border-radius);
}
.headerbar-action:hover {
color: var(--pankow-color-primary-hover);
}
.help-title {
font-size: 20px;
padding-bottom: 10px;
margin: 0;
border-bottom: 1px solid var(--pankow-input-border-color);
}
.notification-item {
margin-bottom: 10px;
padding-bottom: 10px;
cursor: pointer;
border-bottom: 1px solid var(--pankow-input-border-color);
position: relative;
}
.notification-item-title {
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
}
.notification-item-date {
font-size: 10px;
}
.notification-read-button {
visibility: hidden;
margin-right: 10px;
}
.notification-item:hover .notification-read-button {
visibility: visible;
}
.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);
}
</style>