web/NPM Workspaces: ESbuild version cleanup (#14541)
* web: Check JS files. Add types. * web: Fix issues surrounding Vite/ESBuild types. * web: Clean up version constants. Tidy types * web: Clean up docs, types. * web: Clean up package paths. * web: (ESLint) no-lonely-if * web: Render slot before navbar. * web: Fix line-height alignment. * web: Truncate long headers. * web: Clean up page header declarations. Add story. Update paths. * web: Ignore out directory. * web: Lint Lit. * web: Use private alias. * web: Fix implicit CJS mode. * web: Update deps. * web: await all imports.
This commit is contained in:
@ -1,5 +1,4 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import {
|
||||
EventContext,
|
||||
@ -76,7 +75,7 @@ ${context.message as string}
|
||||
|
||||
|
||||
**Version and Deployment (please complete the following information):**
|
||||
- authentik version: ${VERSION}
|
||||
- authentik version: ${import.meta.env.AK_VERSION}
|
||||
- Deployment: [e.g. docker-compose, helm]
|
||||
|
||||
**Additional context**
|
||||
|
||||
84
web/src/components/ak-page-header.ts
Normal file
84
web/src/components/ak-page-header.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import "#components/ak-nav-buttons";
|
||||
import { AKPageNavbar } from "#components/ak-page-navbar";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { CSSResult, LitElement, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
export interface PageHeaderInit {
|
||||
header?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
iconImage?: boolean;
|
||||
}
|
||||
|
||||
//#region Events
|
||||
|
||||
export interface SidebarToggleEventDetail {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
//#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 PageHeaderInit {
|
||||
@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": AKPageHeader;
|
||||
}
|
||||
}
|
||||
428
web/src/components/ak-page-navbar.ts
Normal file
428
web/src/components/ak-page-navbar.ts
Normal file
@ -0,0 +1,428 @@
|
||||
import { EVENT_WS_MESSAGE, TITLE_DEFAULT } from "#common/constants";
|
||||
import { globalAK } from "#common/global";
|
||||
import { UIConfig, UserDisplay, getConfigForUser } from "#common/ui/config";
|
||||
import { DefaultBrand } 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/Interface/brandProvider";
|
||||
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: 1em;
|
||||
width: 1em;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.page-description {
|
||||
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.brand?.brandingTitle || TITLE_DEFAULT;
|
||||
|
||||
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.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.#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;
|
||||
}
|
||||
}
|
||||
58
web/src/components/stories/ak-page-navbar.stories.ts
Normal file
58
web/src/components/stories/ak-page-navbar.stories.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { DefaultBrand } from "#common/ui/config";
|
||||
import "#components/ak-page-header";
|
||||
import { AKPageNavbar } from "#components/ak-page-navbar";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { CurrentBrand } from "@goauthentik/api";
|
||||
|
||||
const metadata: Meta<AKPageNavbar> = {
|
||||
title: "Components / Page Navbar",
|
||||
component: "ak-page-navbar",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A page navbar for the authentik web interface",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
@customElement("story-ak-page-navbar")
|
||||
class AKPageNavbarStory extends AKPageNavbar {
|
||||
brand: CurrentBrand = {
|
||||
...DefaultBrand,
|
||||
brandingLogo: new URL(DefaultBrand.brandingLogo, "http://localhost:9000").toString(),
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"story-ak-page-navbar": AKPageNavbarStory;
|
||||
}
|
||||
}
|
||||
|
||||
export const SimplePageNavbar = () => {
|
||||
return html`
|
||||
<story-ak-page-navbar open @sidebar-toggle=${() => {}}>
|
||||
<ak-page-header header="Page Title" description="Page Description"> </ak-page-header>
|
||||
</story-ak-page-navbar>
|
||||
`;
|
||||
};
|
||||
|
||||
export const PageNavbarWithIcon = () => {
|
||||
return html`
|
||||
<story-ak-page-navbar open @sidebar-toggle=${() => {}}>
|
||||
<ak-page-header
|
||||
header="Page Title"
|
||||
description="Page Description"
|
||||
icon="pf-icon pf-icon-user"
|
||||
>
|
||||
</ak-page-header>
|
||||
</story-ak-page-navbar>
|
||||
`;
|
||||
};
|
||||
Reference in New Issue
Block a user