Web: This is coming together amazingly well. Like, almost too well.

This commit is contained in:
Ken Sternberg 2023-08-09 14:08:24 -07:00
parent 7465929475
commit 2a05a9d012
9 changed files with 446 additions and 0 deletions

View File

@ -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;

View File

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

View 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;

View 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>`;
}
}

View File

@ -0,0 +1,5 @@
import { createContext } from "@lit-labs/context";
export const akWizardCurrentStepContextName = createContext(Symbol("ak-wizard-current-step"));
export default akWizardCurrentStepContextName;

View File

@ -0,0 +1,5 @@
import { createContext } from "@lit-labs/context";
export const akWizardStepsContextName = createContext(Symbol("ak-wizard-steps"));
export default akWizardStepsContextName;

View 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>
`;
}
}

View File

@ -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>`
);
};

View 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"
}