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.
This commit is contained in:
Ken Sternberg 2024-01-12 13:40:41 -08:00
parent 88dafdcf3e
commit 9dfb0424a4
8 changed files with 94 additions and 76 deletions

View file

@ -83,7 +83,7 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel {
required required
value="${provider?.connectionExpiry ?? "hours=8"}" value="${provider?.connectionExpiry ?? "hours=8"}"
help=${msg( 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 required
></ak-text-input> ></ak-text-input>
@ -104,7 +104,7 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel {
?selected=${selected.has(mapping.pk)} ?selected=${selected.has(mapping.pk)}
> >
${mapping.name} ${mapping.name}
</option>` </option>`,
)} )}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">

View file

@ -1,31 +1,22 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert"; import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { EnterpriseApi } from "@goauthentik/api";
@customElement("ak-license-notice") @customElement("ak-license-notice")
export class AkLicenceNotice extends AKElement { export class AkLicenceNotice extends WithLicenseSummary(AKElement) {
@state() @property()
hasLicense = false; message = msg("This feature requires an enterprise license.");
constructor() {
super();
new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((enterprise) => {
this.hasLicense = enterprise.hasLicense;
});
}
render() { render() {
return this.hasLicense return this.hasEnterpriseLicense
? nothing ? nothing
: html` : html`
<ak-alert class="pf-c-radio__description" inline> <ak-alert class="pf-c-radio__description" inline>
${msg("Provider requires enterprise.")} ${this.message}
<a href="#/enterprise/licenses">${msg("Learn more")}</a> <a href="#/enterprise/licenses">${msg("Learn more")}</a>
</ak-alert> </ak-alert>
`; `;

View file

@ -1,9 +1,11 @@
import "@goauthentik/admin/common/ak-license-notice";
import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm"; import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm";
import "@goauthentik/admin/property-mappings/PropertyMappingNotification"; import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm"; import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm"; import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm"; import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm";
import "@goauthentik/admin/property-mappings/PropertyMappingTestForm"; import "@goauthentik/admin/property-mappings/PropertyMappingTestForm";
import { WithLicenseSummary } from "@goauthentik/app/elements/Interface/licenseSummaryProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm"; import "@goauthentik/elements/forms/ProxyForm";
@ -14,23 +16,20 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html, nothing } from "lit"; 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 PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.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") @customElement("ak-property-mapping-wizard-initial")
export class InitialPropertyMappingWizardPage extends WizardPage { export class InitialPropertyMappingWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false }) @property({ attribute: false })
mappingTypes: TypeCreate[] = []; mappingTypes: TypeCreate[] = [];
@property({ attribute: false })
enterprise?: LicenseSummary;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio]; return [PFBase, PFForm, PFButton, PFRadio];
} }
@ -50,6 +49,7 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
render(): TemplateResult { render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<form class="pf-c-form pf-m-horizontal">
${this.mappingTypes.map((type) => { ${this.mappingTypes.map((type) => {
const requiresEnteprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio"> return html`<div class="pf-c-radio">
<input <input
class="pf-c-radio__input" class="pf-c-radio__input"
@ -63,20 +63,17 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
]; ];
this.host.isValid = true; this.host.isValid = true;
}} }}
?disabled=${type.requiresEnterprise ? !this.enterprise?.hasLicense : false} ?disabled=${requiresEnteprise}
/> />
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`} <label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label >${type.name}</label
> >
<span class="pf-c-radio__description">${type.description}</span> <span class="pf-c-radio__description"
${type.requiresEnterprise && !this.enterprise?.hasLicense >${type.description}
? html` ${requiresEnteprise
<ak-alert class="pf-c-radio__description" ?inline=${true}> ? html`<ak-license-notice></ak-license-notice>`
${msg("Provider require enterprise.")} : nothing}</span
<a href="#/enterprise/licenses">${msg("Learn more")}</a> >
</ak-alert>
`
: nothing}
</div>`; </div>`;
})} })}
</form>`; </form>`;
@ -92,16 +89,10 @@ export class PropertyMappingWizard extends AKElement {
@property({ attribute: false }) @property({ attribute: false })
mappingTypes: TypeCreate[] = []; mappingTypes: TypeCreate[] = [];
@state()
enterprise?: LicenseSummary;
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
this.mappingTypes = await new PropertymappingsApi( this.mappingTypes = await new PropertymappingsApi(
DEFAULT_CONFIG, DEFAULT_CONFIG,
).propertymappingsAllTypesList(); ).propertymappingsAllTypesList();
this.enterprise = await new EnterpriseApi(
DEFAULT_CONFIG,
).enterpriseLicenseSummaryRetrieve();
} }
render(): TemplateResult { render(): TemplateResult {

View file

@ -4,6 +4,7 @@ import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderForm"; import "@goauthentik/admin/providers/saml/SAMLProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderImportForm"; import "@goauthentik/admin/providers/saml/SAMLProviderImportForm";
import { WithLicenseSummary } from "@goauthentik/app/elements/Interface/licenseSummaryProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert"; import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -16,7 +17,7 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html, nothing } from "lit"; 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 PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.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 PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.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") @customElement("ak-provider-wizard-initial")
export class InitialProviderWizardPage extends WizardPage { export class InitialProviderWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false }) @property({ attribute: false })
providerTypes: TypeCreate[] = []; providerTypes: TypeCreate[] = [];
@property({ attribute: false })
enterprise?: LicenseSummary;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFForm, PFHint, PFButton, PFRadio]; return [PFBase, PFForm, PFHint, PFButton, PFRadio];
} }
@ -74,6 +72,7 @@ export class InitialProviderWizardPage extends WizardPage {
render(): TemplateResult { render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<form class="pf-c-form pf-m-horizontal">
${this.providerTypes.map((type) => { ${this.providerTypes.map((type) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio"> return html`<div class="pf-c-radio">
<input <input
class="pf-c-radio__input" class="pf-c-radio__input"
@ -84,12 +83,12 @@ export class InitialProviderWizardPage extends WizardPage {
this.host.steps = ["initial", `type-${type.component}`]; this.host.steps = ["initial", `type-${type.component}`];
this.host.isValid = true; this.host.isValid = true;
}} }}
?disabled=${type.requiresEnterprise ? !this.enterprise?.hasLicense : false} ?disabled=${requiresEnterprise}
/> />
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label> <label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
<span class="pf-c-radio__description" <span class="pf-c-radio__description"
>${type.description} >${type.description}
${type.requiresEnterprise ${requiresEnterprise
? html`<ak-license-notice></ak-license-notice>` ? html`<ak-license-notice></ak-license-notice>`
: nothing} : nothing}
</span> </span>
@ -111,9 +110,6 @@ export class ProviderWizard extends AKElement {
@property({ attribute: false }) @property({ attribute: false })
providerTypes: TypeCreate[] = []; providerTypes: TypeCreate[] = [];
@state()
enterprise?: LicenseSummary;
@property({ attribute: false }) @property({ attribute: false })
finalHandler: () => Promise<void> = () => { finalHandler: () => Promise<void> = () => {
return Promise.resolve(); return Promise.resolve();
@ -121,9 +117,6 @@ export class ProviderWizard extends AKElement {
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(); this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList();
this.enterprise = await new EnterpriseApi(
DEFAULT_CONFIG,
).enterpriseLicenseSummaryRetrieve();
} }
render(): TemplateResult { render(): TemplateResult {
@ -136,11 +129,7 @@ export class ProviderWizard extends AKElement {
return this.finalHandler(); return this.finalHandler();
}} }}
> >
<ak-provider-wizard-initial <ak-provider-wizard-initial slot="initial" .providerTypes=${this.providerTypes}>
slot="initial"
.providerTypes=${this.providerTypes}
.enterprise=${this.enterprise}
>
</ak-provider-wizard-initial> </ak-provider-wizard-initial>
${this.providerTypes.map((type) => { ${this.providerTypes.map((type) => {
return html` return html`

View file

@ -1,9 +1,13 @@
import { createContext } from "@lit-labs/context"; 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<Config>(Symbol("authentik-config-context")); export const authentikConfigContext = createContext<Config>(Symbol("authentik-config-context"));
export const authentikEnterpriseContext = createContext<LicenseSummary>(
Symbol("authentik-enterprise-context"),
);
export const authentikTenantContext = createContext<CurrentTenant>( export const authentikTenantContext = createContext<CurrentTenant>(
Symbol("authentik-tenant-context"), Symbol("authentik-tenant-context"),
); );

View file

@ -1,7 +1,9 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { config, tenant } from "@goauthentik/common/api/config"; import { config, tenant } from "@goauthentik/common/api/config";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { import {
authentikConfigContext, authentikConfigContext,
authentikEnterpriseContext,
authentikTenantContext, authentikTenantContext,
} from "@goauthentik/elements/AuthentikContexts"; } from "@goauthentik/elements/AuthentikContexts";
import type { AdoptedStyleSheetsElement } from "@goauthentik/elements/types"; 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 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"; import { AKElement } from "../Base";
@ -63,11 +66,33 @@ export class Interface extends AKElement implements AkInterface {
return this._tenant; 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() { constructor() {
super(); super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)];
tenant().then((tenant) => (this.tenant = tenant)); tenant().then((tenant) => (this.tenant = tenant));
config().then((config) => (this.config = config)); config().then((config) => (this.config = config));
new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((enterprise) => {
this.licenseSummary = enterprise;
});
this.dataset.akInterfaceRoot = "true"; this.dataset.akInterfaceRoot = "true";
} }

View file

@ -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<T = object> = abstract new (...args: any[]) => T;
export function WithLicenseSummary<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true
) {
abstract class WithEnterpriseProvider extends superclass {
@consume({ context: authentikEnterpriseContext, subscribe })
public licenseSummary!: LicenseSummary;
get hasEnterpriseLicense() {
return false;
}
}
return WithEnterpriseProvider;
}

View file

@ -1,19 +1,14 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; 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 PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import { EnterpriseApi, LicenseSummary } from "@goauthentik/api";
@customElement("ak-enterprise-status") @customElement("ak-enterprise-status")
export class EnterpriseStatusBanner extends AKElement { export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
@state()
summary?: LicenseSummary;
@property() @property()
interface: "admin" | "user" | "" = ""; interface: "admin" | "user" | "" = "";
@ -21,12 +16,10 @@ export class EnterpriseStatusBanner extends AKElement {
return [PFBanner]; return [PFBanner];
} }
async firstUpdated(): Promise<void> {
this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve();
}
renderBanner(): TemplateResult { renderBanner(): TemplateResult {
return html`<div class="pf-c-banner ${this.summary?.readOnly ? "pf-m-red" : "pf-m-gold"}"> return html`<div
class="pf-c-banner ${this.licenseSummary?.readOnly ? "pf-m-red" : "pf-m-gold"}"
>
${msg("Warning: The current user count has exceeded the configured licenses.")} ${msg("Warning: The current user count has exceeded the configured licenses.")}
<a href="/if/admin/#/enterprise/licenses"> ${msg("Click here for more info.")} </a> <a href="/if/admin/#/enterprise/licenses"> ${msg("Click here for more info.")} </a>
</div>`; </div>`;
@ -35,12 +28,12 @@ export class EnterpriseStatusBanner extends AKElement {
render(): TemplateResult { render(): TemplateResult {
switch (this.interface.toLowerCase()) { switch (this.interface.toLowerCase()) {
case "admin": case "admin":
if (this.summary?.showAdminWarning || this.summary?.readOnly) { if (this.licenseSummary?.showAdminWarning || this.licenseSummary?.readOnly) {
return this.renderBanner(); return this.renderBanner();
} }
break; break;
case "user": case "user":
if (this.summary?.showUserWarning || this.summary?.readOnly) { if (this.licenseSummary?.showUserWarning || this.licenseSummary?.readOnly) {
return this.renderBanner(); return this.renderBanner();
} }
break; break;