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:
parent
2a05a9d012
commit
ae99ef5fe4
|
@ -4,8 +4,7 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
|||
import { consume } from "@lit-labs/context";
|
||||
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 { applicationWizardContext } from "./ak-application-wizard-context-name";
|
||||
|
||||
|
@ -14,13 +13,12 @@ export class ApplicationWizardPageBase extends CustomEmitterElement(AKElement) {
|
|||
return AwadStyles;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
@consume({ context: applicationWizardContext, subscribe: true })
|
||||
@state()
|
||||
public wizard!: WizardState;
|
||||
|
||||
dispatchWizardUpdate(update: Partial<WizardState>) {
|
||||
this.dispatchCustomEvent("ak-wizard-update", {
|
||||
this.dispatchCustomEvent("ak-application-wizard-update", {
|
||||
...this.wizard,
|
||||
...update,
|
||||
});
|
||||
|
|
43
web/src/admin/applications/wizard/ApplicationWizardSteps.ts
Normal file
43
web/src/admin/applications/wizard/ApplicationWizardSteps.ts
Normal 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,
|
||||
}
|
||||
];
|
|
@ -1,58 +1,43 @@
|
|||
import "@goauthentik/admin/applications/wizard/InitialApplicationWizardPage";
|
||||
import "@goauthentik/admin/applications/wizard/TypeApplicationWizardPage";
|
||||
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 { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
import "@goauthentik/components/ak-wizard-main";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/wizard/Wizard";
|
||||
|
||||
import { provide } from "@lit-labs/context";
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
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 PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
/*
|
||||
const steps = [
|
||||
{
|
||||
name: msg("Application Details"),
|
||||
view: () =>
|
||||
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>`,
|
||||
},
|
||||
];
|
||||
*/
|
||||
import { WizardState, WizardStateEvent } from "./types"
|
||||
import { steps } from "./ApplicationWizardSteps";
|
||||
import applicationWizardContext from "./ak-application-wizard-context-name";
|
||||
|
||||
// my-context.ts
|
||||
|
||||
@customElement("ak-application-wizard")
|
||||
export class ApplicationWizard extends AKElement {
|
||||
export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
||||
static get styles(): CSSResult[] {
|
||||
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 })
|
||||
open = false;
|
||||
|
||||
|
@ -67,23 +52,54 @@ export class ApplicationWizard extends AKElement {
|
|||
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 {
|
||||
return html`
|
||||
<ak-wizard
|
||||
.open=${this.open}
|
||||
.steps=${["ak-application-wizard-initial", "ak-application-wizard-type"]}
|
||||
<ak-wizard-main
|
||||
.steps=${this.steps}
|
||||
header=${msg("New application")}
|
||||
description=${msg("Create a new application.")}
|
||||
.finalHandler=${() => {
|
||||
return this.finalHandler();
|
||||
}}
|
||||
>
|
||||
${this.showButton
|
||||
? html`<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${this.createText}
|
||||
</button>`
|
||||
: html``}
|
||||
</ak-wizard>
|
||||
</ak-wizard-main>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,11 @@ import { customElement } from "@lit/reactive-element/decorators/custom-element.j
|
|||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import ApplicationWizardPageBase from "./ApplicationWizardPageBase";
|
||||
import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
|
||||
|
||||
@customElement("ak-application-wizard-application-details")
|
||||
export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBase {
|
||||
|
||||
handleChange(ev: Event) {
|
||||
if (!ev.target) {
|
||||
console.warn(`Received event with no target: ${ev}`);
|
||||
|
@ -88,4 +89,6 @@ export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBa
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default ApplicationWizardApplicationDetails;
|
|
@ -12,7 +12,7 @@ import { map } from "lit/directives/map.js";
|
|||
|
||||
import type { TypeCreate } from "@goauthentik/api";
|
||||
|
||||
import ApplicationWizardPageBase from "./ApplicationWizardPageBase";
|
||||
import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
|
||||
import providerTypesList from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-method-choice")
|
||||
|
@ -29,13 +29,16 @@ export class ApplicationWizardAuthenticationMethodChoice extends ApplicationWiza
|
|||
}
|
||||
|
||||
renderProvider(type: TypeCreate) {
|
||||
const method = this.wizard.providerType;
|
||||
|
||||
return html`<div class="pf-c-radio">
|
||||
<input
|
||||
class="pf-c-radio__input"
|
||||
type="radio"
|
||||
name="type"
|
||||
id="provider-${type.modelName}"
|
||||
value=${type.modelName}
|
||||
value=${type.modelName}
|
||||
?checked=${type.modelName === method}
|
||||
@change=${this.handleChoice}
|
||||
/>
|
||||
<label class="pf-c-radio__label" for="provider-${type.modelName}">${type.name}</label>
|
|
@ -1,11 +1,11 @@
|
|||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
|
||||
import ApplicationWizardPageBase from "./ApplicationWizardPageBase";
|
||||
import { providerRendererList } from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
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 ApplicationWizardPageBase from "../ApplicationWizardPageBase";
|
||||
import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
|
||||
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";
|
||||
|
||||
// prettier-ignore
|
||||
|
|
@ -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>
|
||||
`);
|
||||
};
|
|
@ -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>`,
|
||||
);
|
||||
};
|
62
web/src/admin/applications/wizard/stories/mockData.ts
Normal file
62
web/src/admin/applications/wizard/stories/mockData.ts
Normal 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,
|
||||
},
|
||||
];
|
27
web/src/admin/applications/wizard/types.ts
Normal file
27
web/src/admin/applications/wizard/types.ts
Normal 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 };
|
||||
|
|
@ -9,6 +9,15 @@ import { WizardStepEvent, } from "./types";
|
|||
import { akWizardCurrentStepContextName } from "./akWizardCurrentStepContextName";
|
||||
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")
|
||||
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.
|
||||
// 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 }>) {
|
||||
const requestedStep = event.detail.step;
|
||||
if (!requestedStep) {
|
|
@ -1,37 +1,38 @@
|
|||
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
|
||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { consume } from "@lit-labs/context";
|
||||
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";
|
||||
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
|
||||
* 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
|
||||
* AKWizardFrame 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
|
||||
* @element ak-wizard-frame
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* NOTE: The event name is configurable as an attribute.
|
||||
*
|
||||
*/
|
||||
|
||||
@customElement("ak-wizard-2")
|
||||
export class AkWizard extends CustomEmitterElement(ModalButton) {
|
||||
@customElement("ak-wizard-frame")
|
||||
export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||
static get styles() {
|
||||
return [...super.styles, PFWizard];
|
||||
}
|
||||
|
@ -48,9 +49,6 @@ export class AkWizard extends CustomEmitterElement(ModalButton) {
|
|||
@property()
|
||||
eventName: string = "ak-wizard-nav";
|
||||
|
||||
@property({ type: Boolean })
|
||||
isValid = false;
|
||||
|
||||
// @ts-expect-error
|
||||
@consume({ context: akWizardStepsContextName, subscribe: true })
|
||||
@state()
|
||||
|
@ -134,7 +132,7 @@ export class AkWizard extends CustomEmitterElement(ModalButton) {
|
|||
renderFooter() {
|
||||
return html`
|
||||
<footer class="pf-c-wizard__footer">
|
||||
${this.currentStep.nextStep ? this.renderFooterNextButton() : nothing }
|
||||
${this.currentStep.nextStep ? this.renderFooterNextButton() : nothing}
|
||||
${this.currentStep.backStep ? this.renderFooterBackButton() : nothing}
|
||||
${this.canCancel ? this.renderFooterCancelButton() : nothing}
|
||||
</footer>
|
||||
|
@ -175,4 +173,4 @@ ${this.currentStep.nextStep ? this.renderFooterNextButton() : nothing }
|
|||
}
|
||||
}
|
||||
|
||||
export default AkWizard;
|
||||
export default AkWizardFrame;
|
86
web/src/components/ak-wizard-main/ak-wizard-main.ts
Normal file
86
web/src/components/ak-wizard-main/ak-wizard-main.ts
Normal 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;
|
5
web/src/components/ak-wizard-main/index.ts
Normal file
5
web/src/components/ak-wizard-main/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import "./ak-wizard-main";
|
||||
import type { WizardStepId, WizardStep } from "./types"
|
||||
import { makeWizardId } from "./types";
|
||||
|
||||
export { WizardStepId, WizardStep, makeWizardId };
|
|
@ -3,13 +3,14 @@ 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 "../ak-wizard-2";
|
||||
import type { WizardStep } from "../types";
|
||||
|
||||
@customElement("ak-demo-wizard")
|
||||
|
@ -24,16 +25,22 @@ export class AkDemoWizard extends AKElement {
|
|||
@property({ type: Boolean })
|
||||
open = false;
|
||||
|
||||
@property()
|
||||
header!: string;
|
||||
|
||||
@property()
|
||||
description?: string;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ak-wizard-context .steps=${this.steps}>
|
||||
<ak-wizard-2
|
||||
<ak-wizard-frame
|
||||
?open=${this.open}
|
||||
header=${"Demo Wizard"}
|
||||
description=${"Just Showing Off The Demo Wizard"}
|
||||
header=${this.header}
|
||||
description=${ifDefined(this.description)}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">Show Wizard</button>
|
||||
</ak-wizard-2>
|
||||
</ak-wizard-frame>
|
||||
</ak-wizard-context>
|
||||
`;
|
||||
}
|
|
@ -3,16 +3,15 @@ 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 "../ak-wizard-main"
|
||||
import AkWizard from "../ak-wizard-main";
|
||||
|
||||
import type { WizardStep } from "../types";
|
||||
import { makeWizardId } from "../types";
|
||||
|
||||
const metadata: Meta<AkWizard> = {
|
||||
title: "Components / Wizard / Basic",
|
||||
component: "ak-wizard-2",
|
||||
component: "ak-wizard-main",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
|
@ -36,8 +35,6 @@ const container = (testItem: TemplateResult) =>
|
|||
</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>`;
|
||||
|
||||
|
||||
|
@ -67,6 +64,6 @@ const dummySteps: WizardStep[] = [
|
|||
|
||||
export const OnePageWizard = () => {
|
||||
return container(
|
||||
html` <ak-demo-wizard .steps=${dummySteps}></ak-demo-wizard>`
|
||||
html` <ak-wizard-main .steps=${dummySteps} prompt="Start the show!"></ak-wizard-main>`
|
||||
);
|
||||
};
|
|
@ -18,7 +18,3 @@ export interface WizardStep {
|
|||
backButtonLabel?: string
|
||||
}
|
||||
|
||||
export enum WizardStepEvent {
|
||||
next = "next",
|
||||
back = "back"
|
||||
}
|
Reference in a new issue