Compare commits
	
		
			49 Commits
		
	
	
		
			website/do
			...
			web/sideba
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b865ed4392 | |||
| f0afac0b87 | |||
| a31588668d | |||
| 9768684c3c | |||
| cde94c2377 | |||
| b0e852afca | |||
| e35cefb63e | |||
| 2a11356961 | |||
| a3673906c7 | |||
| f2834cc7e2 | |||
| 5b898bef01 | |||
| 6b9201907d | |||
| 2ec8932891 | |||
| a9886b047e | |||
| a0dfe7ce78 | |||
| c471428c6b | |||
| 83e934f80c | |||
| 5386f0f4c3 | |||
| d5875a597b | |||
| 25ecc21d6d | |||
| ff78f2f00a | |||
| 3c277f14c8 | |||
| d539884204 | |||
| 476adef4ea | |||
| 3e905cc956 | |||
| e3b1ba63a6 | |||
| 2aed74bd9f | |||
| 2545815f08 | |||
| 657089eac9 | |||
| 19e8b675ae | |||
| bdd92f63d8 | |||
| 829ad5d3f2 | |||
| 58639a5d03 | |||
| 67cae13f93 | |||
| 100a6f02f1 | |||
| 242e5b492b | |||
| 48495f3c53 | |||
| 77549753c2 | |||
| 3b19aa1915 | |||
| 6653bd8224 | |||
| 639a8ceb5a | |||
| 0449fd07c5 | |||
| 8e892373a1 | |||
| 8713a1d120 | |||
| 0123bf61ab | |||
| e8edbdb4ae | |||
| 83338f8c32 | |||
| e51b36c614 | |||
| 314d89b1b7 | 
@ -17,7 +17,6 @@ import "@goauthentik/elements/notifications/NotificationDrawer";
 | 
				
			|||||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
 | 
					import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
 | 
				
			||||||
import "@goauthentik/elements/router/RouterOutlet";
 | 
					import "@goauthentik/elements/router/RouterOutlet";
 | 
				
			||||||
import "@goauthentik/elements/sidebar/Sidebar";
 | 
					import "@goauthentik/elements/sidebar/Sidebar";
 | 
				
			||||||
import "@goauthentik/elements/sidebar/SidebarItem";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CSSResult, TemplateResult, css, html } from "lit";
 | 
					import { CSSResult, TemplateResult, css, html } from "lit";
 | 
				
			||||||
import { customElement, property, state } from "lit/decorators.js";
 | 
					import { customElement, property, state } from "lit/decorators.js";
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants";
 | 
					import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants";
 | 
				
			||||||
 | 
					import { eventActionLabels } from "@goauthentik/common/labels";
 | 
				
			||||||
import { me } from "@goauthentik/common/users";
 | 
					import { me } from "@goauthentik/common/users";
 | 
				
			||||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
					import { AKElement } from "@goauthentik/elements/Base";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -7,17 +8,63 @@ import {
 | 
				
			|||||||
    WithCapabilitiesConfig,
 | 
					    WithCapabilitiesConfig,
 | 
				
			||||||
} from "@goauthentik/elements/Interface/capabilitiesProvider";
 | 
					} from "@goauthentik/elements/Interface/capabilitiesProvider";
 | 
				
			||||||
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
 | 
					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/types";
 | 
				
			||||||
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
 | 
					import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
 | 
				
			||||||
import { spread } from "@open-wc/lit-helpers";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { msg, str } from "@lit/localize";
 | 
					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 { customElement, property, state } from "lit/decorators.js";
 | 
				
			||||||
import { map } from "lit/directives/map.js";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { AdminApi, CoreApi, UiThemeEnum, Version } from "@goauthentik/api";
 | 
					import { AdminApi } from "@goauthentik/api";
 | 
				
			||||||
 | 
					import { CoreApi, Version } from "@goauthentik/api";
 | 
				
			||||||
import type { SessionUser, UserSelf } from "@goauthentik/api";
 | 
					import type { SessionUser, UserSelf } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { flowDesignationTable } from "../flows/utils";
 | 
				
			||||||
 | 
					import ConnectionTypesController from "./SidebarEntries/ConnectionTypesController";
 | 
				
			||||||
 | 
					import PolicyTypesController from "./SidebarEntries/PolicyTypesController";
 | 
				
			||||||
 | 
					import PropertyMappingsController from "./SidebarEntries/PropertyMappingsController";
 | 
				
			||||||
 | 
					import ProviderTypesController from "./SidebarEntries/ProviderTypesController";
 | 
				
			||||||
 | 
					import SourceTypesController from "./SidebarEntries/SourceTypesController";
 | 
				
			||||||
 | 
					import StageTypesController from "./SidebarEntries/StageTypesController";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * AdminSidebar
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * The AdminSidebar has two responsibilities:
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 1. Control the styling of the sidebar host, specifically when to show it and whether to show
 | 
				
			||||||
 | 
					 *    it as an overlay or as a push.
 | 
				
			||||||
 | 
					 * 2. Control what content the sidebar will receive.  The sidebar takes a tree, maximally three deep,
 | 
				
			||||||
 | 
					 *    of type SidebarEventHandler.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SidebarUrl = string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type LocalSidebarEntry = [
 | 
				
			||||||
 | 
					    // - null: This entry is not a link.
 | 
				
			||||||
 | 
					    // - string: the url for the entry
 | 
				
			||||||
 | 
					    // - SidebarEventHandler: a function to run if the entry is clicked.
 | 
				
			||||||
 | 
					    SidebarUrl | SidebarEventHandler | null,
 | 
				
			||||||
 | 
					    // The visible text of the entry.
 | 
				
			||||||
 | 
					    string,
 | 
				
			||||||
 | 
					    // Attributes to which the sidebar responds. See the sidebar for details.
 | 
				
			||||||
 | 
					    (SidebarAttributes | string[] | null)?, // eslint-disable-line
 | 
				
			||||||
 | 
					    // Children of the entry
 | 
				
			||||||
 | 
					    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")
 | 
					@customElement("ak-admin-sidebar")
 | 
				
			||||||
export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
 | 
					export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
 | 
				
			||||||
    @property({ type: Boolean, reflect: true })
 | 
					    @property({ type: Boolean, reflect: true })
 | 
				
			||||||
@ -29,6 +76,13 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
 | 
				
			|||||||
    @state()
 | 
					    @state()
 | 
				
			||||||
    impersonation: UserSelf["username"] | null = null;
 | 
					    impersonation: UserSelf["username"] | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private connectionTypes = new ConnectionTypesController(this);
 | 
				
			||||||
 | 
					    private policyTypes = new PolicyTypesController(this);
 | 
				
			||||||
 | 
					    private propertyMapper = new PropertyMappingsController(this);
 | 
				
			||||||
 | 
					    private providerTypes = new ProviderTypesController(this);
 | 
				
			||||||
 | 
					    private sourceTypes = new SourceTypesController(this);
 | 
				
			||||||
 | 
					    private stageTypes = new StageTypesController(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
        new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
 | 
					        new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
 | 
				
			||||||
@ -74,19 +128,6 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
 | 
				
			|||||||
        super.disconnectedCallback();
 | 
					        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() {
 | 
					    updated() {
 | 
				
			||||||
        // This is permissible as`:host.classList` is not one of the properties Lit uses as a
 | 
					        // 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
 | 
					        // scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger
 | 
				
			||||||
@ -97,118 +138,86 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
 | 
				
			|||||||
        this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed");
 | 
					        this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    renderSidebarItems(): TemplateResult {
 | 
					    get sidebarItems(): SidebarEntry[] {
 | 
				
			||||||
        // 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[],
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // prettier-ignore
 | 
					 | 
				
			||||||
        const sidebarContent: SidebarEntry[] = [
 | 
					 | 
				
			||||||
            ["/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})$`]],
 | 
					 | 
				
			||||||
                ["/outpost/outposts", msg("Outposts")]]],
 | 
					 | 
				
			||||||
            [null, msg("Events"), null, [
 | 
					 | 
				
			||||||
                ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
 | 
					 | 
				
			||||||
                ["/events/rules", msg("Notification Rules")],
 | 
					 | 
				
			||||||
                ["/events/transports", msg("Notification Transports")]]],
 | 
					 | 
				
			||||||
            [null, msg("Customization"), null, [
 | 
					 | 
				
			||||||
                ["/policy/policies", msg("Policies")],
 | 
					 | 
				
			||||||
                ["/core/property-mappings", msg("Property Mappings")],
 | 
					 | 
				
			||||||
                ["/blueprints/instances", msg("Blueprints")],
 | 
					 | 
				
			||||||
                ["/policy/reputation", msg("Reputation scores")]]],
 | 
					 | 
				
			||||||
            [null, msg("Flows and Stages"), null, [
 | 
					 | 
				
			||||||
                ["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
 | 
					 | 
				
			||||||
                ["/flow/stages", msg("Stages")],
 | 
					 | 
				
			||||||
                ["/flow/stages/prompts", msg("Prompts")]]],
 | 
					 | 
				
			||||||
            [null, msg("Directory"), null, [
 | 
					 | 
				
			||||||
                ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
 | 
					 | 
				
			||||||
                ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
 | 
					 | 
				
			||||||
                ["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
 | 
					 | 
				
			||||||
                ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
 | 
					 | 
				
			||||||
                ["/core/tokens", msg("Tokens and App passwords")],
 | 
					 | 
				
			||||||
                ["/flow/stages/invitations", msg("Invitations")]]],
 | 
					 | 
				
			||||||
            [null, msg("System"), null, [
 | 
					 | 
				
			||||||
                ["/core/brands", msg("Brands")],
 | 
					 | 
				
			||||||
                ["/crypto/certificates", msg("Certificates")],
 | 
					 | 
				
			||||||
                ["/outpost/integrations", msg("Outpost Integrations")],
 | 
					 | 
				
			||||||
                ["/admin/settings", msg("Settings")]]],
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 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.renderEnterpriseMenu()}
 | 
					 | 
				
			||||||
        `;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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 = () =>
 | 
					        const reload = () =>
 | 
				
			||||||
            new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
 | 
					            new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
 | 
				
			||||||
                window.location.reload();
 | 
					                window.location.reload();
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return this.impersonation
 | 
					        // prettier-ignore
 | 
				
			||||||
            ? html`<ak-sidebar-item ?highlight=${true} @click=${reload}>
 | 
					        const newVersionMessage: LocalSidebarEntry[] =
 | 
				
			||||||
                  <span slot="label"
 | 
					            this.version && this.version !== VERSION
 | 
				
			||||||
                      >${msg(
 | 
					                ? [[ "https://goauthentik.io", msg("A newer version of the frontend is available."),
 | 
				
			||||||
                          str`You're currently impersonating ${this.impersonation}. Click to stop.`,
 | 
					                     { highlight: true }]]
 | 
				
			||||||
                      )}</span
 | 
					                : [];
 | 
				
			||||||
                  >
 | 
					
 | 
				
			||||||
              </ak-sidebar-item>`
 | 
					        // prettier-ignore
 | 
				
			||||||
            : nothing;
 | 
					        const impersonationMessage: LocalSidebarEntry[] = this.impersonation
 | 
				
			||||||
 | 
					            ? [[reload, msg(str`You're currently impersonating ${this.impersonation}. Click to stop.`)]]
 | 
				
			||||||
 | 
					            : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // prettier-ignore
 | 
				
			||||||
 | 
					        const enterpriseMenu: LocalSidebarEntry[] = this.can(CapabilitiesEnum.IsEnterprise)
 | 
				
			||||||
 | 
					            ? [[null, msg("Enterprise"), null, [["/enterprise/licenses", msg("Licenses")]]]]
 | 
				
			||||||
 | 
					            : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const flowTypes: LocalSidebarEntry[] = flowDesignationTable.map(([_designation, label]) => [
 | 
				
			||||||
 | 
					            `/flow/flows;${encodeURIComponent(JSON.stringify({ search: label }))}`,
 | 
				
			||||||
 | 
					            label,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const eventTypes: LocalSidebarEntry[] = eventActionLabels.map(([_action, label]) => [
 | 
				
			||||||
 | 
					            `/events/log;${encodeURIComponent(JSON.stringify({ search: label }))}`,
 | 
				
			||||||
 | 
					            label,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 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}))?$`], this.providerTypes.entries()],
 | 
				
			||||||
 | 
					                ["/outpost/outposts", msg("Outposts")]]],
 | 
				
			||||||
 | 
					            [null, msg("Events"), null, [
 | 
				
			||||||
 | 
					                ["/events/log", msg("Logs"), [`^/events/log(/(?<id>${UUID_REGEX}))?$`], eventTypes],
 | 
				
			||||||
 | 
					                ["/events/rules", msg("Notification Rules")],
 | 
				
			||||||
 | 
					                ["/events/transports", msg("Notification Transports")]]],
 | 
				
			||||||
 | 
					            [null, msg("Customisation"), null, [
 | 
				
			||||||
 | 
					                ["/policy/policies", msg("Policies"), null, this.policyTypes.entries()],
 | 
				
			||||||
 | 
					                ["/core/property-mappings", msg("Property Mappings"), null, this.propertyMapper.entries()],
 | 
				
			||||||
 | 
					                ["/blueprints/instances", msg("Blueprints")],
 | 
				
			||||||
 | 
					                ["/policy/reputation", msg("Reputation scores")]]],
 | 
				
			||||||
 | 
					            [null, msg("Flows and Stages"), null, [
 | 
				
			||||||
 | 
					                ["/flow/flows", msg("Flows"), [`^/flow/flows(/(?<slug>${SLUG_REGEX}))?$`], flowTypes],
 | 
				
			||||||
 | 
					                ["/flow/stages", msg("Stages"), null, this.stageTypes.entries()],
 | 
				
			||||||
 | 
					                ["/flow/stages/prompts", msg("Prompts")]]],
 | 
				
			||||||
 | 
					            [null, msg("Directory"), null, [
 | 
				
			||||||
 | 
					                ["/identity/users", msg("Users"), [`^/identity/users(/(?<id>${ID_REGEX}))?$`]],
 | 
				
			||||||
 | 
					                ["/identity/groups", msg("Groups"), [`^/identity/groups(/(?<id>${UUID_REGEX}))?$`]],
 | 
				
			||||||
 | 
					                ["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
 | 
				
			||||||
 | 
					                ["/core/sources", msg("Federation and Social login"), [`^/core/sources(/(?<slug>${SLUG_REGEX}))?$`], this.sourceTypes.entries()],
 | 
				
			||||||
 | 
					                ["/core/tokens", msg("Tokens and App passwords")],
 | 
				
			||||||
 | 
					                ["/flow/stages/invitations", msg("Invitations")]]],
 | 
				
			||||||
 | 
					             [null, msg("System"), null, [
 | 
				
			||||||
 | 
					                 ["/core/brands", msg("Brands")],
 | 
				
			||||||
 | 
					                 ["/crypto/certificates", msg("Certificates")],
 | 
				
			||||||
 | 
					                 ["/outpost/integrations", msg("Outpost Integrations"), null, this.connectionTypes.entries()],
 | 
				
			||||||
 | 
					                 ["/admin/settings", msg("Settings")]]],
 | 
				
			||||||
 | 
					            ...(enterpriseMenu)
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return localSidebar.map(localToSidebarEntry);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    renderEnterpriseMenu() {
 | 
					    render() {
 | 
				
			||||||
        return this.can(CapabilitiesEnum.IsEnterprise)
 | 
					        return html`
 | 
				
			||||||
            ? html`
 | 
					            <ak-sidebar class="pf-c-page__sidebar" .entries=${this.sidebarItems}></ak-sidebar>
 | 
				
			||||||
                  <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;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { OutpostsApi } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createTypesController } from "./GenericTypesController";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ConnectionTypesController = createTypesController(
 | 
				
			||||||
 | 
					    () => new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList(),
 | 
				
			||||||
 | 
					    "/outpost/integrations",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ConnectionTypesController;
 | 
				
			||||||
@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					import { ReactiveControllerHost } from "lit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { TypeCreate } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { LocalSidebarEntry } from "../AdminSidebar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					type Fetcher = () => Promise<TypeCreate[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const typeCreateToSidebar = (baseUrl: string, tcreate: TypeCreate[]): LocalSidebarEntry[] =>
 | 
				
			||||||
 | 
					    tcreate.map((t) => [
 | 
				
			||||||
 | 
					        `${baseUrl};${encodeURIComponent(JSON.stringify({ search: t.name }))}`,
 | 
				
			||||||
 | 
					        t.name,
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * createTypesController
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * The Sidebar accesses a number objects of `TypeCreate`, which all have the exact same type, just
 | 
				
			||||||
 | 
					 * different accessors for generating the lists and different paths to which they respond. This
 | 
				
			||||||
 | 
					 * function is a template for a (simple) reactive controller that fetches the data for that type on
 | 
				
			||||||
 | 
					 * construction, then informs the host that the data is available.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * TODO (2023-11-17): This function is unlikely to survive in this form. It would be nice if it were more
 | 
				
			||||||
 | 
					 * generic, able to take a converter that can handle more that TypeCreate[] as its inbound argument,
 | 
				
			||||||
 | 
					 * since we need to refine what's displayed and on what the search is conducted.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createTypesController(
 | 
				
			||||||
 | 
					    fetch: Fetcher,
 | 
				
			||||||
 | 
					    path: string,
 | 
				
			||||||
 | 
					    converter = typeCreateToSidebar,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    return class GenericTypesController {
 | 
				
			||||||
 | 
					        createTypes: TypeCreate[] = [];
 | 
				
			||||||
 | 
					        host: ReactiveControllerHost;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        constructor(host: ReactiveControllerHost) {
 | 
				
			||||||
 | 
					            this.host = host;
 | 
				
			||||||
 | 
					            fetch().then((types) => {
 | 
				
			||||||
 | 
					                this.createTypes = types;
 | 
				
			||||||
 | 
					                host.requestUpdate();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        entries(): LocalSidebarEntry[] {
 | 
				
			||||||
 | 
					            return converter(path, this.createTypes);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default createTypesController;
 | 
				
			||||||
@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { PoliciesApi } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createTypesController } from "./GenericTypesController";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PolicyTypesController = createTypesController(
 | 
				
			||||||
 | 
					    () => new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList(),
 | 
				
			||||||
 | 
					    "/policy/policies",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default PolicyTypesController;
 | 
				
			||||||
@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { PropertymappingsApi } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createTypesController } from "./GenericTypesController";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PropertyMappingsController = createTypesController(
 | 
				
			||||||
 | 
					    () => new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTypesList(),
 | 
				
			||||||
 | 
					    "/core/property-mappings",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default PropertyMappingsController;
 | 
				
			||||||
@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ProvidersApi } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createTypesController } from "./GenericTypesController";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ProviderTypesController = createTypesController(
 | 
				
			||||||
 | 
					    () => new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(),
 | 
				
			||||||
 | 
					    "/core/providers",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ProviderTypesController;
 | 
				
			||||||
@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SourcesApi } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createTypesController } from "./GenericTypesController";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SourceTypesController = createTypesController(
 | 
				
			||||||
 | 
					    () => new SourcesApi(DEFAULT_CONFIG).sourcesAllTypesList(),
 | 
				
			||||||
 | 
					    "/core/sources",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default SourceTypesController;
 | 
				
			||||||
@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { StagesApi } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createTypesController } from "./GenericTypesController";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StageTypesController = createTypesController(
 | 
				
			||||||
 | 
					    () => new StagesApi(DEFAULT_CONFIG).stagesAllTypesList(),
 | 
				
			||||||
 | 
					    "/flow/stages",
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default StageTypesController;
 | 
				
			||||||
@ -6,40 +6,33 @@ export function RenderFlowOption(flow: Flow): string {
 | 
				
			|||||||
    return `${flow.slug} (${flow.name})`;
 | 
					    return `${flow.slug} (${flow.name})`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FlowDesignationPair = [FlowDesignationEnum, string];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const flowDesignationTable: FlowDesignationPair[] = [
 | 
				
			||||||
 | 
					    [FlowDesignationEnum.Authentication, msg("Authentication")],
 | 
				
			||||||
 | 
					    [FlowDesignationEnum.Authorization, msg("Authorization")],
 | 
				
			||||||
 | 
					    [FlowDesignationEnum.Enrollment, msg("Enrollment")],
 | 
				
			||||||
 | 
					    [FlowDesignationEnum.Invalidation, msg("Invalidation")],
 | 
				
			||||||
 | 
					    [FlowDesignationEnum.Recovery, msg("Recovery")],
 | 
				
			||||||
 | 
					    [FlowDesignationEnum.StageConfiguration, msg("Stage Configuration")],
 | 
				
			||||||
 | 
					    [FlowDesignationEnum.Unenrollment, msg("Unenrollment")],
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// prettier-ignore
 | 
				
			||||||
 | 
					const flowDesignations = new Map(flowDesignationTable);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function DesignationToLabel(designation: FlowDesignationEnum): string {
 | 
					export function DesignationToLabel(designation: FlowDesignationEnum): string {
 | 
				
			||||||
    switch (designation) {
 | 
					    return flowDesignations.get(designation) ?? msg("Unknown designation");
 | 
				
			||||||
        case FlowDesignationEnum.Authentication:
 | 
					 | 
				
			||||||
            return msg("Authentication");
 | 
					 | 
				
			||||||
        case FlowDesignationEnum.Authorization:
 | 
					 | 
				
			||||||
            return msg("Authorization");
 | 
					 | 
				
			||||||
        case FlowDesignationEnum.Enrollment:
 | 
					 | 
				
			||||||
            return msg("Enrollment");
 | 
					 | 
				
			||||||
        case FlowDesignationEnum.Invalidation:
 | 
					 | 
				
			||||||
            return msg("Invalidation");
 | 
					 | 
				
			||||||
        case FlowDesignationEnum.Recovery:
 | 
					 | 
				
			||||||
            return msg("Recovery");
 | 
					 | 
				
			||||||
        case FlowDesignationEnum.StageConfiguration:
 | 
					 | 
				
			||||||
            return msg("Stage Configuration");
 | 
					 | 
				
			||||||
        case FlowDesignationEnum.Unenrollment:
 | 
					 | 
				
			||||||
            return msg("Unenrollment");
 | 
					 | 
				
			||||||
        case FlowDesignationEnum.UnknownDefaultOpenApi:
 | 
					 | 
				
			||||||
            return msg("Unknown designation");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const layoutToLabel = new Map<FlowLayoutEnum, string>([
 | 
				
			||||||
 | 
					    [FlowLayoutEnum.Stacked, msg("Stacked")],
 | 
				
			||||||
 | 
					    [FlowLayoutEnum.ContentLeft, msg("Content left")],
 | 
				
			||||||
 | 
					    [FlowLayoutEnum.ContentRight, msg("Content right")],
 | 
				
			||||||
 | 
					    [FlowLayoutEnum.SidebarLeft, msg("Sidebar left")],
 | 
				
			||||||
 | 
					    [FlowLayoutEnum.SidebarRight, msg("Sidebar right")],
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function LayoutToLabel(layout: FlowLayoutEnum): string {
 | 
					export function LayoutToLabel(layout: FlowLayoutEnum): string {
 | 
				
			||||||
    switch (layout) {
 | 
					    return layoutToLabel.get(layout) ?? msg("Unknown layout");
 | 
				
			||||||
        case FlowLayoutEnum.Stacked:
 | 
					 | 
				
			||||||
            return msg("Stacked");
 | 
					 | 
				
			||||||
        case FlowLayoutEnum.ContentLeft:
 | 
					 | 
				
			||||||
            return msg("Content left");
 | 
					 | 
				
			||||||
        case FlowLayoutEnum.ContentRight:
 | 
					 | 
				
			||||||
            return msg("Content right");
 | 
					 | 
				
			||||||
        case FlowLayoutEnum.SidebarLeft:
 | 
					 | 
				
			||||||
            return msg("Sidebar left");
 | 
					 | 
				
			||||||
        case FlowLayoutEnum.SidebarRight:
 | 
					 | 
				
			||||||
            return msg("Sidebar right");
 | 
					 | 
				
			||||||
        case FlowLayoutEnum.UnknownDefaultOpenApi:
 | 
					 | 
				
			||||||
            return msg("Unknown layout");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,8 @@ import { msg } from "@lit/localize";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { Device, EventActions, IntentEnum, SeverityEnum, UserTypeEnum } from "@goauthentik/api";
 | 
					import { Device, EventActions, IntentEnum, SeverityEnum, UserTypeEnum } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Pair<T> = [T, string];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Various tables in the API for which we need to supply labels */
 | 
					/* Various tables in the API for which we need to supply labels */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const intentEnumToLabel = new Map<IntentEnum, string>([
 | 
					export const intentEnumToLabel = new Map<IntentEnum, string>([
 | 
				
			||||||
@ -14,7 +16,7 @@ export const intentEnumToLabel = new Map<IntentEnum, string>([
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const intentToLabel = (intent: IntentEnum) => intentEnumToLabel.get(intent);
 | 
					export const intentToLabel = (intent: IntentEnum) => intentEnumToLabel.get(intent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const eventActionToLabel = new Map<EventActions | undefined, string>([
 | 
					export const eventActionLabels: Pair<EventActions>[] = [
 | 
				
			||||||
    [EventActions.Login, msg("Login")],
 | 
					    [EventActions.Login, msg("Login")],
 | 
				
			||||||
    [EventActions.LoginFailed, msg("Failed login")],
 | 
					    [EventActions.LoginFailed, msg("Failed login")],
 | 
				
			||||||
    [EventActions.Logout, msg("Logout")],
 | 
					    [EventActions.Logout, msg("Logout")],
 | 
				
			||||||
@ -43,7 +45,9 @@ export const eventActionToLabel = new Map<EventActions | undefined, string>([
 | 
				
			|||||||
    [EventActions.ModelDeleted, msg("Model deleted")],
 | 
					    [EventActions.ModelDeleted, msg("Model deleted")],
 | 
				
			||||||
    [EventActions.EmailSent, msg("Email sent")],
 | 
					    [EventActions.EmailSent, msg("Email sent")],
 | 
				
			||||||
    [EventActions.UpdateAvailable, msg("Update available")],
 | 
					    [EventActions.UpdateAvailable, msg("Update available")],
 | 
				
			||||||
]);
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const eventActionToLabel = new Map<EventActions | undefined, string>(eventActionLabels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const actionToLabel = (action?: EventActions): string =>
 | 
					export const actionToLabel = (action?: EventActions): string =>
 | 
				
			||||||
    eventActionToLabel.get(action) ?? action ?? "";
 | 
					    eventActionToLabel.get(action) ?? action ?? "";
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										56
									
								
								web/src/elements/sidebar/Sidebar.css.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								web/src/elements/sidebar/Sidebar.css.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					import { css } from "lit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sidebarStyles = [
 | 
				
			||||||
 | 
					    PFBase,
 | 
				
			||||||
 | 
					    PFPage,
 | 
				
			||||||
 | 
					    PFNav,
 | 
				
			||||||
 | 
					    css`
 | 
				
			||||||
 | 
					        :host {
 | 
				
			||||||
 | 
					            z-index: 100;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        :host([theme="light"]) {
 | 
				
			||||||
 | 
					            border-right-color: transparent !important;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ak-sidebar-items {
 | 
				
			||||||
 | 
					            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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
@ -1,79 +1,32 @@
 | 
				
			|||||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
					import { AKElement } from "@goauthentik/elements/Base";
 | 
				
			||||||
import "@goauthentik/elements/sidebar/SidebarBrand";
 | 
					import "@goauthentik/elements/sidebar/SidebarBrand";
 | 
				
			||||||
 | 
					import "@goauthentik/elements/sidebar/SidebarItems";
 | 
				
			||||||
import "@goauthentik/elements/sidebar/SidebarUser";
 | 
					import "@goauthentik/elements/sidebar/SidebarUser";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CSSResult, TemplateResult, css, html } from "lit";
 | 
					import { 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";
 | 
					 | 
				
			||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { UiThemeEnum } from "@goauthentik/api";
 | 
					import { UiThemeEnum } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { sidebarStyles } from "./Sidebar.css.js";
 | 
				
			||||||
 | 
					import type { SidebarEntry } from "./types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ak-sidebar")
 | 
					@customElement("ak-sidebar")
 | 
				
			||||||
export class Sidebar extends AKElement {
 | 
					export class Sidebar extends AKElement {
 | 
				
			||||||
    static get styles(): CSSResult[] {
 | 
					    @property({ type: Array })
 | 
				
			||||||
        return [
 | 
					    entries: SidebarEntry[] = [];
 | 
				
			||||||
            PFBase,
 | 
					 | 
				
			||||||
            PFPage,
 | 
					 | 
				
			||||||
            PFNav,
 | 
					 | 
				
			||||||
            css`
 | 
					 | 
				
			||||||
                :host {
 | 
					 | 
				
			||||||
                    z-index: 100;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                .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;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                :host([theme="light"]) {
 | 
					 | 
				
			||||||
                    border-right-color: transparent !important;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                .pf-c-nav__section + .pf-c-nav__section {
 | 
					    static get styles() {
 | 
				
			||||||
                    --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm);
 | 
					        return sidebarStyles;
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                .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;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            `,
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render(): TemplateResult {
 | 
					    render() {
 | 
				
			||||||
        return html`<nav
 | 
					        return html`<nav
 | 
				
			||||||
            class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}"
 | 
					            class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}"
 | 
				
			||||||
            aria-label="Global"
 | 
					            aria-label="Global"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
            <ak-sidebar-brand></ak-sidebar-brand>
 | 
					            <ak-sidebar-brand></ak-sidebar-brand>
 | 
				
			||||||
            <ul class="pf-c-nav__list">
 | 
					            <ak-sidebar-items .entries=${this.entries}></ak-sidebar-items>
 | 
				
			||||||
                <slot></slot>
 | 
					 | 
				
			||||||
            </ul>
 | 
					 | 
				
			||||||
            <ak-sidebar-user></ak-sidebar-user>
 | 
					            <ak-sidebar-user></ak-sidebar-user>
 | 
				
			||||||
        </nav>`;
 | 
					        </nav>`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										86
									
								
								web/src/elements/sidebar/SidebarItems.css.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								web/src/elements/sidebar/SidebarItems.css.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					import { css } from "lit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sidebarItemStyles = [
 | 
				
			||||||
 | 
					    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__item .pf-c-nav__item::before {
 | 
				
			||||||
 | 
					            border-bottom-width: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .pf-c-nav__toggle {
 | 
				
			||||||
 | 
					            width: calc(var(--pf-c-nav__toggle--FontSize) + calc(2 * var(--pf-global--spacer--md)));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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__link a {
 | 
				
			||||||
 | 
					            flex: 1 0 max-content;
 | 
				
			||||||
 | 
					            color: var(--pf-c-nav__link--Color);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        a.pf-c-nav__link:hover {
 | 
				
			||||||
 | 
					            color: var(--pf-c-nav__link--Color);
 | 
				
			||||||
 | 
					            text-decoration: var(--pf-global--link--TextDecoration--hover);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
							
								
								
									
										247
									
								
								web/src/elements/sidebar/SidebarItems.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								web/src/elements/sidebar/SidebarItems.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,247 @@
 | 
				
			|||||||
 | 
					import { ROUTE_SEPARATOR } from "@goauthentik/common/constants";
 | 
				
			||||||
 | 
					import { AKElement } from "@goauthentik/elements/Base";
 | 
				
			||||||
 | 
					import { findTable } from "@goauthentik/elements/table/TablePage";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { TemplateResult, 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 { UiThemeEnum } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { sidebarItemStyles } from "./SidebarItems.css.js";
 | 
				
			||||||
 | 
					import type { SidebarEntry } from "./types";
 | 
				
			||||||
 | 
					import { entryKey, findMatchForNavbarUrl, makeParentMap } from "./utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Display the sidebar item tree.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Along with the `reclick()` complaint down below, the other thing I dislike about this design is
 | 
				
			||||||
 | 
					 * that it's effectively two different programs glued together. The first responds to the `click`
 | 
				
			||||||
 | 
					 * and performs the navigation, which either triggers the router or triggers a new search on the
 | 
				
			||||||
 | 
					 * existing view. The second responds to the navigation change event when the URL is changed by the
 | 
				
			||||||
 | 
					 * navigation event, at which point it figures out which entry to highlight as "current," which
 | 
				
			||||||
 | 
					 * causes the re-render.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("ak-sidebar-items")
 | 
				
			||||||
 | 
					export class SidebarItems extends AKElement {
 | 
				
			||||||
 | 
					    static get styles() {
 | 
				
			||||||
 | 
					        return sidebarItemStyles;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property({ type: Array })
 | 
				
			||||||
 | 
					    entries: SidebarEntry[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expanded: Set<string> = new Set();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @state()
 | 
				
			||||||
 | 
					    current = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        super();
 | 
				
			||||||
 | 
					        this.renderItem = this.renderItem.bind(this);
 | 
				
			||||||
 | 
					        this.toggleExpand = this.toggleExpand.bind(this);
 | 
				
			||||||
 | 
					        this.onHashChange = this.onHashChange.bind(this);
 | 
				
			||||||
 | 
					        this.reclick = this.reclick.bind(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connectedCallback() {
 | 
				
			||||||
 | 
					        super.connectedCallback();
 | 
				
			||||||
 | 
					        this.onHashChange();
 | 
				
			||||||
 | 
					        window.addEventListener("hashchange", this.onHashChange);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    disconnectedCallback() {
 | 
				
			||||||
 | 
					        window.removeEventListener("hashchange", this.onHashChange);
 | 
				
			||||||
 | 
					        super.disconnectedCallback();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expandParents(entry: SidebarEntry) {
 | 
				
			||||||
 | 
					        const reverseMap = makeParentMap(this.entries);
 | 
				
			||||||
 | 
					        let start: SidebarEntry | undefined = reverseMap.get(entry);
 | 
				
			||||||
 | 
					        while (start) {
 | 
				
			||||||
 | 
					            this.expanded.add(entryKey(start));
 | 
				
			||||||
 | 
					            start = reverseMap.get(start);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onHashChange() {
 | 
				
			||||||
 | 
					        this.current = "";
 | 
				
			||||||
 | 
					        const match = findMatchForNavbarUrl(this.entries);
 | 
				
			||||||
 | 
					        if (match) {
 | 
				
			||||||
 | 
					            this.current = entryKey(match);
 | 
				
			||||||
 | 
					            this.expandParents(match);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    toggleExpand(entry: SidebarEntry) {
 | 
				
			||||||
 | 
					        const key = entryKey(entry);
 | 
				
			||||||
 | 
					        if (this.expanded.has(key)) {
 | 
				
			||||||
 | 
					            this.expanded.delete(key);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.expanded.add(key);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.requestUpdate();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // This is gross and feels like 2007: using a path from the root through the shadowDoms (see
 | 
				
			||||||
 | 
					    // `TablePage:findTable()`), this code finds the element that *should* be triggered by an event
 | 
				
			||||||
 | 
					    // on the URL, and forcibly injects the text of the search and the click of the search button.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reclick(ev: Event, path: string) {
 | 
				
			||||||
 | 
					        const oldPath = window.location.hash.split(ROUTE_SEPARATOR)[0];
 | 
				
			||||||
 | 
					        const [curPath, ...curSearchComponents] = path.split(ROUTE_SEPARATOR);
 | 
				
			||||||
 | 
					        const curSearch: string =
 | 
				
			||||||
 | 
					            curSearchComponents.length > 0 ? curSearchComponents.join(ROUTE_SEPARATOR) : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (curPath !== oldPath) {
 | 
				
			||||||
 | 
					            // A Tier 1 or Tier 2 change should be handled by the router. (So should a Tier 3
 | 
				
			||||||
 | 
					            // change, but... here we are.)
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const table = findTable();
 | 
				
			||||||
 | 
					        if (!table) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Always wrap the minimal exceptional code possible in an IIFE and supply the failure
 | 
				
			||||||
 | 
					        // alternative. Turn exceptions into expressions with the smallest functional rewind
 | 
				
			||||||
 | 
					        // whenever possible.
 | 
				
			||||||
 | 
					        const search = (() => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                return curSearch ? JSON.parse(decodeURIComponent(curSearch)) : { search: "" };
 | 
				
			||||||
 | 
					            } catch {
 | 
				
			||||||
 | 
					                return { search: "" };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ("search" in search) {
 | 
				
			||||||
 | 
					            ev.preventDefault();
 | 
				
			||||||
 | 
					            ev.stopPropagation();
 | 
				
			||||||
 | 
					            table.search = search.search;
 | 
				
			||||||
 | 
					            table.fetch();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render(): TemplateResult {
 | 
				
			||||||
 | 
					        console.log("C:", this.current);
 | 
				
			||||||
 | 
					        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>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderItem(entry: SidebarEntry) {
 | 
				
			||||||
 | 
					        // Ensure the attributes are undefined, not null; they can be null in the placeholders, but
 | 
				
			||||||
 | 
					        // not when being forwarded to the correct renderer.
 | 
				
			||||||
 | 
					        const hasChildren = !!(entry.children && entry.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 (entry.attributes?.expanded) {
 | 
				
			||||||
 | 
					            this.expanded.add(entryKey(entry));
 | 
				
			||||||
 | 
					            delete entry.attributes.expanded;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const content =
 | 
				
			||||||
 | 
					            entry.path && hasChildren
 | 
				
			||||||
 | 
					                ? this.renderLinkAndChildren(entry)
 | 
				
			||||||
 | 
					                : hasChildren
 | 
				
			||||||
 | 
					                  ? this.renderLabelAndChildren(entry)
 | 
				
			||||||
 | 
					                  : entry.path
 | 
				
			||||||
 | 
					                    ? this.renderLink(entry)
 | 
				
			||||||
 | 
					                    : this.renderLabel(entry);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const expanded = {
 | 
				
			||||||
 | 
					            "highlighted": !!entry.attributes?.highlight,
 | 
				
			||||||
 | 
					            "pf-m-expanded": this.expanded.has(entryKey(entry)),
 | 
				
			||||||
 | 
					            "pf-m-expandable": hasChildren,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return html`<li class="pf-c-nav__item ${classMap(expanded)}">${content}</li>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getLinkClasses(entry: SidebarEntry) {
 | 
				
			||||||
 | 
					        const a = entry.attributes ?? {};
 | 
				
			||||||
 | 
					        const key = entryKey(entry);
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            "pf-m-current": key === this.current,
 | 
				
			||||||
 | 
					            "pf-c-nav__link": true,
 | 
				
			||||||
 | 
					            "highlight": !!(typeof a.highlight === "function" ? a.highlight() : a.highlight),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderLabel(entry: SidebarEntry) {
 | 
				
			||||||
 | 
					        return html`<div class=${classMap(this.getLinkClasses(entry))}>${entry.label}</div>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // note the responsibilities pushed up to the caller
 | 
				
			||||||
 | 
					    renderLink(entry: SidebarEntry) {
 | 
				
			||||||
 | 
					        if (typeof entry.path === "function") {
 | 
				
			||||||
 | 
					            return html` <a @click=${entry.path} class=${classMap(this.getLinkClasses(entry))}>
 | 
				
			||||||
 | 
					                ${entry.label}
 | 
				
			||||||
 | 
					            </a>`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const path = `${entry.attributes?.isAbsoluteLink ? "" : "#"}${entry.path}`;
 | 
				
			||||||
 | 
					        return html` <a
 | 
				
			||||||
 | 
					            href=${path}
 | 
				
			||||||
 | 
					            @click=${(ev: Event) => this.reclick(ev, path)}
 | 
				
			||||||
 | 
					            class=${classMap(this.getLinkClasses(entry))}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            ${entry.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);
 | 
				
			||||||
 | 
					        const current = { "pf-m-current": this.current === entryKey(entry) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return html` <div class="pf-c-nav__link  ${classMap(current)}">
 | 
				
			||||||
 | 
					                <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(entryKey(entry))
 | 
				
			||||||
 | 
					                ? this.renderChildren(entry.children ?? [])
 | 
				
			||||||
 | 
					                : nothing}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderLinkAndChildren(entry: SidebarEntry): TemplateResult {
 | 
				
			||||||
 | 
					        const handler = () => this.toggleExpand(entry);
 | 
				
			||||||
 | 
					        const current = { "pf-m-current": this.current === entryKey(entry) };
 | 
				
			||||||
 | 
					        const path = `${entry.attributes?.isAbsoluteLink ? "" : "#"}${entry.path}`;
 | 
				
			||||||
 | 
					        return html` <div class="pf-c-nav__link ${classMap(current)}">
 | 
				
			||||||
 | 
					                <a
 | 
				
			||||||
 | 
					                    href=${path}
 | 
				
			||||||
 | 
					                    @click=${(ev: Event) => this.reclick(ev, path)}
 | 
				
			||||||
 | 
					                    class="ak-nav__link"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    ${entry.label}
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					                <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(entryKey(entry))
 | 
				
			||||||
 | 
					                ? this.renderChildren(entry.children ?? [])
 | 
				
			||||||
 | 
					                : nothing}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								web/src/elements/sidebar/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web/src/elements/sidebar/types.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { TemplateResult } from "lit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
							
								
								
									
										60
									
								
								web/src/elements/sidebar/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								web/src/elements/sidebar/utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import { ROUTE_SEPARATOR } from "@goauthentik/common/constants";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SidebarEntry } from "./types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function entryKey(entry: SidebarEntry) {
 | 
				
			||||||
 | 
					    return `${entry.path || "no-path"}:${entry.label}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// "Never store what you can calculate." (At least, if it's cheap.)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Takes tree and creates a map where every key is an entry in the tree and every value is that
 | 
				
			||||||
 | 
					 * entry's parent.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function makeParentMap(entries: SidebarEntry[]) {
 | 
				
			||||||
 | 
					    const reverseMap = new WeakMap<SidebarEntry, SidebarEntry>();
 | 
				
			||||||
 | 
					    function reverse(entry: SidebarEntry) {
 | 
				
			||||||
 | 
					        (entry.children ?? []).forEach((e) => {
 | 
				
			||||||
 | 
					            reverseMap.set(e, entry);
 | 
				
			||||||
 | 
					            reverse(e);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    entries.forEach(reverse);
 | 
				
			||||||
 | 
					    return reverseMap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Given the current path and the collection of entries, identify which entry is currently live.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const trailingSlash = new RegExp("/$");
 | 
				
			||||||
 | 
					const fixed = (s: string) => s.replace(trailingSlash, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function scanner(entry: SidebarEntry, activePath: string): SidebarEntry | undefined {
 | 
				
			||||||
 | 
					    if (typeof entry.path === "string" && fixed(activePath) === fixed(entry.path)) {
 | 
				
			||||||
 | 
					        return entry;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const matcher of entry.attributes?.activeWhen ?? []) {
 | 
				
			||||||
 | 
					        const matchtest = new RegExp(matcher);
 | 
				
			||||||
 | 
					        if (matchtest.test(activePath)) {
 | 
				
			||||||
 | 
					            return entry;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (entry.children ?? []).find((e) => scanner(e, activePath));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function findMatchForNavbarUrl(entries: SidebarEntry[]) {
 | 
				
			||||||
 | 
					    const activePath = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
 | 
				
			||||||
 | 
					    for (const entry of entries) {
 | 
				
			||||||
 | 
					        const result = scanner(entry, activePath);
 | 
				
			||||||
 | 
					        if (result) {
 | 
				
			||||||
 | 
					            return result;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -20,6 +20,11 @@ export abstract class TablePage<T> extends Table<T> {
 | 
				
			|||||||
        return super.styles.concat(PFPage, PFContent, PFSidebar);
 | 
					        return super.styles.concat(PFPage, PFContent, PFSidebar);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        super();
 | 
				
			||||||
 | 
					        this.dataset.akApiTable = "true";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    renderSidebarBefore(): TemplateResult {
 | 
					    renderSidebarBefore(): TemplateResult {
 | 
				
			||||||
        return html``;
 | 
					        return html``;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -92,3 +97,18 @@ export abstract class TablePage<T> extends Table<T> {
 | 
				
			|||||||
            ${this.renderSectionAfter()}`;
 | 
					            ${this.renderSectionAfter()}`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This painstakingly researched path is nonetheless surprisingly robust; it works for every extant
 | 
				
			||||||
 | 
					// TablePage, but only because Jens has been utterly consistent in where he puts his TablePage
 | 
				
			||||||
 | 
					// elements with respect to the Interface object.  If we ever re-arrange this code, we're going
 | 
				
			||||||
 | 
					// to have to re-arrange this as well.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function findTable<T, U extends TablePage<T>>(): U | undefined {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        (document.body
 | 
				
			||||||
 | 
					            ?.querySelector("[data-ak-interface-root]")
 | 
				
			||||||
 | 
					            ?.shadowRoot?.querySelector("ak-locale-context")
 | 
				
			||||||
 | 
					            ?.querySelector("ak-router-outlet")
 | 
				
			||||||
 | 
					            ?.shadowRoot?.querySelector("[data-ak-api-table]") as U) ?? undefined
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user