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): 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("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`page icon`; } const icon = this.icon.replaceAll("fa://", "fa "); return html``; } return nothing; } render(): TemplateResult { return html`

${this.hasIcon ? html`${this.renderIcon()}` : nothing} ${this.header}

${this.description ? html`

${this.description}

` : nothing}
`; } //#endregion } declare global { interface HTMLElementTagNameMap { "ak-page-navbar": AKPageNavbar; } }