Web: This is coming together amazingly well. Like, almost too well.
This commit is contained in:
parent
7465929475
commit
2a05a9d012
|
@ -0,0 +1,39 @@
|
||||||
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||||
|
import "@goauthentik/components/ak-file-input";
|
||||||
|
import "@goauthentik/components/ak-text-input";
|
||||||
|
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 { FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
|
import ApplicationWizardProviderPageBase from "../ApplicationWizardProviderPageBase";
|
||||||
|
|
||||||
|
@customElement("ak-application-wizard-authentication-by-saml-import")
|
||||||
|
export class ApplicationWizardProviderSamlImport extends ApplicationWizardProviderPageBase {
|
||||||
|
render() {
|
||||||
|
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||||
|
<ak-text-input name="name" label=${msg("Name")} required></ak-text-input>
|
||||||
|
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Authorization flow")}
|
||||||
|
?required=${true}
|
||||||
|
name="authorizationFlow"
|
||||||
|
>
|
||||||
|
<ak-flow-search-no-default
|
||||||
|
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||||
|
required
|
||||||
|
></ak-flow-search-no-default>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg("Flow used when authorizing this provider.")}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
|
<ak-file-input name="metadata" label=${msg("Metadata")} required></ak-file-input>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplicationWizardProviderSamlImport;
|
|
@ -11,6 +11,7 @@ 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";
|
||||||
import "../saml/ak-application-wizard-authentication-by-saml-configuration";
|
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 "./ak-application-context-display-for-test";
|
||||||
import {
|
import {
|
||||||
dummyAuthenticationFlowsSearch,
|
dummyAuthenticationFlowsSearch,
|
||||||
|
@ -182,3 +183,14 @@ export const ConfigureSamlManually = () => {
|
||||||
</ak-application-wizard-context>`,
|
</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>`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
178
web/src/components/ak-wizard-2/ak-wizard-2.ts
Normal file
178
web/src/components/ak-wizard-2/ak-wizard-2.ts
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
|
||||||
|
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AKWizard is a 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
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
@customElement("ak-wizard-2")
|
||||||
|
export class AkWizard extends CustomEmitterElement(ModalButton) {
|
||||||
|
static get styles() {
|
||||||
|
return [...super.styles, PFWizard];
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
canCancel = true;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
header?: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
eventName: string = "ak-wizard-nav";
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
isValid = false;
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
@consume({ context: akWizardStepsContextName, subscribe: true })
|
||||||
|
@state()
|
||||||
|
steps!: WizardStep[];
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
@consume({ context: akWizardCurrentStepContextName, subscribe: true })
|
||||||
|
@state()
|
||||||
|
currentStep!: WizardStep;
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderModalInner() {
|
||||||
|
// prettier-ignore
|
||||||
|
return html`<div class="pf-c-wizard">
|
||||||
|
${this.renderHeader()}
|
||||||
|
<div class="pf-c-wizard__outer-wrap">
|
||||||
|
<div class="pf-c-wizard__inner-wrap">
|
||||||
|
${this.renderNavigation()}
|
||||||
|
${this.renderMainSection()}
|
||||||
|
</div>
|
||||||
|
${this.renderFooter()}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader() {
|
||||||
|
return html`<div class="pf-c-wizard__header">
|
||||||
|
${this.canCancel ? this.renderHeaderCancelIcon() : nothing}
|
||||||
|
<h1 class="pf-c-title pf-m-3xl pf-c-wizard__title">${this.header}</h1>
|
||||||
|
<p class="pf-c-wizard__description">${this.description}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeaderCancelIcon() {
|
||||||
|
return html`<button
|
||||||
|
class="pf-c-button pf-m-plain pf-c-wizard__close"
|
||||||
|
type="button"
|
||||||
|
aria-label="${msg("Close")}"
|
||||||
|
@click=${() => this.reset()}
|
||||||
|
>
|
||||||
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNavigation() {
|
||||||
|
return html`<nav class="pf-c-wizard__nav">
|
||||||
|
<ol class="pf-c-wizard__nav-list">
|
||||||
|
${this.steps.map((step) => this.renderNavigationStep(step))}
|
||||||
|
</ol>
|
||||||
|
</nav>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNavigationStep(step: WizardStep) {
|
||||||
|
const buttonClasses = {
|
||||||
|
"pf-c-wizard__nav-link": true,
|
||||||
|
"pf-m-current": step.id === this.currentStep.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<li class="pf-c-wizard__nav-item">
|
||||||
|
<button
|
||||||
|
class=${classMap(buttonClasses)}
|
||||||
|
?disabled=${step.disabled}
|
||||||
|
@click=${() => this.dispatchCustomEvent(this.eventName, { step: step.id })}
|
||||||
|
>
|
||||||
|
${step.label}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMainSection() {
|
||||||
|
return html`<main class="pf-c-wizard__main">
|
||||||
|
<div class="pf-c-wizard__main-body">${this.currentStep.renderer()}</div>
|
||||||
|
</main>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFooter() {
|
||||||
|
return html`
|
||||||
|
<footer class="pf-c-wizard__footer">
|
||||||
|
${this.currentStep.nextStep ? this.renderFooterNextButton() : nothing }
|
||||||
|
${this.currentStep.backStep ? this.renderFooterBackButton() : nothing}
|
||||||
|
${this.canCancel ? this.renderFooterCancelButton() : nothing}
|
||||||
|
</footer>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFooterNextButton() {
|
||||||
|
return html`<button
|
||||||
|
class="pf-c-button pf-m-primary"
|
||||||
|
type="submit"
|
||||||
|
?disabled=${!this.currentStep.valid}
|
||||||
|
@click=${() =>
|
||||||
|
this.dispatchCustomEvent(this.eventName, { step: this.currentStep.nextStep })}
|
||||||
|
>
|
||||||
|
${this.currentStep.nextButtonLabel}
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFooterBackButton() {
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class="pf-c-button pf-m-secondary"
|
||||||
|
type="button"
|
||||||
|
@click=${() =>
|
||||||
|
this.dispatchCustomEvent(this.eventName, { step: this.currentStep.backStep })}
|
||||||
|
>
|
||||||
|
${this.currentStep.backButtonLabel}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFooterCancelButton() {
|
||||||
|
return html`<div class="pf-c-wizard__footer-cancel">
|
||||||
|
<button class="pf-c-button pf-m-link" type="button" @click=${() => this.reset()}>
|
||||||
|
${msg("Cancel")}
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AkWizard;
|
71
web/src/components/ak-wizard-2/ak-wizard-context.ts
Normal file
71
web/src/components/ak-wizard-2/ak-wizard-context.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
|
|
||||||
|
import { provide } from "@lit-labs/context";
|
||||||
|
import { customElement, property, state } from "@lit/reactive-element/decorators.js";
|
||||||
|
import { LitElement, html } from "lit";
|
||||||
|
|
||||||
|
import type { WizardStep, WizardStepId } from "./types";
|
||||||
|
import { WizardStepEvent, } from "./types";
|
||||||
|
import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName";
|
||||||
|
import { akWizardStepsContextName } from "./akWizardStepsContextName";
|
||||||
|
|
||||||
|
|
||||||
|
@customElement("ak-wizard-context")
|
||||||
|
export class AkWizardContext extends CustomListenerElement(LitElement) {
|
||||||
|
|
||||||
|
@property()
|
||||||
|
eventName: string = "ak-wizard-nav";
|
||||||
|
|
||||||
|
@provide({ context: akWizardStepsContextName })
|
||||||
|
@property({ attribute: false })
|
||||||
|
steps: WizardStep[] = [];
|
||||||
|
|
||||||
|
@provide({ context: akWizardCurrentStepContextName })
|
||||||
|
@state()
|
||||||
|
currentStep!: WizardStep;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.handleNavigation = this.handleNavigation.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the only case where currentStep could be anything other than a valid entry. Unless,
|
||||||
|
// of course, a step itself is so badly messed up it can't point to a real object.
|
||||||
|
willUpdate(_changedProperties: Map<string, any>) {
|
||||||
|
if (this.currentStep === undefined) {
|
||||||
|
this.currentStep = this.steps[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
handleNavigation(event: CustomEvent<{ step: WizardStepId | WizardStepEvent }>) {
|
||||||
|
const requestedStep = event.detail.step;
|
||||||
|
if (!requestedStep) {
|
||||||
|
throw new Error("Request for next step when no next step is available")
|
||||||
|
}
|
||||||
|
const step = this.steps.find(({ id }) => id === requestedStep);
|
||||||
|
if (!step) {
|
||||||
|
throw new Error("Request for next step when no next step is available.");
|
||||||
|
}
|
||||||
|
if (step.disabled) {
|
||||||
|
throw new Error("Request for next step when the next step is disabled.");
|
||||||
|
}
|
||||||
|
this.currentStep = step;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.addCustomListener(this.eventName, this.handleNavigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.removeCustomListener(this.eventName, this.handleNavigation);
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createContext } from "@lit-labs/context";
|
||||||
|
|
||||||
|
export const akWizardCurrentStepContextName = createContext(Symbol("ak-wizard-current-step"));
|
||||||
|
|
||||||
|
export default akWizardCurrentStepContextName;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createContext } from "@lit-labs/context";
|
||||||
|
|
||||||
|
export const akWizardStepsContextName = createContext(Symbol("ak-wizard-steps"));
|
||||||
|
|
||||||
|
export default akWizardStepsContextName;
|
40
web/src/components/ak-wizard-2/stories/ak-demo-wizard.ts
Normal file
40
web/src/components/ak-wizard-2/stories/ak-demo-wizard.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
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 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-context";
|
||||||
|
import "../ak-wizard-2";
|
||||||
|
import type { WizardStep } from "../types";
|
||||||
|
|
||||||
|
@customElement("ak-demo-wizard")
|
||||||
|
export class AkDemoWizard extends AKElement {
|
||||||
|
static get styles() {
|
||||||
|
return [PFBase, PFButton, PFRadio];
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
steps: WizardStep[] = [];
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
open = false;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<ak-wizard-context .steps=${this.steps}>
|
||||||
|
<ak-wizard-2
|
||||||
|
?open=${this.open}
|
||||||
|
header=${"Demo Wizard"}
|
||||||
|
description=${"Just Showing Off The Demo Wizard"}
|
||||||
|
>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-primary">Show Wizard</button>
|
||||||
|
</ak-wizard-2>
|
||||||
|
</ak-wizard-context>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import "@goauthentik/elements/messages/MessageContainer";
|
||||||
|
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 type { WizardStep } from "../types";
|
||||||
|
import { makeWizardId } from "../types";
|
||||||
|
|
||||||
|
const metadata: Meta<AkWizard> = {
|
||||||
|
title: "Components / Wizard / Basic",
|
||||||
|
component: "ak-wizard-2",
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: "A container for our wizard.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default metadata;
|
||||||
|
|
||||||
|
const container = (testItem: TemplateResult) =>
|
||||||
|
html` <div style="background: #fff; padding: 2em">
|
||||||
|
<style>
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<ak-message-container></ak-message-container>
|
||||||
|
${testItem}
|
||||||
|
<p>Messages received from the button:</p>
|
||||||
|
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const dummySteps: WizardStep[] = [
|
||||||
|
{
|
||||||
|
id: makeWizardId("0"),
|
||||||
|
label: "Test Step1",
|
||||||
|
renderer: () => html`<h2>This space intentionally left blank today</h2>`,
|
||||||
|
disabled: false,
|
||||||
|
valid: true,
|
||||||
|
nextStep: makeWizardId("1"),
|
||||||
|
nextButtonLabel: "Next",
|
||||||
|
backButtonLabel: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: makeWizardId("1"),
|
||||||
|
label: "Test Step 2",
|
||||||
|
renderer: () => html`<h2>This space also intentionally left blank</h2>`,
|
||||||
|
disabled: false,
|
||||||
|
valid: true,
|
||||||
|
backStep: makeWizardId("0"),
|
||||||
|
nextButtonLabel: undefined,
|
||||||
|
backButtonLabel: "Back",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OnePageWizard = () => {
|
||||||
|
return container(
|
||||||
|
html` <ak-demo-wizard .steps=${dummySteps}></ak-demo-wizard>`
|
||||||
|
);
|
||||||
|
};
|
24
web/src/components/ak-wizard-2/types.ts
Normal file
24
web/src/components/ak-wizard-2/types.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { TemplateResult } from "lit";
|
||||||
|
|
||||||
|
type PhantomType<Type, Data> = {_type: Type} & Data;
|
||||||
|
|
||||||
|
export type WizardStepId = PhantomType<"WizardId", string>
|
||||||
|
|
||||||
|
export const makeWizardId = (id: string): WizardStepId => id as WizardStepId;
|
||||||
|
|
||||||
|
export interface WizardStep {
|
||||||
|
id: WizardStepId,
|
||||||
|
nextStep?: WizardStepId,
|
||||||
|
backStep?: WizardStepId,
|
||||||
|
label: string,
|
||||||
|
valid: boolean,
|
||||||
|
renderer: () => TemplateResult,
|
||||||
|
disabled: boolean,
|
||||||
|
nextButtonLabel?: string,
|
||||||
|
backButtonLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WizardStepEvent {
|
||||||
|
next = "next",
|
||||||
|
back = "back"
|
||||||
|
}
|
Reference in a new issue