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
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
></ak-text-input>
@ -104,7 +104,7 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel {
?selected=${selected.has(mapping.pk)}
>
${mapping.name}
</option>`
</option>`,
)}
</select>
<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 { 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`
<ak-alert class="pf-c-radio__description" inline>
${msg("Provider requires enterprise.")}
${this.message}
<a href="#/enterprise/licenses">${msg("Learn more")}</a>
</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/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`<form class="pf-c-form pf-m-horizontal">
${this.mappingTypes.map((type) => {
const requiresEnteprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
@ -63,20 +63,17 @@ export class InitialPropertyMappingWizardPage extends WizardPage {
];
this.host.isValid = true;
}}
?disabled=${type.requiresEnterprise ? !this.enterprise?.hasLicense : false}
?disabled=${requiresEnteprise}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}</span>
${type.requiresEnterprise && !this.enterprise?.hasLicense
? html`
<ak-alert class="pf-c-radio__description" ?inline=${true}>
${msg("Provider require enterprise.")}
<a href="#/enterprise/licenses">${msg("Learn more")}</a>
</ak-alert>
`
: nothing}
<span class="pf-c-radio__description"
>${type.description}
${requiresEnteprise
? html`<ak-license-notice></ak-license-notice>`
: nothing}</span
>
</div>`;
})}
</form>`;
@ -92,16 +89,10 @@ export class PropertyMappingWizard extends AKElement {
@property({ attribute: false })
mappingTypes: TypeCreate[] = [];
@state()
enterprise?: LicenseSummary;
async firstUpdated(): Promise<void> {
this.mappingTypes = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsAllTypesList();
this.enterprise = await new EnterpriseApi(
DEFAULT_CONFIG,
).enterpriseLicenseSummaryRetrieve();
}
render(): TemplateResult {

View file

@ -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`<form class="pf-c-form pf-m-horizontal">
${this.providerTypes.map((type) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div 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.isValid = true;
}}
?disabled=${type.requiresEnterprise ? !this.enterprise?.hasLicense : false}
?disabled=${requiresEnterprise}
/>
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
<span class="pf-c-radio__description"
>${type.description}
${type.requiresEnterprise
${requiresEnterprise
? html`<ak-license-notice></ak-license-notice>`
: nothing}
</span>
@ -111,9 +110,6 @@ export class ProviderWizard extends AKElement {
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
@state()
enterprise?: LicenseSummary;
@property({ attribute: false })
finalHandler: () => Promise<void> = () => {
return Promise.resolve();
@ -121,9 +117,6 @@ export class ProviderWizard extends AKElement {
async firstUpdated(): Promise<void> {
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();
}}
>
<ak-provider-wizard-initial
slot="initial"
.providerTypes=${this.providerTypes}
.enterprise=${this.enterprise}
>
<ak-provider-wizard-initial slot="initial" .providerTypes=${this.providerTypes}>
</ak-provider-wizard-initial>
${this.providerTypes.map((type) => {
return html`

View file

@ -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<Config>(Symbol("authentik-config-context"));
export const authentikEnterpriseContext = createContext<LicenseSummary>(
Symbol("authentik-enterprise-context"),
);
export const authentikTenantContext = createContext<CurrentTenant>(
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 { 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";
}

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 { 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<void> {
this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve();
}
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.")}
<a href="/if/admin/#/enterprise/licenses"> ${msg("Click here for more info.")} </a>
</div>`;
@ -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;