diff --git a/tests/wdio/.gitignore b/tests/wdio/.gitignore new file mode 100644 index 000000000..8a156a97e --- /dev/null +++ b/tests/wdio/.gitignore @@ -0,0 +1,112 @@ +reports/ + +# Created by https://www.gitignore.io/api/node +# Edit at https://www.gitignore.io/?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt +dist + +# Uncomment the public line if your project uses Gatsby +# https://nextjs.org/blog/next-9-1#public-directory-support +# https://create-react-app.dev/docs/using-the-public-folder/#docsNav +# public + +# Storybook build outputs +.out +.storybook-out + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Temporary folders +tmp/ +temp/ + +# End of https://www.gitignore.io/api/node +api/** +storybook-static/ diff --git a/web/authentik-live-tests/Makefile b/tests/wdio/Makefile similarity index 90% rename from web/authentik-live-tests/Makefile rename to tests/wdio/Makefile index 234027565..7024edb27 100644 --- a/web/authentik-live-tests/Makefile +++ b/tests/wdio/Makefile @@ -4,8 +4,6 @@ help: ## Print out this help message. sort -nr | head -1) && \ perl -ne "m/^((\w|-)*):.*##\s*(.*)/ && print(sprintf(\"%s: %s\t%s\n\", \$$1, \" \"x($$M-length(\$$1)), \$$3))" Makefile @echo "" - @echo "Set env NODE_ENV to 'development' to run this against the dev server" - @echo "" .PHONY: update-local-chromedriver update-local-chromedriver: ## Update the chrome driver to match the local chrome version, restoring package.json diff --git a/web/authentik-live-tests/lib/idiom.js b/tests/wdio/lib/idiom.js similarity index 100% rename from web/authentik-live-tests/lib/idiom.js rename to tests/wdio/lib/idiom.js diff --git a/web/authentik-live-tests/lib/utils.js b/tests/wdio/lib/utils.js similarity index 100% rename from web/authentik-live-tests/lib/utils.js rename to tests/wdio/lib/utils.js diff --git a/web/authentik-live-tests/package-lock.json b/tests/wdio/package-lock.json similarity index 100% rename from web/authentik-live-tests/package-lock.json rename to tests/wdio/package-lock.json diff --git a/web/authentik-live-tests/package.json b/tests/wdio/package.json similarity index 100% rename from web/authentik-live-tests/package.json rename to tests/wdio/package.json diff --git a/web/authentik-live-tests/scripts/check_local_chromedriver b/tests/wdio/scripts/check_local_chromedriver similarity index 100% rename from web/authentik-live-tests/scripts/check_local_chromedriver rename to tests/wdio/scripts/check_local_chromedriver diff --git a/web/authentik-live-tests/scripts/update_local_chromedriver b/tests/wdio/scripts/update_local_chromedriver similarity index 100% rename from web/authentik-live-tests/scripts/update_local_chromedriver rename to tests/wdio/scripts/update_local_chromedriver diff --git a/web/authentik-live-tests/tests/application-plus-ldap.test.js b/tests/wdio/tests/application-plus-ldap.test.js similarity index 100% rename from web/authentik-live-tests/tests/application-plus-ldap.test.js rename to tests/wdio/tests/application-plus-ldap.test.js diff --git a/web/authentik-live-tests/wdio.conf-safari.js b/tests/wdio/wdio.conf-safari.js similarity index 100% rename from web/authentik-live-tests/wdio.conf-safari.js rename to tests/wdio/wdio.conf-safari.js diff --git a/web/authentik-live-tests/wdio.conf.js b/tests/wdio/wdio.conf.js similarity index 100% rename from web/authentik-live-tests/wdio.conf.js rename to tests/wdio/wdio.conf.js diff --git a/web/authentik-live-tests/.gitignore b/web/authentik-live-tests/.gitignore deleted file mode 100644 index a9a1bd38a..000000000 --- a/web/authentik-live-tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -reports/ diff --git a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts index 64fcf7233..95e7798a1 100644 --- a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts +++ b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts @@ -103,7 +103,6 @@ export class ApplicationWizardCommitApplication extends BasePanel { Promise.allSettled([network, timeout]).then(([network_resolution]) => { if (network_resolution.status === "rejected") { this.commitState = errorState; - console.log(network_resolution.reason); return; } diff --git a/web/src/admin/applications/wizard/steps.ts b/web/src/admin/applications/wizard/steps.ts index 629402c25..e1f6171fb 100644 --- a/web/src/admin/applications/wizard/steps.ts +++ b/web/src/admin/applications/wizard/steps.ts @@ -1,4 +1,11 @@ import { WizardStep } from "@goauthentik/components/ak-wizard-main"; +import { + BackStep, + CancelWizard, + CloseWizard, + NextStep, + SubmitStep, +} from "@goauthentik/components/ak-wizard-main/commonWizardButtons"; import { msg } from "@lit/localize"; import { html } from "lit"; @@ -15,7 +22,7 @@ export const steps: WizardStep[] = [ renderer: () => html``, disabled: false, - nextButtonLabel: msg("Next"), + buttons: [NextStep, CancelWizard], valid: true, }, { @@ -24,8 +31,7 @@ export const steps: WizardStep[] = [ renderer: () => html``, disabled: false, - nextButtonLabel: msg("Next"), - backButtonLabel: msg("Back"), + buttons: [NextStep, BackStep, CancelWizard], valid: true, }, { @@ -34,8 +40,7 @@ export const steps: WizardStep[] = [ renderer: () => html``, disabled: true, - nextButtonLabel: msg("Next"), - backButtonLabel: msg("Back"), + buttons: [SubmitStep, BackStep, CancelWizard], valid: true, }, { @@ -44,8 +49,7 @@ export const steps: WizardStep[] = [ renderer: () => html``, disabled: true, - nextButtonLabel: msg("Submit"), - backButtonLabel: msg("Back"), + buttons: [BackStep, CancelWizard], valid: true, }, ]; diff --git a/web/src/components/ak-wizard-main/ak-wizard-frame.ts b/web/src/components/ak-wizard-main/ak-wizard-frame.ts index 51d93d3b7..0237a31ff 100644 --- a/web/src/components/ak-wizard-main/ak-wizard-frame.ts +++ b/web/src/components/ak-wizard-main/ak-wizard-frame.ts @@ -5,15 +5,16 @@ import { msg } from "@lit/localize"; import { customElement, property, query } from "@lit/reactive-element/decorators.js"; import { html, nothing } from "lit"; import { classMap } from "lit/directives/class-map.js"; +import { map } from "lit/directives/map.js"; import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css"; -import type { WizardStep } from "./types"; +import { type WizardButton, type WizardStep } from "./types"; /** * AKWizardFrame is the main container for displaying Wizard pages. * - * AKWizardFrame is one component of a total Wizard development environment. It provides the header, + * AKWizardFrame is one component of a 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 @@ -34,9 +35,7 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) { return [...super.styles, PFWizard]; } - @property({ type: Boolean }) - canCancel = true; - + /* Prop-drilled. Do not alter. */ @property() header?: string; @@ -49,28 +48,26 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) { @property({ attribute: false, type: Array }) steps!: WizardStep[]; - @property({ attribute: false, type: Object }) - currentStep!: WizardStep; + @property({ attribute: false, type: Number }) + currentStep!: number; @query("#main-content *:first-child") content!: HTMLElement; - reset() { - this.open = false; + @property({ type: Boolean }) + canCancel!: boolean; + + get step() { + const step = this.steps[this.currentStep]; + if (!step) { + throw new Error(`Request for step that does not exist: ${this.currentStep}`); + } + return step; } - get maxStep() { - return this.steps.length - 1; - } - - get nextStep() { - const idx = this.steps.findIndex((step) => step === this.currentStep); - return idx < this.maxStep ? this.steps[idx + 1] : undefined; - } - - get backStep() { - const idx = this.steps.findIndex((step) => step === this.currentStep); - return idx > 0 ? this.steps[idx - 1] : undefined; + constructor() { + super(); + this.renderButtons = this.renderButtons.bind(this); } renderModalInner() { @@ -100,7 +97,7 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) { class="pf-c-button pf-m-plain pf-c-wizard__close" type="button" aria-label="${msg("Close")}" - @click=${() => this.reset()} + @click=${() => this.dispatchCustomEvent(this.eventName, { command: "close" })} > `; @@ -109,15 +106,15 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) { renderNavigation() { return html``; } - renderNavigationStep(step: WizardStep) { + renderNavigationStep(step: WizardStep, idx: number) { const buttonClasses = { "pf-c-wizard__nav-link": true, - "pf-m-current": step.id === this.currentStep.id, + "pf-m-current": idx === this.currentStep, }; return html` @@ -125,7 +122,8 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) { @@ -137,48 +135,53 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) { // independent context. renderMainSection() { return html`
-
- ${this.currentStep.renderer()} -
+
${this.step.renderer()}
`; } renderFooter() { return html` `; } - renderFooterNextButton(nextStep: WizardStep) { + renderButtons([label, command]: WizardButton) { + switch (command) { + case "next": + return this.renderButton(label, "pf-m-primary", command); + case "back": + return this.renderButton(label, "pf-m-secondary", command); + case "close": + return this.renderLink(label, "pf-m-link"); + default: + throw new Error(`Button type not understood: ${command} for ${label}`); + } + } + + renderButton(label: string, classname: string, command: string) { + const buttonClasses = { "pf-c-button": true, [classname]: true }; return html``; } - renderFooterBackButton(backStep: WizardStep) { - return html` `; - } - - renderFooterCancelButton() { + renderLink(label: string, classname: string) { + const buttonClasses = { "pf-c-button": true, [classname]: true }; return html``; } diff --git a/web/src/components/ak-wizard-main/ak-wizard-main.ts b/web/src/components/ak-wizard-main/ak-wizard-main.ts index 1189882cc..8443799a2 100644 --- a/web/src/components/ak-wizard-main/ak-wizard-main.ts +++ b/web/src/components/ak-wizard-main/ak-wizard-main.ts @@ -26,11 +26,22 @@ const hasValidator = (v: any): v is Required> => * * @element ak-wizard-main * - * This is the entry point for the wizard. Its tasks are: + * This is the controller for a multi-form wizard. It provides an interface for describing a pop-up + * (modal) wizard, the contents of which are independent of the navigation. This controller only + * handles the navigation. + * + * Each step (see the `types.ts` file) provides label, a "currently valid" boolean, a "disabled" + * boolean, a function that returns the HTML of the object to be rendered, a `disabled` flag + * indicating + + Its tasks are: * - keep the collection of steps * - maintain the open/close status of the modal * - listens for navigation events * - if a navigation event is valid, switch to the panel requested + * + * + */ @customElement("ak-wizard-main") @@ -56,7 +67,7 @@ export class AkWizardMain extends CustomListenerElement(AKElement) { * @attribute */ @state() - currentStep!: WizardStep; + currentStep: number = 0; constructor() { super(); @@ -71,14 +82,6 @@ export class AkWizardMain extends CustomListenerElement(AKElement) { @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. * @@ -95,18 +98,17 @@ export class AkWizardMain extends CustomListenerElement(AKElement) { @property() description?: string; + /** + * Whether or not to show the "cancel" button in the wizard. + * + * @attribute + */ + @property({ type: Boolean }) + canCancel!: boolean; + @query("ak-wizard-frame") frame!: AkWizardFrame; - // Guarantee that if the current step was not passed in by the client, that we know - // and set to the first step. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - willUpdate(_changedProperties: Map) { - if (this.currentStep === undefined) { - this.currentStep = this.steps[0]; - } - } - connectedCallback() { super.connectedCallback(); this.addCustomListener(this.eventName, this.handleNavigation); @@ -117,30 +119,55 @@ export class AkWizardMain extends CustomListenerElement(AKElement) { super.disconnectedCallback(); } - // 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: string; action: string }>) { - 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 (event.detail.action === "next" && !this.validated()) { - return false; - } - this.currentStep = step; - return true; + get maxStep() { + return this.steps.length - 1; } - validated() { + get nextStep() { + return this.currentStep < this.maxStep ? this.currentStep + 1 : undefined; + } + + get backStep() { + return this.currentStep > 0 ? this.currentStep - 1 : undefined; + } + + handleNavigation(event: CustomEvent<{ command: string; step?: number }>) { + const command = event.detail.command; + console.log(command); + switch (command) { + case "back": { + if (this.backStep !== undefined && this.steps[this.backStep]) { + this.currentStep = this.backStep; + } + return; + } + case "goto": { + if ( + typeof event.detail.step === "number" && + event.detail.step >= 0 && + event.detail.step <= this.maxStep + ) + this.currentStep = event.detail.step; + return; + } + case "next": { + if ( + this.nextStep && + this.steps[this.nextStep] && + !this.steps[this.nextStep].disabled && + this.validated + ) { + this.currentStep = this.nextStep; + } + return; + } + case "close": { + this.frame.open = this.open; + } + } + } + + get validated() { if (hasValidator(this.frame.content)) { return this.frame.content.validator(); } @@ -150,7 +177,7 @@ export class AkWizardMain extends CustomListenerElement(AKElement) { render() { return html` diff --git a/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts b/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts index e35f9de5e..73c1ecfd9 100644 --- a/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts +++ b/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts @@ -1,6 +1,6 @@ import "@goauthentik/elements/messages/MessageContainer"; import { Meta } from "@storybook/web-components"; - +import { NextStep, BackStep, CancelWizard, CloseWizard } from "../commonWizardButtons"; import { TemplateResult, html } from "lit"; import "../ak-wizard-main"; @@ -37,27 +37,21 @@ const container = (testItem: TemplateResult) => const dummySteps: WizardStep[] = [ { - id: "0", label: "Test Step1", renderer: () => html`

This space intentionally left blank today

`, disabled: false, - valid: true, - nextButtonLabel: "Next", - backButtonLabel: undefined, + buttons: [NextStep, CancelWizard], }, { - id: "1", label: "Test Step 2", renderer: () => html`

This space also intentionally left blank

`, disabled: false, - valid: true, - nextButtonLabel: undefined, - backButtonLabel: "Back", + buttons: [BackStep, CloseWizard], }, ]; export const OnePageWizard = () => { return container( - html` `, + html` `, ); }; diff --git a/web/src/components/ak-wizard-main/types.ts b/web/src/components/ak-wizard-main/types.ts index 2c629f1b2..41830be08 100644 --- a/web/src/components/ak-wizard-main/types.ts +++ b/web/src/components/ak-wizard-main/types.ts @@ -1,13 +1,38 @@ import { TemplateResult } from "lit"; +export type WizardNavCommand = "next" | "back" | "close" | ["goto", number]; + + +// The label of the button, the command the button should execute, and if the button +// should be marked "disabled." +export type WizardButton = [string, WizardNavCommand, boolean?]; + + export interface WizardStep { - id: string; - label: string; - valid: boolean; + // The name of the step, as shown in the navigation. + label: string; + + // A function which returns the html for rendering the actual content of the step, its form and + // such. renderer: () => TemplateResult; + + // A collection of buttons, in render order, that are to be shown in the button bar. The + // semantics of the buttons are simple: 'next' will navigate to currentStep + 1, 'back' will + // navigate to currentStep - 1, 'close' will close the window, and ['goto', number] will + // navigate to a specific step in order. + // + // It is possible for the controlling component that wraps ak-wizard-main to supply a modified + // collection of steps at any time, thus altering the behavior of future steps, or providing a + // tree-like structure to the wizard. + // + // Note that if you change the steps radically (inserting some in front of the currentStep, + // which is something you should never, ever do... never, ever make the customer go backward to + // solve a problem that was your responsibility. "Going back" to fix their own mistakes is, of + // course, their responsibility) you may have to set the currentStep as well. + buttons: WizardButton[]; + + // If this step is "disabled," the prior step's next button will be disabled. disabled: boolean; - nextButtonLabel?: string; - backButtonLabel?: string; } export interface WizardPanel extends HTMLElement {