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

View File

@ -3,7 +3,7 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
import { customElement, property, query } from "@lit/reactive-element/decorators.js";
import { TemplateResult, html, nothing } from "lit";
import { TemplateResult, css, html, nothing } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
@ -31,7 +31,15 @@ import { type WizardButton, WizardStepLabel } from "./types";
@customElement("ak-wizard-frame")
export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
static get styles() {
return [...super.styles, PFWizard];
return [
...super.styles,
PFWizard,
css`
.pf-c-modal-box {
height: 75%;
}
`,
];
}
/**
@ -84,7 +92,7 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
${this.renderHeader()}
<div class="pf-c-wizard__outer-wrap">
<div class="pf-c-wizard__inner-wrap">
${this.renderNavigation()}
${this.renderNavigation()}
${this.renderMainSection()}
</div>
${this.renderFooter()}

View File

@ -17,6 +17,8 @@ export enum Level {
export class Alert extends AKElement {
@property({ type: Boolean })
inline = false;
@property({ type: Boolean })
plain = false;
@property()
level: Level = Level.Warning;
@ -26,7 +28,11 @@ export class Alert extends AKElement {
}
render(): TemplateResult {
return html`<div class="pf-c-alert ${this.inline ? "pf-m-inline" : ""} ${this.level}">
return html`<div
class="pf-c-alert ${this.inline ? "pf-m-inline" : ""} ${this.plain
? "pf-m-plain"
: ""} ${this.level}"
>
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>

View File

@ -1,5 +1,5 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";

View File

@ -1,5 +1,5 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";

View File

@ -1,5 +1,5 @@
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";

View File

@ -1,7 +1,7 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { getRelativeTime } from "@goauthentik/authentik/common/utils";
import { AKElement } from "@goauthentik/authentik/elements/Base";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/events/LogViewer";

View File

@ -1,7 +1,6 @@
import { sourceLocale, targetLocales } from "@goauthentik/authentik/locale-codes";
import { configureLocalization } from "@lit/localize";
import { sourceLocale, targetLocales } from "../../locale-codes";
import { getBestMatchLocale } from "./helpers";
type LocaleGetter = ReturnType<typeof configureLocalization>["getLocale"];

View File

@ -0,0 +1,146 @@
import "@goauthentik/admin/common/ak-license-notice";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { TypeCreate } from "@goauthentik/api";
export enum TypeCreateWizardPageLayouts {
list = "list",
grid = "grid",
}
@customElement("ak-wizard-page-type-create")
export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
@property({ attribute: false })
types: TypeCreate[] = [];
@property({ attribute: false })
selectedType?: TypeCreate;
@property({ type: String })
layout: TypeCreateWizardPageLayouts = TypeCreateWizardPageLayouts.list;
static get styles(): CSSResult[] {
return [
PFBase,
PFForm,
PFGrid,
PFRadio,
PFCard,
css`
.pf-c-card__header-main img {
max-height: 2em;
min-height: 2em;
}
:host([theme="dark"]) .pf-c-card__header-main img {
filter: invert(1);
}
`,
];
}
sidebarLabel = () => msg("Select type");
activeCallback: () => Promise<void> = async () => {
this.host.isValid = false;
if (this.selectedType) {
this.selectDispatch(this.selectedType);
}
};
private selectDispatch(type: TypeCreate) {
this.dispatchEvent(
new CustomEvent("select", {
detail: type,
bubbles: true,
composed: true,
}),
);
}
renderGrid(): TemplateResult {
return html`<div class="pf-l-grid pf-m-gutter">
${this.types.map((type, idx) => {
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
return html`<div
class="pf-l-grid__item pf-m-3-col pf-c-card ${requiresEnterprise
? "pf-m-non-selectable-raised"
: "pf-m-selectable-raised"} ${this.selectedType == type
? "pf-m-selected-raised"
: ""}"
tabindex=${idx}
@click=${() => {
if (requiresEnterprise) {
return;
}
this.selectDispatch(type);
this.selectedType = type;
}}
>
${type.iconUrl
? html`<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<img src=${type.iconUrl} alt=${msg(str`${type.name} Icon`)} />
</div>
</div>`
: nothing}
<div class="pf-c-card__title">${type.name}</div>
<div class="pf-c-card__body">${type.description}</div>
${requiresEnterprise
? html`<div class="pf-c-card__footer">
<ak-license-notice></ak-license-notice>
</div> `
: nothing}
</div>`;
})}
</div>`;
}
renderList(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.types.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=${() => {
this.selectDispatch(type);
}}
?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>
</div>`;
})}
</form>`;
}
render(): TemplateResult {
switch (this.layout) {
case TypeCreateWizardPageLayouts.grid:
return this.renderGrid();
case TypeCreateWizardPageLayouts.list:
return this.renderList();
}
}
}

View File

@ -1,5 +1,5 @@
import { dateTimeLocal } from "@goauthentik/authentik/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { dateTimeLocal } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";