web: Application Wizard
This commit combines a working (but very unpolished) version of the Application Wizard with Jen's code for the CoreTransactionApplicationRequest, resulting in a successful round trip. It fixes a number of bugs with the way ContextProducer decorators were being processed, such that they just weren't working with our current configuration (although they did work fine in Storybook); consumers didn't need to be fixed. It also *removes* the steps-aware context from the Wizard. That *may* be a mistake. To re-iterate, the `WizardFrame` provides the chrome for a Wizard: the button bar div, the breadcrumbs div, the header div, and it takes the steps object as its source of truth for all of the content. The `WizardContent` part of the application has two parts: The `WizardMain`, which wraps the frame and supplies the context for all the `WizardPanels`, and the `WizardPanels` themselves, which are dependent on a context from `WizardMain` for the data that populates each panel. YAGNI right now that the panels need to know anything about the steps, and the `WizardMain` can just pass a fresh `.steps` object to the `WizardFrame` when they need updating. Using props drilling may make more sense here. It certainy does *not* make sense for the panels. They need to be renderable on-demand, and they need to make sense of what they're rendering on-demand, so the function is ``` (panel code) => (context) => (rendered panel) ``` (Yes, that's curried notation. Deal.)
This commit is contained in:
parent
cbdca55e57
commit
58bc1c3656
|
@ -1,4 +1,5 @@
|
|||
import "@goauthentik/admin/applications/ApplicationForm";
|
||||
import "@goauthentik/admin/applications/wizard/ak-application-wizard";
|
||||
import { PFSize } from "@goauthentik/app/elements/Spinner";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
|
@ -7,7 +8,7 @@ import "@goauthentik/elements/Markdown";
|
|||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
||||
// import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||
|
@ -162,6 +163,7 @@ export class ApplicationListPage extends TablePage<Application> {
|
|||
];
|
||||
}
|
||||
|
||||
/*
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html`<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
||||
<span slot="submit"> ${msg("Create")} </span>
|
||||
|
@ -170,4 +172,9 @@ export class ApplicationListPage extends TablePage<Application> {
|
|||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
</ak-forms-modal>`;
|
||||
}
|
||||
*/
|
||||
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html`<ak-application-wizard></ak-application-wizard>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { WizardPanel } from "@goauthentik/components/ak-wizard-main/types";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
|
@ -9,7 +10,10 @@ import { styles as AwadStyles } from "./BasePanel.css";
|
|||
import { applicationWizardContext } from "./ak-application-wizard-context-name";
|
||||
import type { WizardState } from "./types";
|
||||
|
||||
export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) {
|
||||
export class ApplicationWizardPageBase
|
||||
extends CustomEmitterElement(AKElement)
|
||||
implements WizardPanel
|
||||
{
|
||||
static get styles() {
|
||||
return AwadStyles;
|
||||
}
|
||||
|
@ -19,7 +23,6 @@ export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) {
|
|||
|
||||
rendered = false;
|
||||
|
||||
// @ts-expect-error
|
||||
@consume({ context: applicationWizardContext })
|
||||
public wizard!: WizardState;
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { createContext } from "@lit-labs/context";
|
||||
|
||||
export const applicationWizardContext = createContext(Symbol("ak-application-wizard-context"));
|
||||
import { WizardState } from "./types";
|
||||
|
||||
export const applicationWizardContext = createContext<WizardState>(
|
||||
Symbol("ak-application-wizard-state-context"),
|
||||
);
|
||||
export default applicationWizardContext;
|
||||
|
|
|
@ -3,9 +3,9 @@ import "@goauthentik/components/ak-wizard-main";
|
|||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { provide } from "@lit-labs/context";
|
||||
import { ContextProvider, ContextRoot } from "@lit-labs/context";
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { CSSResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
|
@ -27,19 +27,22 @@ export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
|||
return [PFBase, PFButton, PFRadio];
|
||||
}
|
||||
|
||||
/**
|
||||
* Providing a context at the root element
|
||||
*/
|
||||
@provide({ context: applicationWizardContext })
|
||||
@state()
|
||||
wizardState: WizardState = {
|
||||
step: 0,
|
||||
providerType: "",
|
||||
application: {},
|
||||
providerModel: "",
|
||||
app: {},
|
||||
provider: {},
|
||||
};
|
||||
|
||||
@state()
|
||||
/**
|
||||
* Providing a context at the root element
|
||||
*/
|
||||
wizardStateProvider = new ContextProvider(this, {
|
||||
context: applicationWizardContext,
|
||||
initialValue: this.wizardState,
|
||||
});
|
||||
|
||||
steps = steps;
|
||||
|
||||
@property()
|
||||
|
@ -54,6 +57,7 @@ export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
|||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
new ContextRoot().attach(this.parentElement!);
|
||||
this.addCustomListener("ak-application-wizard-update", this.handleUpdate);
|
||||
}
|
||||
|
||||
|
@ -68,14 +72,14 @@ export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
|||
|
||||
// Are we changing provider type? If so, swap the caches of the various provider types the
|
||||
// user may have filled in, and enable the next step.
|
||||
const providerType = update.providerType;
|
||||
const providerModel = update.providerModel;
|
||||
if (
|
||||
providerType &&
|
||||
typeof providerType === "string" &&
|
||||
providerType !== this.wizardState.providerType
|
||||
providerModel &&
|
||||
typeof providerModel === "string" &&
|
||||
providerModel !== this.wizardState.providerModel
|
||||
) {
|
||||
this.providerCache.set(this.wizardState.providerType, this.wizardState.provider);
|
||||
const prevProvider = this.providerCache.get(providerType);
|
||||
this.providerCache.set(this.wizardState.providerModel, this.wizardState.provider);
|
||||
const prevProvider = this.providerCache.get(providerModel);
|
||||
this.wizardState.provider = prevProvider ?? {};
|
||||
const newSteps = [...this.steps];
|
||||
const method = newSteps.find(({ id }) => id === "auth-method");
|
||||
|
@ -87,10 +91,10 @@ export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
|||
}
|
||||
|
||||
this.wizardState = merge(this.wizardState, update) as WizardState;
|
||||
console.log(JSON.stringify(this.wizardState, null, 2));
|
||||
this.wizardStateProvider.setValue(this.wizardState);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
render() {
|
||||
return html`
|
||||
<ak-wizard-main
|
||||
.steps=${this.steps}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-slug-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
|
@ -21,34 +22,41 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
|
|||
console.warn(`Received event with no target: ${ev}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = ev.target as HTMLInputElement;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
this.dispatchWizardUpdate({
|
||||
application: {
|
||||
app: {
|
||||
[target.name]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
validator() {
|
||||
return this.form.reportValidity();
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(this.wizard.application?.name)}
|
||||
value=${ifDefined(this.wizard.app?.name)}
|
||||
label=${msg("Name")}
|
||||
required
|
||||
help=${msg("Application's display Name.")}
|
||||
id="ak-application-wizard-details-name"
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
<ak-slug-input
|
||||
name="slug"
|
||||
value=${ifDefined(this.wizard.application?.slug)}
|
||||
value=${ifDefined(this.wizard.app?.slug)}
|
||||
label=${msg("Slug")}
|
||||
source="#ak-application-wizard-details-name"
|
||||
required
|
||||
help=${msg("Internal application name used in URLs.")}
|
||||
></ak-text-input>
|
||||
></ak-slug-input>
|
||||
<ak-text-input
|
||||
name="group"
|
||||
value=${ifDefined(this.wizard.application?.group)}
|
||||
value=${ifDefined(this.wizard.app?.group)}
|
||||
label=${msg("Group")}
|
||||
help=${msg(
|
||||
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
|
||||
|
@ -59,7 +67,7 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
|
|||
required
|
||||
name="policyEngineMode"
|
||||
.options=${policyOptions}
|
||||
.value=${this.wizard.application?.policyEngineMode}
|
||||
.value=${this.wizard.app?.policyEngineMode}
|
||||
></ak-radio-input>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("UI settings")} </span>
|
||||
|
@ -67,14 +75,14 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
|
|||
<ak-text-input
|
||||
name="metaLaunchUrl"
|
||||
label=${msg("Launch URL")}
|
||||
value=${ifDefined(this.wizard.application?.metaLaunchUrl)}
|
||||
value=${ifDefined(this.wizard.app?.metaLaunchUrl)}
|
||||
help=${msg(
|
||||
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-switch-input
|
||||
name="openInNewTab"
|
||||
?checked=${first(this.wizard.application?.openInNewTab, false)}
|
||||
?checked=${first(this.wizard.app?.openInNewTab, false)}
|
||||
label=${msg("Open in new tab")}
|
||||
help=${msg(
|
||||
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
|
|
|
@ -1,70 +1,132 @@
|
|||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import type { TypeCreate } from "@goauthentik/api";
|
||||
import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api";
|
||||
import { ProviderModelEnum } from "@goauthentik/api";
|
||||
import type {
|
||||
LDAPProviderRequest,
|
||||
ModelRequest,
|
||||
OAuth2ProviderRequest,
|
||||
ProxyProviderRequest,
|
||||
SAMLProviderRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { OneOfProvider } from "../types";
|
||||
|
||||
type ProviderRenderer = () => TemplateResult;
|
||||
|
||||
type ProviderType = [string, string, string, ProviderRenderer];
|
||||
type ProviderType = [string, string, string, ProviderRenderer, ProviderModelEnumType];
|
||||
|
||||
type ModelConverter = (provider: OneOfProvider) => ModelRequest;
|
||||
|
||||
export type LocalTypeCreate = TypeCreate & {
|
||||
formName: string;
|
||||
modelName: ProviderModelEnumType;
|
||||
converter: ModelConverter;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
const _providerTypesTable: ProviderType[] = [
|
||||
const _providerModelsTable: ProviderType[] = [
|
||||
[
|
||||
"oauth2provider",
|
||||
msg("OAuth2/OpenID"),
|
||||
msg("Modern applications, APIs and Single-page applications."),
|
||||
() => html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`
|
||||
() => html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
|
||||
ProviderModelEnum.Oauth2Oauth2provider,
|
||||
],
|
||||
|
||||
[
|
||||
"ldapprovider",
|
||||
msg("LDAP"),
|
||||
msg("Provide an LDAP interface for applications and users to authenticate against."),
|
||||
() => html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`
|
||||
() => html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
|
||||
ProviderModelEnum.LdapLdapprovider,
|
||||
],
|
||||
|
||||
[
|
||||
"proxyprovider-proxy",
|
||||
msg("Transparent Reverse Proxy"),
|
||||
msg("For transparent reverse proxies with required authentication"),
|
||||
() => html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`
|
||||
() => html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
|
||||
ProviderModelEnum.ProxyProxyprovider
|
||||
],
|
||||
|
||||
[
|
||||
"proxyprovider-forwardsingle",
|
||||
msg("Forward Single Proxy"),
|
||||
msg("For nginx's auth_request or traefix's forwardAuth"),
|
||||
() => html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`
|
||||
],
|
||||
() => html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
|
||||
ProviderModelEnum.ProxyProxyprovider
|
||||
|
||||
],
|
||||
[
|
||||
"samlprovider-manual",
|
||||
msg("SAML Manual configuration"),
|
||||
msg("Configure SAML provider manually"),
|
||||
() => html`<p>Under construction</p>`
|
||||
() => html`<p>Under construction</p>`,
|
||||
ProviderModelEnum.SamlSamlprovider
|
||||
],
|
||||
|
||||
[
|
||||
"samlprovider-import",
|
||||
msg("SAML Import Configuration"),
|
||||
msg("Create a SAML provider by importing its metadata"),
|
||||
() => html`<p>Under construction</p>`
|
||||
() => html`<p>Under construction</p>`,
|
||||
ProviderModelEnum.SamlSamlprovider
|
||||
],
|
||||
];
|
||||
|
||||
function mapProviders([modelName, name, description]: ProviderType): TypeCreate {
|
||||
const converters = new Map<ProviderModelEnumType, ModelConverter>([
|
||||
[
|
||||
ProviderModelEnum.Oauth2Oauth2provider,
|
||||
(provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
|
||||
...(provider as OAuth2ProviderRequest),
|
||||
}),
|
||||
],
|
||||
[
|
||||
ProviderModelEnum.LdapLdapprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.LdapLdapprovider,
|
||||
...(provider as LDAPProviderRequest),
|
||||
}),
|
||||
],
|
||||
[
|
||||
ProviderModelEnum.ProxyProxyprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
}),
|
||||
],
|
||||
[
|
||||
ProviderModelEnum.SamlSamlprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.SamlSamlprovider,
|
||||
...(provider as SAMLProviderRequest),
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
// Contract enforcement
|
||||
const getConverter = (modelName: ProviderModelEnumType): ModelConverter => {
|
||||
const maybeConverter = converters.get(modelName);
|
||||
if (!maybeConverter) {
|
||||
throw new Error(`ModelName lookup failed in model converter definition: ${"modelName"}`);
|
||||
}
|
||||
return maybeConverter;
|
||||
};
|
||||
|
||||
function mapProviders([formName, name, description, _, modelName]: ProviderType): LocalTypeCreate {
|
||||
return {
|
||||
modelName,
|
||||
formName,
|
||||
name,
|
||||
description,
|
||||
component: "",
|
||||
modelName,
|
||||
converter: getConverter(modelName),
|
||||
};
|
||||
}
|
||||
|
||||
export const providerTypesList = _providerTypesTable.map(mapProviders);
|
||||
export const providerModelsList = _providerModelsTable.map(mapProviders);
|
||||
|
||||
export const providerRendererList = new Map<string, ProviderRenderer>(
|
||||
_providerTypesTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]),
|
||||
_providerModelsTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]),
|
||||
);
|
||||
|
||||
export default providerTypesList;
|
||||
export default providerModelsList;
|
||||
|
|
|
@ -10,10 +10,9 @@ import { customElement } from "@lit/reactive-element/decorators/custom-element.j
|
|||
import { html } from "lit";
|
||||
import { map } from "lit/directives/map.js";
|
||||
|
||||
import type { TypeCreate } from "@goauthentik/api";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import providerTypesList from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-method-choice")
|
||||
export class ApplicationWizardAuthenticationMethodChoice extends BasePanel {
|
||||
|
@ -25,31 +24,31 @@ export class ApplicationWizardAuthenticationMethodChoice extends BasePanel {
|
|||
|
||||
handleChoice(ev: InputEvent) {
|
||||
const target = ev.target as HTMLInputElement;
|
||||
this.dispatchWizardUpdate({ providerType: target.value });
|
||||
this.dispatchWizardUpdate({ providerModel: target.value });
|
||||
}
|
||||
|
||||
renderProvider(type: TypeCreate) {
|
||||
const method = this.wizard.providerType;
|
||||
renderProvider(type: LocalTypeCreate) {
|
||||
const method = this.wizard.providerModel;
|
||||
|
||||
return html`<div class="pf-c-radio">
|
||||
<input
|
||||
class="pf-c-radio__input"
|
||||
type="radio"
|
||||
name="type"
|
||||
id="provider-${type.modelName}"
|
||||
value=${type.modelName}
|
||||
?checked=${type.modelName === method}
|
||||
id="provider-${type.formName}"
|
||||
value=${type.formName}
|
||||
?checked=${type.formName === method}
|
||||
@change=${this.handleChoice}
|
||||
/>
|
||||
<label class="pf-c-radio__label" for="provider-${type.modelName}">${type.name}</label>
|
||||
<label class="pf-c-radio__label" for="provider-${type.formName}">${type.name}</label>
|
||||
<span class="pf-c-radio__description">${type.description}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return providerTypesList.length > 0
|
||||
return providerModelsList.length > 0
|
||||
? html`<form class="pf-c-form pf-m-horizontal">
|
||||
${map(providerTypesList, this.renderProvider)}
|
||||
${map(providerModelsList, this.renderProvider)}
|
||||
</form>`
|
||||
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
|
@ -7,84 +6,91 @@ import "@goauthentik/elements/forms/FormGroup";
|
|||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
ApplicationRequest,
|
||||
CoreApi,
|
||||
TransactionApplicationRequest,
|
||||
TransactionApplicationResponse,
|
||||
} from "@goauthentik/api";
|
||||
import type { ModelRequest } from "@goauthentik/api";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
|
||||
|
||||
function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest {
|
||||
return {
|
||||
name: "",
|
||||
slug: "",
|
||||
...app,
|
||||
};
|
||||
}
|
||||
|
||||
type ProviderModelType = Exclude<ModelRequest["providerModel"], "11184809">;
|
||||
|
||||
@customElement("ak-application-wizard-commit-application")
|
||||
export class ApplicationWizardCommitApplication extends BasePanel {
|
||||
handleChange(ev: Event) {
|
||||
if (!ev.target) {
|
||||
console.warn(`Received event with no target: ${ev}`);
|
||||
state: "idle" | "running" | "done" = "idle";
|
||||
response?: TransactionApplicationResponse;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
willUpdate(_changedProperties: Map<string, any>) {
|
||||
if (this.state === "idle") {
|
||||
this.response = undefined;
|
||||
this.state = "running";
|
||||
const provider = providerModelsList.find(
|
||||
({ formName }) => formName === this.wizard.providerModel,
|
||||
);
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`Could not determine provider model from user request: ${JSON.stringify(
|
||||
this.wizard,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const request: TransactionApplicationRequest = {
|
||||
providerModel: provider.modelName as ProviderModelType,
|
||||
app: cleanApplication(this.wizard.app),
|
||||
provider: provider.converter(this.wizard.provider),
|
||||
};
|
||||
|
||||
this.send(request);
|
||||
return;
|
||||
}
|
||||
const target = ev.target as HTMLInputElement;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
this.dispatchWizardUpdate({
|
||||
application: {
|
||||
[target.name]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async send(
|
||||
data: TransactionApplicationRequest,
|
||||
): Promise<TransactionApplicationResponse | void> {
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({ transactionApplicationRequest: data })
|
||||
.then(
|
||||
(response) => {
|
||||
this.response = response;
|
||||
this.state = "done";
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(this.wizard.application?.name)}
|
||||
label=${msg("Name")}
|
||||
required
|
||||
help=${msg("Application's display Name.")}
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
name="slug"
|
||||
value=${ifDefined(this.wizard.application?.slug)}
|
||||
label=${msg("Slug")}
|
||||
required
|
||||
help=${msg("Internal application name used in URLs.")}
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
name="group"
|
||||
value=${ifDefined(this.wizard.application?.group)}
|
||||
label=${msg("Group")}
|
||||
help=${msg(
|
||||
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-radio-input
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
name="policyEngineMode"
|
||||
.options=${policyOptions}
|
||||
.value=${this.wizard.application?.policyEngineMode}
|
||||
></ak-radio-input>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("UI settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="metaLaunchUrl"
|
||||
label=${msg("Launch URL")}
|
||||
value=${ifDefined(this.wizard.application?.metaLaunchUrl)}
|
||||
help=${msg(
|
||||
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-switch-input
|
||||
name="openInNewTab"
|
||||
?checked=${first(this.wizard.application?.openInNewTab, false)}
|
||||
label=${msg("Open in new tab")}
|
||||
help=${msg(
|
||||
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
return html`
|
||||
<div>
|
||||
<h3>Current result:</h3>
|
||||
<p>State: ${this.state}</p>
|
||||
<pre>${JSON.stringify(this.wizard, null, 2)}</pre>
|
||||
<p>Response:</p>
|
||||
<pre>${JSON.stringify(this.response, null, 2)}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardApplicationDetails;
|
||||
export default ApplicationWizardCommitApplication;
|
||||
|
|
|
@ -14,6 +14,10 @@ export class ApplicationWizardProviderPageBase extends BasePanel {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
validator() {
|
||||
return this.form.reportValidity();
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardProviderPageBase;
|
||||
|
|
|
@ -12,7 +12,7 @@ import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
|
|||
@customElement("ak-application-wizard-authentication-method")
|
||||
export class ApplicationWizardApplicationDetails extends BasePanel {
|
||||
render() {
|
||||
const handler = providerRendererList.get(this.wizard.providerType);
|
||||
const handler = providerRendererList.get(this.wizard.providerModel);
|
||||
if (!handler) {
|
||||
throw new Error(
|
||||
"Unrecognized authentication method in ak-application-wizard-authentication-method",
|
||||
|
|
|
@ -5,6 +5,7 @@ import { html } from "lit";
|
|||
|
||||
import "./application/ak-application-wizard-application-details";
|
||||
import "./auth-method-choice/ak-application-wizard-authentication-method-choice";
|
||||
import "./commit/ak-application-wizard-commit-application";
|
||||
import "./methods/ak-application-wizard-authentication-method";
|
||||
|
||||
export const steps: WizardStep[] = [
|
||||
|
@ -47,5 +48,4 @@ export const steps: WizardStep[] = [
|
|||
backButtonLabel: msg("Back"),
|
||||
valid: true,
|
||||
},
|
||||
|
||||
];
|
||||
|
|
|
@ -47,7 +47,7 @@ const container = (testItem: TemplateResult) => {
|
|||
|
||||
export const MainPage = () => {
|
||||
return container(html`
|
||||
<ak-application-wizard>></ak-application-wizard>
|
||||
<ak-application-wizard></ak-application-wizard>
|
||||
<hr />
|
||||
<ak-application-context-display-for-test></ak-application-context-display-for-test>
|
||||
`);
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import {
|
||||
Application,
|
||||
LDAPProvider,
|
||||
OAuth2Provider,
|
||||
ProxyProvider,
|
||||
RadiusProvider,
|
||||
SAMLProvider,
|
||||
SCIMProvider,
|
||||
ApplicationRequest,
|
||||
LDAPProviderRequest,
|
||||
OAuth2ProviderRequest,
|
||||
ProxyProviderRequest,
|
||||
RadiusProviderRequest,
|
||||
SAMLProviderRequest,
|
||||
SCIMProviderRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export type OneOfProvider =
|
||||
| Partial<SCIMProvider>
|
||||
| Partial<SAMLProvider>
|
||||
| Partial<RadiusProvider>
|
||||
| Partial<ProxyProvider>
|
||||
| Partial<OAuth2Provider>
|
||||
| Partial<LDAPProvider>;
|
||||
| Partial<SCIMProviderRequest>
|
||||
| Partial<SAMLProviderRequest>
|
||||
| Partial<RadiusProviderRequest>
|
||||
| Partial<ProxyProviderRequest>
|
||||
| Partial<OAuth2ProviderRequest>
|
||||
| Partial<LDAPProviderRequest>;
|
||||
|
||||
export interface WizardState {
|
||||
step: number;
|
||||
providerType: string;
|
||||
application: Partial<Application>;
|
||||
providerModel: string;
|
||||
app: Partial<ApplicationRequest>;
|
||||
provider: OneOfProvider;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import { ReactiveController } from 'lit';
|
||||
import type { ReactiveControllerHost } from 'lit';
|
||||
|
||||
export class ApplicationWizardController implements ReactiveController {
|
||||
host: ReactiveControllerHost;
|
||||
|
||||
value = new Date();
|
||||
timeout: number;
|
||||
private _timerID?: number;
|
||||
|
||||
constructor(host: ReactiveControllerHost, timeout = 1000) {
|
||||
(this.host = host).addController(this);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
hostConnected() {
|
||||
// Start a timer when the host is connected
|
||||
this._timerID = setInterval(() => {
|
||||
this.value = new Date();
|
||||
// Update the host with new value
|
||||
this.host.requestUpdate();
|
||||
}, this.timeout);
|
||||
}
|
||||
hostDisconnected() {
|
||||
// Clear the timer when the host is disconnected
|
||||
clearInterval(this._timerID);
|
||||
this._timerID = undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
import { convertToSlug } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-slug-input")
|
||||
export class AkSlugInput extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
hidden = false;
|
||||
|
||||
@property({ type: Object })
|
||||
bighelp!: TemplateResult | TemplateResult[];
|
||||
|
||||
@property({ type: String })
|
||||
source = "";
|
||||
|
||||
origin?: HTMLInputElement | null;
|
||||
|
||||
@query("input")
|
||||
input!: HTMLInputElement;
|
||||
|
||||
touched: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.slugify = this.slugify.bind(this);
|
||||
this.handleTouch = this.handleTouch.bind(this);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.input.addEventListener("input", this.handleTouch);
|
||||
}
|
||||
|
||||
renderHelp() {
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
}
|
||||
|
||||
// Do not stop propagation of this event; it must be sent up the tree so that a parent
|
||||
// component, such as a custom forms manager, may receive it.
|
||||
handleTouch(ev: Event) {
|
||||
this.input.value = convertToSlug(this.input.value);
|
||||
this.value = this.input.value;
|
||||
|
||||
if (this.origin && this.origin.value === "" && this.input.value === "") {
|
||||
this.touched = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev && ev.target && ev.target instanceof HTMLInputElement) {
|
||||
this.touched = true;
|
||||
}
|
||||
}
|
||||
|
||||
slugify(ev: Event) {
|
||||
// A very primitive heuristic: if the previous iteration of the slug and the current
|
||||
// iteration are *similar enough*, set the input value. "Similar enough" here is defined as
|
||||
// "any event which adds or removes a character but leaves the rest of the slug looking like
|
||||
// the previous iteration, set it to the current iteration."
|
||||
if (ev && ev.target && ev.target instanceof HTMLInputElement) {
|
||||
if (this.touched) {
|
||||
if (ev.target.value === "" && this.input.value === "") {
|
||||
this.touched = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newSlug = convertToSlug(ev.target.value);
|
||||
const oldSlug = this.input.value;
|
||||
const [shorter, longer] =
|
||||
newSlug.length < oldSlug.length ? [newSlug, oldSlug] : [oldSlug, newSlug];
|
||||
if (longer.substring(0, shorter.length) === shorter) {
|
||||
this.input.value = newSlug;
|
||||
|
||||
// The browser, as a security measure, sets the originating HTML object to be the
|
||||
// target; developers cannot change it. In order to provide a meaningful value
|
||||
// to listeners, both the name and value of the host must match those of the target
|
||||
// input. The name is already handled since it's both required and automatically
|
||||
// forwarded to our templated input, but the value must also be set.
|
||||
this.value = this.input.value;
|
||||
this.dispatchEvent(
|
||||
new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// Set up listener on source element, so we can slugify the content.
|
||||
setTimeout(() => {
|
||||
if (this.source) {
|
||||
const rootNode = this.getRootNode();
|
||||
if (rootNode instanceof ShadowRoot || rootNode instanceof Document) {
|
||||
this.origin = rootNode.querySelector(this.source);
|
||||
}
|
||||
if (this.origin) {
|
||||
this.origin.addEventListener("input", this.slugify);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.origin) {
|
||||
this.origin.removeEventListener("input", this.slugify);
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
/>
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,13 @@
|
|||
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, property, state } from "@lit/reactive-element/decorators.js";
|
||||
import { customElement, property, query } from "@lit/reactive-element/decorators.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||
|
||||
import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName";
|
||||
import { akWizardStepsContextName } from "./akWizardStepsContextName";
|
||||
import type { WizardStep } from "./types";
|
||||
|
||||
/**
|
||||
|
@ -49,16 +46,15 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
|||
@property()
|
||||
eventName: string = "ak-wizard-nav";
|
||||
|
||||
// @ts-expect-error
|
||||
@consume({ context: akWizardStepsContextName, subscribe: true })
|
||||
@state()
|
||||
@property({ attribute: false, type: Array })
|
||||
steps!: WizardStep[];
|
||||
|
||||
// @ts-expect-error
|
||||
@consume({ context: akWizardCurrentStepContextName, subscribe: true })
|
||||
@state()
|
||||
@property({ attribute: false, type: Object })
|
||||
currentStep!: WizardStep;
|
||||
|
||||
@query("#main-content *:first-child")
|
||||
content!: HTMLElement;
|
||||
|
||||
reset() {
|
||||
this.open = false;
|
||||
}
|
||||
|
@ -141,7 +137,9 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
|||
// independent context.
|
||||
renderMainSection() {
|
||||
return html`<main class="pf-c-wizard__main">
|
||||
<div class="pf-c-wizard__main-body">${this.currentStep.renderer()}</div>
|
||||
<div id="main-content" class="pf-c-wizard__main-body">
|
||||
${this.currentStep.renderer()}
|
||||
</div>
|
||||
</main>`;
|
||||
}
|
||||
|
||||
|
@ -159,23 +157,22 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
|||
return html`<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
type="submit"
|
||||
?disabled=${!this.currentStep.valid}
|
||||
@click=${() => this.dispatchCustomEvent(this.eventName, { step: nextStep.id })}
|
||||
@click=${() =>
|
||||
this.dispatchCustomEvent(this.eventName, { step: nextStep.id, action: "next" })}
|
||||
>
|
||||
${this.currentStep.nextButtonLabel}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
renderFooterBackButton(backStep: WizardStep) {
|
||||
return html`
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
type="button"
|
||||
@click=${() => this.dispatchCustomEvent(this.eventName, { step: backStep.id })}
|
||||
>
|
||||
${this.currentStep.backButtonLabel}
|
||||
</button>
|
||||
`;
|
||||
return html`<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
type="button"
|
||||
@click=${() =>
|
||||
this.dispatchCustomEvent(this.eventName, { step: backStep.id, action: "back" })}
|
||||
>
|
||||
${this.currentStep.backButtonLabel}
|
||||
</button> `;
|
||||
}
|
||||
|
||||
renderFooterCancelButton() {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { provide } from "@lit-labs/context";
|
||||
import { html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
|
@ -11,9 +10,16 @@ import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
|
|||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import "./ak-wizard-frame";
|
||||
import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName";
|
||||
import { akWizardStepsContextName } from "./akWizardStepsContextName";
|
||||
import type { WizardStep } from "./types";
|
||||
import { AkWizardFrame } from "./ak-wizard-frame";
|
||||
import type { WizardPanel, WizardStep } from "./types";
|
||||
|
||||
// Not just a check that it has a validator, but a check that satisfies Typescript that we're using
|
||||
// it correctly; anything within the hasValidator conditional block will know it's dealing with
|
||||
// a fully operational WizardPanel.
|
||||
//
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const hasValidator = (v: any): v is Required<Pick<WizardPanel, "validator">> =>
|
||||
"validator" in v && typeof v.validator === "function";
|
||||
|
||||
/**
|
||||
* AKWizardMain
|
||||
|
@ -41,7 +47,6 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
|||
*
|
||||
* @attribute
|
||||
*/
|
||||
@provide({ context: akWizardStepsContextName })
|
||||
@property({ attribute: false })
|
||||
steps: WizardStep[] = [];
|
||||
|
||||
|
@ -50,7 +55,6 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
|||
*
|
||||
* @attribute
|
||||
*/
|
||||
@provide({ context: akWizardCurrentStepContextName })
|
||||
@state()
|
||||
currentStep!: WizardStep;
|
||||
|
||||
|
@ -91,6 +95,9 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
|||
@property()
|
||||
description?: string;
|
||||
|
||||
@query("ak-wizard-frame")
|
||||
frame!: AkWizardFrame;
|
||||
|
||||
// Guarantee that if the current step was not passed in by the client, that we know
|
||||
// and set to the first step.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -117,7 +124,7 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
|||
// before setting the currentStep. Especially since setting the currentStep triggers a second
|
||||
// asynchronous event-- scheduling a re-render of everything interested in the currentStep
|
||||
// object.
|
||||
handleNavigation(event: CustomEvent<{ step: string }>) {
|
||||
handleNavigation(event: CustomEvent<{ step: string; action: string }>) {
|
||||
const requestedStep = event.detail.step;
|
||||
if (!requestedStep) {
|
||||
throw new Error("Request for next step when no next step is available");
|
||||
|
@ -126,11 +133,18 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
|||
if (!step) {
|
||||
throw new Error("Request for next step when no next step is available.");
|
||||
}
|
||||
if (step.disabled) {
|
||||
throw new Error("Request for next step when the next step is disabled.");
|
||||
if (event.detail.action === "next" && !this.validated()) {
|
||||
return false;
|
||||
}
|
||||
this.currentStep = step;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
validated() {
|
||||
if (hasValidator(this.frame.content)) {
|
||||
return this.frame.content.validator();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -140,6 +154,8 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
|||
header=${this.header}
|
||||
description=${ifDefined(this.description)}
|
||||
eventName=${this.eventName}
|
||||
.steps=${this.steps}
|
||||
.currentStep=${this.currentStep}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${this.prompt}</button>
|
||||
</ak-wizard-frame>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { createContext } from "@lit-labs/context";
|
||||
|
||||
export const akWizardCurrentStepContextName = createContext(Symbol("ak-wizard-current-step"));
|
||||
import { WizardStep } from "./types";
|
||||
|
||||
export const akWizardCurrentStepContextName = createContext<WizardStep>(
|
||||
Symbol("ak-wizard-current-step"),
|
||||
);
|
||||
|
||||
export default akWizardCurrentStepContextName;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { createContext } from "@lit-labs/context";
|
||||
|
||||
export const akWizardStepsContextName = createContext(Symbol("ak-wizard-steps"));
|
||||
import { WizardStep } from "./types";
|
||||
|
||||
export const akWizardStepsContextName = createContext<WizardStep[]>(Symbol("ak-wizard-steps"));
|
||||
|
||||
export default akWizardStepsContextName;
|
||||
|
|
|
@ -9,3 +9,7 @@ export interface WizardStep {
|
|||
nextButtonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
}
|
||||
|
||||
export interface WizardPanel extends HTMLElement {
|
||||
validator?: () => boolean;
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ export const ButtonWithSuccess = () => {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
console.log(ev.type, ev.target.name, ev.target.value, ev.detail);
|
||||
document.getElementById("radio-message-pad")!.innerText = `Value selected: ${JSON.stringify(
|
||||
ev.target.value,
|
||||
null,
|
||||
|
|
|
@ -2,13 +2,11 @@ import { EVENT_LOCALE_CHANGE } from "@goauthentik/common/constants";
|
|||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
||||
import { customEvent, isCustomEvent } from "@goauthentik/elements/utils/customEvents";
|
||||
|
||||
import { provide } from "@lit-labs/context";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { initializeLocalization } from "./configureLocale";
|
||||
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
||||
import locale from "./context";
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
autoDetectLanguage,
|
||||
|
@ -32,7 +30,6 @@ import {
|
|||
@customElement("ak-locale-context")
|
||||
export class LocaleContext extends LitElement {
|
||||
/// @attribute The text representation of the current locale */
|
||||
@provide({ context: locale })
|
||||
@property({ attribute: true, type: String })
|
||||
locale = DEFAULT_LOCALE;
|
||||
|
||||
|
|
Reference in New Issue