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-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,
|
||||
|
@ -182,3 +183,14 @@ export const ConfigureSamlManually = () => {
|
|||
</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