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/ApplicationForm";
|
||||||
|
import "@goauthentik/admin/applications/wizard/ak-application-wizard";
|
||||||
import { PFSize } from "@goauthentik/app/elements/Spinner";
|
import { PFSize } from "@goauthentik/app/elements/Spinner";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
|
@ -7,7 +8,7 @@ import "@goauthentik/elements/Markdown";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
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 { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
|
@ -162,6 +163,7 @@ export class ApplicationListPage extends TablePage<Application> {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
renderObjectCreate(): TemplateResult {
|
renderObjectCreate(): TemplateResult {
|
||||||
return html`<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
return html`<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
||||||
<span slot="submit"> ${msg("Create")} </span>
|
<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>
|
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||||
</ak-forms-modal>`;
|
</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 { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
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 { applicationWizardContext } from "./ak-application-wizard-context-name";
|
||||||
import type { WizardState } from "./types";
|
import type { WizardState } from "./types";
|
||||||
|
|
||||||
export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) {
|
export class ApplicationWizardPageBase
|
||||||
|
extends CustomEmitterElement(AKElement)
|
||||||
|
implements WizardPanel
|
||||||
|
{
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return AwadStyles;
|
return AwadStyles;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +23,6 @@ export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) {
|
||||||
|
|
||||||
rendered = false;
|
rendered = false;
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
@consume({ context: applicationWizardContext })
|
@consume({ context: applicationWizardContext })
|
||||||
public wizard!: WizardState;
|
public wizard!: WizardState;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { createContext } from "@lit-labs/context";
|
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;
|
export default applicationWizardContext;
|
||||||
|
|
|
@ -3,9 +3,9 @@ import "@goauthentik/components/ak-wizard-main";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
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 { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, html } from "lit";
|
import { CSSResult, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
@ -27,19 +27,22 @@ export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
||||||
return [PFBase, PFButton, PFRadio];
|
return [PFBase, PFButton, PFRadio];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Providing a context at the root element
|
|
||||||
*/
|
|
||||||
@provide({ context: applicationWizardContext })
|
|
||||||
@state()
|
@state()
|
||||||
wizardState: WizardState = {
|
wizardState: WizardState = {
|
||||||
step: 0,
|
step: 0,
|
||||||
providerType: "",
|
providerModel: "",
|
||||||
application: {},
|
app: {},
|
||||||
provider: {},
|
provider: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
@state()
|
/**
|
||||||
|
* Providing a context at the root element
|
||||||
|
*/
|
||||||
|
wizardStateProvider = new ContextProvider(this, {
|
||||||
|
context: applicationWizardContext,
|
||||||
|
initialValue: this.wizardState,
|
||||||
|
});
|
||||||
|
|
||||||
steps = steps;
|
steps = steps;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
|
@ -54,6 +57,7 @@ export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
new ContextRoot().attach(this.parentElement!);
|
||||||
this.addCustomListener("ak-application-wizard-update", this.handleUpdate);
|
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
|
// 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.
|
// user may have filled in, and enable the next step.
|
||||||
const providerType = update.providerType;
|
const providerModel = update.providerModel;
|
||||||
if (
|
if (
|
||||||
providerType &&
|
providerModel &&
|
||||||
typeof providerType === "string" &&
|
typeof providerModel === "string" &&
|
||||||
providerType !== this.wizardState.providerType
|
providerModel !== this.wizardState.providerModel
|
||||||
) {
|
) {
|
||||||
this.providerCache.set(this.wizardState.providerType, this.wizardState.provider);
|
this.providerCache.set(this.wizardState.providerModel, this.wizardState.provider);
|
||||||
const prevProvider = this.providerCache.get(providerType);
|
const prevProvider = this.providerCache.get(providerModel);
|
||||||
this.wizardState.provider = prevProvider ?? {};
|
this.wizardState.provider = prevProvider ?? {};
|
||||||
const newSteps = [...this.steps];
|
const newSteps = [...this.steps];
|
||||||
const method = newSteps.find(({ id }) => id === "auth-method");
|
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;
|
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`
|
return html`
|
||||||
<ak-wizard-main
|
<ak-wizard-main
|
||||||
.steps=${this.steps}
|
.steps=${this.steps}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm";
|
import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
|
import "@goauthentik/components/ak-slug-input";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
|
@ -21,34 +22,41 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
|
||||||
console.warn(`Received event with no target: ${ev}`);
|
console.warn(`Received event with no target: ${ev}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = ev.target as HTMLInputElement;
|
const target = ev.target as HTMLInputElement;
|
||||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||||
this.dispatchWizardUpdate({
|
this.dispatchWizardUpdate({
|
||||||
application: {
|
app: {
|
||||||
[target.name]: value,
|
[target.name]: value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validator() {
|
||||||
|
return this.form.reportValidity();
|
||||||
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="name"
|
name="name"
|
||||||
value=${ifDefined(this.wizard.application?.name)}
|
value=${ifDefined(this.wizard.app?.name)}
|
||||||
label=${msg("Name")}
|
label=${msg("Name")}
|
||||||
required
|
required
|
||||||
help=${msg("Application's display Name.")}
|
help=${msg("Application's display Name.")}
|
||||||
|
id="ak-application-wizard-details-name"
|
||||||
></ak-text-input>
|
></ak-text-input>
|
||||||
<ak-text-input
|
<ak-slug-input
|
||||||
name="slug"
|
name="slug"
|
||||||
value=${ifDefined(this.wizard.application?.slug)}
|
value=${ifDefined(this.wizard.app?.slug)}
|
||||||
label=${msg("Slug")}
|
label=${msg("Slug")}
|
||||||
|
source="#ak-application-wizard-details-name"
|
||||||
required
|
required
|
||||||
help=${msg("Internal application name used in URLs.")}
|
help=${msg("Internal application name used in URLs.")}
|
||||||
></ak-text-input>
|
></ak-slug-input>
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="group"
|
name="group"
|
||||||
value=${ifDefined(this.wizard.application?.group)}
|
value=${ifDefined(this.wizard.app?.group)}
|
||||||
label=${msg("Group")}
|
label=${msg("Group")}
|
||||||
help=${msg(
|
help=${msg(
|
||||||
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
|
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
|
||||||
|
@ -59,7 +67,7 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
|
||||||
required
|
required
|
||||||
name="policyEngineMode"
|
name="policyEngineMode"
|
||||||
.options=${policyOptions}
|
.options=${policyOptions}
|
||||||
.value=${this.wizard.application?.policyEngineMode}
|
.value=${this.wizard.app?.policyEngineMode}
|
||||||
></ak-radio-input>
|
></ak-radio-input>
|
||||||
<ak-form-group>
|
<ak-form-group>
|
||||||
<span slot="header"> ${msg("UI settings")} </span>
|
<span slot="header"> ${msg("UI settings")} </span>
|
||||||
|
@ -67,14 +75,14 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="metaLaunchUrl"
|
name="metaLaunchUrl"
|
||||||
label=${msg("Launch URL")}
|
label=${msg("Launch URL")}
|
||||||
value=${ifDefined(this.wizard.application?.metaLaunchUrl)}
|
value=${ifDefined(this.wizard.app?.metaLaunchUrl)}
|
||||||
help=${msg(
|
help=${msg(
|
||||||
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
|
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
|
||||||
)}
|
)}
|
||||||
></ak-text-input>
|
></ak-text-input>
|
||||||
<ak-switch-input
|
<ak-switch-input
|
||||||
name="openInNewTab"
|
name="openInNewTab"
|
||||||
?checked=${first(this.wizard.application?.openInNewTab, false)}
|
?checked=${first(this.wizard.app?.openInNewTab, false)}
|
||||||
label=${msg("Open in new tab")}
|
label=${msg("Open in new tab")}
|
||||||
help=${msg(
|
help=${msg(
|
||||||
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
|
"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 { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
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 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
|
// prettier-ignore
|
||||||
const _providerTypesTable: ProviderType[] = [
|
const _providerModelsTable: ProviderType[] = [
|
||||||
[
|
[
|
||||||
"oauth2provider",
|
"oauth2provider",
|
||||||
msg("OAuth2/OpenID"),
|
msg("OAuth2/OpenID"),
|
||||||
msg("Modern applications, APIs and Single-page applications."),
|
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",
|
"ldapprovider",
|
||||||
msg("LDAP"),
|
msg("LDAP"),
|
||||||
msg("Provide an LDAP interface for applications and users to authenticate against."),
|
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",
|
"proxyprovider-proxy",
|
||||||
msg("Transparent Reverse Proxy"),
|
msg("Transparent Reverse Proxy"),
|
||||||
msg("For transparent reverse proxies with required authentication"),
|
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",
|
"proxyprovider-forwardsingle",
|
||||||
msg("Forward Single Proxy"),
|
msg("Forward Single Proxy"),
|
||||||
msg("For nginx's auth_request or traefix's forwardAuth"),
|
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",
|
"samlprovider-manual",
|
||||||
msg("SAML Manual configuration"),
|
msg("SAML Manual configuration"),
|
||||||
msg("Configure SAML provider manually"),
|
msg("Configure SAML provider manually"),
|
||||||
() => html`<p>Under construction</p>`
|
() => html`<p>Under construction</p>`,
|
||||||
|
ProviderModelEnum.SamlSamlprovider
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
"samlprovider-import",
|
"samlprovider-import",
|
||||||
msg("SAML Import Configuration"),
|
msg("SAML Import Configuration"),
|
||||||
msg("Create a SAML provider by importing its metadata"),
|
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 {
|
return {
|
||||||
modelName,
|
formName,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
component: "",
|
component: "",
|
||||||
|
modelName,
|
||||||
|
converter: getConverter(modelName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const providerTypesList = _providerTypesTable.map(mapProviders);
|
export const providerModelsList = _providerModelsTable.map(mapProviders);
|
||||||
|
|
||||||
export const providerRendererList = new Map<string, ProviderRenderer>(
|
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 { html } from "lit";
|
||||||
import { map } from "lit/directives/map.js";
|
import { map } from "lit/directives/map.js";
|
||||||
|
|
||||||
import type { TypeCreate } from "@goauthentik/api";
|
|
||||||
|
|
||||||
import BasePanel from "../BasePanel";
|
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")
|
@customElement("ak-application-wizard-authentication-method-choice")
|
||||||
export class ApplicationWizardAuthenticationMethodChoice extends BasePanel {
|
export class ApplicationWizardAuthenticationMethodChoice extends BasePanel {
|
||||||
|
@ -25,31 +24,31 @@ export class ApplicationWizardAuthenticationMethodChoice extends BasePanel {
|
||||||
|
|
||||||
handleChoice(ev: InputEvent) {
|
handleChoice(ev: InputEvent) {
|
||||||
const target = ev.target as HTMLInputElement;
|
const target = ev.target as HTMLInputElement;
|
||||||
this.dispatchWizardUpdate({ providerType: target.value });
|
this.dispatchWizardUpdate({ providerModel: target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderProvider(type: TypeCreate) {
|
renderProvider(type: LocalTypeCreate) {
|
||||||
const method = this.wizard.providerType;
|
const method = this.wizard.providerModel;
|
||||||
|
|
||||||
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"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="type"
|
name="type"
|
||||||
id="provider-${type.modelName}"
|
id="provider-${type.formName}"
|
||||||
value=${type.modelName}
|
value=${type.formName}
|
||||||
?checked=${type.modelName === method}
|
?checked=${type.formName === method}
|
||||||
@change=${this.handleChoice}
|
@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>
|
<span class="pf-c-radio__description">${type.description}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return providerTypesList.length > 0
|
return providerModelsList.length > 0
|
||||||
? html`<form class="pf-c-form pf-m-horizontal">
|
? html`<form class="pf-c-form pf-m-horizontal">
|
||||||
${map(providerTypesList, this.renderProvider)}
|
${map(providerModelsList, this.renderProvider)}
|
||||||
</form>`
|
</form>`
|
||||||
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
|
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { first } from "@goauthentik/common/utils";
|
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/components/ak-text-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/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
|
||||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||||
import { TemplateResult, html } from "lit";
|
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 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")
|
@customElement("ak-application-wizard-commit-application")
|
||||||
export class ApplicationWizardCommitApplication extends BasePanel {
|
export class ApplicationWizardCommitApplication extends BasePanel {
|
||||||
handleChange(ev: Event) {
|
state: "idle" | "running" | "done" = "idle";
|
||||||
if (!ev.target) {
|
response?: TransactionApplicationResponse;
|
||||||
console.warn(`Received event with no target: ${ev}`);
|
|
||||||
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
const target = ev.target as HTMLInputElement;
|
}
|
||||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
|
||||||
this.dispatchWizardUpdate({
|
async send(
|
||||||
application: {
|
data: TransactionApplicationRequest,
|
||||||
[target.name]: value,
|
): Promise<TransactionApplicationResponse | void> {
|
||||||
|
new CoreApi(DEFAULT_CONFIG)
|
||||||
|
.coreTransactionalApplicationsUpdate({ transactionApplicationRequest: data })
|
||||||
|
.then(
|
||||||
|
(response) => {
|
||||||
|
this.response = response;
|
||||||
|
this.state = "done";
|
||||||
},
|
},
|
||||||
});
|
(error) => {
|
||||||
|
console.log(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
return html`
|
||||||
<ak-text-input
|
<div>
|
||||||
name="name"
|
<h3>Current result:</h3>
|
||||||
value=${ifDefined(this.wizard.application?.name)}
|
<p>State: ${this.state}</p>
|
||||||
label=${msg("Name")}
|
<pre>${JSON.stringify(this.wizard, null, 2)}</pre>
|
||||||
required
|
<p>Response:</p>
|
||||||
help=${msg("Application's display Name.")}
|
<pre>${JSON.stringify(this.response, null, 2)}</pre>
|
||||||
></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>
|
</div>
|
||||||
</ak-form-group>
|
`;
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApplicationWizardApplicationDetails;
|
export default ApplicationWizardCommitApplication;
|
||||||
|
|
|
@ -14,6 +14,10 @@ export class ApplicationWizardProviderPageBase extends BasePanel {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validator() {
|
||||||
|
return this.form.reportValidity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApplicationWizardProviderPageBase;
|
export default ApplicationWizardProviderPageBase;
|
||||||
|
|
|
@ -12,7 +12,7 @@ import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
|
||||||
@customElement("ak-application-wizard-authentication-method")
|
@customElement("ak-application-wizard-authentication-method")
|
||||||
export class ApplicationWizardApplicationDetails extends BasePanel {
|
export class ApplicationWizardApplicationDetails extends BasePanel {
|
||||||
render() {
|
render() {
|
||||||
const handler = providerRendererList.get(this.wizard.providerType);
|
const handler = providerRendererList.get(this.wizard.providerModel);
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Unrecognized authentication method in ak-application-wizard-authentication-method",
|
"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 "./application/ak-application-wizard-application-details";
|
||||||
import "./auth-method-choice/ak-application-wizard-authentication-method-choice";
|
import "./auth-method-choice/ak-application-wizard-authentication-method-choice";
|
||||||
|
import "./commit/ak-application-wizard-commit-application";
|
||||||
import "./methods/ak-application-wizard-authentication-method";
|
import "./methods/ak-application-wizard-authentication-method";
|
||||||
|
|
||||||
export const steps: WizardStep[] = [
|
export const steps: WizardStep[] = [
|
||||||
|
@ -47,5 +48,4 @@ export const steps: WizardStep[] = [
|
||||||
backButtonLabel: msg("Back"),
|
backButtonLabel: msg("Back"),
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -47,7 +47,7 @@ const container = (testItem: TemplateResult) => {
|
||||||
|
|
||||||
export const MainPage = () => {
|
export const MainPage = () => {
|
||||||
return container(html`
|
return container(html`
|
||||||
<ak-application-wizard>></ak-application-wizard>
|
<ak-application-wizard></ak-application-wizard>
|
||||||
<hr />
|
<hr />
|
||||||
<ak-application-context-display-for-test></ak-application-context-display-for-test>
|
<ak-application-context-display-for-test></ak-application-context-display-for-test>
|
||||||
`);
|
`);
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import {
|
import {
|
||||||
Application,
|
ApplicationRequest,
|
||||||
LDAPProvider,
|
LDAPProviderRequest,
|
||||||
OAuth2Provider,
|
OAuth2ProviderRequest,
|
||||||
ProxyProvider,
|
ProxyProviderRequest,
|
||||||
RadiusProvider,
|
RadiusProviderRequest,
|
||||||
SAMLProvider,
|
SAMLProviderRequest,
|
||||||
SCIMProvider,
|
SCIMProviderRequest,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
export type OneOfProvider =
|
export type OneOfProvider =
|
||||||
| Partial<SCIMProvider>
|
| Partial<SCIMProviderRequest>
|
||||||
| Partial<SAMLProvider>
|
| Partial<SAMLProviderRequest>
|
||||||
| Partial<RadiusProvider>
|
| Partial<RadiusProviderRequest>
|
||||||
| Partial<ProxyProvider>
|
| Partial<ProxyProviderRequest>
|
||||||
| Partial<OAuth2Provider>
|
| Partial<OAuth2ProviderRequest>
|
||||||
| Partial<LDAPProvider>;
|
| Partial<LDAPProviderRequest>;
|
||||||
|
|
||||||
export interface WizardState {
|
export interface WizardState {
|
||||||
step: number;
|
step: number;
|
||||||
providerType: string;
|
providerModel: string;
|
||||||
application: Partial<Application>;
|
app: Partial<ApplicationRequest>;
|
||||||
provider: OneOfProvider;
|
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 { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
|
||||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
|
|
||||||
import { consume } from "@lit-labs/context";
|
|
||||||
import { msg } from "@lit/localize";
|
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 { html, nothing } from "lit";
|
||||||
import { classMap } from "lit/directives/class-map.js";
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
|
||||||
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||||
|
|
||||||
import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName";
|
|
||||||
import { akWizardStepsContextName } from "./akWizardStepsContextName";
|
|
||||||
import type { WizardStep } from "./types";
|
import type { WizardStep } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,16 +46,15 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||||
@property()
|
@property()
|
||||||
eventName: string = "ak-wizard-nav";
|
eventName: string = "ak-wizard-nav";
|
||||||
|
|
||||||
// @ts-expect-error
|
@property({ attribute: false, type: Array })
|
||||||
@consume({ context: akWizardStepsContextName, subscribe: true })
|
|
||||||
@state()
|
|
||||||
steps!: WizardStep[];
|
steps!: WizardStep[];
|
||||||
|
|
||||||
// @ts-expect-error
|
@property({ attribute: false, type: Object })
|
||||||
@consume({ context: akWizardCurrentStepContextName, subscribe: true })
|
|
||||||
@state()
|
|
||||||
currentStep!: WizardStep;
|
currentStep!: WizardStep;
|
||||||
|
|
||||||
|
@query("#main-content *:first-child")
|
||||||
|
content!: HTMLElement;
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
}
|
}
|
||||||
|
@ -141,7 +137,9 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||||
// independent context.
|
// independent context.
|
||||||
renderMainSection() {
|
renderMainSection() {
|
||||||
return html`<main class="pf-c-wizard__main">
|
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>`;
|
</main>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,23 +157,22 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||||
return html`<button
|
return html`<button
|
||||||
class="pf-c-button pf-m-primary"
|
class="pf-c-button pf-m-primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
?disabled=${!this.currentStep.valid}
|
@click=${() =>
|
||||||
@click=${() => this.dispatchCustomEvent(this.eventName, { step: nextStep.id })}
|
this.dispatchCustomEvent(this.eventName, { step: nextStep.id, action: "next" })}
|
||||||
>
|
>
|
||||||
${this.currentStep.nextButtonLabel}
|
${this.currentStep.nextButtonLabel}
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooterBackButton(backStep: WizardStep) {
|
renderFooterBackButton(backStep: WizardStep) {
|
||||||
return html`
|
return html`<button
|
||||||
<button
|
|
||||||
class="pf-c-button pf-m-secondary"
|
class="pf-c-button pf-m-secondary"
|
||||||
type="button"
|
type="button"
|
||||||
@click=${() => this.dispatchCustomEvent(this.eventName, { step: backStep.id })}
|
@click=${() =>
|
||||||
|
this.dispatchCustomEvent(this.eventName, { step: backStep.id, action: "back" })}
|
||||||
>
|
>
|
||||||
${this.currentStep.backButtonLabel}
|
${this.currentStep.backButtonLabel}
|
||||||
</button>
|
</button> `;
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooterCancelButton() {
|
renderFooterCancelButton() {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
|
|
||||||
import { provide } from "@lit-labs/context";
|
|
||||||
import { html } from "lit";
|
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 { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import "./ak-wizard-frame";
|
import "./ak-wizard-frame";
|
||||||
import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName";
|
import { AkWizardFrame } from "./ak-wizard-frame";
|
||||||
import { akWizardStepsContextName } from "./akWizardStepsContextName";
|
import type { WizardPanel, WizardStep } from "./types";
|
||||||
import type { 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
|
* AKWizardMain
|
||||||
|
@ -41,7 +47,6 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
*
|
*
|
||||||
* @attribute
|
* @attribute
|
||||||
*/
|
*/
|
||||||
@provide({ context: akWizardStepsContextName })
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
steps: WizardStep[] = [];
|
steps: WizardStep[] = [];
|
||||||
|
|
||||||
|
@ -50,7 +55,6 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
*
|
*
|
||||||
* @attribute
|
* @attribute
|
||||||
*/
|
*/
|
||||||
@provide({ context: akWizardCurrentStepContextName })
|
|
||||||
@state()
|
@state()
|
||||||
currentStep!: WizardStep;
|
currentStep!: WizardStep;
|
||||||
|
|
||||||
|
@ -91,6 +95,9 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
@property()
|
@property()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@query("ak-wizard-frame")
|
||||||
|
frame!: AkWizardFrame;
|
||||||
|
|
||||||
// Guarantee that if the current step was not passed in by the client, that we know
|
// Guarantee that if the current step was not passed in by the client, that we know
|
||||||
// and set to the first step.
|
// and set to the first step.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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
|
// before setting the currentStep. Especially since setting the currentStep triggers a second
|
||||||
// asynchronous event-- scheduling a re-render of everything interested in the currentStep
|
// asynchronous event-- scheduling a re-render of everything interested in the currentStep
|
||||||
// object.
|
// object.
|
||||||
handleNavigation(event: CustomEvent<{ step: string }>) {
|
handleNavigation(event: CustomEvent<{ step: string; action: string }>) {
|
||||||
const requestedStep = event.detail.step;
|
const requestedStep = event.detail.step;
|
||||||
if (!requestedStep) {
|
if (!requestedStep) {
|
||||||
throw new Error("Request for next step when no next step is available");
|
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) {
|
if (!step) {
|
||||||
throw new Error("Request for next step when no next step is available.");
|
throw new Error("Request for next step when no next step is available.");
|
||||||
}
|
}
|
||||||
if (step.disabled) {
|
if (event.detail.action === "next" && !this.validated()) {
|
||||||
throw new Error("Request for next step when the next step is disabled.");
|
return false;
|
||||||
}
|
}
|
||||||
this.currentStep = step;
|
this.currentStep = step;
|
||||||
return;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
validated() {
|
||||||
|
if (hasValidator(this.frame.content)) {
|
||||||
|
return this.frame.content.validator();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -140,6 +154,8 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
header=${this.header}
|
header=${this.header}
|
||||||
description=${ifDefined(this.description)}
|
description=${ifDefined(this.description)}
|
||||||
eventName=${this.eventName}
|
eventName=${this.eventName}
|
||||||
|
.steps=${this.steps}
|
||||||
|
.currentStep=${this.currentStep}
|
||||||
>
|
>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-primary">${this.prompt}</button>
|
<button slot="trigger" class="pf-c-button pf-m-primary">${this.prompt}</button>
|
||||||
</ak-wizard-frame>
|
</ak-wizard-frame>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { createContext } from "@lit-labs/context";
|
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;
|
export default akWizardCurrentStepContextName;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { createContext } from "@lit-labs/context";
|
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;
|
export default akWizardStepsContextName;
|
||||||
|
|
|
@ -9,3 +9,7 @@ export interface WizardStep {
|
||||||
nextButtonLabel?: string;
|
nextButtonLabel?: string;
|
||||||
backButtonLabel?: 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const displayChange = (ev: 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(
|
document.getElementById("radio-message-pad")!.innerText = `Value selected: ${JSON.stringify(
|
||||||
ev.target.value,
|
ev.target.value,
|
||||||
null,
|
null,
|
||||||
|
|
|
@ -2,13 +2,11 @@ import { EVENT_LOCALE_CHANGE } from "@goauthentik/common/constants";
|
||||||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
||||||
import { customEvent, isCustomEvent } from "@goauthentik/elements/utils/customEvents";
|
import { customEvent, isCustomEvent } from "@goauthentik/elements/utils/customEvents";
|
||||||
|
|
||||||
import { provide } from "@lit-labs/context";
|
|
||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import { initializeLocalization } from "./configureLocale";
|
import { initializeLocalization } from "./configureLocale";
|
||||||
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
||||||
import locale from "./context";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE,
|
||||||
autoDetectLanguage,
|
autoDetectLanguage,
|
||||||
|
@ -32,7 +30,6 @@ import {
|
||||||
@customElement("ak-locale-context")
|
@customElement("ak-locale-context")
|
||||||
export class LocaleContext extends LitElement {
|
export class LocaleContext extends LitElement {
|
||||||
/// @attribute The text representation of the current locale */
|
/// @attribute The text representation of the current locale */
|
||||||
@provide({ context: locale })
|
|
||||||
@property({ attribute: true, type: String })
|
@property({ attribute: true, type: String })
|
||||||
locale = DEFAULT_LOCALE;
|
locale = DEFAULT_LOCALE;
|
||||||
|
|
||||||
|
|
Reference in New Issue