
* fix spacing between header and page desc Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix icon alignment Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fallback text when we dont have a user yet Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
427 lines
14 KiB
TypeScript
427 lines
14 KiB
TypeScript
import { EVENT_WS_MESSAGE } from "#common/constants";
|
|
import { globalAK } from "#common/global";
|
|
import { UIConfig, UserDisplay, getConfigForUser } from "#common/ui/config";
|
|
import { me } from "#common/users";
|
|
import "#components/ak-nav-buttons";
|
|
import type { PageHeaderInit, SidebarToggleEventDetail } from "#components/ak-page-header";
|
|
import { AKElement } from "#elements/Base";
|
|
import { WithBrandConfig } from "#elements/mixins/branding";
|
|
import { isAdminRoute } from "#elements/router/utils";
|
|
import { themeImage } from "#elements/utils/images";
|
|
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
|
|
|
import { msg } from "@lit/localize";
|
|
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
|
import { customElement, property, state } from "lit/decorators.js";
|
|
|
|
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
|
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
|
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
|
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
|
|
import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css";
|
|
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|
|
|
import { SessionUser } from "@goauthentik/api";
|
|
|
|
/**
|
|
* A global navbar component at the top of the page.
|
|
*
|
|
* Internally, this component listens for the `ak-page-header` event, which is
|
|
* dispatched by the `ak-page-header` component.
|
|
*/
|
|
@customElement("ak-page-navbar")
|
|
export class AKPageNavbar
|
|
extends WithBrandConfig(AKElement)
|
|
implements PageHeaderInit, SidebarToggleEventDetail
|
|
{
|
|
//#region Static Properties
|
|
|
|
private static elementRef: AKPageNavbar | null = null;
|
|
|
|
static readonly setNavbarDetails = (detail: Partial<PageHeaderInit>): void => {
|
|
const { elementRef } = AKPageNavbar;
|
|
if (!elementRef) {
|
|
console.debug(
|
|
`ak-page-header: Could not find ak-page-navbar, skipping event dispatch.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const { header, description, icon, iconImage } = detail;
|
|
|
|
elementRef.header = header;
|
|
elementRef.description = description;
|
|
elementRef.icon = icon;
|
|
elementRef.iconImage = iconImage || false;
|
|
elementRef.hasIcon = !!icon;
|
|
};
|
|
|
|
static get styles(): CSSResult[] {
|
|
return [
|
|
PFBase,
|
|
PFButton,
|
|
PFPage,
|
|
PFDrawer,
|
|
|
|
PFNotificationBadge,
|
|
PFContent,
|
|
PFAvatar,
|
|
PFDropdown,
|
|
css`
|
|
:host {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: var(--pf-global--ZIndex--lg);
|
|
--pf-c-page__header-tools--MarginRight: 0;
|
|
--ak-brand-logo-height: var(--pf-global--FontSize--4xl, 2.25rem);
|
|
--ak-brand-background-color: var(
|
|
--pf-c-page__sidebar--m-light--BackgroundColor
|
|
);
|
|
--host-navbar-height: var(--ak-c-page-header--height, 7.5rem);
|
|
}
|
|
|
|
:host([theme="dark"]) {
|
|
--ak-brand-background-color: var(--pf-c-page__sidebar--BackgroundColor);
|
|
--pf-c-page__sidebar--BackgroundColor: var(--ak-dark-background-light);
|
|
color: var(--ak-dark-foreground);
|
|
}
|
|
|
|
navbar {
|
|
border-bottom: var(--pf-global--BorderWidth--sm);
|
|
border-bottom-style: solid;
|
|
border-bottom-color: var(--pf-global--BorderColor--100);
|
|
background-color: var(--pf-c-page--BackgroundColor);
|
|
|
|
display: flex;
|
|
flex-direction: row;
|
|
|
|
display: grid;
|
|
column-gap: var(--pf-global--spacer--sm);
|
|
grid-template-columns: [brand] auto [toggle] auto [primary] 1fr [secondary] auto;
|
|
grid-template-rows: auto auto;
|
|
grid-template-areas:
|
|
"brand toggle primary secondary"
|
|
"brand toggle description secondary";
|
|
|
|
@media (min-width: 769px) {
|
|
height: var(--host-navbar-height);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
row-gap: var(--pf-global--spacer--xs);
|
|
|
|
align-items: center;
|
|
grid-template-areas:
|
|
"toggle primary secondary"
|
|
"toggle description description";
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
.items {
|
|
display: block;
|
|
|
|
&.primary {
|
|
grid-column: primary;
|
|
grid-row: primary / description;
|
|
|
|
align-self: center;
|
|
padding-block: var(--pf-global--spacer--md);
|
|
|
|
@media (max-width: 768px) {
|
|
padding-block: var(--pf-global--spacer--sm);
|
|
}
|
|
|
|
&.block-sibling {
|
|
align-self: end;
|
|
}
|
|
|
|
@media (min-width: 426px) {
|
|
&.block-sibling {
|
|
padding-block-end: 0;
|
|
grid-row: primary;
|
|
}
|
|
}
|
|
|
|
.accent-icon {
|
|
height: 1.2em;
|
|
width: 1em;
|
|
|
|
@media (max-width: 768px) {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.page-description {
|
|
padding-top: 0.3em;
|
|
grid-area: description;
|
|
margin-block-end: var(--pf-global--spacer--md);
|
|
|
|
display: box;
|
|
display: -webkit-box;
|
|
line-clamp: 2;
|
|
-webkit-line-clamp: 2;
|
|
box-orient: vertical;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
|
|
@media (max-width: 425px) {
|
|
display: none;
|
|
}
|
|
|
|
@media (min-width: 769px) {
|
|
text-wrap: balance;
|
|
}
|
|
}
|
|
|
|
&.secondary {
|
|
grid-area: secondary;
|
|
flex: 0 0 auto;
|
|
justify-self: end;
|
|
padding-block: var(--pf-global--spacer--sm);
|
|
padding-inline-end: var(--pf-global--spacer--sm);
|
|
|
|
@media (min-width: 769px) {
|
|
align-content: center;
|
|
padding-block: var(--pf-global--spacer--md);
|
|
padding-inline-end: var(--pf-global--spacer--xl);
|
|
}
|
|
}
|
|
}
|
|
|
|
.brand {
|
|
grid-area: brand;
|
|
background-color: var(--ak-brand-background-color);
|
|
height: 100%;
|
|
width: var(--pf-c-page__sidebar--Width);
|
|
align-items: center;
|
|
padding-inline: var(--pf-global--spacer--sm);
|
|
|
|
display: flex;
|
|
justify-content: center;
|
|
|
|
&.pf-m-collapsed {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 1199px) {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.sidebar-trigger {
|
|
grid-area: toggle;
|
|
height: 100%;
|
|
}
|
|
|
|
.logo {
|
|
flex: 0 0 auto;
|
|
height: var(--ak-brand-logo-height);
|
|
|
|
& img {
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
.sidebar-trigger,
|
|
.notification-trigger {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.notification-trigger.has-notifications {
|
|
color: var(--pf-global--active-color--100);
|
|
}
|
|
|
|
.pf-c-content .page-title {
|
|
display: box;
|
|
display: -webkit-box;
|
|
line-clamp: 2;
|
|
-webkit-line-clamp: 2;
|
|
box-orient: vertical;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
h1 {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center !important;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Properties
|
|
|
|
@state()
|
|
icon?: string;
|
|
|
|
@state()
|
|
iconImage = false;
|
|
|
|
@state()
|
|
header?: string;
|
|
|
|
@state()
|
|
description?: string;
|
|
|
|
@state()
|
|
hasIcon = true;
|
|
|
|
@property({
|
|
type: Boolean,
|
|
})
|
|
public open?: boolean;
|
|
|
|
@state()
|
|
protected session?: SessionUser;
|
|
|
|
@state()
|
|
protected uiConfig!: UIConfig;
|
|
|
|
//#endregion
|
|
|
|
//#region Private Methods
|
|
|
|
#setTitle(header?: string) {
|
|
let title = this.brandingTitle;
|
|
|
|
if (isAdminRoute()) {
|
|
title = `${msg("Admin")} - ${title}`;
|
|
}
|
|
// Prepend the header to the title
|
|
if (header) {
|
|
title = `${header} - ${title}`;
|
|
}
|
|
document.title = title;
|
|
}
|
|
|
|
#toggleSidebar() {
|
|
this.open = !this.open;
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent<SidebarToggleEventDetail>("sidebar-toggle", {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: { open: this.open },
|
|
}),
|
|
);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Lifecycle
|
|
|
|
public connectedCallback(): void {
|
|
super.connectedCallback();
|
|
AKPageNavbar.elementRef = this;
|
|
|
|
window.addEventListener(EVENT_WS_MESSAGE, () => {
|
|
this.firstUpdated();
|
|
});
|
|
}
|
|
|
|
public disconnectedCallback(): void {
|
|
super.disconnectedCallback();
|
|
AKPageNavbar.elementRef = null;
|
|
}
|
|
|
|
public async firstUpdated() {
|
|
this.session = await me();
|
|
this.uiConfig = getConfigForUser(this.session.user);
|
|
this.uiConfig.navbar.userDisplay = UserDisplay.none;
|
|
}
|
|
|
|
willUpdate() {
|
|
// Always update title, even if there's no header value set,
|
|
// as in that case we still need to return to the generic title
|
|
this.#setTitle(this.header);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Render
|
|
|
|
renderIcon() {
|
|
if (this.icon) {
|
|
if (this.iconImage && !this.icon.startsWith("fa://")) {
|
|
return html`<img class="accent-icon pf-icon" src="${this.icon}" alt="page icon" />`;
|
|
}
|
|
|
|
const icon = this.icon.replaceAll("fa://", "fa ");
|
|
|
|
return html`<i class="accent-icon ${icon}"></i>`;
|
|
}
|
|
return nothing;
|
|
}
|
|
|
|
render(): TemplateResult {
|
|
return html` <slot></slot>
|
|
<navbar aria-label="Main" class="navbar">
|
|
<aside class="brand ${this.open ? "" : "pf-m-collapsed"}">
|
|
<a href="#/">
|
|
<div class="logo">
|
|
<img
|
|
src=${themeImage(this.brandingLogo)}
|
|
alt="${msg("authentik Logo")}"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
</a>
|
|
</aside>
|
|
<button
|
|
class="sidebar-trigger pf-c-button pf-m-plain"
|
|
@click=${this.#toggleSidebar}
|
|
aria-label=${msg("Toggle sidebar")}
|
|
aria-expanded=${this.open ? "true" : "false"}
|
|
>
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
|
|
<section
|
|
class="items primary pf-c-content ${this.description ? "block-sibling" : ""}"
|
|
>
|
|
<h1 class="page-title">
|
|
${this.hasIcon
|
|
? html`<slot name="icon">${this.renderIcon()}</slot>`
|
|
: nothing}
|
|
${this.header}
|
|
</h1>
|
|
</section>
|
|
${this.description
|
|
? html`<section class="items page-description pf-c-content">
|
|
<p>${this.description}</p>
|
|
</section>`
|
|
: nothing}
|
|
|
|
<section class="items secondary">
|
|
<div class="pf-c-page__header-tools-group">
|
|
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}>
|
|
<a
|
|
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
|
|
href="${globalAK().api.base}if/user/"
|
|
slot="extra"
|
|
>
|
|
${msg("User interface")}
|
|
</a>
|
|
</ak-nav-buttons>
|
|
</div>
|
|
</section>
|
|
</navbar>`;
|
|
}
|
|
|
|
//#endregion
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"ak-page-navbar": AKPageNavbar;
|
|
}
|
|
}
|