Files
authentik/web/src/admin/AdminInterface/AdminSidebar.ts
Ken Sternberg d539884204 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.
2023-11-16 10:38:36 -08:00

186 lines
8.3 KiB
TypeScript

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>${SLUG_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})$`]],
["/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>${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/tenants", msg("Tenants")],
["/crypto/certificates", msg("Certificates")],
["/outpost/integrations", msg("Outpost Integrations")]]],
...(enterpriseMenu)
];
return localSidebar.map(localToSidebarEntry);
}
render() {
return html` <ak-sidebar class="pf-c-page__sidebar" .entries=${this.sidebarItems}></ak-sidebar> `;
}
}