Skip to content

Commit

Permalink
web: update application wizard to use name-based navigation
Browse files Browse the repository at this point in the history
- Replace index-based steps with name-based step management in application wizard
- Step definitions now live with, and are interfaced with, their renderers
- A bit of plumbing to make that all work
- A bit of linting highlighted during rollout

To support the loops and branches needed for a multiple-entry mechanism like Bindings.

Running: chrome (v128.0.6613.119) on mac
Session ID: 55696bc77272f005ccf19bbb8f4e2bd7

» /test/specs/new-application-by-wizard.ts
Configure Applications with the Application Wizard
   ✓ Should configure a simple LDAP Application
   ✓ Should configure a simple Oauth2 Application
   ✓ Should configure a simple SAML Application
   ✓ Should configure a simple SCIM Application
   ✓ Should configure a simple Radius Application
   ✓ Should configure a simple Transparent Proxy Application
   ✓ Should configure a simple Forward Proxy Application

7 passing (49.2s)
  • Loading branch information
kensternberg-authentik committed Sep 5, 2024
1 parent 42dace1 commit 267599b
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 193 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 11 additions & 8 deletions web/src/admin/applications/wizard/BasePanel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WizardPanel } from "@goauthentik/components/ak-wizard-main/types";
import { WizardStep } from "@goauthentik/components/ak-wizard-main/AkWizardStep";
import { WizardUpdateEvent } from "@goauthentik/components/ak-wizard-main/events";
import { AKElement } from "@goauthentik/elements/Base";
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
Expand All @@ -21,10 +22,7 @@ import type { ApplicationWizardState, ApplicationWizardStateUpdate } from "./typ
*
*/

export class ApplicationWizardPageBase
extends CustomEmitterElement(AKElement)
implements WizardPanel
{
export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) {
static get styles() {
return AwadStyles;
}
Expand All @@ -35,6 +33,13 @@ export class ApplicationWizardPageBase
@query("form")
form!: HTMLFormElement;

step: WizardStep;

constructor(step: WizardStep) {
super();
this.step = step;
}

/**
* Provide access to the values on the current form. Child implementations use this to craft the
* update that will be sent using `dispatchWizardUpdate` below.
Expand All @@ -57,15 +62,13 @@ export class ApplicationWizardPageBase
return this.form.checkValidity();
}

rendered = false;

/**
* Provide a single source of truth for the token used to notify the orchestrator that an event
* happens. The token `ak-wizard-update` is used by the Wizard framework's reactive controller
* to route "data on the current step has changed" events to the orchestrator.
*/
dispatchWizardUpdate(update: ApplicationWizardStateUpdate) {
this.dispatchCustomEvent("ak-wizard-update", update);
this.dispatchEvent(new WizardUpdateEvent(update));
}
}

Expand Down
47 changes: 24 additions & 23 deletions web/src/admin/applications/wizard/ak-application-wizard.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard.js";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";

import { ContextProvider } from "@lit/context";
import { msg } from "@lit/localize";
import { customElement, state } from "lit/decorators.js";

import { applicationWizardContext } from "./ContextIdentity";
import { newSteps } from "./steps";
import {
ApplicationStep,
ApplicationWizardState,
ApplicationWizardStateUpdate,
OneOfProvider,
} from "./types";
import { ApplicationStep } from "./application/ak-application-wizard-application-details.js";
import { ProviderMethodStep } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.js";
import { SubmitApplicationStep } from "./commit/ak-application-wizard-commit-application.js";
import { ProviderDetailsStep } from "./methods/ak-application-wizard-authentication-method.js";
import { ApplicationWizardState, ApplicationWizardStateUpdate, OneOfProvider } from "./types";

const freshWizardState = (): ApplicationWizardState => ({
providerModel: "",
Expand All @@ -25,11 +23,6 @@ const freshWizardState = (): ApplicationWizardState => ({
export class ApplicationWizard extends CustomListenerElement(
AkWizard<ApplicationWizardStateUpdate, ApplicationStep>,
) {
constructor() {
super(msg("Create With Wizard"), msg("New application"), msg("Create a new application"));
this.steps = newSteps();
}

/**
* We're going to be managing the content of the forms by percolating all of the data up to this
* class, which will ultimately transmit all of it to the server as a transaction. The
Expand All @@ -56,8 +49,21 @@ export class ApplicationWizard extends CustomListenerElement(
*/
providerCache: Map<string, OneOfProvider> = new Map();

constructor() {
super(msg("Create With Wizard"), msg("New application"), msg("Create a new application"));
}

public override newSteps() {
return [
new ApplicationStep(),
new ProviderMethodStep(),
new ProviderDetailsStep(),
new SubmitApplicationStep(),
];
}

// And this is where all the special cases go...
handleUpdate(detail: ApplicationWizardStateUpdate) {
public override handleUpdate(detail: ApplicationWizardStateUpdate) {
if (detail.status === "submitted") {
this.step.valid = true;
this.requestUpdate();
Expand Down Expand Up @@ -90,22 +96,17 @@ export class ApplicationWizard extends CustomListenerElement(
}

close() {
this.steps = newSteps();
this.currentStep = 0;
super.close();
this.wizardState = freshWizardState();
this.providerCache = new Map();
this.wizardStateProvider.setValue(this.wizardState);
this.frame.value!.open = false;
}

handleNav(stepId: number | undefined) {
if (stepId === undefined || this.steps[stepId] === undefined) {
navigateTo(stepId: string | undefined) {
if (stepId === undefined || this.findStep(stepId) === undefined) {
throw new Error(`Attempt to navigate to undefined step: ${stepId}`);
}
if (stepId > this.currentStep && !this.step.valid) {
return;
}
this.currentStep = stepId;
this.currentStepId = stepId;
this.requestUpdate();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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 { WizardStep } from "@goauthentik/components/ak-wizard-main/AkWizardStep";
import { WizardButton } from "@goauthentik/components/ak-wizard-main/types";
import { bound } from "@goauthentik/elements/decorators/bound";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";

Expand All @@ -14,13 +16,37 @@ import { ifDefined } from "lit/directives/if-defined.js";

import BasePanel from "../BasePanel";

export class ApplicationStep extends WizardStep {
id = "application";
label = msg("Application Details");
disabled = false;
valid = false;

get buttons(): WizardButton[] {
return [
this.valid
? { kind: "next", destination: "provider-method" }
: { kind: "next", disabled: true },
{ kind: "cancel" },
];
}

render() {
return html`<ak-application-wizard-application-details
.step=${this}
></ak-application-wizard-application-details>`;
}
}

@customElement("ak-application-wizard-application-details")
export class ApplicationWizardApplicationDetails extends BasePanel {
@bound
handleChange(_ev: Event) {
const formValues = this.formValues;
if (!formValues) {
throw new Error("No application values on form?");
}
this.step.valid = this.valid;
this.dispatchWizardUpdate({
update: {
...this.wizard,
Expand Down Expand Up @@ -81,7 +107,7 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
></ak-text-input>
<ak-switch-input
name="openInNewTab"
?checked=${first(this.wizard.app?.openInNewTab, false)}
?checked=${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.",
Expand All @@ -94,8 +120,6 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
}
}

export default ApplicationWizardApplicationDetails;

declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-application-details": ApplicationWizardApplicationDetails;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "@goauthentik/admin/common/ak-license-notice";
import { type WizardStep } from "@goauthentik/components/ak-wizard-main/AkWizardStep";

import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
Expand All @@ -18,7 +19,7 @@ import type {

import { OneOfProvider } from "../types";

type ProviderRenderer = () => TemplateResult;
type ProviderRenderer = (step: WizardStep) => TemplateResult;

type ModelConverter = (provider: OneOfProvider) => ModelRequest;

Expand All @@ -38,8 +39,10 @@ export const providerModelsList: LocalTypeCreate[] = [
formName: "oauth2provider",
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
description: msg("Modern applications, APIs and Single-page applications."),
renderer: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-by-oauth
.step=${step}
></ak-application-wizard-authentication-by-oauth>`,
modelName: ProviderModelEnum.Oauth2Oauth2provider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
Expand All @@ -54,8 +57,10 @@ export const providerModelsList: LocalTypeCreate[] = [
description: msg(
"Provide an LDAP interface for applications and users to authenticate against.",
),
renderer: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-by-ldap
.step=${step}
></ak-application-wizard-authentication-by-ldap>`,
modelName: ProviderModelEnum.LdapLdapprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.LdapLdapprovider,
Expand All @@ -68,8 +73,10 @@ export const providerModelsList: LocalTypeCreate[] = [
formName: "proxyprovider-proxy",
name: msg("Transparent Reverse Proxy"),
description: msg("For transparent reverse proxies with required authentication"),
renderer: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-for-reverse-proxy
.step=${step}
></ak-application-wizard-authentication-for-reverse-proxy>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
Expand All @@ -83,8 +90,10 @@ export const providerModelsList: LocalTypeCreate[] = [
formName: "proxyprovider-forwardsingle",
name: msg("Forward Auth (Single Application)"),
description: msg("For nginx's auth_request or traefik's forwardAuth"),
renderer: () =>
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-for-single-forward-proxy
.step=${step}
></ak-application-wizard-authentication-for-single-forward-proxy>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
Expand All @@ -98,8 +107,10 @@ export const providerModelsList: LocalTypeCreate[] = [
formName: "proxyprovider-forwarddomain",
name: msg("Forward Auth (Domain Level)"),
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
renderer: () =>
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-for-forward-proxy-domain
.step=${step}
></ak-application-wizard-authentication-for-forward-proxy-domain>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
Expand All @@ -113,8 +124,10 @@ export const providerModelsList: LocalTypeCreate[] = [
formName: "racprovider",
name: msg("Remote Access Provider"),
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
renderer: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-for-rac
.step=${step}
></ak-application-wizard-authentication-for-rac>`,
modelName: ProviderModelEnum.RacRacprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RacRacprovider,
Expand All @@ -129,8 +142,10 @@ export const providerModelsList: LocalTypeCreate[] = [
formName: "samlprovider",
name: msg("SAML (Security Assertion Markup Language)"),
description: msg("Configure SAML provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-by-saml-configuration
.step=${step}
></ak-application-wizard-authentication-by-saml-configuration>`,
modelName: ProviderModelEnum.SamlSamlprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.SamlSamlprovider,
Expand All @@ -143,8 +158,10 @@ export const providerModelsList: LocalTypeCreate[] = [
formName: "radiusprovider",
name: msg("RADIUS (Remote Authentication Dial-In User Service)"),
description: msg("Configure RADIUS provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-by-radius
.step=${step}
></ak-application-wizard-authentication-by-radius>`,
modelName: ProviderModelEnum.RadiusRadiusprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RadiusRadiusprovider,
Expand All @@ -157,8 +174,10 @@ export const providerModelsList: LocalTypeCreate[] = [
formName: "scimprovider",
name: msg("SCIM (System for Cross-domain Identity Management)"),
description: msg("Configure SCIM provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
renderer: (step: WizardStep) =>
html`<ak-application-wizard-authentication-by-scim
.step=${step}
></ak-application-wizard-authentication-by-scim>`,
modelName: ProviderModelEnum.ScimScimprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ScimScimprovider,
Expand Down
Loading

0 comments on commit 267599b

Please sign in to comment.