web: abstract rootInterface()?.config?.capabilities.includes() into .can() (#7737)
				
					
				
			* 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();
```
* web: Added a README with a description of the applications' "mental model," essentially an architectural description.
* web: prettier had opinions about the README
* web: Jens requested that subscription be  by default, and it's the right call.
* 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();
```
* web: Added a README with a description of the applications' "mental model," essentially an architectural description.
* web: prettier had opinions about the README
* web: Jens requested that subscription be  by default, and it's the right call.
* web: adjust RAC to point to the (now independent) Interface.
- Also, removed redundant check.
			
			
This commit is contained in:
		| @ -1,20 +1,18 @@ | ||||
| import { config, tenant } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; | ||||
| import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; | ||||
| import { UIConfig } from "@goauthentik/common/ui/config"; | ||||
| import { adaptCSS } from "@goauthentik/common/utils"; | ||||
| import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; | ||||
| import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; | ||||
|  | ||||
| import { ContextProvider } from "@lit-labs/context"; | ||||
| import { localized } from "@lit/localize"; | ||||
| import { CSSResult, LitElement } from "lit"; | ||||
| import { state } from "lit/decorators.js"; | ||||
| import { LitElement } from "lit"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import ThemeDark from "@goauthentik/common/styles/theme-dark.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api"; | ||||
|  | ||||
| import { AdoptedStyleSheetsElement } from "./types"; | ||||
|  | ||||
| type AkInterface = HTMLElement & { | ||||
|     getTheme: () => Promise<UiThemeEnum>; | ||||
|     tenant?: CurrentTenant; | ||||
| @ -25,13 +23,6 @@ type AkInterface = HTMLElement & { | ||||
| export const rootInterface = <T extends AkInterface>(): T | undefined => | ||||
|     (document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined; | ||||
|  | ||||
| export function ensureCSSStyleSheet(css: CSSStyleSheet | CSSResult): CSSStyleSheet { | ||||
|     if (css instanceof CSSResult) { | ||||
|         return css.styleSheet!; | ||||
|     } | ||||
|     return css; | ||||
| } | ||||
|  | ||||
| let css: Promise<string[]> | undefined; | ||||
| function fetchCustomCSS(): Promise<string[]> { | ||||
|     if (!css) { | ||||
| @ -52,10 +43,6 @@ function fetchCustomCSS(): Promise<string[]> { | ||||
|     return css; | ||||
| } | ||||
|  | ||||
| export interface AdoptedStyleSheetsElement { | ||||
|     adoptedStyleSheets: readonly CSSStyleSheet[]; | ||||
| } | ||||
|  | ||||
| const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; | ||||
|  | ||||
| @localized() | ||||
| @ -175,49 +162,3 @@ export class AKElement extends LitElement { | ||||
|         this.requestUpdate(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export class Interface extends AKElement implements AkInterface { | ||||
|     @state() | ||||
|     tenant?: CurrentTenant; | ||||
|  | ||||
|     @state() | ||||
|     uiConfig?: UIConfig; | ||||
|  | ||||
|     _configContext = new ContextProvider(this, { | ||||
|         context: authentikConfigContext, | ||||
|         initialValue: undefined, | ||||
|     }); | ||||
|  | ||||
|     _config?: Config; | ||||
|  | ||||
|     @state() | ||||
|     set config(c: Config) { | ||||
|         this._config = c; | ||||
|         this._configContext.setValue(c); | ||||
|         this.requestUpdate(); | ||||
|     } | ||||
|  | ||||
|     get config(): Config | undefined { | ||||
|         return this._config; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; | ||||
|         tenant().then((tenant) => (this.tenant = tenant)); | ||||
|         config().then((config) => (this.config = config)); | ||||
|         this.dataset.akInterfaceRoot = "true"; | ||||
|     } | ||||
|  | ||||
|     _activateTheme(root: AdoptedStyleSheetsElement, theme: UiThemeEnum): void { | ||||
|         super._activateTheme(root, theme); | ||||
|         super._activateTheme(document, theme); | ||||
|     } | ||||
|  | ||||
|     async getTheme(): Promise<UiThemeEnum> { | ||||
|         if (!this.uiConfig) { | ||||
|             this.uiConfig = await uiConfig(); | ||||
|         } | ||||
|         return this.uiConfig.theme?.base || UiThemeEnum.Automatic; | ||||
|     } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Ken Sternberg
					Ken Sternberg