web: continuing with the Sidebar

I've finally reached a stage where I have a framework I can build upon, but what
a pain in the posterior it was to get here.  Keeping the entire navigation list
within a single DOM is a solid idea, but porting from the original code to this
proved to be unreasonably kludegy.  Instead, I started from scratch, adding each
step along the way, a sort of Transformation Priority Premise, and testing each
step to make sure it was all behaving as expected.

So far, so good.

The remaining details are not trivial: I have to figure out how to express the
different classes of actions, and get the third tier working, but at least
the React version gives us hints.
This commit is contained in:
Ken Sternberg 2023-11-16 10:38:36 -08:00
parent 476adef4ea
commit d539884204
6 changed files with 335 additions and 336 deletions

View file

@ -17,7 +17,6 @@ import "@goauthentik/elements/notifications/NotificationDrawer";
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
import "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/sidebar/Sidebar";
import "@goauthentik/elements/sidebar/SidebarItem";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";

View file

@ -4,18 +4,40 @@ import { me } from "@goauthentik/common/users";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { AKElement } from "@goauthentik/elements/Base";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import "@goauthentik/elements/sidebar/Sidebar";
import { SidebarAttributes, SidebarEntry, SidebarEventHandler } from "@goauthentik/elements/sidebar/SidebarItems";
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
import { spread } from "@open-wc/lit-helpers";
import { consume } from "@lit-labs/context";
import { msg, str } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit";
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import { AdminApi, CapabilitiesEnum, CoreApi, UiThemeEnum, Version } from "@goauthentik/api";
import { ProvidersApi, TypeCreate } from "@goauthentik/api";
import { AdminApi, CapabilitiesEnum, CoreApi, Version } from "@goauthentik/api";
import type { Config, SessionUser, UserSelf } from "@goauthentik/api";
/**
* AdminSidebar
*
* Encapsulates the logic for the administration sidebar: what to show and, initially, when to show
* it. Rendering decisions are left to the sidebar itself.
*/
type LocalSidebarEntry = [
string | SidebarEventHandler | null,
string,
(SidebarAttributes | string[] | null)?, // eslint-disable-line
LocalSidebarEntry[]?,
];
const localToSidebarEntry = (l: LocalSidebarEntry): SidebarEntry => ({
path: l[0],
label: l[1],
...(l[2]? { attributes: Array.isArray(l[2]) ? { activeWhen: l[2] } : l[2] } : {}),
...(l[3] ? { children: l[3].map(localToSidebarEntry) } : {}),
});
@customElement("ak-admin-sidebar")
export class AkAdminSidebar extends AKElement {
@property({ type: Boolean, reflect: true })
@ -27,6 +49,9 @@ export class AkAdminSidebar extends AKElement {
@state()
impersonation: UserSelf["username"] | null = null;
@state()
providerTypes: TypeCreate[] = [];
@consume({ context: authentikConfigContext })
public config!: Config;
@ -38,6 +63,9 @@ export class AkAdminSidebar extends AKElement {
me().then((user: SessionUser) => {
this.impersonation = user.original ? user.user.username : null;
});
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => {
this.providerTypes = types;
});
this.toggleOpen = this.toggleOpen.bind(this);
this.checkWidth = this.checkWidth.bind(this);
}
@ -51,9 +79,7 @@ export class AkAdminSidebar extends AKElement {
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.
const minWidth =
parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) *
parseFloat(getRootStyle("font-size"));
const minWidth = parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) * parseFloat(getRootStyle("font-size"));
this.open = window.innerWidth >= minWidth;
}
@ -75,19 +101,6 @@ export class AkAdminSidebar extends AKElement {
super.disconnectedCallback();
}
render() {
return html`
<ak-sidebar
class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this
.activeTheme === UiThemeEnum.Light
? "pf-m-light"
: ""}"
>
${this.renderSidebarItems()}
</ak-sidebar>
`;
}
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
@ -98,26 +111,43 @@ export class AkAdminSidebar extends AKElement {
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.
type SidebarEntry = [
path: string | null,
label: string,
attributes?: Record<string, any> | string[] | null, // eslint-disable-line
children?: SidebarEntry[],
];
get sidebarItems(): SidebarEntry[] {
const reload = () =>
new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
window.location.reload();
});
// prettier-ignore
const sidebarContent: SidebarEntry[] = [
["/if/user/", msg("User interface"), { "?isAbsoluteLink": true, "?highlight": true }],
[null, msg("Dashboards"), { "?expanded": true }, [
const newVersionMessage: LocalSidebarEntry[] = this.version && this.version !== VERSION
? [["https://goauthentik.io", msg("A newer version of the frontend is available."), { "?highlight": true }]]
: [];
// prettier-ignore
const impersonationMessage: LocalSidebarEntry[] = this.impersonation
? [[reload, msg(str`You're currently impersonating ${this.impersonation}. Click to stop.`)]]
: [];
// prettier-ignore
const enterpriseMenu: LocalSidebarEntry[] = this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise)
? [[null, msg("Enterprise"), null, [["/enterprise/licenses", msg("Licenses")]]]]
: [];
// prettier-ignore
const providerTypes: LocalSidebarEntry[] = this.providerTypes.map((ptype) =>
([`/core/providers;${encodeURIComponent(JSON.stringify({ search: ptype.modelName.replace(/provider$/, "") }))}`, ptype.name]));
// prettier-ignore
const localSidebar: LocalSidebarEntry[] = [
...(newVersionMessage),
...(impersonationMessage),
["/if/user/", msg("User interface"), { isAbsoluteLink: true, highlight: true }],
[null, msg("Dashboards"), { expanded: true }, [
["/administration/overview", msg("Overview")],
["/administration/dashboard/users", msg("User Statistics")],
["/administration/system-tasks", msg("System Tasks")]]],
[null, msg("Applications"), null, [
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`], providerTypes],
["/outpost/outposts", msg("Outposts")]]],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
@ -142,73 +172,14 @@ export class AkAdminSidebar extends AKElement {
[null, msg("System"), null, [
["/core/tenants", msg("Tenants")],
["/crypto/certificates", msg("Certificates")],
["/outpost/integrations", msg("Outpost Integrations")]]]
["/outpost/integrations", msg("Outpost Integrations")]]],
...(enterpriseMenu)
];
// Typescript requires the type here to correctly type the recursive path
type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => {
const properties = Array.isArray(attributes)
? { ".activeWhen": attributes }
: attributes ?? {};
if (path) {
properties["path"] = path;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${map(children, renderOneSidebarItem)}
</ak-sidebar-item>`;
};
// prettier-ignore
return html`
${this.renderNewVersionMessage()}
${this.renderImpersonationMessage()}
${map(sidebarContent, renderOneSidebarItem)}
${this.renderEnterpriseMessage()}
`;
return localSidebar.map(localToSidebarEntry);
}
renderNewVersionMessage() {
return this.version && this.version !== VERSION
? html`
<ak-sidebar-item ?highlight=${true}>
<span slot="label"
>${msg("A newer version of the frontend is available.")}</span
>
</ak-sidebar-item>
`
: nothing;
}
renderImpersonationMessage() {
const reload = () =>
new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
window.location.reload();
});
return this.impersonation
? html`<ak-sidebar-item ?highlight=${true} @click=${reload}>
<span slot="label"
>${msg(
str`You're currently impersonating ${this.impersonation}. Click to stop.`,
)}</span
>
</ak-sidebar-item>`
: nothing;
}
renderEnterpriseMessage() {
return this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span>
<ak-sidebar-item path="/enterprise/licenses">
<span slot="label">${msg("Licenses")}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`
: nothing;
render() {
return html` <ak-sidebar class="pf-c-page__sidebar" .entries=${this.sidebarItems}></ak-sidebar> `;
}
}

View file

@ -1,9 +1,10 @@
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/sidebar/SidebarBrand";
import "@goauthentik/elements/sidebar/SidebarUser";
import "@goauthentik/elements/sidebar/SidebarItems";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { customElement, property } from "lit/decorators.js";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
@ -11,8 +12,13 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { UiThemeEnum } from "@goauthentik/api";
import type { SidebarEntry } from "./SidebarItems";
@customElement("ak-sidebar")
export class Sidebar extends AKElement {
@property({ type: Array })
entries: SidebarEntry[] = [];
static get styles(): CSSResult[] {
return [
PFBase,
@ -45,7 +51,8 @@ export class Sidebar extends AKElement {
height: 100%;
overflow-y: hidden;
}
.pf-c-nav__list {
ak-sidebar-items {
flex-grow: 1;
overflow-y: auto;
}
@ -66,14 +73,10 @@ export class Sidebar extends AKElement {
}
render(): TemplateResult {
return html`<nav
class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}"
aria-label="Global"
>
console.log(this.entries);
return html`<nav class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}" aria-label="Global">
<ak-sidebar-brand></ak-sidebar-brand>
<ul class="pf-c-nav__list">
<slot></slot>
</ul>
<ak-sidebar-items .entries=${this.entries}></ak-sidebar-items>
<ak-sidebar-user></ak-sidebar-user>
</nav>`;
}

View file

@ -1,227 +0,0 @@
import { ROUTE_SEPARATOR } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
import { CSSResult, css } from "lit";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-sidebar-item")
export class SidebarItem extends AKElement {
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFNav,
css`
:host {
z-index: 100;
box-shadow: none !important;
}
:host([highlight]) .pf-c-nav__item {
background-color: var(--ak-accent);
margin: 16px;
}
:host([highlight]) .pf-c-nav__item .pf-c-nav__link {
padding-left: 0.5rem;
}
.pf-c-nav__link.pf-m-current::after,
.pf-c-nav__link.pf-m-current:hover::after,
.pf-c-nav__item.pf-m-current:not(.pf-m-expanded) .pf-c-nav__link::after {
--pf-c-nav__link--m-current--after--BorderColor: #fd4b2d;
}
.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;
max-height: 100vh;
height: 100%;
overflow-y: hidden;
}
.pf-c-nav__list {
flex-grow: 1;
overflow-y: auto;
}
.pf-c-nav__link {
--pf-c-nav__link--PaddingTop: 0.5rem;
--pf-c-nav__link--PaddingRight: 0.5rem;
--pf-c-nav__link--PaddingBottom: 0.5rem;
}
.pf-c-nav__section-title {
font-size: 12px;
}
.pf-c-nav__item {
--pf-c-nav__item--MarginTop: 0px;
}
`,
];
}
@property()
path?: string;
activeMatchers: RegExp[] = [];
@property({ type: Boolean })
expanded = false;
@property({ type: Boolean })
isActive = false;
@property({ type: Boolean })
isAbsoluteLink?: boolean;
@property({ type: Boolean })
highlight?: boolean;
parent?: SidebarItem;
get childItems(): SidebarItem[] {
const children = Array.from(this.querySelectorAll<SidebarItem>("ak-sidebar-item") || []);
children.forEach((child) => (child.parent = this));
return children;
}
@property({ attribute: false })
set activeWhen(regexp: string[]) {
regexp.forEach((r) => {
this.activeMatchers.push(new RegExp(r));
});
}
firstUpdated(): void {
this.onHashChange();
window.addEventListener("hashchange", () => this.onHashChange());
}
onHashChange(): void {
const activePath = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
this.childItems.forEach((item) => {
this.expandParentRecursive(activePath, item);
});
this.isActive = this.matchesPath(activePath);
}
private matchesPath(path: string): boolean {
if (!this.path) {
return false;
}
if (this.path) {
const ourPath = this.path.split(";")[0];
if (new RegExp(`^${ourPath}$`).exec(path)) {
return true;
}
}
return this.activeMatchers.some((v) => {
const match = v.exec(path);
if (match !== null) {
return true;
}
return false;
});
}
expandParentRecursive(activePath: string, item: SidebarItem): void {
if (item.matchesPath(activePath) && item.parent) {
item.parent.expanded = true;
this.requestUpdate();
}
item.childItems.forEach((i) => this.expandParentRecursive(activePath, i));
}
render(): TemplateResult {
return this.renderInner();
}
renderWithChildren() {
return html`<li
class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}"
>
<button
class="pf-c-nav__link"
aria-expanded="true"
@click=${() => {
this.expanded = !this.expanded;
}}
>
<slot name="label"></slot>
<span class="pf-c-nav__toggle">
<span class="pf-c-nav__toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</span>
</button>
<section class="pf-c-nav__subnav" ?hidden=${!this.expanded}>
<ul class="pf-c-nav__list">
<slot></slot>
</ul>
</section>
</li>`;
}
renderWithPathAndChildren() {
return html`<li
class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}"
>
<slot name="label"></slot>
<button
class="pf-c-nav__link"
aria-expanded="true"
@click=${() => {
this.expanded = !this.expanded;
}}
>
<span class="pf-c-nav__toggle">
<span class="pf-c-nav__toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</span>
</button>
<section class="pf-c-nav__subnav" ?hidden=${!this.expanded}>
<ul class="pf-c-nav__list">
<slot></slot>
</ul>
</section>
</li>`;
}
renderWithPath() {
return html`
<a
href="${this.isAbsoluteLink ? "" : "#"}${this.path}"
class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}"
>
<slot name="label"></slot>
</a>
`;
}
renderWithLabel() {
html`
<span class="pf-c-nav__link">
<slot name="label"></slot>
</span>
`;
}
renderInner() {
if (this.childItems.length > 0) {
return this.path ? this.renderWithPathAndChildren() : this.renderWithChildren();
}
return html`<li class="pf-c-nav__item">
${this.path ? this.renderWithPath() : this.renderWithLabel()}
</li>`;
}
}

View file

@ -0,0 +1,255 @@
import { ROUTE_SEPARATOR } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { UiThemeEnum } from "@goauthentik/api";
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
// commonplace and singular enough to merit its own handler.
export type SidebarEventHandler = () => void;
export type SidebarAttributes = {
isAbsoluteLink?: boolean | (() => boolean);
highlight?: boolean | (() => boolean);
expanded?: boolean | (() => boolean);
activeWhen?: string[];
isActive?: boolean;
};
export type SidebarEntry = {
path: string | SidebarEventHandler | null;
label: string;
attributes?: SidebarAttributes | null; // eslint-disable-line
children?: SidebarEntry[];
};
// Typescript requires the type here to correctly type the recursive path
export type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
@customElement("ak-sidebar-items")
export class SidebarItems extends AKElement {
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFNav,
css`
:host {
z-index: 100;
box-shadow: none !important;
}
.highlighted {
background-color: var(--ak-accent);
margin: 16px;
}
.highlighted .pf-c-nav__link {
padding-left: 0.5rem;
}
.pf-c-nav__link.pf-m-current::after,
.pf-c-nav__link.pf-m-current:hover::after,
.pf-c-nav__item.pf-m-current:not(.pf-m-expanded) .pf-c-nav__link::after {
--pf-c-nav__link--m-current--after--BorderColor: #fd4b2d;
}
.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;
max-height: 100vh;
height: 100%;
overflow-y: hidden;
}
.pf-c-nav__list {
flex: 1 0 1fr;
overflow-y: auto;
}
.pf-c-nav__link {
--pf-c-nav__link--PaddingTop: 0.5rem;
--pf-c-nav__link--PaddingRight: 0.5rem;
--pf-c-nav__link--PaddingBottom: 0.5rem;
}
.pf-c-nav__section-title {
font-size: 12px;
}
.pf-c-nav__item {
--pf-c-nav__item--MarginTop: 0px;
}
.pf-c-nav__toggle-icon {
padding: var(--pf-global--spacer--sm) var(--pf-global--spacer--md);
}
`,
];
}
@property({ type: Array })
entries: SidebarEntry[] = [];
@state()
expanded: WeakSet<SidebarEntry> = new WeakSet();
constructor() {
super();
// this.onToggle = this.onToggle.bind(this);
this.onHashChange = this.onHashChange.bind(this);
this.isActive = this.isActive.bind(this);
this.renderItem = this.renderItem.bind(this);
this.toggleExpand = this.toggleExpand.bind(this);
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("hashchange", this.onHashChange);
}
disconnectedCallback() {
window.removeEventListener("hashchange", this.onHashChange);
super.disconnectedCallback();
}
firstUpdated(): void {
this.onHashChange();
}
onHashChange(): void {
/* no op */
}
isActive(path: string) {
return true;
}
render(): TemplateResult {
const lightThemed = { "pf-m-light": this.activeTheme === UiThemeEnum.Light };
return html` <nav class="pf-c-nav ${classMap(lightThemed)}" aria-label="Navigation">
<ul class="pf-c-nav__list">
${map(this.entries, this.renderItem)}
</ul>
</nav>`;
}
toggleExpand(entry: SidebarEntry) {
console.log(this, entry, this.expanded.has(entry));
if (this.expanded.has(entry)) {
this.expanded.delete(entry);
} else {
this.expanded.add(entry);
}
this.requestUpdate();
}
renderItem(entry: SidebarEntry) {
const { path, label, attributes, children } = entry;
// Ensure the attributes are undefined, not null; they can be null in the placeholders, but
// not when being forwarded to the correct renderer.
const attr = attributes ?? undefined;
const hasChildren = !!(children && children.length > 0);
// This is grossly imperative, in that it HAS to come before the content is rendered
// to make sure the content gets the right settings with respect to expansion.
if (attr?.expanded) {
this.expanded.add(entry);
delete attr.expanded;
}
const content = path
? this.renderLink(label, path, attr)
: hasChildren
? this.renderLabelAndChildren(entry)
: this.renderLabel(label, attr);
const expanded = {
"highlighted": !!attr?.highlight,
"pf-m-expanded": this.expanded.has(entry),
"pf-m-expandable": hasChildren,
};
return html`<li class="pf-c-nav__item ${classMap(expanded)}">${content}</li>`;
}
toLinkClasses(attr: SidebarAttributes) {
return {
"pf-m-current": !!attr.isActive,
"pf-c-nav__link": true,
"highlight": !!(typeof attr.highlight === "function"
? attr.highlight()
: attr.highlight),
};
}
renderLabel(label: string, attr: SidebarAttributes = {}) {
return html`<div class=${classMap(this.toLinkClasses(attr))}>${label}</div>`;
}
// note the responsibilities pushed up to the caller
renderLink(label: string, path: string | SidebarEventHandler, attr: SidebarAttributes = {}) {
if (typeof path === "function") {
return html` <a @click=${path} class=${classMap(this.toLinkClasses(attr))}>
${label}
</a>`;
}
return html` <a
href="${attr.isAbsoluteLink ? "" : "#"}${path}"
class=${classMap(this.toLinkClasses(attr))}
>
${label}
</a>`;
}
renderChildren(children: SidebarEntry[]) {
return html`<section class="pf-c-nav__subnav">
<ul class="pf-c-nav__list">
${map(children, this.renderItem)}
</ul>
</section>`;
}
renderLabelAndChildren(entry: SidebarEntry): TemplateResult {
const handler = () => this.toggleExpand(entry);
return html` <div class="pf-c-nav__link">
<div class="ak-nav__link">${entry.label}</div>
<span class="pf-c-nav__toggle" @click=${handler}>
<span class="pf-c-nav__toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</span>
</div>
${this.expanded.has(entry) ? this.renderChildren(entry.children ?? []) : nothing}`;
}
renderLinkAndChildren(
label: string,
children: SidebarEntry[],
attr: SidebarAttributes = {}
): TemplateResult {
return html` <div class="pf-c-nav__link">
<div class="ak-nav__link">${label}</div>
<span class="pf-c-nav__toggle">
<span class="pf-c-nav__toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</span>
</div>
${attr.expanded ? this.renderChildren(children) : nothing}`;
}
}

View file

@ -18,9 +18,7 @@ import "@goauthentik/elements/notifications/APIDrawer";
import "@goauthentik/elements/notifications/NotificationDrawer";
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
import "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/sidebar/Sidebar";
import { DefaultTenant } from "@goauthentik/elements/sidebar/SidebarBrand";
import "@goauthentik/elements/sidebar/SidebarItem";
import { ROUTES } from "@goauthentik/user/Routes";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";