Now, it's starting to look like a complete package. The LDAP method is working, but there is a bug:

the radio is sending the wrong value !?!?!?. Track that down, dammit. The search wrappers now resend
their events as standard `input` events, and that actually seems to work well; the browser is
decorating it with the right target, with the right `name` attribute, and since we have good
definitions of the `value` as a string (the real value of any search object is its UUID4), that
works quite well. Added search wrappers for CoreGroup and CryptoCertificate (CertificateKeyPairs),
and the latter has flags for "use the first one if it's the only one" and "allow the display of
keyless certificates."

Not sure why `state()` is blocking the transmission of typing information from the typed element
to the context handler, but it's a bug in the typechecker, and it's not a problem so far.
This commit is contained in:
Ken Sternberg 2023-08-01 16:16:57 -07:00
parent bf26e5d11e
commit b4d3b75434
6 changed files with 154 additions and 24 deletions

View File

@ -0,0 +1,29 @@
import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { consume } from "@lit-labs/context";
import { state } from "@lit/reactive-element/decorators/state.js";
import { styles as AwadStyles } from "./ak-application-wizard-application-details.css";
import type { WizardState } from "./ak-application-wizard-context";
import applicationWizardContext from "./ak-application-wizard-context-name";
export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) {
static get styles() {
return AwadStyles;
}
@consume({ context: applicationWizardContext, subscribe: true })
@state()
private wizard!: WizardState;
dispatchWizardUpdate(update: Partial<WizardState>) {
this.dispatchCustomEvent("ak-wizard-update", {
...this.wizard,
...update,
});
}
}
export default ApplicationWizardPageBase;

View File

@ -6,6 +6,7 @@ import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css"; import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css"; import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
@ -15,12 +16,16 @@ export const styles = [
PFButton, PFButton,
PFForm, PFForm,
PFAlert, PFAlert,
PFRadio,
PFInputGroup, PFInputGroup,
PFFormControl, PFFormControl,
PFSwitch, PFSwitch,
css` css`
select[multiple] { .pf-c-radio__label {
height: 15em; color: #212427;
} }
`, select[multiple] {
height: 15em;
}
`,
]; ];

View File

@ -3,39 +3,22 @@ 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";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup"; 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 { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { consume } from "@lit-labs/context";
import { msg } from "@lit/localize"; 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 { state } from "@lit/reactive-element/decorators/state.js";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { styles as AwadStyles } from "./ak-application-wizard-application-details.css"; import ApplicationWizardPageBase from "./ApplicationWizardPageBase";
import type { WizardState } from "./ak-application-wizard-context";
import applicationWizardContext from "./ak-application-wizard-context-name";
@customElement("ak-application-wizard-application-details") @customElement("ak-application-wizard-application-details")
export class ApplicationWizardApplicationDetails extends CustomEmitterElement(AKElement) { export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBase {
static get styles() {
return AwadStyles;
}
@consume({ context: applicationWizardContext, subscribe: true })
@state()
private wizard!: WizardState;
handleChange(ev: Event) { handleChange(ev: Event) {
const value = ev.target.type === "checkbox" ? ev.target.checked : ev.target.value; const value = ev.target.type === "checkbox" ? ev.target.checked : ev.target.value;
this.dispatchWizardUpdate({
this.dispatchCustomEvent("ak-wizard-update", {
...this.wizard,
application: { application: {
...this.wizard.application, ...this.wizard.application,
[ev.target.name]: value, [ev.target.name]: value,

View File

@ -0,0 +1,68 @@
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";
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 { html } from "lit";
import { state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import { ProvidersApi } from "@goauthentik/api";
import type { TypeCreate } from "@goauthentik/api";
import ApplicationWizardPageBase from "./ApplicationWizardPageBase";
@customElement("ak-application-wizard-authentication-method-choice")
export class ApplicationWizardAuthenticationMethodChoice extends ApplicationWizardPageBase {
@state()
providerTypes: TypeCreate[] = [];
constructor() {
super();
this.handleChoice = this.handleChoice.bind(this);
this.renderProvider = this.renderProvider.bind(this);
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => {
this.providerTypes = types;
});
}
handleChoice(ev: Event) {
this.dispatchWizardUpdate({ providerType: ev.target.value });
}
renderProvider(type: Provider) {
// Special case; the SAML-by-import method is handled differently
// prettier-ignore
const model = /^SAML/.test(type.name) && type.modelName === ""
? "samlimporter"
: type.modelName;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${type.component}
value=${model}
@change=${this.handleChoice}
/>
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
<span class="pf-c-radio__description">${type.description}</span>
</div>`;
}
render() {
return this.providerTypes.length > 0
? html`<form class="pf-c-form pf-m-horizontal">
${map(this.providerTypes, this.renderProvider)}
</form>`
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
}
}
export default ApplicationWizardAuthenticationMethodChoice;

View File

@ -29,6 +29,7 @@ type OneOfProvider =
export type WizardState = { export type WizardState = {
step: number; step: number;
providerType: string;
application: Partial<Application>; application: Partial<Application>;
provider: OneOfProvider; provider: OneOfProvider;
}; };
@ -42,6 +43,7 @@ export class AkApplicationWizardContext extends CustomListenerElement(LitElement
@property({ attribute: false }) @property({ attribute: false })
wizardState: WizardState = { wizardState: WizardState = {
step: 0, step: 0,
providerType: "",
application: {}, application: {},
provider: {}, provider: {},
}; };

View File

@ -4,9 +4,35 @@ import { TemplateResult, html } from "lit";
import "../ak-application-wizard-application-details"; import "../ak-application-wizard-application-details";
import AkApplicationWizardApplicationDetails from "../ak-application-wizard-application-details"; import AkApplicationWizardApplicationDetails from "../ak-application-wizard-application-details";
import "../ak-application-wizard-authentication-method-choice";
import "../ak-application-wizard-context"; import "../ak-application-wizard-context";
import "./ak-application-context-display-for-test"; import "./ak-application-context-display-for-test";
// prettier-ignore
const providerTypes = [
["LDAP Provider", "ldapprovider",
"Allow applications to authenticate against authentik's users using LDAP.",
],
["OAuth2/OpenID Provider", "oauth2provider",
"OAuth2 Provider for generic OAuth and OpenID Connect Applications.",
],
["Proxy Provider", "proxyprovider",
"Protect applications that don't support any of the other\n Protocols by using a Reverse-Proxy.",
],
["Radius Provider", "radiusprovider",
"Allow applications to authenticate against authentik's users using Radius.",
],
["SAML Provider", "samlprovider",
"SAML 2.0 Endpoint for applications which support SAML.",
],
["SCIM Provider", "scimprovider",
"SCIM 2.0 provider to create users and groups in external applications",
],
["SAML Provider from Metadata", "",
"Create a SAML Provider by importing its Metadata.",
],
].map(([name, model_name, description]) => ({ name, description, model_name }));
const metadata: Meta<AkApplicationWizardApplicationDetails> = { const metadata: Meta<AkApplicationWizardApplicationDetails> = {
title: "Elements / Application Wizard / Page 1", title: "Elements / Application Wizard / Page 1",
component: "ak-application-wizard-application-details", component: "ak-application-wizard-application-details",
@ -16,6 +42,14 @@ const metadata: Meta<AkApplicationWizardApplicationDetails> = {
component: "The first page of the application wizard", component: "The first page of the application wizard",
}, },
}, },
mockData: [
{
url: "/api/v3/providers/all/types/",
method: "GET",
status: 200,
response: providerTypes,
},
],
}, },
}; };
@ -42,3 +76,12 @@ export const PageOne = () => {
</ak-application-wizard-context>` </ak-application-wizard-context>`
); );
}; };
export const PageTwo = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-authentication-method-choice></ak-application-wizard-authentication-method-choice>
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`
);
};