import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants"; 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 { consume } from "@lit-labs/context"; import { msg, str } from "@lit/localize"; import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; 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 }) open = true; @state() version: Version["versionCurrent"] | null = null; @state() impersonation: UserSelf["username"] | null = null; @state() providerTypes: TypeCreate[] = []; @consume({ context: authentikConfigContext }) public config!: Config; constructor() { super(); new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => { this.version = version.versionCurrent; }); 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); } // This has to be a bound method so the event listener can be removed on disconnection as // needed. toggleOpen() { this.open = !this.open; } checkWidth() { // This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in // REMs. If that changes, this code will have to be adjusted as well. const minWidth = parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) * parseFloat(getRootStyle("font-size")); this.open = window.innerWidth >= minWidth; } connectedCallback() { super.connectedCallback(); window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); window.addEventListener("resize", this.checkWidth); // After connecting to the DOM, we can now perform this check to see if the sidebar should // be open by default. this.checkWidth(); } // The symmetry (☟, ☝) here is critical in that you want to start adding these handlers after // connection, and removing them before disconnection. disconnectedCallback() { window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); window.removeEventListener("resize", this.checkWidth); super.disconnectedCallback(); } updated() { // This is permissible as`:host.classList` is not one of the properties Lit uses as a // scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger // a browser reflow, which may trigger some other styling the application is monitoring, // triggering a re-render which triggers a browser reflow, ad infinitum. But we've been // living with that since jQuery, and it's both well-known and fortunately rare. this.classList.remove("pf-m-expanded", "pf-m-collapsed"); this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed"); } get sidebarItems(): SidebarEntry[] { const reload = () => new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => { window.location.reload(); }); // prettier-ignore 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_REGEX})$`]], ["/core/providers", msg("Providers"), [`^/core/providers/(?${ID_REGEX})$`], providerTypes], ["/outpost/outposts", msg("Outposts")]]], [null, msg("Events"), null, [ ["/events/log", msg("Logs"), [`^/events/log/(?${UUID_REGEX})$`]], ["/events/rules", msg("Notification Rules")], ["/events/transports", msg("Notification Transports")]]], [null, msg("Customisation"), 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_REGEX})$`]], ["/flow/stages", msg("Stages")], ["/flow/stages/prompts", msg("Prompts")]]], [null, msg("Directory"), null, [ ["/identity/users", msg("Users"), [`^/identity/users/(?${ID_REGEX})$`]], ["/identity/groups", msg("Groups"), [`^/identity/groups/(?${UUID_REGEX})$`]], ["/identity/roles", msg("Roles"), [`^/identity/roles/(?${UUID_REGEX})$`]], ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?${SLUG_REGEX})$`]], ["/core/tokens", msg("Tokens and App passwords")], ["/flow/stages/invitations", msg("Invitations")]]], [null, msg("System"), null, [ ["/core/tenants", msg("Tenants")], ["/crypto/certificates", msg("Certificates")], ["/outpost/integrations", msg("Outpost Integrations")]]], ...(enterpriseMenu) ]; return localSidebar.map(localToSidebarEntry); } render() { return html` `; } }