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.
This commit is contained in:
Ken Sternberg 2023-08-10 11:52:08 -07:00
parent 2a05a9d012
commit ae99ef5fe4
21 changed files with 401 additions and 288 deletions

View File

@ -4,8 +4,7 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import { state } from "@lit/reactive-element/decorators/state.js"; 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 type { WizardState } from "./ak-application-wizard-context";
import { applicationWizardContext } from "./ak-application-wizard-context-name"; import { applicationWizardContext } from "./ak-application-wizard-context-name";
@ -14,13 +13,12 @@ export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) {
return AwadStyles; return AwadStyles;
} }
// @ts-expect-error
@consume({ context: applicationWizardContext, subscribe: true }) @consume({ context: applicationWizardContext, subscribe: true })
@state() @state()
public wizard!: WizardState; public wizard!: WizardState;
dispatchWizardUpdate(update: Partial<WizardState>) { dispatchWizardUpdate(update: Partial<WizardState>) {
this.dispatchCustomEvent("ak-wizard-update", { this.dispatchCustomEvent("ak-application-wizard-update", {
...this.wizard, ...this.wizard,
...update, ...update,
}); });

View File

@ -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`<ak-application-wizard-application-details></ak-application-wizard-application-details>`,
disabled: false,
nextButtonLabel: msg("Next"),
valid: true,
},
{
id: makeWizardId("auth-method-choice"),
backStep: makeWizardId("application"),
nextStep: makeWizardId("auth-method"),
label: "Authentication Method",
renderer: () =>
html`<ak-application-wizard-authentication-method-choice></ak-application-wizard-authentication-method-choice>`,
disabled: false,
nextButtonLabel: msg("Next"),
backButtonLabel: msg("Back"),
valid: true,
},
{
id: makeWizardId("auth-method"),
backStep: makeWizardId("auth-method-choice"),
label: "Authentication Details",
renderer: () =>
html`<ak-application-wizard-authentication-method></ak-application-wizard-authentication-method>`,
disabled: true,
nextButtonLabel: msg("Submit"),
backButtonLabel: msg("Back"),
valid: true,
}
];

View File

@ -1,58 +1,43 @@
import "@goauthentik/admin/applications/wizard/InitialApplicationWizardPage"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import "@goauthentik/admin/applications/wizard/TypeApplicationWizardPage"; import "@goauthentik/components/ak-wizard-main";
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 { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/wizard/Wizard";
import { provide } 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 { CSSResult, TemplateResult, html } from "lit"; 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 PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; 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 { WizardState, WizardStateEvent } from "./types"
const steps = [ import { steps } from "./ApplicationWizardSteps";
{ import applicationWizardContext from "./ak-application-wizard-context-name";
name: msg("Application Details"),
view: () => // my-context.ts
html`<ak-application-wizard-application-details></ak-application-wizard-application-details>`,
},
{
name: msg("Authentication Method"),
view: () =>
html`<ak-application-wizard-authentication-choice></ak-application-wizard-authentication-choice>`,
},
{
name: msg("Authentication Details"),
view: () =>
html`<ak-application-wizard-authentication-details></ak-application-wizard-authentication-details>`,
},
{
name: msg("Save Application"),
view: () =>
html`<ak-application-wizard-application-commit></ak-application-wizard-application-commit>`,
},
];
*/
@customElement("ak-application-wizard") @customElement("ak-application-wizard")
export class ApplicationWizard extends AKElement { export class ApplicationWizard extends CustomListenerElement(AKElement) {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio]; 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 }) @property({ type: Boolean })
open = false; open = false;
@ -67,23 +52,54 @@ export class ApplicationWizard extends AKElement {
return Promise.resolve(); 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<WizardStateEvent>) {
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 { render(): TemplateResult {
return html` return html`
<ak-wizard <ak-wizard-main
.open=${this.open} .steps=${this.steps}
.steps=${["ak-application-wizard-initial", "ak-application-wizard-type"]}
header=${msg("New application")} header=${msg("New application")}
description=${msg("Create a new application.")} description=${msg("Create a new application.")}
.finalHandler=${() => {
return this.finalHandler();
}}
> >
${this.showButton ${this.showButton
? html`<button slot="trigger" class="pf-c-button pf-m-primary"> ? html`<button slot="trigger" class="pf-c-button pf-m-primary">
${this.createText} ${this.createText}
</button>` </button>`
: html``} : html``}
</ak-wizard> </ak-wizard-main>
`; `;
} }
} }

View File

@ -12,10 +12,11 @@ import { customElement } from "@lit/reactive-element/decorators/custom-element.j
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 ApplicationWizardPageBase from "./ApplicationWizardPageBase"; import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
@customElement("ak-application-wizard-application-details") @customElement("ak-application-wizard-application-details")
export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBase { export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBase {
handleChange(ev: Event) { handleChange(ev: Event) {
if (!ev.target) { if (!ev.target) {
console.warn(`Received event with no target: ${ev}`); console.warn(`Received event with no target: ${ev}`);
@ -88,4 +89,6 @@ export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBa
} }
} }
export default ApplicationWizardApplicationDetails; export default ApplicationWizardApplicationDetails;

View File

@ -12,7 +12,7 @@ import { map } from "lit/directives/map.js";
import type { TypeCreate } from "@goauthentik/api"; import type { TypeCreate } from "@goauthentik/api";
import ApplicationWizardPageBase from "./ApplicationWizardPageBase"; import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
import providerTypesList from "./ak-application-wizard-authentication-method-choice.choices"; import providerTypesList from "./ak-application-wizard-authentication-method-choice.choices";
@customElement("ak-application-wizard-authentication-method-choice") @customElement("ak-application-wizard-authentication-method-choice")
@ -29,6 +29,8 @@ export class ApplicationWizardAuthenticationMethodChoice extends ApplicationWiza
} }
renderProvider(type: TypeCreate) { renderProvider(type: TypeCreate) {
const method = this.wizard.providerType;
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"
@ -36,6 +38,7 @@ export class ApplicationWizardAuthenticationMethodChoice extends ApplicationWiza
name="type" name="type"
id="provider-${type.modelName}" id="provider-${type.modelName}"
value=${type.modelName} value=${type.modelName}
?checked=${type.modelName === 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.modelName}">${type.name}</label>

View File

@ -1,11 +1,11 @@
import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import ApplicationWizardPageBase from "./ApplicationWizardPageBase"; import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
import { providerRendererList } from "./ak-application-wizard-authentication-method-choice.choices"; import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
import "./ldap/ak-application-wizard-authentication-by-ldap"; import "../ldap/ak-application-wizard-authentication-by-ldap";
import "./oauth/ak-application-wizard-authentication-by-oauth"; import "../oauth/ak-application-wizard-authentication-by-oauth";
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy"; import "../proxy/ak-application-wizard-authentication-for-reverse-proxy";
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy"; import "../proxy/ak-application-wizard-authentication-for-single-forward-proxy";
// prettier-ignore // prettier-ignore

View File

@ -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<ApplicationWizard> = {
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` <div style="background: #fff; padding: 1.0rem;">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
${testItem}
</div>`;
};
export const MainPage = () => {
return container(html`
<ak-application-wizard>></ak-application-wizard>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
`);
};

View File

@ -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<AkApplicationWizardApplicationDetails> = {
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` <div style="background: #fff; padding: 1.0rem;">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
${testItem}
</div>`;
};
export const DescribeApplication = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-application-details></ak-application-wizard-application-details>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`,
);
};
export const ChooseAuthMethod = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-authentication-method-choice></ak-application-wizard-authentication-method-choice>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`,
);
};
export const ConfigureLdap = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`,
);
};
export const ConfigureOauth2 = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`,
);
};
export const ConfigureReverseProxy = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`,
);
};
export const ConfigureSingleForwardProxy = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`,
);
};
export const ConfigureSamlManually = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`,
);
};
export const SamlImport = () => {
return container(
html`<ak-application-wizard-context>
<ak-application-wizard-authentication-by-saml-import></ak-application-wizard-authentication-by-saml-import>
<hr />
<ak-application-context-display-for-test></ak-application-context-display-for-test>
</ak-application-wizard-context>`,
);
};

View File

@ -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,
},
];

View File

@ -0,0 +1,27 @@
import {
Application,
LDAPProvider,
OAuth2Provider,
ProxyProvider,
RadiusProvider,
SAMLProvider,
SCIMProvider,
} from "@goauthentik/api";
export type OneOfProvider =
| Partial<SCIMProvider>
| Partial<SAMLProvider>
| Partial<RadiusProvider>
| Partial<ProxyProvider>
| Partial<OAuth2Provider>
| Partial<LDAPProvider>;
export interface WizardState {
step: number;
providerType: string;
application: Partial<Application>;
provider: OneOfProvider;
}
export type WizardStateEvent = WizardState & { target?: HTMLInputElement };

View File

@ -9,6 +9,15 @@ import { WizardStepEvent, } from "./types";
import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName"; import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName";
import { akWizardStepsContextName } from "./akWizardStepsContextName"; 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") @customElement("ak-wizard-context")
export class AkWizardContext extends CustomListenerElement(LitElement) { 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. // 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. // 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 }>) { handleNavigation(event: CustomEvent<{ step: WizardStepId | WizardStepEvent }>) {
const requestedStep = event.detail.step; const requestedStep = event.detail.step;
if (!requestedStep) { if (!requestedStep) {

View File

@ -1,37 +1,38 @@
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, state } 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 { consume } from "@lit-labs/context"
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css"; import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
import type { WizardStep } from "./types";
import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName"; import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName";
import { akWizardStepsContextName } from "./akWizardStepsContextName"; 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 * AKWizardFrame is one component of a total Wizard development environment. It provides the header,
* navigation sidebar, and bottom row button bar. It takes its cues about what to render from two * titled navigation sidebar, and bottom row button bar. It takes its cues about what to render from
* data structure, `this.steps: WizardStep[]`, which lists all the current steps *in order* and * 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 * doesn't care otherwise about their structure, and `this.currentStep: WizardStep` which must be a
* _reference_ to a member of `this.steps`. * _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 * @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. * only event that causes this wizard to change its appearance.
* *
* NOTE: The event name is configurable as an attribute.
*
*/ */
@customElement("ak-wizard-2") @customElement("ak-wizard-frame")
export class AkWizard extends CustomEmitterElement(ModalButton) { export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
static get styles() { static get styles() {
return [...super.styles, PFWizard]; return [...super.styles, PFWizard];
} }
@ -48,9 +49,6 @@ export class AkWizard extends CustomEmitterElement(ModalButton) {
@property() @property()
eventName: string = "ak-wizard-nav"; eventName: string = "ak-wizard-nav";
@property({ type: Boolean })
isValid = false;
// @ts-expect-error // @ts-expect-error
@consume({ context: akWizardStepsContextName, subscribe: true }) @consume({ context: akWizardStepsContextName, subscribe: true })
@state() @state()
@ -175,4 +173,4 @@ ${this.currentStep.nextStep ? this.renderFooterNextButton() : nothing }
} }
} }
export default AkWizard; export default AkWizardFrame;

View File

@ -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`
<ak-wizard-context .steps=${this.steps}>
<ak-wizard-frame
?open=${this.open}
header=${this.header}
description=${ifDefined(this.description)}
>
<button slot="trigger" class="pf-c-button pf-m-primary">${this.prompt}</button>
</ak-wizard-frame>
</ak-wizard-context>
`;
}
}
export default AkWizardMain;

View File

@ -0,0 +1,5 @@
import "./ak-wizard-main";
import type { WizardStepId, WizardStep } from "./types"
import { makeWizardId } from "./types";
export { WizardStepId, WizardStep, makeWizardId };

View File

@ -3,13 +3,14 @@ import { AKElement } from "@goauthentik/elements/Base";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html } from "lit"; import { html } from "lit";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.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";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; 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-context"; import "../ak-wizard-context";
import "../ak-wizard-2";
import type { WizardStep } from "../types"; import type { WizardStep } from "../types";
@customElement("ak-demo-wizard") @customElement("ak-demo-wizard")
@ -24,16 +25,22 @@ export class AkDemoWizard extends AKElement {
@property({ type: Boolean }) @property({ type: Boolean })
open = false; open = false;
@property()
header!: string;
@property()
description?: string;
render() { render() {
return html` return html`
<ak-wizard-context .steps=${this.steps}> <ak-wizard-context .steps=${this.steps}>
<ak-wizard-2 <ak-wizard-frame
?open=${this.open} ?open=${this.open}
header=${"Demo Wizard"} header=${this.header}
description=${"Just Showing Off The Demo Wizard"} description=${ifDefined(this.description)}
> >
<button slot="trigger" class="pf-c-button pf-m-primary">Show Wizard</button> <button slot="trigger" class="pf-c-button pf-m-primary">Show Wizard</button>
</ak-wizard-2> </ak-wizard-frame>
</ak-wizard-context> </ak-wizard-context>
`; `;
} }

View File

@ -3,16 +3,15 @@ import { Meta } from "@storybook/web-components";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import "../ak-wizard-2" import "../ak-wizard-main"
import "./ak-demo-wizard"; import AkWizard from "../ak-wizard-main";
import AkWizard from "../ak-wizard-2";
import type { WizardStep } from "../types"; import type { WizardStep } from "../types";
import { makeWizardId } from "../types"; import { makeWizardId } from "../types";
const metadata: Meta<AkWizard> = { const metadata: Meta<AkWizard> = {
title: "Components / Wizard / Basic", title: "Components / Wizard / Basic",
component: "ak-wizard-2", component: "ak-wizard-main",
parameters: { parameters: {
docs: { docs: {
description: { description: {
@ -36,8 +35,6 @@ const container = (testItem: TemplateResult) =>
</style> </style>
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
${testItem} ${testItem}
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`; </div>`;
@ -67,6 +64,6 @@ const dummySteps: WizardStep[] = [
export const OnePageWizard = () => { export const OnePageWizard = () => {
return container( return container(
html` <ak-demo-wizard .steps=${dummySteps}></ak-demo-wizard>` html` <ak-wizard-main .steps=${dummySteps} prompt="Start the show!"></ak-wizard-main>`
); );
}; };

View File

@ -18,7 +18,3 @@ export interface WizardStep {
backButtonLabel?: string backButtonLabel?: string
} }
export enum WizardStepEvent {
next = "next",
back = "back"
}