From 9dfb0424a4ef29b372d1346e8a239217047a1d95 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 12 Jan 2024 13:40:41 -0800 Subject: [PATCH] web: move the license summary information into a top-level context. Rather than repeatedly fetching the license summary, this commit fetches it once at the top-level and keeps it until an EVENT_REFRESH reaches the top level. This prevents the FOUC (Flash Of Unavailable Content) while loading and awaiting the end of the load. --- ...plication-wizard-authentication-for-rac.ts | 4 +-- web/src/admin/common/ak-license-notice.ts | 23 ++++-------- .../PropertyMappingWizard.ts | 35 +++++++------------ web/src/admin/providers/ProviderWizard.ts | 27 +++++--------- web/src/elements/AuthentikContexts.ts | 6 +++- web/src/elements/Interface/Interface.ts | 27 +++++++++++++- .../Interface/licenseSummaryProvider.ts | 25 +++++++++++++ .../enterprise/EnterpriseStatusBanner.ts | 23 +++++------- 8 files changed, 94 insertions(+), 76 deletions(-) create mode 100644 web/src/elements/Interface/licenseSummaryProvider.ts diff --git a/web/src/admin/applications/wizard/methods/rac/ak-application-wizard-authentication-for-rac.ts b/web/src/admin/applications/wizard/methods/rac/ak-application-wizard-authentication-for-rac.ts index 92ec346c5..a4fb8c00b 100644 --- a/web/src/admin/applications/wizard/methods/rac/ak-application-wizard-authentication-for-rac.ts +++ b/web/src/admin/applications/wizard/methods/rac/ak-application-wizard-authentication-for-rac.ts @@ -83,7 +83,7 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel { required value="${provider?.connectionExpiry ?? "hours=8"}" help=${msg( - "Determines how long a session lasts before being disconnected and requiring re-authorization." + "Determines how long a session lasts before being disconnected and requiring re-authorization.", )} required > @@ -104,7 +104,7 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel { ?selected=${selected.has(mapping.pk)} > ${mapping.name} - ` + `, )}

diff --git a/web/src/admin/common/ak-license-notice.ts b/web/src/admin/common/ak-license-notice.ts index 4cc8acb6c..57fc461a3 100644 --- a/web/src/admin/common/ak-license-notice.ts +++ b/web/src/admin/common/ak-license-notice.ts @@ -1,31 +1,22 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import "@goauthentik/elements/Alert"; import { AKElement } from "@goauthentik/elements/Base"; +import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; import { msg } from "@lit/localize"; import { html, nothing } from "lit"; -import { customElement, state } from "lit/decorators.js"; - -import { EnterpriseApi } from "@goauthentik/api"; +import { customElement, property } from "lit/decorators.js"; @customElement("ak-license-notice") -export class AkLicenceNotice extends AKElement { - @state() - hasLicense = false; - - constructor() { - super(); - new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((enterprise) => { - this.hasLicense = enterprise.hasLicense; - }); - } +export class AkLicenceNotice extends WithLicenseSummary(AKElement) { + @property() + message = msg("This feature requires an enterprise license."); render() { - return this.hasLicense + return this.hasEnterpriseLicense ? nothing : html` - ${msg("Provider requires enterprise.")} + ${this.message} ${msg("Learn more")} `; diff --git a/web/src/admin/property-mappings/PropertyMappingWizard.ts b/web/src/admin/property-mappings/PropertyMappingWizard.ts index 4f0ab6122..15dc6047a 100644 --- a/web/src/admin/property-mappings/PropertyMappingWizard.ts +++ b/web/src/admin/property-mappings/PropertyMappingWizard.ts @@ -1,9 +1,11 @@ +import "@goauthentik/admin/common/ak-license-notice"; import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm"; import "@goauthentik/admin/property-mappings/PropertyMappingNotification"; import "@goauthentik/admin/property-mappings/PropertyMappingRACForm"; import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm"; import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm"; import "@goauthentik/admin/property-mappings/PropertyMappingTestForm"; +import { WithLicenseSummary } from "@goauthentik/app/elements/Interface/licenseSummaryProvider"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/forms/ProxyForm"; @@ -14,23 +16,20 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; import { msg, str } from "@lit/localize"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { CSSResult, TemplateResult, html, nothing } from "lit"; -import { property, state } from "lit/decorators.js"; +import { property } from "lit/decorators.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { EnterpriseApi, LicenseSummary, PropertymappingsApi, TypeCreate } from "@goauthentik/api"; +import { PropertymappingsApi, TypeCreate } from "@goauthentik/api"; @customElement("ak-property-mapping-wizard-initial") -export class InitialPropertyMappingWizardPage extends WizardPage { +export class InitialPropertyMappingWizardPage extends WithLicenseSummary(WizardPage) { @property({ attribute: false }) mappingTypes: TypeCreate[] = []; - @property({ attribute: false }) - enterprise?: LicenseSummary; - static get styles(): CSSResult[] { return [PFBase, PFForm, PFButton, PFRadio]; } @@ -50,6 +49,7 @@ export class InitialPropertyMappingWizardPage extends WizardPage { render(): TemplateResult { return html`

${this.mappingTypes.map((type) => { + const requiresEnteprise = type.requiresEnterprise && !this.hasEnterpriseLicense; return html`
- ${type.description} - ${type.requiresEnterprise && !this.enterprise?.hasLicense - ? html` - - ${msg("Provider require enterprise.")} - ${msg("Learn more")} - - ` - : nothing} + ${type.description} + ${requiresEnteprise + ? html`` + : nothing}
`; })}
`; @@ -92,16 +89,10 @@ export class PropertyMappingWizard extends AKElement { @property({ attribute: false }) mappingTypes: TypeCreate[] = []; - @state() - enterprise?: LicenseSummary; - async firstUpdated(): Promise { this.mappingTypes = await new PropertymappingsApi( DEFAULT_CONFIG, ).propertymappingsAllTypesList(); - this.enterprise = await new EnterpriseApi( - DEFAULT_CONFIG, - ).enterpriseLicenseSummaryRetrieve(); } render(): TemplateResult { diff --git a/web/src/admin/providers/ProviderWizard.ts b/web/src/admin/providers/ProviderWizard.ts index 094c784a5..ca80f995e 100644 --- a/web/src/admin/providers/ProviderWizard.ts +++ b/web/src/admin/providers/ProviderWizard.ts @@ -4,6 +4,7 @@ import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; import "@goauthentik/admin/providers/saml/SAMLProviderForm"; import "@goauthentik/admin/providers/saml/SAMLProviderImportForm"; +import { WithLicenseSummary } from "@goauthentik/app/elements/Interface/licenseSummaryProvider"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import "@goauthentik/elements/Alert"; import { AKElement } from "@goauthentik/elements/Base"; @@ -16,7 +17,7 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; import { msg, str } from "@lit/localize"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { CSSResult, TemplateResult, html, nothing } from "lit"; -import { property, state } from "lit/decorators.js"; +import { property } from "lit/decorators.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; @@ -24,16 +25,13 @@ import PFHint from "@patternfly/patternfly/components/Hint/hint.css"; import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { EnterpriseApi, LicenseSummary, ProvidersApi, TypeCreate } from "@goauthentik/api"; +import { ProvidersApi, TypeCreate } from "@goauthentik/api"; @customElement("ak-provider-wizard-initial") -export class InitialProviderWizardPage extends WizardPage { +export class InitialProviderWizardPage extends WithLicenseSummary(WizardPage) { @property({ attribute: false }) providerTypes: TypeCreate[] = []; - @property({ attribute: false }) - enterprise?: LicenseSummary; - static get styles(): CSSResult[] { return [PFBase, PFForm, PFHint, PFButton, PFRadio]; } @@ -74,6 +72,7 @@ export class InitialProviderWizardPage extends WizardPage { render(): TemplateResult { return html`
${this.providerTypes.map((type) => { + const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense; return html`
${type.description} - ${type.requiresEnterprise + ${requiresEnterprise ? html`` : nothing} @@ -111,9 +110,6 @@ export class ProviderWizard extends AKElement { @property({ attribute: false }) providerTypes: TypeCreate[] = []; - @state() - enterprise?: LicenseSummary; - @property({ attribute: false }) finalHandler: () => Promise = () => { return Promise.resolve(); @@ -121,9 +117,6 @@ export class ProviderWizard extends AKElement { async firstUpdated(): Promise { this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(); - this.enterprise = await new EnterpriseApi( - DEFAULT_CONFIG, - ).enterpriseLicenseSummaryRetrieve(); } render(): TemplateResult { @@ -136,11 +129,7 @@ export class ProviderWizard extends AKElement { return this.finalHandler(); }} > - + ${this.providerTypes.map((type) => { return html` diff --git a/web/src/elements/AuthentikContexts.ts b/web/src/elements/AuthentikContexts.ts index 02fa89316..7e3a1e78b 100644 --- a/web/src/elements/AuthentikContexts.ts +++ b/web/src/elements/AuthentikContexts.ts @@ -1,9 +1,13 @@ import { createContext } from "@lit-labs/context"; -import type { Config, CurrentTenant } from "@goauthentik/api"; +import type { Config, CurrentTenant, LicenseSummary } from "@goauthentik/api"; export const authentikConfigContext = createContext(Symbol("authentik-config-context")); +export const authentikEnterpriseContext = createContext( + Symbol("authentik-enterprise-context"), +); + export const authentikTenantContext = createContext( Symbol("authentik-tenant-context"), ); diff --git a/web/src/elements/Interface/Interface.ts b/web/src/elements/Interface/Interface.ts index b2470cfd2..5cdf7a082 100644 --- a/web/src/elements/Interface/Interface.ts +++ b/web/src/elements/Interface/Interface.ts @@ -1,7 +1,9 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { config, tenant } from "@goauthentik/common/api/config"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { authentikConfigContext, + authentikEnterpriseContext, authentikTenantContext, } from "@goauthentik/elements/AuthentikContexts"; import type { AdoptedStyleSheetsElement } from "@goauthentik/elements/types"; @@ -12,7 +14,8 @@ import { state } from "lit/decorators.js"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api"; +import type { Config, CurrentTenant, LicenseSummary } from "@goauthentik/api"; +import { EnterpriseApi, UiThemeEnum } from "@goauthentik/api"; import { AKElement } from "../Base"; @@ -63,11 +66,33 @@ export class Interface extends AKElement implements AkInterface { return this._tenant; } + _licenseSummaryContext = new ContextProvider(this, { + context: authentikEnterpriseContext, + initialValue: undefined, + }); + + _licenseSummary?: LicenseSummary; + + @state() + set licenseSummary(c: LicenseSummary) { + this._licenseSummary = c; + this._licenseSummaryContext.setValue(c); + this.requestUpdate(); + } + + get licenseSummary(): LicenseSummary | undefined { + return this._licenseSummary; + } + constructor() { super(); document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; tenant().then((tenant) => (this.tenant = tenant)); config().then((config) => (this.config = config)); + new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((enterprise) => { + this.licenseSummary = enterprise; + }); + this.dataset.akInterfaceRoot = "true"; } diff --git a/web/src/elements/Interface/licenseSummaryProvider.ts b/web/src/elements/Interface/licenseSummaryProvider.ts new file mode 100644 index 000000000..7e1d92537 --- /dev/null +++ b/web/src/elements/Interface/licenseSummaryProvider.ts @@ -0,0 +1,25 @@ +import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts"; + +import { consume } from "@lit-labs/context"; +import type { LitElement } from "lit"; + +import type { LicenseSummary } from "@goauthentik/api"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Constructor = abstract new (...args: any[]) => T; + +export function WithLicenseSummary>( + superclass: T, + subscribe = true +) { + abstract class WithEnterpriseProvider extends superclass { + @consume({ context: authentikEnterpriseContext, subscribe }) + public licenseSummary!: LicenseSummary; + + get hasEnterpriseLicense() { + return false; + } + } + + return WithEnterpriseProvider; +} diff --git a/web/src/elements/enterprise/EnterpriseStatusBanner.ts b/web/src/elements/enterprise/EnterpriseStatusBanner.ts index 09d376759..b3360fb59 100644 --- a/web/src/elements/enterprise/EnterpriseStatusBanner.ts +++ b/web/src/elements/enterprise/EnterpriseStatusBanner.ts @@ -1,19 +1,14 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKElement } from "@goauthentik/elements/Base"; +import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; import { msg } from "@lit/localize"; import { CSSResult, TemplateResult, html } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; +import { customElement, property } from "lit/decorators.js"; import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; -import { EnterpriseApi, LicenseSummary } from "@goauthentik/api"; - @customElement("ak-enterprise-status") -export class EnterpriseStatusBanner extends AKElement { - @state() - summary?: LicenseSummary; - +export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) { @property() interface: "admin" | "user" | "" = ""; @@ -21,12 +16,10 @@ export class EnterpriseStatusBanner extends AKElement { return [PFBanner]; } - async firstUpdated(): Promise { - this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve(); - } - renderBanner(): TemplateResult { - return html`
+ return html`
${msg("Warning: The current user count has exceeded the configured licenses.")} ${msg("Click here for more info.")}
`; @@ -35,12 +28,12 @@ export class EnterpriseStatusBanner extends AKElement { render(): TemplateResult { switch (this.interface.toLowerCase()) { case "admin": - if (this.summary?.showAdminWarning || this.summary?.readOnly) { + if (this.licenseSummary?.showAdminWarning || this.licenseSummary?.readOnly) { return this.renderBanner(); } break; case "user": - if (this.summary?.showUserWarning || this.summary?.readOnly) { + if (this.licenseSummary?.showUserWarning || this.licenseSummary?.readOnly) { return this.renderBanner(); } break;