Compare commits
	
		
			1 Commits
		
	
	
		
			website/do
			...
			safari-cra
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 76c3c7d968 | 
| @ -4,6 +4,7 @@ import { ROUTES } from "@goauthentik/admin/Routes"; | ||||
| import { | ||||
|     EVENT_API_DRAWER_TOGGLE, | ||||
|     EVENT_NOTIFICATION_DRAWER_TOGGLE, | ||||
|     EVENT_SIDEBAR_TOGGLE, | ||||
| } from "@goauthentik/common/constants"; | ||||
| import { configureSentry } from "@goauthentik/common/sentry"; | ||||
| import { me } from "@goauthentik/common/users"; | ||||
| @ -11,6 +12,8 @@ import { WebsocketClient } from "@goauthentik/common/ws"; | ||||
| import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; | ||||
| import "@goauthentik/elements/ak-locale-context"; | ||||
| import "@goauthentik/elements/banner/EnterpriseStatusBanner"; | ||||
| import "@goauthentik/elements/banner/EnterpriseStatusBanner"; | ||||
| import "@goauthentik/elements/banner/VersionBanner"; | ||||
| import "@goauthentik/elements/banner/VersionBanner"; | ||||
| import "@goauthentik/elements/messages/MessageContainer"; | ||||
| import "@goauthentik/elements/messages/MessageContainer"; | ||||
| @ -40,6 +43,8 @@ if (process.env.NODE_ENV === "development") { | ||||
|  | ||||
| @customElement("ak-interface-admin") | ||||
| export class AdminInterface extends AuthenticatedInterface { | ||||
|     //#region Properties | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); | ||||
|  | ||||
| @ -54,6 +59,17 @@ export class AdminInterface extends AuthenticatedInterface { | ||||
|     @query("ak-about-modal") | ||||
|     aboutModal?: AboutModal; | ||||
|  | ||||
|     @state() | ||||
|     sidebarVisible = false; | ||||
|  | ||||
|     #toggleSidebar = () => { | ||||
|         this.sidebarVisible = !this.sidebarVisible; | ||||
|     }; | ||||
|  | ||||
|     //#endregion | ||||
|  | ||||
|     //#region Styles | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
| @ -67,23 +83,30 @@ export class AdminInterface extends AuthenticatedInterface { | ||||
|                     z-index: auto !important; | ||||
|                     background-color: transparent; | ||||
|                 } | ||||
|  | ||||
|                 .display-none { | ||||
|                     display: none; | ||||
|                 } | ||||
|  | ||||
|                 .pf-c-page { | ||||
|                     background-color: var(--pf-c-page--BackgroundColor) !important; | ||||
|                 } | ||||
|  | ||||
|                 :host([theme="dark"]) { | ||||
|                     /* Global page background colour */ | ||||
|                 :host([theme="dark"]) .pf-c-page { | ||||
|                     .pf-c-page { | ||||
|                         --pf-c-page--BackgroundColor: var(--ak-dark-background); | ||||
|                     } | ||||
|                 ak-enterprise-status, | ||||
|                 ak-version-banner { | ||||
|                 } | ||||
|  | ||||
|                 ak-page-navbar { | ||||
|                     grid-area: header; | ||||
|                 } | ||||
|  | ||||
|                 ak-admin-sidebar { | ||||
|                     grid-area: nav; | ||||
|                 } | ||||
|  | ||||
|                 .pf-c-drawer__panel { | ||||
|                     z-index: var(--pf-global--ZIndex--xl); | ||||
|                 } | ||||
| @ -91,6 +114,10 @@ export class AdminInterface extends AuthenticatedInterface { | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     //#endregion | ||||
|  | ||||
|     //#region Lifecycle | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.ws = new WebsocketClient(); | ||||
| @ -123,12 +150,26 @@ export class AdminInterface extends AuthenticatedInterface { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async connectedCallback(): Promise<void> { | ||||
|         super.connectedCallback(); | ||||
|  | ||||
|         window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar); | ||||
|     } | ||||
|  | ||||
|     disconnectedCallback(): void { | ||||
|         super.disconnectedCallback(); | ||||
|         window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar); | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         const sidebarClasses = { | ||||
|             "pf-m-light": this.activeTheme === UiThemeEnum.Light, | ||||
|             "pf-m-expanded": !this.sidebarVisible, | ||||
|             "pf-m-collapsed": this.sidebarVisible, | ||||
|         }; | ||||
|  | ||||
|         const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; | ||||
|  | ||||
|         const drawerClasses = { | ||||
|             "pf-m-expanded": drawerOpen, | ||||
|             "pf-m-collapsed": !drawerOpen, | ||||
| @ -136,11 +177,16 @@ export class AdminInterface extends AuthenticatedInterface { | ||||
|  | ||||
|         return html` <ak-locale-context> | ||||
|             <div class="pf-c-page"> | ||||
|                 <ak-enterprise-status interface="admin"></ak-enterprise-status> | ||||
|                 <ak-page-navbar> | ||||
|                     <ak-version-banner></ak-version-banner> | ||||
|                     <ak-enterprise-status interface="admin"></ak-enterprise-status> | ||||
|                 </ak-page-navbar> | ||||
|  | ||||
|                 <ak-admin-sidebar | ||||
|                     class="pf-c-page__sidebar ${classMap(sidebarClasses)}" | ||||
|                     class="pf-c-page__sidebar | ||||
|                      ${classMap(sidebarClasses)}" | ||||
|                 ></ak-admin-sidebar> | ||||
|  | ||||
|                 <div class="pf-c-page__drawer"> | ||||
|                     <div class="pf-c-drawer ${classMap(drawerClasses)}"> | ||||
|                         <div class="pf-c-drawer__main"> | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; | ||||
| import { me } from "@goauthentik/common/users"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { | ||||
| @ -31,16 +30,9 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement | ||||
|         me().then((user: SessionUser) => { | ||||
|             this.impersonation = user.original ? user.user.username : null; | ||||
|         }); | ||||
|         this.toggleOpen = this.toggleOpen.bind(this); | ||||
|         this.checkWidth = this.checkWidth.bind(this); | ||||
|     } | ||||
|  | ||||
|     // This has to be a bound method so the event listener can be removed on disconnection as | ||||
|     // needed. | ||||
|     toggleOpen() { | ||||
|         this.open = !this.open; | ||||
|     } | ||||
|  | ||||
|     checkWidth() { | ||||
|         // This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in | ||||
|         // REMs. If that changes, this code will have to be adjusted as well. | ||||
| @ -52,7 +44,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement | ||||
|  | ||||
|     connectedCallback() { | ||||
|         super.connectedCallback(); | ||||
|         window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); | ||||
|  | ||||
|         window.addEventListener("resize", this.checkWidth); | ||||
|         // After connecting to the DOM, we can now perform this check to see if the sidebar should | ||||
|         // be open by default. | ||||
| @ -63,7 +55,6 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement | ||||
|     // connection, and removing them before disconnection. | ||||
|  | ||||
|     disconnectedCallback() { | ||||
|         window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); | ||||
|         window.removeEventListener("resize", this.checkWidth); | ||||
|         super.disconnectedCallback(); | ||||
|     } | ||||
| @ -71,8 +62,9 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement | ||||
|     render() { | ||||
|         return html` | ||||
|             <ak-sidebar | ||||
|                 class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this | ||||
|                     .activeTheme === UiThemeEnum.Light | ||||
|                 class="pf-c-page__sidebar | ||||
|                 ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this.activeTheme === | ||||
|                 UiThemeEnum.Light | ||||
|                     ? "pf-m-light" | ||||
|                     : ""}" | ||||
|             > | ||||
| @ -81,19 +73,6 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     updated() { | ||||
|         // This is permissible as`:host.classList` is not one of the properties Lit uses as a | ||||
|         // scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger | ||||
|         // a browser reflow, which may trigger some other styling the application is monitoring, | ||||
|         // triggering a re-render which triggers a browser reflow, ad infinitum. But we've been | ||||
|         // living with that since jQuery, and it's both well-known and fortunately rare. | ||||
|  | ||||
|         // eslint-disable-next-line wc/no-self-class | ||||
|         this.classList.remove("pf-m-expanded", "pf-m-collapsed"); | ||||
|         // eslint-disable-next-line wc/no-self-class | ||||
|         this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed"); | ||||
|     } | ||||
|  | ||||
|     renderSidebarItems(): TemplateResult { | ||||
|         // The second attribute type is of string[] to help with the 'activeWhen' control, which was | ||||
|         // commonplace and singular enough to merit its own handler. | ||||
|  | ||||
| @ -94,10 +94,13 @@ export class AdminOverviewPage extends AdminOverviewBase { | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         const name = this.user?.user.name ?? this.user?.user.username; | ||||
|         const username = this.user?.user.name || this.user?.user.username; | ||||
|  | ||||
|         return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}> | ||||
|                 <span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span> | ||||
|         return html` <ak-page-header | ||||
|                 header=${msg(str`Welcome, ${username || ""}.`)} | ||||
|                 description=${msg("General system status")} | ||||
|                 ?hasIcon=${false} | ||||
|             > | ||||
|             </ak-page-header> | ||||
|             <section class="pf-c-page__main-section"> | ||||
|                 <div class="pf-l-grid pf-m-gutter"> | ||||
|  | ||||
| @ -83,13 +83,10 @@ export class AdminSettingsPage extends AKElement { | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         if (!this.settings) { | ||||
|             return nothing; | ||||
|         } | ||||
|         if (!this.settings) return nothing; | ||||
|  | ||||
|         return html` | ||||
|             <ak-page-header icon="fa fa-cog" header="" description=""> | ||||
|                 <span slot="header"> ${msg("System settings")} </span> | ||||
|             </ak-page-header> | ||||
|             <ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header> | ||||
|             <section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> | ||||
|                 <div class="pf-c-card"> | ||||
|                     <div class="pf-c-card__body"> | ||||
|  | ||||
| @ -17,6 +17,13 @@ | ||||
|  | ||||
|     /* Minimum width after which the sidebar becomes automatic */ | ||||
|     --ak-sidebar--minimum-auto-width: 80rem; | ||||
|  | ||||
|     /** | ||||
|      * The height of the navbar and branded sidebar. | ||||
|      * @todo This shouldn't be necessary. The sidebar can instead use a grid layout | ||||
|      * ensuring they share the same height. | ||||
|      */ | ||||
|     --ak-navbar--height: 7rem; | ||||
| } | ||||
|  | ||||
| @supports selector(::-webkit-scrollbar) { | ||||
|  | ||||
| @ -67,6 +67,12 @@ export class NavigationButtons extends AKElement { | ||||
|                 :host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button { | ||||
|                     color: var(--ak-global--Color--100) !important; | ||||
|                 } | ||||
|  | ||||
|                 @media (max-width: 768px) { | ||||
|                     .pf-c-avatar { | ||||
|                         display: none; | ||||
|                     } | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
| @ -156,9 +162,7 @@ export class NavigationButtons extends AKElement { | ||||
|     } | ||||
|  | ||||
|     renderImpersonation() { | ||||
|         if (!this.me?.original) { | ||||
|             return nothing; | ||||
|         } | ||||
|         if (!this.me?.original) return nothing; | ||||
|  | ||||
|         const onClick = async () => { | ||||
|             await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); | ||||
| @ -175,6 +179,14 @@ export class NavigationButtons extends AKElement { | ||||
|             </div>`; | ||||
|     } | ||||
|  | ||||
|     renderAvatar() { | ||||
|         return html`<img | ||||
|             class="pf-c-avatar" | ||||
|             src=${ifDefined(this.me?.user.avatar)} | ||||
|             alt="${msg("Avatar image")}" | ||||
|         />`; | ||||
|     } | ||||
|  | ||||
|     get userDisplayName() { | ||||
|         return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay) | ||||
|             .with(UserDisplay.username, () => this.me?.user.username) | ||||
| @ -212,11 +224,7 @@ export class NavigationButtons extends AKElement { | ||||
|                       </div> | ||||
|                   </div>` | ||||
|                 : nothing} | ||||
|             <img | ||||
|                 class="pf-c-avatar" | ||||
|                 src=${ifDefined(this.me?.user.avatar)} | ||||
|                 alt="${msg("Avatar image")}" | ||||
|             /> | ||||
|             ${this.renderAvatar()} | ||||
|         </div>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -10,15 +10,18 @@ import { me } from "@goauthentik/common/users"; | ||||
| import "@goauthentik/components/ak-nav-buttons"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; | ||||
| import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; | ||||
| import { themeImage } from "@goauthentik/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 { CSSResult, LitElement, 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"; | ||||
| @ -26,34 +29,52 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { SessionUser } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-page-header") | ||||
| export class PageHeader extends WithBrandConfig(AKElement) { | ||||
|     @property() | ||||
|     icon?: string; | ||||
| //#region Page Navbar | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     iconImage = false; | ||||
|  | ||||
|     @property() | ||||
|     header = ""; | ||||
|  | ||||
|     @property() | ||||
| export interface PageNavbarDetails { | ||||
|     header?: string; | ||||
|     description?: string; | ||||
|     icon?: string; | ||||
|     iconImage?: boolean; | ||||
| } | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     hasIcon = true; | ||||
| /** | ||||
|  * 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 PageNavbarDetails { | ||||
|     //#region Static Properties | ||||
|  | ||||
|     @state() | ||||
|     me?: SessionUser; | ||||
|     private static elementRef: AKPageNavbar | null = null; | ||||
|  | ||||
|     @state() | ||||
|     uiConfig!: UIConfig; | ||||
|     static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): 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, | ||||
| @ -63,55 +84,212 @@ export class PageHeader extends WithBrandConfig(AKElement) { | ||||
|                     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 | ||||
|                     ); | ||||
|                 } | ||||
|                 .bar { | ||||
|  | ||||
|                 :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; | ||||
|                     min-height: 114px; | ||||
|                     max-height: 114px; | ||||
|                     background-color: var(--pf-c-page--BackgroundColor); | ||||
|                     min-height: 6rem; | ||||
|  | ||||
|                     display: grid; | ||||
|                     row-gap: var(--pf-global--spacer--sm); | ||||
|                     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 (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%; | ||||
|                     } | ||||
|                 .pf-c-page__main-section.pf-m-light { | ||||
|                     background-color: transparent; | ||||
|                 } | ||||
|                 .pf-c-page__main-section { | ||||
|                     flex-grow: 1; | ||||
|                     flex-shrink: 1; | ||||
|  | ||||
|                 .items { | ||||
|                     display: block; | ||||
|  | ||||
|                     &.primary { | ||||
|                         grid-column: primary; | ||||
|                         grid-row: primary / description; | ||||
|  | ||||
|                         align-content: center; | ||||
|                         padding-block: var(--pf-global--spacer--md); | ||||
|  | ||||
|                         @media (min-width: 426px) { | ||||
|                             &.block-sibling { | ||||
|                                 padding-block-end: 0; | ||||
|                                 grid-row: primary; | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         @media (max-width: 768px) { | ||||
|                             padding-block: var(--pf-global--spacer--sm); | ||||
|                         } | ||||
|  | ||||
|                         .accent-icon { | ||||
|                             height: 1em; | ||||
|                             width: 1em; | ||||
|  | ||||
|                             @media (max-width: 768px) { | ||||
|                                 display: none; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     &.page-description { | ||||
|                         grid-area: description; | ||||
|                         padding-block-end: var(--pf-global--spacer--md); | ||||
|  | ||||
|                         @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; | ||||
|                     flex-direction: column; | ||||
|                     justify-content: center; | ||||
|  | ||||
|                     &.pf-m-collapsed { | ||||
|                         display: none; | ||||
|                     } | ||||
|                 img.pf-icon { | ||||
|                     max-height: 24px; | ||||
|  | ||||
|                     @media (max-width: 1279px) { | ||||
|                         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: 24px; | ||||
|                     font-size: 1.5rem; | ||||
|                 } | ||||
|  | ||||
|                 .notification-trigger.has-notifications { | ||||
|                     color: var(--pf-global--active-color--100); | ||||
|                 } | ||||
|  | ||||
|                 .page-title { | ||||
|                     display: flex; | ||||
|                     gap: var(--pf-global--spacer--xs); | ||||
|                 } | ||||
|  | ||||
|                 h1 { | ||||
|                     display: flex; | ||||
|                     flex-direction: row; | ||||
|                     align-items: center !important; | ||||
|                 } | ||||
|                 .pf-c-page__header-tools { | ||||
|                     flex-shrink: 0; | ||||
|                 } | ||||
|                 .pf-c-page__header-tools-group { | ||||
|                     height: 100%; | ||||
|                 } | ||||
|                 :host([theme="dark"]) .pf-c-page__header-tools { | ||||
|                     color: var(--ak-dark-foreground) !important; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     //#endregion | ||||
|  | ||||
|     //#region Properties | ||||
|  | ||||
|     @property({ type: String }) | ||||
|     icon?: string; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     iconImage = false; | ||||
|  | ||||
|     @property({ type: String }) | ||||
|     header?: string; | ||||
|  | ||||
|     @property({ type: String }) | ||||
|     description?: string; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     hasIcon = true; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     open = true; | ||||
|  | ||||
|     @state() | ||||
|     me?: SessionUser; | ||||
|  | ||||
|     @state() | ||||
|     uiConfig!: UIConfig; | ||||
|  | ||||
|     //#endregion | ||||
|  | ||||
|     //#endregion | ||||
|     //#region Methods | ||||
|  | ||||
|     #toggleSidebar() { | ||||
|         this.open = !this.open; | ||||
|  | ||||
|         this.dispatchEvent( | ||||
|             new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||
|                 bubbles: true, | ||||
|                 composed: true, | ||||
|             }), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     //#region Constructor | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         window.addEventListener(EVENT_WS_MESSAGE, () => { | ||||
| @ -119,13 +297,23 @@ export class PageHeader extends WithBrandConfig(AKElement) { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async firstUpdated() { | ||||
|     connectedCallback(): void { | ||||
|         super.connectedCallback(); | ||||
|         AKPageNavbar.elementRef = this; | ||||
|     } | ||||
|  | ||||
|     disconnectedCallback(): void { | ||||
|         super.disconnectedCallback(); | ||||
|         AKPageNavbar.elementRef = null; | ||||
|     } | ||||
|  | ||||
|     public async firstUpdated() { | ||||
|         this.me = await me(); | ||||
|         this.uiConfig = await uiConfig(); | ||||
|         this.uiConfig.navbar.userDisplay = UserDisplay.none; | ||||
|     } | ||||
|  | ||||
|     setTitle(header?: string) { | ||||
|     #setTitle(header?: string) { | ||||
|         const currentIf = currentInterface(); | ||||
|         let title = this.brand?.brandingTitle || TITLE_DEFAULT; | ||||
|         if (currentIf === "admin") { | ||||
| @ -141,47 +329,65 @@ export class PageHeader extends WithBrandConfig(AKElement) { | ||||
|     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); | ||||
|         this.#setTitle(this.header); | ||||
|     } | ||||
|  | ||||
|     //#region Render | ||||
|  | ||||
|     renderIcon() { | ||||
|         if (this.icon) { | ||||
|             if (this.iconImage && !this.icon.startsWith("fa://")) { | ||||
|                 return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`; | ||||
|                 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=${icon}></i>`; | ||||
|  | ||||
|             return html`<i class="accent-icon ${icon}"></i>`; | ||||
|         } | ||||
|         return nothing; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html`<div class="bar"> | ||||
|         return html`<navbar aria-label="Main" class="navbar"> | ||||
|                 <aside class="brand ${this.open ? "" : "pf-m-collapsed"}"> | ||||
|                     <a href="#/"> | ||||
|                         <div class="logo"> | ||||
|                             <img | ||||
|                                 src=${themeImage( | ||||
|                                     this.brand?.brandingLogo ?? DefaultBrand.brandingLogo, | ||||
|                                 )} | ||||
|                                 alt="${msg("authentik Logo")}" | ||||
|                                 loading="lazy" | ||||
|                             /> | ||||
|                         </div> | ||||
|                     </a> | ||||
|                 </aside> | ||||
|                 <button | ||||
|                     class="sidebar-trigger pf-c-button pf-m-plain" | ||||
|                 @click=${() => { | ||||
|                     this.dispatchEvent( | ||||
|                         new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||
|                             bubbles: true, | ||||
|                             composed: true, | ||||
|                         }), | ||||
|                     ); | ||||
|                 }} | ||||
|                     @click=${this.#toggleSidebar} | ||||
|                     aria-label=${msg("Toggle sidebar")} | ||||
|                     aria-expanded=${this.open ? "true" : "false"} | ||||
|                 > | ||||
|                     <i class="fas fa-bars"></i> | ||||
|                 </button> | ||||
|             <section class="pf-c-page__main-section pf-m-light"> | ||||
|                 <div class="pf-c-content"> | ||||
|                     <h1> | ||||
|  | ||||
|                 <section | ||||
|                     class="items primary pf-c-content ${this.description ? "block-sibling" : ""}" | ||||
|                 > | ||||
|                     <h1 class="page-title"> | ||||
|                         ${this.hasIcon | ||||
|                             ? html`<slot name="icon">${this.renderIcon()}</slot> ` | ||||
|                             ? html`<slot name="icon">${this.renderIcon()}</slot>` | ||||
|                             : nothing} | ||||
|                         <slot name="header">${this.header}</slot> | ||||
|                         ${this.header} | ||||
|                     </h1> | ||||
|                     ${this.description ? html`<p>${this.description}</p>` : html``} | ||||
|                 </div> | ||||
|                 </section> | ||||
|             <div class="pf-c-page__header-tools"> | ||||
|                 ${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.me}> | ||||
|                             <a | ||||
| @ -193,13 +399,76 @@ export class PageHeader extends WithBrandConfig(AKElement) { | ||||
|                             </a> | ||||
|                         </ak-nav-buttons> | ||||
|                     </div> | ||||
|             </div> | ||||
|         </div>`; | ||||
|                 </section> | ||||
|             </navbar> | ||||
|             <slot></slot>`; | ||||
|     } | ||||
|  | ||||
|     //#endregion | ||||
| } | ||||
|  | ||||
| //#endregion | ||||
|  | ||||
| //#region Page Header | ||||
|  | ||||
| /** | ||||
|  * A page header component, used to display the page title and description. | ||||
|  * | ||||
|  * Internally, this component dispatches the `ak-page-header` event, which is | ||||
|  * listened to by the `ak-page-navbar` component. | ||||
|  * | ||||
|  * @singleton | ||||
|  */ | ||||
| @customElement("ak-page-header") | ||||
| export class AKPageHeader extends LitElement implements PageNavbarDetails { | ||||
|     @property({ type: String }) | ||||
|     header?: string; | ||||
|  | ||||
|     @property({ type: String }) | ||||
|     description?: string; | ||||
|  | ||||
|     @property({ type: String }) | ||||
|     icon?: string; | ||||
|  | ||||
|     @property({ type: Boolean }) | ||||
|     iconImage = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             css` | ||||
|                 :host { | ||||
|                     display: none; | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     connectedCallback(): void { | ||||
|         super.connectedCallback(); | ||||
|  | ||||
|         AKPageNavbar.setNavbarDetails({ | ||||
|             header: this.header, | ||||
|             description: this.description, | ||||
|             icon: this.icon, | ||||
|             iconImage: this.iconImage, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     updated(): void { | ||||
|         AKPageNavbar.setNavbarDetails({ | ||||
|             header: this.header, | ||||
|             description: this.description, | ||||
|             icon: this.icon, | ||||
|             iconImage: this.iconImage, | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| //#endregion | ||||
|  | ||||
| declare global { | ||||
|     interface HTMLElementTagNameMap { | ||||
|         "ak-page-header": PageHeader; | ||||
|         "ak-page-header": AKPageHeader; | ||||
|         "ak-page-navbar": AKPageNavbar; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -35,10 +35,7 @@ export class Sidebar extends AKElement { | ||||
|                 .pf-c-nav__section + .pf-c-nav__section { | ||||
|                     --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm); | ||||
|                 } | ||||
|                 .pf-c-nav__list .sidebar-brand { | ||||
|                     max-height: 82px; | ||||
|                     margin-bottom: -0.5rem; | ||||
|                 } | ||||
|  | ||||
|                 nav { | ||||
|                     display: flex; | ||||
|                     flex-direction: column; | ||||
| @ -70,7 +67,6 @@ export class Sidebar extends AKElement { | ||||
|             class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}" | ||||
|             aria-label=${msg("Global")} | ||||
|         > | ||||
|             <ak-sidebar-brand></ak-sidebar-brand> | ||||
|             <ul class="pf-c-nav__list"> | ||||
|                 <slot></slot> | ||||
|             </ul> | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; | ||||
| import { themeImage } from "@goauthentik/elements/utils/images"; | ||||
| @ -42,22 +41,16 @@ export class SidebarBrand extends WithBrandConfig(AKElement) { | ||||
|                     display: flex; | ||||
|                     flex-direction: row; | ||||
|                     align-items: center; | ||||
|                     height: 114px; | ||||
|                     min-height: 114px; | ||||
|                     height: var(--ak-navbar-height); | ||||
|                     border-bottom: var(--pf-global--BorderWidth--sm); | ||||
|                     border-bottom-style: solid; | ||||
|                     border-bottom-color: var(--pf-global--BorderColor--100); | ||||
|                 } | ||||
|  | ||||
|                 .pf-c-brand img { | ||||
|                     padding: 0 0.5rem; | ||||
|                     height: 42px; | ||||
|                 } | ||||
|                 button.pf-c-button.sidebar-trigger { | ||||
|                     background-color: transparent; | ||||
|                     border-radius: 0px; | ||||
|                     height: 100%; | ||||
|                     color: var(--ak-dark-foreground); | ||||
|                 } | ||||
|             `, | ||||
|         ]; | ||||
|     } | ||||
| @ -70,24 +63,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) { | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         return html` ${window.innerWidth <= MIN_WIDTH | ||||
|                 ? html` | ||||
|                       <button | ||||
|                           class="sidebar-trigger pf-c-button" | ||||
|                           @click=${() => { | ||||
|                               this.dispatchEvent( | ||||
|                                   new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||
|                                       bubbles: true, | ||||
|                                       composed: true, | ||||
|                                   }), | ||||
|                               ); | ||||
|                           }} | ||||
|                       > | ||||
|                           <i class="fas fa-bars"></i> | ||||
|                       </button> | ||||
|                   ` | ||||
|                 : html``} | ||||
|             <a href="#/" class="pf-c-page__header-brand-link"> | ||||
|         return html` <a href="#/" class="pf-c-page__header-brand-link"> | ||||
|             <div class="pf-c-brand ak-brand"> | ||||
|                 <img | ||||
|                     src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)} | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	