web/admin: rework initial wizard pages and add grid layout (#9668)

* remove @goauthentik/authentik as TS path

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* initial implementation

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* oh yeah

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format earlier changes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* support plain alert

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* initial attempt at dedupe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make it a base class

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate all wizards

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* create type create mixin to dedupe more, add icon to source create

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add ldap icon

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Optimised images with calibre/image-actions

* match inverting

we should probably replace all icons with coloured ones so we don't need to invert them...I guess

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make everything more explicit

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add icons to provider

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add remaining provider icons

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rework to not use inheritance

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix unrelated typo

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make app wizard use grid layout

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* keep wizard height consistent

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
This commit is contained in:
Jens L
2024-05-22 02:41:33 +02:00
committed by GitHub
parent 0ed4bba5a5
commit 6c4c535d57
58 changed files with 726 additions and 791 deletions

View File

@ -1,7 +1,7 @@
import { PaginatedResponse } from "@goauthentik/authentik/elements/table/Table";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import "@goauthentik/elements/forms/ConfirmationForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { ChartData, ChartOptions } from "chart.js";
import { msg } from "@lit/localize";

View File

@ -25,167 +25,152 @@ type ModelConverter = (provider: OneOfProvider) => ModelRequest;
type ProviderNoteProvider = () => TemplateResult | undefined;
type ProviderNote = ProviderNoteProvider | undefined;
/**
* There's an internal key and an API key because "Proxy" has three different subtypes.
*/
// prettier-ignore
type ProviderType = [
string, // internal key used by the wizard to distinguish between providers
string, // Name of the provider
string, // Description
ProviderRenderer, // Function that returns the provider's wizard panel as a TemplateResult
ProviderModelEnumType, // key used by the API to distinguish between providers
ModelConverter, // Handler that takes a generic provider and returns one specifically typed to its panel
ProviderNote?,
];
export type LocalTypeCreate = TypeCreate & {
formName: string;
modelName: ProviderModelEnumType;
converter: ModelConverter;
note?: ProviderNote;
renderer: ProviderRenderer;
};
// prettier-ignore
const _providerModelsTable: ProviderType[] = [
[
"oauth2provider",
msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
msg("Modern applications, APIs and Single-page applications."),
() =>
export const providerModelsList: LocalTypeCreate[] = [
{
formName: "oauth2provider",
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
description: msg("Modern applications, APIs and Single-page applications."),
renderer: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
ProviderModelEnum.Oauth2Oauth2provider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.Oauth2Oauth2provider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
...(provider as OAuth2ProviderRequest),
}),
],
[
"ldapprovider",
msg("LDAP (Lightweight Directory Access Protocol)"),
msg("Provide an LDAP interface for applications and users to authenticate against."),
() =>
component: "",
iconUrl: "/static/authentik/sources/openidconnect.svg",
},
{
formName: "ldapprovider",
name: msg("LDAP (Lightweight Directory Access Protocol)"),
description: msg(
"Provide an LDAP interface for applications and users to authenticate against.",
),
renderer: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
ProviderModelEnum.LdapLdapprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.LdapLdapprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.LdapLdapprovider,
...(provider as LDAPProviderRequest),
}),
],
[
"proxyprovider-proxy",
msg("Transparent Reverse Proxy"),
msg("For transparent reverse proxies with required authentication"),
() =>
component: "",
iconUrl: "/static/authentik/sources/ldap.png",
},
{
formName: "proxyprovider-proxy",
name: msg("Transparent Reverse Proxy"),
description: msg("For transparent reverse proxies with required authentication"),
renderer: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
ProviderModelEnum.ProxyProxyprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.Proxy,
}),
],
[
"proxyprovider-forwardsingle",
msg("Forward Auth (Single Application)"),
msg("For nginx's auth_request or traefik's forwardAuth"),
() =>
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwardsingle",
name: msg("Forward Auth (Single Application)"),
description: msg("For nginx's auth_request or traefik's forwardAuth"),
renderer: () =>
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
ProviderModelEnum.ProxyProxyprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardSingle,
}),
],
[
"proxyprovider-forwarddomain",
msg("Forward Auth (Domain Level)"),
msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
() =>
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwarddomain",
name: msg("Forward Auth (Domain Level)"),
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
renderer: () =>
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
ProviderModelEnum.ProxyProxyprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardDomain,
}),
],
[
"racprovider",
msg("Remote Access Provider"),
msg("Remotely access computers/servers via RDP/SSH/VNC"),
() =>
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "racprovider",
name: msg("Remote Access Provider"),
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
renderer: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
ProviderModelEnum.RacRacprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.RacRacprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RacRacprovider,
...(provider as RACProviderRequest),
}),
() => html`<ak-license-notice></ak-license-notice>`
],
[
"samlprovider",
msg("SAML (Security Assertion Markup Language)"),
msg("Configure SAML provider manually"),
() =>
note: () => html`<ak-license-notice></ak-license-notice>`,
requiresEnterprise: true,
component: "",
iconUrl: "/static/authentik/sources/rac.svg",
},
{
formName: "samlprovider",
name: msg("SAML (Security Assertion Markup Language)"),
description: msg("Configure SAML provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
ProviderModelEnum.SamlSamlprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.SamlSamlprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.SamlSamlprovider,
...(provider as SAMLProviderRequest),
}),
],
[
"radiusprovider",
msg("RADIUS (Remote Authentication Dial-In User Service)"),
msg("Configure RADIUS provider manually"),
() =>
component: "",
iconUrl: "/static/authentik/sources/saml.png",
},
{
formName: "radiusprovider",
name: msg("RADIUS (Remote Authentication Dial-In User Service)"),
description: msg("Configure RADIUS provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
ProviderModelEnum.RadiusRadiusprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.RadiusRadiusprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RadiusRadiusprovider,
...(provider as RadiusProviderRequest),
}),
],
[
"scimprovider",
msg("SCIM (System for Cross-domain Identity Management)"),
msg("Configure SCIM provider manually"),
() =>
component: "",
iconUrl: "/static/authentik/sources/radius.svg",
},
{
formName: "scimprovider",
name: msg("SCIM (System for Cross-domain Identity Management)"),
description: msg("Configure SCIM provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
ProviderModelEnum.ScimScimprovider,
(provider: OneOfProvider) => ({
modelName: ProviderModelEnum.ScimScimprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ScimScimprovider,
...(provider as SCIMProviderRequest),
}),
],
component: "",
iconUrl: "/static/authentik/sources/scim.png",
},
];
function mapProviders([
formName,
name,
description,
_,
modelName,
converter,
note,
]: ProviderType): LocalTypeCreate {
return {
formName,
name,
description,
component: "",
modelName,
converter,
note,
};
}
export const providerModelsList = _providerModelsTable.map(mapProviders);
export const providerRendererList = new Map<string, ProviderRenderer>(
_providerModelsTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]),
providerModelsList.map((tc) => [tc.formName, tc.renderer]),
);
export default providerModelsList;

View File

@ -3,63 +3,41 @@ import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html, nothing } from "lit";
import { map } from "lit/directives/map.js";
import { html } from "lit";
import BasePanel from "../BasePanel";
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
@customElement("ak-application-wizard-authentication-method-choice")
export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) {
constructor() {
super();
this.handleChoice = this.handleChoice.bind(this);
this.renderProvider = this.renderProvider.bind(this);
}
handleChoice(ev: InputEvent) {
const target = ev.target as HTMLInputElement;
this.dispatchWizardUpdate({
update: {
...this.wizard,
providerModel: target.value,
errors: {},
},
status: this.valid ? "valid" : "invalid",
});
}
renderProvider(type: LocalTypeCreate) {
const method = this.wizard.providerModel;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id="provider-${type.formName}"
?disabled=${type.formName === "racprovider" && !this.hasEnterpriseLicense}
value=${type.formName}
?checked=${type.formName === method}
@change=${this.handleChoice}
/>
<label class="pf-c-radio__label" for="provider-${type.formName}">${type.name}</label>
<span class="pf-c-radio__description"
>${type.description}${type.note ? type.note() : nothing}</span
>
</div>`;
}
render() {
const selectedTypes = providerModelsList.filter(
(t) => t.formName === this.wizard.providerModel,
);
return providerModelsList.length > 0
? html`<form class="pf-c-form pf-m-horizontal">
${map(providerModelsList, this.renderProvider)}
<ak-wizard-page-type-create
.types=${providerModelsList}
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
this.dispatchWizardUpdate({
update: {
...this.wizard,
providerModel: ev.detail.formName,
errors: {},
},
status: this.valid ? "valid" : "invalid",
});
}}
></ak-wizard-page-type-create>
</form>`
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
}

View File

@ -9,7 +9,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { TemplateResult, css, html, nothing } from "lit";
import { PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { classMap } from "lit/directives/class-map.js";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
@ -94,8 +94,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
response?: TransactionApplicationResponse;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
willUpdate(_changedProperties: Map<string, any>) {
willUpdate(_changedProperties: PropertyValues<this>) {
if (this.commitState === idleState) {
this.response = undefined;
this.commitState = runningState;

View File

@ -12,8 +12,6 @@ import "./radius/ak-application-wizard-authentication-by-radius";
import "./saml/ak-application-wizard-authentication-by-saml-configuration";
import "./scim/ak-application-wizard-authentication-by-scim";
// prettier-ignore
@customElement("ak-application-wizard-authentication-method")
export class ApplicationWizardApplicationDetails extends BasePanel {
render() {

View File

@ -9,15 +9,14 @@ import { customElement, property } from "lit/decorators.js";
@customElement("ak-license-notice")
export class AkLicenceNotice extends WithLicenseSummary(AKElement) {
@property()
notice = msg("This feature requires an enterprise license.");
notice = msg("Enterprise only");
render() {
return this.hasEnterpriseLicense
? nothing
: html`
<ak-alert class="pf-c-radio__description" inline>
${this.notice}
<a href="#/enterprise/licenses">${msg("Learn more")}</a>
<ak-alert class="pf-c-radio__description" inline plain>
<a href="#/enterprise/licenses">${this.notice}</a>
</ak-alert>
`;
}

View File

@ -4,73 +4,24 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } 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, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { OutpostsApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-service-connection-wizard-initial")
export class InitialServiceConnectionWizardPage extends WizardPage {
@property({ attribute: false })
connectionTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.connectionTypes.map((type) => {
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
this.host.steps = [
"initial",
`type-${type.component}-${type.modelName}`,
];
this.host.isValid = true;
}}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-service-connection-wizard")
export class ServiceConnectionWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property()
@ -79,6 +30,9 @@ export class ServiceConnectionWizard extends AKElement {
@property({ attribute: false })
connectionTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList().then((types) => {
this.connectionTypes = types;
@ -92,11 +46,19 @@ export class ServiceConnectionWizard extends AKElement {
header=${msg("New outpost integration")}
description=${msg("Create a new outpost integration.")}
>
<ak-service-connection-wizard-initial
<ak-wizard-page-type-create
slot="initial"
.connectionTypes=${this.connectionTypes}
.types=${this.connectionTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = [
"initial",
`type-${ev.detail.component}-${ev.detail.modelName}`,
];
this.wizard.isValid = true;
}}
>
</ak-service-connection-wizard-initial>
</ak-wizard-page-type-create>
${this.connectionTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -10,80 +10,24 @@ import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import { FormWizardPage } from "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } 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, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { PoliciesApi, Policy, PolicyBinding, TypeCreate } from "@goauthentik/api";
@customElement("ak-policy-wizard-initial")
export class InitialPolicyWizardPage extends WizardPage {
@property({ attribute: false })
policyTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.policyTypes.map((type) => {
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
const idx = this.host.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.host.steps = this.host.steps.filter(
(step) => !step.startsWith("type-"),
);
this.host.steps.splice(
idx,
0,
`type-${type.component}-${type.modelName}`,
);
this.host.isValid = true;
}}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-policy-wizard")
export class PolicyWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property()
@ -98,6 +42,9 @@ export class PolicyWizard extends AKElement {
@property({ attribute: false })
policyTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList().then((types) => {
this.policyTypes = types;
@ -111,8 +58,26 @@ export class PolicyWizard extends AKElement {
header=${msg("New policy")}
description=${msg("Create a new policy.")}
>
<ak-policy-wizard-initial slot="initial" .policyTypes=${this.policyTypes}>
</ak-policy-wizard-initial>
<ak-wizard-page-type-create
slot="initial"
.types=${this.policyTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
const idx = this.wizard.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.wizard.steps = this.wizard.steps.filter(
(step) => !step.startsWith("type-"),
);
this.wizard.steps.splice(
idx,
0,
`type-${ev.detail.component}-${ev.detail.modelName}`,
);
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.policyTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -1,4 +1,3 @@
import "@goauthentik/admin/common/ak-license-notice";
import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm";
import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
@ -6,90 +5,35 @@ import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm";
import "@goauthentik/admin/property-mappings/PropertyMappingTestForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { TemplateResult, html, nothing } from "lit";
import { property } from "lit/decorators.js";
import { TemplateResult, html } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { PropertymappingsApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-property-mapping-wizard-initial")
export class InitialPropertyMappingWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
mappingTypes: TypeCreate[] = [];
static get styles() {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.mappingTypes.map((type) => {
const requiresEnteprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
this.host.steps = [
"initial",
`type-${type.component}-${type.modelName}`,
];
this.host.isValid = true;
}}
?disabled=${type.requiresEnterprise ? this.hasEnterpriseLicense : false}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description"
>${type.description}
${requiresEnteprise
? html`<ak-license-notice></ak-license-notice>`
: nothing}</span
>
</div>`;
})}
</form>`;
}
}
@customElement("ak-property-mapping-wizard")
export class PropertyMappingWizard extends AKElement {
static get styles() {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property({ attribute: false })
mappingTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
async firstUpdated(): Promise<void> {
this.mappingTypes = await new PropertymappingsApi(
DEFAULT_CONFIG,
@ -103,11 +47,19 @@ export class PropertyMappingWizard extends AKElement {
header=${msg("New property mapping")}
description=${msg("Create a new property mapping.")}
>
<ak-property-mapping-wizard-initial
<ak-wizard-page-type-create
slot="initial"
.mappingTypes=${this.mappingTypes}
.types=${this.mappingTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = [
"initial",
`type-${ev.detail.component}-${ev.detail.modelName}`,
];
this.wizard.isValid = true;
}}
>
</ak-property-mapping-wizard-initial>
</ak-wizard-page-type-create>
${this.mappingTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -5,103 +5,28 @@ import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderImportForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/ProxyForm";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html, nothing } from "lit";
import { property } from "lit/decorators.js";
import { CSSResult, TemplateResult, html } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFHint from "@patternfly/patternfly/components/Hint/hint.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ProvidersApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-provider-wizard-initial")
export class InitialProviderWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFHint, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
renderHint(): TemplateResult {
return html`<div class="pf-c-hint">
<div class="pf-c-hint__title">${msg("Try the new application wizard")}</div>
<div class="pf-c-hint__body">
${msg(
"The new application wizard greatly simplifies the steps required to create applications and providers.",
)}
</div>
<div class="pf-c-hint__footer">
<a
class="pf-c-button pf-m-link pf-m-inline"
href=${paramURL("/core/applications", {
createForm: true,
})}
>${msg("Try it now")}</a
>
</div>
</div>
<br />`;
}
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.providerTypes.map((type) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${type.component}
@change=${() => {
this.host.steps = ["initial", `type-${type.component}`];
this.host.isValid = true;
}}
?disabled=${requiresEnterprise}
/>
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
<span class="pf-c-radio__description"
>${type.description}
${requiresEnterprise
? html`<ak-license-notice></ak-license-notice>`
: nothing}
</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-provider-wizard")
export class ProviderWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property()
@ -115,6 +40,9 @@ export class ProviderWizard extends AKElement {
return Promise.resolve();
};
@query("ak-wizard")
wizard?: Wizard;
async firstUpdated(): Promise<void> {
this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList();
}
@ -129,8 +57,17 @@ export class ProviderWizard extends AKElement {
return this.finalHandler();
}}
>
<ak-provider-wizard-initial slot="initial" .providerTypes=${this.providerTypes}>
</ak-provider-wizard-initial>
<ak-wizard-page-type-create
slot="initial"
layout=${TypeCreateWizardPageLayouts.grid}
.types=${this.providerTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = ["initial", `type-${ev.detail.component}`];
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.providerTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -7,78 +7,32 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import type { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } 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, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SourcesApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-source-wizard-initial")
export class InitialSourceWizardPage extends WizardPage {
@property({ attribute: false })
sourceTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.sourceTypes.map((type) => {
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
this.host.steps = [
"initial",
`type-${type.component}-${type.modelName}`,
];
this.host.isValid = true;
}}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-source-wizard")
export class SourceWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property({ attribute: false })
sourceTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new SourcesApi(DEFAULT_CONFIG).sourcesAllTypesList().then((types) => {
this.sourceTypes = types;
@ -92,8 +46,20 @@ export class SourceWizard extends AKElement {
header=${msg("New source")}
description=${msg("Create a new source.")}
>
<ak-source-wizard-initial slot="initial" .sourceTypes=${this.sourceTypes}>
</ak-source-wizard-initial>
<ak-wizard-page-type-create
slot="initial"
.types=${this.sourceTypes}
layout=${TypeCreateWizardPageLayouts.grid}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
this.wizard.steps = [
"initial",
`type-${ev.detail.component}-${ev.detail.modelName}`,
];
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.sourceTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -1,5 +1,5 @@
import { placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { placeholderHelperText } from "@goauthentik/authentik/admin/helperText";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";

View File

@ -22,89 +22,27 @@ import "@goauthentik/admin/stages/user_logout/UserLogoutStageForm";
import "@goauthentik/admin/stages/user_write/UserWriteStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/wizard/FormWizardPage";
import { FormWizardPage } from "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { Wizard } from "@goauthentik/elements/wizard/Wizard";
import { msg, str } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html, nothing } from "lit";
import { property } from "lit/decorators.js";
import { CSSResult, TemplateResult, html } from "lit";
import { property, query } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { FlowStageBinding, Stage, StagesApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-stage-wizard-initial")
export class InitialStageWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
stageTypes: TypeCreate[] = [];
sidebarLabel = () => msg("Select type");
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFRadio];
}
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>("input[type=radio]")
.forEach((radio) => {
if (radio.checked) {
radio.dispatchEvent(new CustomEvent("change"));
}
});
};
render(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.stageTypes.map((type) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${`${type.component}-${type.modelName}`}
@change=${() => {
const idx = this.host.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.host.steps = this.host.steps.filter(
(step) => !step.startsWith("type-"),
);
this.host.steps.splice(
idx,
0,
`type-${type.component}-${type.modelName}`,
);
this.host.isValid = true;
}}
?disabled=${requiresEnterprise}
/>
<label class="pf-c-radio__label" for=${`${type.component}-${type.modelName}`}
>${type.name}</label
>
<span class="pf-c-radio__description">${type.description}${
requiresEnterprise ? html`<ak-license-notice></ak-license-notice>` : nothing
}</span>
</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-stage-wizard")
export class StageWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFRadio];
return [PFBase, PFButton];
}
@property()
@ -119,6 +57,9 @@ export class StageWizard extends AKElement {
@property({ attribute: false })
stageTypes: TypeCreate[] = [];
@query("ak-wizard")
wizard?: Wizard;
firstUpdated(): void {
new StagesApi(DEFAULT_CONFIG).stagesAllTypesList().then((types) => {
this.stageTypes = types;
@ -132,8 +73,26 @@ export class StageWizard extends AKElement {
header=${msg("New stage")}
description=${msg("Create a new stage.")}
>
<ak-stage-wizard-initial slot="initial" .stageTypes=${this.stageTypes}>
</ak-stage-wizard-initial>
<ak-wizard-page-type-create
slot="initial"
.types=${this.stageTypes}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
const idx = this.wizard.steps.indexOf("initial") + 1;
// Exclude all current steps starting with type-,
// this happens when the user selects a type and then goes back
this.wizard.steps = this.wizard.steps.filter(
(step) => !step.startsWith("type-"),
);
this.wizard.steps.splice(
idx,
0,
`type-${ev.detail.component}-${ev.detail.modelName}`,
);
this.wizard.isValid = true;
}}
>
</ak-wizard-page-type-create>
${this.stageTypes.map((type) => {
return html`
<ak-wizard-page-form

View File

@ -1,10 +1,10 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DataProvision } from "@goauthentik/authentik/elements/ak-dual-select/types";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";