From ae99ef5fe45794963d9bc8e7f27aa163341796b9 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Thu, 10 Aug 2023 11:52:08 -0700 Subject: [PATCH] web: 80% of the way there This commit includes the first three pages of the wizard, the completion of the wizard framework with evented handling, and control over progression. Some shortcomings of this design have become evident: it isn't possible to communicate between the steps' wrappers, as they are POJOs without access to the context. An imperative decision-making process has to be inserted in the orchestration layer, which is kinda annoying. But it looks good and it behaves correctly, to the extent that I've given it behavior. It's an excellent foundation. --- ...details.css.ts => ApplicationWizardCss.ts} | 0 .../wizard/ApplicationWizardPageBase.ts | 6 +- .../wizard/ApplicationWizardSteps.ts | 43 ++++ .../wizard/ak-application-wizard.ts | 110 +++++----- ...-application-wizard-application-details.ts | 5 +- ...rd-authentication-method-choice.choices.ts | 0 ...ion-wizard-authentication-method-choice.ts | 7 +- ...pplication-wizard-authentication-method.ts | 12 +- .../ak-application-wizard-main.stories.ts | 54 +++++ .../stories/ak-application-wizard.stories.ts | 196 ------------------ .../applications/wizard/stories/mockData.ts | 62 ++++++ web/src/admin/applications/wizard/types.ts | 27 +++ .../ak-wizard-context.ts | 14 ++ .../ak-wizard-frame.ts} | 30 ++- .../ak-wizard-main/ak-wizard-main.ts | 86 ++++++++ .../akWizardCurrentStepContextName.ts | 0 .../akWizardStepsContextName.ts | 0 web/src/components/ak-wizard-main/index.ts | 5 + .../stories/ak-demo-wizard.ts | 17 +- .../stories/ak-wizard-main.stories.ts} | 11 +- .../{ak-wizard-2 => ak-wizard-main}/types.ts | 4 - 21 files changed, 401 insertions(+), 288 deletions(-) rename web/src/admin/applications/wizard/{ak-application-wizard-application-details.css.ts => ApplicationWizardCss.ts} (100%) create mode 100644 web/src/admin/applications/wizard/ApplicationWizardSteps.ts rename web/src/admin/applications/wizard/{ => application}/ak-application-wizard-application-details.ts (98%) rename web/src/admin/applications/wizard/{ => auth-method-choice}/ak-application-wizard-authentication-method-choice.choices.ts (100%) rename web/src/admin/applications/wizard/{ => auth-method-choice}/ak-application-wizard-authentication-method-choice.ts (91%) rename web/src/admin/applications/wizard/{ => auth-method}/ak-application-wizard-authentication-method.ts (56%) create mode 100644 web/src/admin/applications/wizard/stories/ak-application-wizard-main.stories.ts delete mode 100644 web/src/admin/applications/wizard/stories/ak-application-wizard.stories.ts create mode 100644 web/src/admin/applications/wizard/stories/mockData.ts create mode 100644 web/src/admin/applications/wizard/types.ts rename web/src/components/{ak-wizard-2 => ak-wizard-main}/ak-wizard-context.ts (81%) rename web/src/components/{ak-wizard-2/ak-wizard-2.ts => ak-wizard-main/ak-wizard-frame.ts} (87%) create mode 100644 web/src/components/ak-wizard-main/ak-wizard-main.ts rename web/src/components/{ak-wizard-2 => ak-wizard-main}/akWizardCurrentStepContextName.ts (100%) rename web/src/components/{ak-wizard-2 => ak-wizard-main}/akWizardStepsContextName.ts (100%) create mode 100644 web/src/components/ak-wizard-main/index.ts rename web/src/components/{ak-wizard-2 => ak-wizard-main}/stories/ak-demo-wizard.ts (75%) rename web/src/components/{ak-wizard-2/stories/ak-wizard-2.stories.ts => ak-wizard-main/stories/ak-wizard-main.stories.ts} (83%) rename web/src/components/{ak-wizard-2 => ak-wizard-main}/types.ts (87%) diff --git a/web/src/admin/applications/wizard/ak-application-wizard-application-details.css.ts b/web/src/admin/applications/wizard/ApplicationWizardCss.ts similarity index 100% rename from web/src/admin/applications/wizard/ak-application-wizard-application-details.css.ts rename to web/src/admin/applications/wizard/ApplicationWizardCss.ts diff --git a/web/src/admin/applications/wizard/ApplicationWizardPageBase.ts b/web/src/admin/applications/wizard/ApplicationWizardPageBase.ts index fbf4efa71..bde4b04d9 100644 --- a/web/src/admin/applications/wizard/ApplicationWizardPageBase.ts +++ b/web/src/admin/applications/wizard/ApplicationWizardPageBase.ts @@ -4,8 +4,7 @@ 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 { styles as AwadStyles } from "./ApplicationWizardCss"; import type { WizardState } from "./ak-application-wizard-context"; import { applicationWizardContext } from "./ak-application-wizard-context-name"; @@ -14,13 +13,12 @@ export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) { return AwadStyles; } - // @ts-expect-error @consume({ context: applicationWizardContext, subscribe: true }) @state() public wizard!: WizardState; dispatchWizardUpdate(update: Partial) { - this.dispatchCustomEvent("ak-wizard-update", { + this.dispatchCustomEvent("ak-application-wizard-update", { ...this.wizard, ...update, }); diff --git a/web/src/admin/applications/wizard/ApplicationWizardSteps.ts b/web/src/admin/applications/wizard/ApplicationWizardSteps.ts new file mode 100644 index 000000000..078729dd8 --- /dev/null +++ b/web/src/admin/applications/wizard/ApplicationWizardSteps.ts @@ -0,0 +1,43 @@ +import { WizardStep, makeWizardId } from "@goauthentik/components/ak-wizard-main"; +import "./application/ak-application-wizard-application-details"; +import "./auth-method-choice/ak-application-wizard-authentication-method-choice"; +import "./auth-method/ak-application-wizard-authentication-method"; + +import { msg } from "@lit/localize"; +import { html } from "lit"; + +export const steps: WizardStep[] = [ + { + id: makeWizardId("application"), + nextStep: makeWizardId("auth-method-choice"), + label: "Application Details", + renderer: () => + html``, + disabled: false, + nextButtonLabel: msg("Next"), + valid: true, + }, + { + id: makeWizardId("auth-method-choice"), + backStep: makeWizardId("application"), + nextStep: makeWizardId("auth-method"), + label: "Authentication Method", + renderer: () => + html``, + disabled: false, + nextButtonLabel: msg("Next"), + backButtonLabel: msg("Back"), + valid: true, + }, + { + id: makeWizardId("auth-method"), + backStep: makeWizardId("auth-method-choice"), + label: "Authentication Details", + renderer: () => + html``, + disabled: true, + nextButtonLabel: msg("Submit"), + backButtonLabel: msg("Back"), + valid: true, + } +]; diff --git a/web/src/admin/applications/wizard/ak-application-wizard.ts b/web/src/admin/applications/wizard/ak-application-wizard.ts index 586e3e315..9f8d1de84 100644 --- a/web/src/admin/applications/wizard/ak-application-wizard.ts +++ b/web/src/admin/applications/wizard/ak-application-wizard.ts @@ -1,58 +1,43 @@ -import "@goauthentik/admin/applications/wizard/InitialApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/TypeApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/ldap/TypeLDAPApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/link/TypeLinkApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/proxy/TypeProxyApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/saml/TypeSAMLApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/saml/TypeSAMLImportApplicationWizardPage"; +import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; +import "@goauthentik/components/ak-wizard-main"; import { AKElement } from "@goauthentik/elements/Base"; -import "@goauthentik/elements/wizard/Wizard"; +import { provide } from "@lit-labs/context"; import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { CSSResult, TemplateResult, html } from "lit"; -import { property } from "lit/decorators.js"; +import { property, customElement, state } from "lit/decorators.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -/* -const steps = [ - { - name: msg("Application Details"), - view: () => - html``, - }, - { - name: msg("Authentication Method"), - view: () => - html``, - }, - { - name: msg("Authentication Details"), - view: () => - html``, - }, - { - name: msg("Save Application"), - view: () => - html``, - }, - ]; - */ +import { WizardState, WizardStateEvent } from "./types" +import { steps } from "./ApplicationWizardSteps"; +import applicationWizardContext from "./ak-application-wizard-context-name"; + +// my-context.ts @customElement("ak-application-wizard") -export class ApplicationWizard extends AKElement { +export class ApplicationWizard extends CustomListenerElement(AKElement) { static get styles(): CSSResult[] { return [PFBase, PFButton, PFRadio]; } + /** + * Providing a context at the root element + */ + @provide({ context: applicationWizardContext }) + @property({ attribute: false }) + wizardState: WizardState = { + step: 0, + providerType: "", + application: {}, + provider: {}, + }; + + @state() + steps = steps; + @property({ type: Boolean }) open = false; @@ -67,23 +52,54 @@ export class ApplicationWizard extends AKElement { return Promise.resolve(); }; + constructor() { + super(); + this.handleUpdate = this.handleUpdate.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + this.addCustomListener("ak-application-wizard-update", this.handleUpdate); + } + + disconnectedCallback() { + this.removeCustomListener("ak-application-wizard-update", this.handleUpdate); + super.disconnectedCallback(); + } + + // And this is where all the special cases go... + handleUpdate(event: CustomEvent) { + delete event.detail.target; + const newWizardState: WizardState = event.detail; + + // When the user sets the authentication method type, the corresponding authentication + // method page becomes available. + if (newWizardState.providerType !== "") { + const newSteps = [...this.steps]; + const method = newSteps.find(({ id }) => id === "auth-method"); + if (!method) { + throw new Error("Could not find Authentication Method page?"); + } + method.disabled = false; + this.steps = newSteps; + } + + this.wizardState = newWizardState; + } + render(): TemplateResult { return html` - { - return this.finalHandler(); - }} > ${this.showButton ? html`` : html``} - + `; } } diff --git a/web/src/admin/applications/wizard/ak-application-wizard-application-details.ts b/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts similarity index 98% rename from web/src/admin/applications/wizard/ak-application-wizard-application-details.ts rename to web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts index 31cf2823c..0c4d443ef 100644 --- a/web/src/admin/applications/wizard/ak-application-wizard-application-details.ts +++ b/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts @@ -12,10 +12,11 @@ import { customElement } from "@lit/reactive-element/decorators/custom-element.j import { TemplateResult, html } from "lit"; import { ifDefined } from "lit/directives/if-defined.js"; -import ApplicationWizardPageBase from "./ApplicationWizardPageBase"; +import ApplicationWizardPageBase from "../ApplicationWizardPageBase"; @customElement("ak-application-wizard-application-details") export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBase { + handleChange(ev: Event) { if (!ev.target) { console.warn(`Received event with no target: ${ev}`); @@ -88,4 +89,6 @@ export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBa } } + + export default ApplicationWizardApplicationDetails; diff --git a/web/src/admin/applications/wizard/ak-application-wizard-authentication-method-choice.choices.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts similarity index 100% rename from web/src/admin/applications/wizard/ak-application-wizard-authentication-method-choice.choices.ts rename to web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts diff --git a/web/src/admin/applications/wizard/ak-application-wizard-authentication-method-choice.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts similarity index 91% rename from web/src/admin/applications/wizard/ak-application-wizard-authentication-method-choice.ts rename to web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts index 82ec4ecfb..a868d4e41 100644 --- a/web/src/admin/applications/wizard/ak-application-wizard-authentication-method-choice.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts @@ -12,7 +12,7 @@ import { map } from "lit/directives/map.js"; import type { TypeCreate } from "@goauthentik/api"; -import ApplicationWizardPageBase from "./ApplicationWizardPageBase"; +import ApplicationWizardPageBase from "../ApplicationWizardPageBase"; import providerTypesList from "./ak-application-wizard-authentication-method-choice.choices"; @customElement("ak-application-wizard-authentication-method-choice") @@ -29,13 +29,16 @@ export class ApplicationWizardAuthenticationMethodChoice extends ApplicationWiza } renderProvider(type: TypeCreate) { + const method = this.wizard.providerType; + return html`
diff --git a/web/src/admin/applications/wizard/ak-application-wizard-authentication-method.ts b/web/src/admin/applications/wizard/auth-method/ak-application-wizard-authentication-method.ts similarity index 56% rename from web/src/admin/applications/wizard/ak-application-wizard-authentication-method.ts rename to web/src/admin/applications/wizard/auth-method/ak-application-wizard-authentication-method.ts index ff6db0417..ab727ba07 100644 --- a/web/src/admin/applications/wizard/ak-application-wizard-authentication-method.ts +++ b/web/src/admin/applications/wizard/auth-method/ak-application-wizard-authentication-method.ts @@ -1,11 +1,11 @@ import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import ApplicationWizardPageBase from "./ApplicationWizardPageBase"; -import { providerRendererList } from "./ak-application-wizard-authentication-method-choice.choices"; -import "./ldap/ak-application-wizard-authentication-by-ldap"; -import "./oauth/ak-application-wizard-authentication-by-oauth"; -import "./proxy/ak-application-wizard-authentication-for-reverse-proxy"; -import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy"; +import ApplicationWizardPageBase from "../ApplicationWizardPageBase"; +import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices"; +import "../ldap/ak-application-wizard-authentication-by-ldap"; +import "../oauth/ak-application-wizard-authentication-by-oauth"; +import "../proxy/ak-application-wizard-authentication-for-reverse-proxy"; +import "../proxy/ak-application-wizard-authentication-for-single-forward-proxy"; // prettier-ignore diff --git a/web/src/admin/applications/wizard/stories/ak-application-wizard-main.stories.ts b/web/src/admin/applications/wizard/stories/ak-application-wizard-main.stories.ts new file mode 100644 index 000000000..76d0f1035 --- /dev/null +++ b/web/src/admin/applications/wizard/stories/ak-application-wizard-main.stories.ts @@ -0,0 +1,54 @@ +import { Meta } from "@storybook/web-components"; + +import { TemplateResult, html } from "lit"; + +import { ApplicationWizard } from "../ak-application-wizard"; +import "../ak-application-wizard"; +import { mockData } from "./mockData"; + +const metadata: Meta = { + title: "Elements / Application Wizard Implementation / Main", + component: "ak-application-wizard", + parameters: { + docs: { + description: { + component: "The first page of the application wizard", + }, + }, + mockData, + }, +}; + +const LIGHT = "pf-t-light"; +function injectTheme() { + setTimeout(() => { + if (!document.body.classList.contains(LIGHT)) { + document.body.classList.add(LIGHT); + } + }); +} + +export default metadata; + +const container = (testItem: TemplateResult) => { + injectTheme(); + return html`
+ + ${testItem} +
`; +}; + +export const MainPage = () => { + return container(html` + > +
+ + `); +}; diff --git a/web/src/admin/applications/wizard/stories/ak-application-wizard.stories.ts b/web/src/admin/applications/wizard/stories/ak-application-wizard.stories.ts deleted file mode 100644 index 41b8a22af..000000000 --- a/web/src/admin/applications/wizard/stories/ak-application-wizard.stories.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { Meta } from "@storybook/web-components"; - -import { TemplateResult, html } from "lit"; - -import "../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 "../ldap/ak-application-wizard-authentication-by-ldap"; -import "../oauth/ak-application-wizard-authentication-by-oauth"; -import "../proxy/ak-application-wizard-authentication-for-reverse-proxy"; -import "../proxy/ak-application-wizard-authentication-for-single-forward-proxy"; -import "../saml/ak-application-wizard-authentication-by-saml-configuration"; -import "../saml/ak-application-wizard-authentication-by-saml-import"; -import "./ak-application-context-display-for-test"; -import { - dummyAuthenticationFlowsSearch, - dummyAuthorizationFlowsSearch, - dummyCoreGroupsSearch, - dummyCryptoCertsSearch, - dummyHasJwks, - dummyPropertyMappings, - dummyProviderTypesList, - dummySAMLProviderMappings, -} from "./samples"; - -const metadata: Meta = { - title: "Elements / Application Wizard / Page 1", - component: "ak-application-wizard-application-details", - parameters: { - docs: { - description: { - component: "The first page of the application wizard", - }, - }, - mockData: [ - { - url: "/api/v3/providers/all/types/", - method: "GET", - status: 200, - response: dummyProviderTypesList, - }, - { - url: "/api/v3/core/groups/?ordering=name", - method: "GET", - status: 200, - response: dummyCoreGroupsSearch, - }, - - { - url: "/api/v3/crypto/certificatekeypairs/?has_key=true&include_details=false&ordering=name", - method: "GET", - status: 200, - response: dummyCryptoCertsSearch, - }, - { - url: "/api/v3/flows/instances/?designation=authentication&ordering=slug", - method: "GET", - status: 200, - response: dummyAuthenticationFlowsSearch, - }, - { - url: "/api/v3/flows/instances/?designation=authorization&ordering=slug", - method: "GET", - status: 200, - response: dummyAuthorizationFlowsSearch, - }, - { - url: "/api/v3/propertymappings/scope/?ordering=scope_name", - method: "GET", - status: 200, - response: dummyPropertyMappings, - }, - { - url: "/api/v3/sources/oauth/?has_jwks=true&ordering=name", - method: "GET", - status: 200, - response: dummyHasJwks, - }, - { - url: "/api/v3/propertymappings/saml/?ordering=saml_name", - method: "GET", - status: 200, - response: dummySAMLProviderMappings, - }, - - ], - }, -}; - -const LIGHT = "pf-t-light"; -function injectTheme() { - setTimeout(() => { - if (!document.body.classList.contains(LIGHT)) { - document.body.classList.add(LIGHT); - } - }); -} - -export default metadata; - -const container = (testItem: TemplateResult) => { - injectTheme(); - return html`
- - ${testItem} -
`; -}; - -export const DescribeApplication = () => { - return container( - html` - -
- -
`, - ); -}; - -export const ChooseAuthMethod = () => { - return container( - html` - -
- -
`, - ); -}; - -export const ConfigureLdap = () => { - return container( - html` - -
- -
`, - ); -}; - -export const ConfigureOauth2 = () => { - return container( - html` - -
- -
`, - ); -}; - -export const ConfigureReverseProxy = () => { - return container( - html` - -
- -
`, - ); -}; - -export const ConfigureSingleForwardProxy = () => { - return container( - html` - -
- -
`, - ); -}; - -export const ConfigureSamlManually = () => { - return container( - html` - -
- -
`, - ); -}; - - -export const SamlImport = () => { - return container( - html` - -
- -
`, - ); -}; diff --git a/web/src/admin/applications/wizard/stories/mockData.ts b/web/src/admin/applications/wizard/stories/mockData.ts new file mode 100644 index 000000000..3bd5be087 --- /dev/null +++ b/web/src/admin/applications/wizard/stories/mockData.ts @@ -0,0 +1,62 @@ +import { + dummyAuthenticationFlowsSearch, + dummyAuthorizationFlowsSearch, + dummyCoreGroupsSearch, + dummyCryptoCertsSearch, + dummyHasJwks, + dummyPropertyMappings, + dummyProviderTypesList, + dummySAMLProviderMappings, +} from "./samples"; + +export const mockData = [ + { + url: "/api/v3/providers/all/types/", + method: "GET", + status: 200, + response: dummyProviderTypesList, + }, + { + url: "/api/v3/core/groups/?ordering=name", + method: "GET", + status: 200, + response: dummyCoreGroupsSearch, + }, + + { + url: "/api/v3/crypto/certificatekeypairs/?has_key=true&include_details=false&ordering=name", + method: "GET", + status: 200, + response: dummyCryptoCertsSearch, + }, + { + url: "/api/v3/flows/instances/?designation=authentication&ordering=slug", + method: "GET", + status: 200, + response: dummyAuthenticationFlowsSearch, + }, + { + url: "/api/v3/flows/instances/?designation=authorization&ordering=slug", + method: "GET", + status: 200, + response: dummyAuthorizationFlowsSearch, + }, + { + url: "/api/v3/propertymappings/scope/?ordering=scope_name", + method: "GET", + status: 200, + response: dummyPropertyMappings, + }, + { + url: "/api/v3/sources/oauth/?has_jwks=true&ordering=name", + method: "GET", + status: 200, + response: dummyHasJwks, + }, + { + url: "/api/v3/propertymappings/saml/?ordering=saml_name", + method: "GET", + status: 200, + response: dummySAMLProviderMappings, + }, +]; diff --git a/web/src/admin/applications/wizard/types.ts b/web/src/admin/applications/wizard/types.ts new file mode 100644 index 000000000..05f0df578 --- /dev/null +++ b/web/src/admin/applications/wizard/types.ts @@ -0,0 +1,27 @@ +import { + Application, + LDAPProvider, + OAuth2Provider, + ProxyProvider, + RadiusProvider, + SAMLProvider, + SCIMProvider, +} from "@goauthentik/api"; + +export type OneOfProvider = + | Partial + | Partial + | Partial + | Partial + | Partial + | Partial; + +export interface WizardState { + step: number; + providerType: string; + application: Partial; + provider: OneOfProvider; +} + +export type WizardStateEvent = WizardState & { target?: HTMLInputElement }; + diff --git a/web/src/components/ak-wizard-2/ak-wizard-context.ts b/web/src/components/ak-wizard-main/ak-wizard-context.ts similarity index 81% rename from web/src/components/ak-wizard-2/ak-wizard-context.ts rename to web/src/components/ak-wizard-main/ak-wizard-context.ts index 19afc8cca..426683092 100644 --- a/web/src/components/ak-wizard-2/ak-wizard-context.ts +++ b/web/src/components/ak-wizard-main/ak-wizard-context.ts @@ -9,6 +9,15 @@ import { WizardStepEvent, } from "./types"; import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName"; import { akWizardStepsContextName } from "./akWizardStepsContextName"; +/** + * AkWizardContext + * + * @element ak-wizard-context + * + * The WizardContext controls the navigation for the wizard. It listens for navigation events from + * the wizard frame and responds with changes to the view, including handling the close button. + * + */ @customElement("ak-wizard-context") export class AkWizardContext extends CustomListenerElement(LitElement) { @@ -39,6 +48,11 @@ export class AkWizardContext extends CustomListenerElement(LitElement) { // Note that we always scan for the valid next step and throw an error if we can't find it. // There should never be a question that the currentStep is a *valid* step. + // + // TODO: Put a phase in there so that the current step can validate the contents asynchronously + // 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: WizardStepId | WizardStepEvent }>) { const requestedStep = event.detail.step; if (!requestedStep) { diff --git a/web/src/components/ak-wizard-2/ak-wizard-2.ts b/web/src/components/ak-wizard-main/ak-wizard-frame.ts similarity index 87% rename from web/src/components/ak-wizard-2/ak-wizard-2.ts rename to web/src/components/ak-wizard-main/ak-wizard-frame.ts index 2ec560d05..70a6f8bf8 100644 --- a/web/src/components/ak-wizard-2/ak-wizard-2.ts +++ b/web/src/components/ak-wizard-main/ak-wizard-frame.ts @@ -1,37 +1,38 @@ 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 { html, nothing } from "lit"; - - import { classMap } from "lit/directives/class-map.js"; -import { consume } from "@lit-labs/context" + import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css"; -import type { WizardStep } from "./types"; import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName"; import { akWizardStepsContextName } from "./akWizardStepsContextName"; +import type { WizardStep } from "./types"; /** - * AKWizard is a container for displaying Wizard pages. + * AKWizardFrame is the main container for displaying Wizard pages. * - * AKWizard is one component of a total Wizard development environment. It provides the header, titled - * navigation sidebar, and bottom row button bar. It takes its cues about what to render from two - * data structure, `this.steps: WizardStep[]`, which lists all the current steps *in order* and + * AKWizardFrame is one component of a total Wizard development environment. It provides the header, + * titled navigation sidebar, and bottom row button bar. It takes its cues about what to render from + * two data structure, `this.steps: WizardStep[]`, which lists all the current steps *in order* and * doesn't care otherwise about their structure, and `this.currentStep: WizardStep` which must be a * _reference_ to a member of `this.steps`. * - * @element ak-wizard-2 + * @element ak-wizard-frame * * @fires ak-wizard-nav - Tell the orchestrator what page the user wishes to move to. This is the * only event that causes this wizard to change its appearance. * + * NOTE: The event name is configurable as an attribute. + * */ -@customElement("ak-wizard-2") -export class AkWizard extends CustomEmitterElement(ModalButton) { +@customElement("ak-wizard-frame") +export class AkWizardFrame extends CustomEmitterElement(ModalButton) { static get styles() { return [...super.styles, PFWizard]; } @@ -48,9 +49,6 @@ export class AkWizard extends CustomEmitterElement(ModalButton) { @property() eventName: string = "ak-wizard-nav"; - @property({ type: Boolean }) - isValid = false; - // @ts-expect-error @consume({ context: akWizardStepsContextName, subscribe: true }) @state() @@ -134,7 +132,7 @@ export class AkWizard extends CustomEmitterElement(ModalButton) { renderFooter() { return html`
-${this.currentStep.nextStep ? this.renderFooterNextButton() : nothing } + ${this.currentStep.nextStep ? this.renderFooterNextButton() : nothing} ${this.currentStep.backStep ? this.renderFooterBackButton() : nothing} ${this.canCancel ? this.renderFooterCancelButton() : nothing}
@@ -175,4 +173,4 @@ ${this.currentStep.nextStep ? this.renderFooterNextButton() : nothing } } } -export default AkWizard; +export default AkWizardFrame; diff --git a/web/src/components/ak-wizard-main/ak-wizard-main.ts b/web/src/components/ak-wizard-main/ak-wizard-main.ts new file mode 100644 index 000000000..905904333 --- /dev/null +++ b/web/src/components/ak-wizard-main/ak-wizard-main.ts @@ -0,0 +1,86 @@ +import { AKElement } from "@goauthentik/elements/Base"; + +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { html } from "lit"; +import { property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import "./ak-wizard-frame"; +import "./ak-wizard-context"; +import type { WizardStep } from "./types"; + +/** + * AKWizardMain + * + * @element ak-wizard-main + * + * This is the entry point for the wizard. + * + */ + +@customElement("ak-wizard-main") +export class AkWizardMain extends AKElement { + static get styles() { + return [PFBase, PFButton, PFRadio]; + } + + /** + * The steps of the Wizard. + * + * @attribute + */ + @property({ attribute: false }) + steps: WizardStep[] = []; + + /** + * The text of the button + * + * @attribute + */ + @property({ type: String }) + prompt = "Show Wizard" + + /** + * Mostly a control on the ModalButton that summons the wizard component. + * + * @attribute + */ + @property({ type: Boolean, reflect: true }) + open = false; + + /** + * The text of the header on the wizard, upper bar. + * + * @attribute + */ + @property() + header!: string; + + /** + * The text of the description under the header. + * + * @attribute + */ + @property() + description?: string; + + render() { + return html` + + + + + + `; + } +} + +export default AkWizardMain; diff --git a/web/src/components/ak-wizard-2/akWizardCurrentStepContextName.ts b/web/src/components/ak-wizard-main/akWizardCurrentStepContextName.ts similarity index 100% rename from web/src/components/ak-wizard-2/akWizardCurrentStepContextName.ts rename to web/src/components/ak-wizard-main/akWizardCurrentStepContextName.ts diff --git a/web/src/components/ak-wizard-2/akWizardStepsContextName.ts b/web/src/components/ak-wizard-main/akWizardStepsContextName.ts similarity index 100% rename from web/src/components/ak-wizard-2/akWizardStepsContextName.ts rename to web/src/components/ak-wizard-main/akWizardStepsContextName.ts diff --git a/web/src/components/ak-wizard-main/index.ts b/web/src/components/ak-wizard-main/index.ts new file mode 100644 index 000000000..2d4c1a937 --- /dev/null +++ b/web/src/components/ak-wizard-main/index.ts @@ -0,0 +1,5 @@ +import "./ak-wizard-main"; +import type { WizardStepId, WizardStep } from "./types" +import { makeWizardId } from "./types"; + +export { WizardStepId, WizardStep, makeWizardId }; diff --git a/web/src/components/ak-wizard-2/stories/ak-demo-wizard.ts b/web/src/components/ak-wizard-main/stories/ak-demo-wizard.ts similarity index 75% rename from web/src/components/ak-wizard-2/stories/ak-demo-wizard.ts rename to web/src/components/ak-wizard-main/stories/ak-demo-wizard.ts index e8f9009af..00a58dcb5 100644 --- a/web/src/components/ak-wizard-2/stories/ak-demo-wizard.ts +++ b/web/src/components/ak-wizard-main/stories/ak-demo-wizard.ts @@ -3,13 +3,14 @@ import { AKElement } from "@goauthentik/elements/Base"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { html } from "lit"; import { property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import "../ak-wizard-frame"; import "../ak-wizard-context"; -import "../ak-wizard-2"; import type { WizardStep } from "../types"; @customElement("ak-demo-wizard") @@ -24,16 +25,22 @@ export class AkDemoWizard extends AKElement { @property({ type: Boolean }) open = false; + @property() + header!: string; + + @property() + description?: string; + render() { return html` - - + `; } diff --git a/web/src/components/ak-wizard-2/stories/ak-wizard-2.stories.ts b/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts similarity index 83% rename from web/src/components/ak-wizard-2/stories/ak-wizard-2.stories.ts rename to web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts index 71c7c0bfc..98d4d4cd6 100644 --- a/web/src/components/ak-wizard-2/stories/ak-wizard-2.stories.ts +++ b/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts @@ -3,16 +3,15 @@ import { Meta } from "@storybook/web-components"; import { TemplateResult, html } from "lit"; -import "../ak-wizard-2" -import "./ak-demo-wizard"; -import AkWizard from "../ak-wizard-2"; +import "../ak-wizard-main" +import AkWizard from "../ak-wizard-main"; import type { WizardStep } from "../types"; import { makeWizardId } from "../types"; const metadata: Meta = { title: "Components / Wizard / Basic", - component: "ak-wizard-2", + component: "ak-wizard-main", parameters: { docs: { description: { @@ -36,8 +35,6 @@ const container = (testItem: TemplateResult) => ${testItem} -

Messages received from the button:

-
    `; @@ -67,6 +64,6 @@ const dummySteps: WizardStep[] = [ export const OnePageWizard = () => { return container( - html` ` + html` ` ); }; diff --git a/web/src/components/ak-wizard-2/types.ts b/web/src/components/ak-wizard-main/types.ts similarity index 87% rename from web/src/components/ak-wizard-2/types.ts rename to web/src/components/ak-wizard-main/types.ts index 272a6fc59..b6f4e725e 100644 --- a/web/src/components/ak-wizard-2/types.ts +++ b/web/src/components/ak-wizard-main/types.ts @@ -18,7 +18,3 @@ export interface WizardStep { backButtonLabel?: string } -export enum WizardStepEvent { - next = "next", - back = "back" -}