web: Replace calls to rootInterface()?.tenant? with a contextual this.tenant object (#7778)

* This commit abstracts access to the object `rootInterface()?.config?` into a single accessor,
`authentikConfig`, that can be mixed into any AKElement object that requires access to it.

Since access to `rootInterface()?.config?` is _universally_ used for a single (and repetitive)
boolean check, a separate accessor has been provided that converts all calls of the form:

``` javascript
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
```

into:

``` javascript
this.can(CapabilitiesEnum.CanImpersonate)
```

It does this via a Mixin, `WithCapabilitiesConfig`, which understands that these calls only make
sense in the context of a running, fully configured authentik instance, and that their purpose is to
inform authentik components of a user’s capabilities. The latter is why I don’t feel uncomfortable
turning a function call into a method; we should make it explicit that this is a relationship
between components.

The mixin has a single single field, `[WCC.capabilitiesConfig]`, where its association with the
upper-level configuration is made. If that syntax looks peculiar to you, good! I’ve used an explict
unique symbol as the field name; it is inaccessable an innumerable in the object list. The debugger
shows it only as:

    Symbol(): {
        cacheTimeout: 300
        cacheTimeoutFlows: 300
        cacheTimeoutPolicies: 300
        cacheTimeoutReputation: 300
        capabilities: (5) ['can_save_media', 'can_geo_ip', 'can_impersonate', 'can_debug', 'is_enterprise']
    }

Since you can’t reference it by identity, you can’t write to it. Until every browser supports actual
private fields, this is the best we can do; it does guarantee that field name collisions are
impossible, which is a win.

The mixin takes a second optional boolean; setting this to true will cause any web component using
the mixin to automatically schedule a re-render if the capabilities list changes.

The mixin is also generic; despite the "...into a Lit-Context" in the title, the internals of the
Mixin can be replaced with anything so long as the signature of `.can()` is preserved.

Because this work builds off the work I did to give the Sidebar access to the configuration without
ad-hoc retrieval or prop-drilling, it wasn’t necessary to create a new context for it. That will be
necessary for the following:

TODO:

``` javascript
rootInterface()?.uiConfig;
rootInterface()?.tenant;
me();
```

* This commit abstracts access to the object `rootInterface()?.tenant?` into a single accessor,
`tenant`, that can be mixed into any AKElement object that requires access to it.

Like `WithCapabilitiesConfig` and `WithAuthentikConfig`, this one is named `WithTenantConfig`.

TODO:

``` javascript
rootInterface()?.uiConfig;
me();
```

* web: Added a README with a description of the applications' "mental model," essentially an architectural description.

* web: prettier did a thing

* web: prettier had opinions about the README

* web: Jens requested that subscription be  by default, and it's the right call.

* web: Jens requested that the default subscription state for contexts be , and it's the right call.

* web: prettier having opinions after merging with dependent branch

* web: prettier still having opinions.
This commit is contained in:
Ken Sternberg
2024-01-08 13:03:00 -08:00
committed by GitHub
parent d555c0db41
commit a2dce3fb63
13 changed files with 81 additions and 42 deletions

View File

@ -1,7 +1,11 @@
import { createContext } from "@lit-labs/context";
import { type Config } from "@goauthentik/api";
import type { Config, CurrentTenant } from "@goauthentik/api";
export const authentikConfigContext = createContext<Config>(Symbol("authentik-config-context"));
export const authentikTenantContext = createContext<CurrentTenant>(
Symbol("authentik-tenant-context"),
);
export default authentikConfigContext;

View File

@ -1,6 +1,9 @@
import { config, tenant } from "@goauthentik/common/api/config";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import {
authentikConfigContext,
authentikTenantContext,
} from "@goauthentik/elements/AuthentikContexts";
import type { AdoptedStyleSheetsElement } from "@goauthentik/elements/types";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
@ -21,9 +24,6 @@ type AkInterface = HTMLElement & {
};
export class Interface extends AKElement implements AkInterface {
@state()
tenant?: CurrentTenant;
@state()
uiConfig?: UIConfig;
@ -45,6 +45,24 @@ export class Interface extends AKElement implements AkInterface {
return this._config;
}
_tenantContext = new ContextProvider(this, {
context: authentikTenantContext,
initialValue: undefined,
});
_tenant?: CurrentTenant;
@state()
set tenant(c: CurrentTenant) {
this._tenant = c;
this._tenantContext.setValue(c);
this.requestUpdate();
}
get tenant(): CurrentTenant | undefined {
return this._tenant;
}
constructor() {
super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)];

View File

@ -12,7 +12,7 @@ export function WithAuthentikConfig<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,
) {
class WithAkConfigProvider extends superclass {
abstract class WithAkConfigProvider extends superclass {
@consume({ context: authentikConfigContext, subscribe })
public authentikConfig!: Config;
}

View File

@ -0,0 +1,20 @@
import { authentikTenantContext } from "@goauthentik/elements/AuthentikContexts";
import { consume } from "@lit-labs/context";
import type { LitElement } from "lit";
import type { CurrentTenant } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
export function WithTenantConfig<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,
) {
abstract class WithTenantProvider extends superclass {
@consume({ context: authentikTenantContext, subscribe })
public tenant!: CurrentTenant;
}
return WithTenantProvider;
}

View File

@ -8,7 +8,8 @@ import {
} from "@goauthentik/common/constants";
import { currentInterface } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { AKElement } from "@goauthentik/elements/Base";
import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
@ -23,7 +24,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventsApi } from "@goauthentik/api";
@customElement("ak-page-header")
export class PageHeader extends AKElement {
export class PageHeader extends WithTenantConfig(AKElement) {
@property()
icon?: string;
@ -35,9 +36,8 @@ export class PageHeader extends AKElement {
@property()
set header(value: string) {
const tenant = rootInterface()?.tenant;
const currentIf = currentInterface();
let title = tenant?.brandingTitle || TITLE_DEFAULT;
let title = this.tenant?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") {
title = `${msg("Admin")} - ${title}`;
}

View File

@ -1,6 +1,6 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { first } from "@goauthentik/common/utils";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { AKElement } from "@goauthentik/elements/Base";
import { WithTenantConfig } from "@goauthentik/elements/Interface/tenantProvider";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
@ -27,7 +27,7 @@ export const DefaultTenant: CurrentTenant = {
};
@customElement("ak-sidebar-brand")
export class SidebarBrand extends AKElement {
export class SidebarBrand extends WithTenantConfig(AKElement) {
static get styles(): CSSResult[] {
return [
PFBase,
@ -85,10 +85,7 @@ export class SidebarBrand extends AKElement {
<a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand">
<img
src="${first(
rootInterface()?.tenant?.brandingLogo,
DefaultTenant.brandingLogo,
)}"
src=${this.tenant?.brandingLogo ?? DefaultTenant.brandingLogo}
alt="authentik Logo"
loading="lazy"
/>