web/elements: only render form once instance is loaded (#5049)

* web/elements: only render form once instance is loaded

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

* use radio for transport

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

* only wait for instance to be loaded if set

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

* add hook to load additional data in form

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

* make send an abstract function instead of attribute

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

* ensure form is updated after data is loaded

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

* remove until for select and multi-selects in forms

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

* don't use until for file uploads

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

* remove last until from form

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

* remove deprecated import

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

* prevent form double load, add error handling for PreventFormSubmit

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

* fix double creation of inner element in proxy form

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

* make PreventFormSubmit work correctly

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-03-23 14:05:14 +01:00 committed by GitHub
parent 20522558fe
commit 14f0034a0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 900 additions and 995 deletions

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils"; import { first, groupBy } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
@ -13,7 +14,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
Application, Application,
@ -195,70 +195,58 @@ export class ApplicationForm extends ModelForm<Application, string> {
${t`If checked, the launch URL will open in a new browser tab or window from the user's application library.`} ${t`If checked, the launch URL will open in a new browser tab or window from the user's application library.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${until( ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
config().then((c) => { ? html`<ak-form-element-horizontal label=${t`Icon`} name="metaIcon">
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { <input type="file" value="" class="pf-c-form-control" />
return html`<ak-form-element-horizontal ${this.instance?.metaIcon
label=${t`Icon`} ? html`
name="metaIcon" <p class="pf-c-form__helper-text">
> ${t`Currently set to:`} ${this.instance?.metaIcon}
<input type="file" value="" class="pf-c-form-control" /> </p>
${this.instance?.metaIcon `
? html` : html``}
<p class="pf-c-form__helper-text"> </ak-form-element-horizontal>
${t`Currently set to:`} ${this.instance?.metaIcon
${this.instance?.metaIcon} ? html`
</p> <ak-form-element-horizontal>
` <label class="pf-c-switch">
: html``} <input
</ak-form-element-horizontal> class="pf-c-switch__input"
${this.instance?.metaIcon type="checkbox"
? html` @change=${(ev: Event) => {
<ak-form-element-horizontal> const target =
<label class="pf-c-switch"> ev.target as HTMLInputElement;
<input this.clearIcon = target.checked;
class="pf-c-switch__input" }}
type="checkbox" />
@change=${(ev: Event) => { <span class="pf-c-switch__toggle">
const target = <span class="pf-c-switch__toggle-icon">
ev.target as HTMLInputElement; <i
this.clearIcon = target.checked; class="fas fa-check"
}} aria-hidden="true"
/> ></i>
<span class="pf-c-switch__toggle"> </span>
<span class="pf-c-switch__toggle-icon"> </span>
<i <span class="pf-c-switch__label">
class="fas fa-check" ${t`Clear icon`}
aria-hidden="true" </span>
></i> </label>
</span> <p class="pf-c-form__helper-text">
</span> ${t`Delete currently set icon.`}
<span class="pf-c-switch__label"> </p>
${t`Clear icon`} </ak-form-element-horizontal>
</span> `
</label> : html``}`
<p class="pf-c-form__helper-text"> : html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
${t`Delete currently set icon.`} <input
</p> type="text"
</ak-form-element-horizontal> value="${first(this.instance?.metaIcon, "")}"
` class="pf-c-form-control"
: html``}`; />
} <p class="pf-c-form__helper-text">
return html`<ak-form-element-horizontal ${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
label=${t`Icon`} </p>
name="metaIcon" </ak-form-element-horizontal>`}
>
<input
type="text"
value="${first(this.instance?.metaIcon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-element-horizontal label=${t`Publisher`} name="metaPublisher"> <ak-form-element-horizontal label=${t`Publisher`} name="metaPublisher">
<input <input
type="text" type="text"

View File

@ -9,7 +9,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CoreApi, CoreApi,
@ -17,17 +16,26 @@ import {
EventsApi, EventsApi,
Group, Group,
NotificationRule, NotificationRule,
PaginatedNotificationTransportList,
SeverityEnum, SeverityEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-event-rule-form") @customElement("ak-event-rule-form")
export class RuleForm extends ModelForm<NotificationRule, string> { export class RuleForm extends ModelForm<NotificationRule, string> {
eventTransports?: PaginatedNotificationTransportList;
loadInstance(pk: string): Promise<NotificationRule> { loadInstance(pk: string): Promise<NotificationRule> {
return new EventsApi(DEFAULT_CONFIG).eventsRulesRetrieve({ return new EventsApi(DEFAULT_CONFIG).eventsRulesRetrieve({
pbmUuid: pk, pbmUuid: pk,
}); });
} }
async load(): Promise<void> {
this.eventTransports = await new EventsApi(DEFAULT_CONFIG).eventsTransportsList({
ordering: "name",
});
}
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated rule.`; return t`Successfully updated rule.`;
@ -86,28 +94,14 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Transports`} ?required=${true} name="transports"> <ak-form-element-horizontal label=${t`Transports`} ?required=${true} name="transports">
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.eventTransports?.results.map((transport) => {
new EventsApi(DEFAULT_CONFIG) const selected = Array.from(this.instance?.transports || []).some((su) => {
.eventsTransportsList({ return su == transport.pk;
ordering: "name", });
}) return html`<option value=${ifDefined(transport.pk)} ?selected=${selected}>
.then((transports) => { ${transport.name}
return transports.results.map((transport) => { </option>`;
const selected = Array.from( })}
this.instance?.transports || [],
).some((su) => {
return su == transport.pk;
});
return html`<option
value=${ifDefined(transport.pk)}
?selected=${selected}
>
${transport.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.`} ${t`Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.`}
@ -120,7 +114,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
<ak-radio <ak-radio
.options=${[ .options=${[
{ {
label: "Alert", label: t`Alert`,
value: SeverityEnum.Alert, value: SeverityEnum.Alert,
default: true, default: true,
}, },

View File

@ -2,6 +2,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
@ -56,35 +57,6 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
} }
}; };
renderTransportModes(): TemplateResult {
return html`
<option
value=${NotificationTransportModeEnum.Local}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Local}
>
${t`Local (notifications will be created within authentik)`}
</option>
<option
value=${NotificationTransportModeEnum.Email}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Email}
>
${t`Email`}
</option>
<option
value=${NotificationTransportModeEnum.Webhook}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Webhook}
>
${t`Webhook (generic)`}
</option>
<option
value=${NotificationTransportModeEnum.WebhookSlack}
?selected=${this.instance?.mode === NotificationTransportModeEnum.WebhookSlack}
>
${t`Webhook (Slack/Discord)`}
</option>
`;
}
onModeChange(mode: string | undefined): void { onModeChange(mode: string | undefined): void {
if ( if (
mode === NotificationTransportModeEnum.Webhook || mode === NotificationTransportModeEnum.Webhook ||
@ -107,15 +79,32 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Mode`} ?required=${true} name="mode"> <ak-form-element-horizontal label=${t`Mode`} ?required=${true} name="mode">
<select <ak-radio
class="pf-c-form-control" @change=${(ev: CustomEvent<NotificationTransportModeEnum>) => {
@change=${(ev: Event) => { this.onModeChange(ev.detail);
const current = (ev.target as HTMLInputElement).value;
this.onModeChange(current);
}} }}
.options=${[
{
label: t`Local (notifications will be created within authentik)`,
value: NotificationTransportModeEnum.Local,
default: true,
},
{
label: t`Email`,
value: NotificationTransportModeEnum.Email,
},
{
label: t`Webhook (generic)`,
value: NotificationTransportModeEnum.Webhook,
},
{
label: t`Webhook (Slack/Discord)`,
value: NotificationTransportModeEnum.WebhookSlack,
},
]}
.value=${this.instance?.mode}
> >
${this.renderTransportModes()} </ak-radio>
</select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
?hidden=${!this.showWebhook} ?hidden=${!this.showWebhook}

View File

@ -2,6 +2,7 @@ import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/util
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum"; import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -12,7 +13,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CapabilitiesEnum, CapabilitiesEnum,
@ -315,73 +315,62 @@ export class FlowForm extends ModelForm<Flow, string> {
</option> </option>
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${until( ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
config().then((c) => { ? html`<ak-form-element-horizontal label=${t`Background`} name="background">
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { <input type="file" value="" class="pf-c-form-control" />
return html`<ak-form-element-horizontal ${this.instance?.background
label=${t`Background`} ? html`
name="background" <p class="pf-c-form__helper-text">
> ${t`Currently set to:`} ${this.instance?.background}
<input type="file" value="" class="pf-c-form-control" /> </p>
${this.instance?.background `
? html` : html``}
<p class="pf-c-form__helper-text">
${t`Currently set to:`} <p class="pf-c-form__helper-text">
${this.instance?.background} ${t`Background shown during execution.`}
</p> </p>
` </ak-form-element-horizontal>
: html``} ${this.instance?.background
<p class="pf-c-form__helper-text"> ? html`
${t`Background shown during execution.`} <ak-form-element-horizontal>
</p> <label class="pf-c-switch">
</ak-form-element-horizontal> <input
${this.instance?.background class="pf-c-switch__input"
? html` type="checkbox"
<ak-form-element-horizontal> @change=${(ev: Event) => {
<label class="pf-c-switch"> const target =
<input ev.target as HTMLInputElement;
class="pf-c-switch__input" this.clearBackground = target.checked;
type="checkbox" }}
@change=${(ev: Event) => { />
const target = <span class="pf-c-switch__toggle">
ev.target as HTMLInputElement; <span class="pf-c-switch__toggle-icon">
this.clearBackground = target.checked; <i
}} class="fas fa-check"
/> aria-hidden="true"
<span class="pf-c-switch__toggle"> ></i>
<span class="pf-c-switch__toggle-icon"> </span>
<i </span>
class="fas fa-check" <span class="pf-c-switch__label">
aria-hidden="true" ${t`Clear background`}
></i> </span>
</span> </label>
</span> <p class="pf-c-form__helper-text">
<span class="pf-c-switch__label"> ${t`Delete currently set background image.`}
${t`Clear icon`} </p>
</span> </ak-form-element-horizontal>
</label> `
<p class="pf-c-form__helper-text"> : html``}`
${t`Delete currently set background image.`} : html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
</p> <input
</ak-form-element-horizontal> type="text"
` value="${first(this.instance?.background, "")}"
: html``}`; class="pf-c-form-control"
} />
return html`<ak-form-element-horizontal <p class="pf-c-form__helper-text">
label=${t`Background`} ${t`Background shown during execution.`}
name="background" </p>
> </ak-form-element-horizontal>`}
<input
type="text"
value="${first(this.instance?.background, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Background shown during execution.`}
</p>
</ak-form-element-horizontal>`;
}),
)}
</div> </div>
</ak-form-group> </ak-form-group>
</form>`; </form>`;

View File

@ -1,3 +1,4 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils"; import { first, groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -10,11 +11,13 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
Flow,
FlowStageBinding, FlowStageBinding,
FlowsApi, FlowsApi,
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
InvalidResponseActionEnum, InvalidResponseActionEnum,
PolicyEngineMode, PolicyEngineMode,
Stage, Stage,
@ -85,23 +88,32 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
`; `;
} }
return html`<ak-form-element-horizontal label=${t`Target`} ?required=${true} name="target"> return html`<ak-form-element-horizontal label=${t`Target`} ?required=${true} name="target">
<select class="pf-c-form-control"> <ak-search-select
${until( .fetchObjects=${async (query?: string): Promise<Flow[]> => {
new FlowsApi(DEFAULT_CONFIG) const args: FlowsInstancesListRequest = {
.flowsInstancesList({ ordering: "slug",
ordering: "slug", designation: FlowsInstancesListDesignationEnum.Authorization,
}) };
.then((flows) => { if (query !== undefined) {
return flows.results.map((flow) => { args.search = query;
// No ?selected check here, as this input isn't shown on update forms }
return html`<option value=${ifDefined(flow.pk)}> const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
${flow.name} (${flow.slug}) return flows.results;
</option>`; }}
}); .renderElement=${(flow: Flow): string => {
}), return RenderFlowOption(flow);
html`<option>${t`Loading...`}</option>`, }}
)} .renderDescription=${(flow: Flow): TemplateResult => {
</select> return html`${flow.name}`;
}}
.value=${(flow: Flow | undefined): string | undefined => {
return flow?.pk;
}}
.selected=${(flow: Flow): boolean => {
return flow.pk === this.instance?.target;
}}
>
</ak-search-select>
</ak-form-element-horizontal>`; </ak-form-element-horizontal>`;
} }

View File

@ -10,15 +10,18 @@ import YAML from "yaml";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
Outpost, Outpost,
OutpostDefaultConfig,
OutpostTypeEnum, OutpostTypeEnum,
OutpostsApi, OutpostsApi,
OutpostsServiceConnectionsAllListRequest, OutpostsServiceConnectionsAllListRequest,
PaginatedLDAPProviderList,
PaginatedProxyProviderList,
PaginatedRadiusProviderList,
ProvidersApi, ProvidersApi,
ServiceConnection, ServiceConnection,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -31,6 +34,14 @@ export class OutpostForm extends ModelForm<Outpost, string> {
@property({ type: Boolean }) @property({ type: Boolean })
embedded = false; embedded = false;
@state()
providers?:
| PaginatedProxyProviderList
| PaginatedLDAPProviderList
| PaginatedRadiusProviderList;
defaultConfig?: OutpostDefaultConfig;
async loadInstance(pk: string): Promise<Outpost> { async loadInstance(pk: string): Promise<Outpost> {
const o = await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesRetrieve({ const o = await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesRetrieve({
uuid: pk, uuid: pk,
@ -39,6 +50,34 @@ export class OutpostForm extends ModelForm<Outpost, string> {
return o; return o;
} }
async load(): Promise<void> {
this.defaultConfig = await new OutpostsApi(
DEFAULT_CONFIG,
).outpostsInstancesDefaultSettingsRetrieve();
switch (this.type) {
case OutpostTypeEnum.Proxy:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersProxyList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.Ldap:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersLdapList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.Radius:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersRadiusList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.UnknownDefaultOpenApi:
this.providers = undefined;
}
}
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated outpost.`; return t`Successfully updated outpost.`;
@ -60,78 +99,6 @@ export class OutpostForm extends ModelForm<Outpost, string> {
} }
}; };
renderProviders(): Promise<TemplateResult[]> {
switch (this.type) {
case OutpostTypeEnum.Proxy:
return new ProvidersApi(DEFAULT_CONFIG)
.providersProxyList({
ordering: "name",
applicationIsnull: false,
})
.then((providers) => {
return providers.results.map((provider) => {
const selected = Array.from(this.instance?.providers || []).some(
(sp) => {
return sp == provider.pk;
},
);
return html`<option
value=${ifDefined(provider.pk)}
?selected=${selected}
>
${provider.assignedApplicationName} (${provider.externalHost})
</option>`;
});
});
case OutpostTypeEnum.Ldap:
return new ProvidersApi(DEFAULT_CONFIG)
.providersLdapList({
ordering: "name",
applicationIsnull: false,
})
.then((providers) => {
return providers.results.map((provider) => {
const selected = Array.from(this.instance?.providers || []).some(
(sp) => {
return sp == provider.pk;
},
);
return html`<option
value=${ifDefined(provider.pk)}
?selected=${selected}
>
${provider.assignedApplicationName} (${provider.name})
</option>`;
});
});
case OutpostTypeEnum.Radius:
return new ProvidersApi(DEFAULT_CONFIG)
.providersRadiusList({
ordering: "name",
applicationIsnull: false,
})
.then((providers) => {
return providers.results.map((provider) => {
const selected = Array.from(this.instance?.providers || []).some(
(sp) => {
return sp == provider.pk;
},
);
return html`<option
value=${ifDefined(provider.pk)}
?selected=${selected}
>
${provider.assignedApplicationName} (${provider.name})
</option>`;
});
});
case OutpostTypeEnum.UnknownDefaultOpenApi:
return Promise.resolve([
html` <option value="">${t`Unknown outpost type`}</option>`,
]);
}
}
renderForm(): TemplateResult { renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
@ -148,6 +115,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
@change=${(ev: Event) => { @change=${(ev: Event) => {
const target = ev.target as HTMLSelectElement; const target = ev.target as HTMLSelectElement;
this.type = target.selectedOptions[0].value as OutpostTypeEnum; this.type = target.selectedOptions[0].value as OutpostTypeEnum;
this.load();
}} }}
> >
<option <option
@ -162,6 +130,12 @@ export class OutpostForm extends ModelForm<Outpost, string> {
> >
${t`LDAP`} ${t`LDAP`}
</option> </option>
<option
value=${OutpostTypeEnum.Radius}
?selected=${this.instance?.type === OutpostTypeEnum.Radius}
>
${t`Radius`}
</option>
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Integration`} name="serviceConnection"> <ak-form-element-horizontal label=${t`Integration`} name="serviceConnection">
@ -213,7 +187,14 @@ export class OutpostForm extends ModelForm<Outpost, string> {
name="providers" name="providers"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until(this.renderProviders(), html`<option>${t`Loading...`}</option>`)} ${this.providers?.results.map((provider) => {
const selected = Array.from(this.instance?.providers || []).some((sp) => {
return sp == provider.pk;
});
return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>
${provider.assignedApplicationName} (${provider.name})
</option>`;
})}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`You can only select providers that match the type of the outpost.`} ${t`You can only select providers that match the type of the outpost.`}
@ -223,19 +204,10 @@ export class OutpostForm extends ModelForm<Outpost, string> {
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Configuration`} name="config"> <ak-form-element-horizontal label=${t`Configuration`} name="config">
<!-- @ts-ignore -->
<ak-codemirror <ak-codemirror
mode="yaml" mode="yaml"
value="${until( value="${YAML.stringify(
new OutpostsApi(DEFAULT_CONFIG) this.instance ? this.instance.config : this.defaultConfig?.config,
.outpostsInstancesDefaultSettingsRetrieve()
.then((config) => {
let fc = config.config;
if (this.instance) {
fc = this.instance.config;
}
return YAML.stringify(fc);
}),
)}" )}"
></ak-codemirror> ></ak-codemirror>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">

View File

@ -10,9 +10,15 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { AdminApi, EventMatcherPolicy, EventsApi, PoliciesApi, TypeCreate } from "@goauthentik/api"; import {
AdminApi,
App,
EventMatcherPolicy,
EventsApi,
PoliciesApi,
TypeCreate,
} from "@goauthentik/api";
@customElement("ak-policy-event-matcher-form") @customElement("ak-policy-event-matcher-form")
export class EventMatcherPolicyForm extends ModelForm<EventMatcherPolicy, string> { export class EventMatcherPolicyForm extends ModelForm<EventMatcherPolicy, string> {
@ -22,6 +28,12 @@ export class EventMatcherPolicyForm extends ModelForm<EventMatcherPolicy, string
}); });
} }
async load(): Promise<void> {
this.apps = await new AdminApi(DEFAULT_CONFIG).adminAppsList();
}
apps?: App[];
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated policy.`; return t`Successfully updated policy.`;
@ -118,19 +130,14 @@ export class EventMatcherPolicyForm extends ModelForm<EventMatcherPolicy, string
<option value="" ?selected=${this.instance?.app === undefined}> <option value="" ?selected=${this.instance?.app === undefined}>
--------- ---------
</option> </option>
${until( ${this.apps?.map((app) => {
new AdminApi(DEFAULT_CONFIG).adminAppsList().then((apps) => { return html`<option
return apps.map((app) => { value=${app.name}
return html`<option ?selected=${this.instance?.app === app.name}
value=${app.name} >
?selected=${this.instance?.app === app.name} ${app.label}
> </option>`;
${app.label} })}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Match events created by selected application. When left empty, all applications are matched.`} ${t`Match events created by selected application. When left empty, all applications are matched.`}

View File

@ -11,9 +11,8 @@ import "@goauthentik/elements/utils/TimeDeltaHelp";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CertificateKeyPair, CertificateKeyPair,
@ -26,6 +25,8 @@ import {
FlowsInstancesListRequest, FlowsInstancesListRequest,
IssuerModeEnum, IssuerModeEnum,
OAuth2Provider, OAuth2Provider,
PaginatedOAuthSourceList,
PaginatedScopeMappingList,
PropertymappingsApi, PropertymappingsApi,
ProvidersApi, ProvidersApi,
SourcesApi, SourcesApi,
@ -34,19 +35,31 @@ import {
@customElement("ak-provider-oauth2-form") @customElement("ak-provider-oauth2-form")
export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> { export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
loadInstance(pk: number): Promise<OAuth2Provider> { propertyMappings?: PaginatedScopeMappingList;
return new ProvidersApi(DEFAULT_CONFIG) oauthSources?: PaginatedOAuthSourceList;
.providersOauth2Retrieve({
id: pk, @state()
}) showClientSecret = true;
.then((provider) => {
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential; async loadInstance(pk: number): Promise<OAuth2Provider> {
return provider; const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
}); id: pk,
});
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
return provider;
} }
@property({ type: Boolean }) async load(): Promise<void> {
showClientSecret = true; this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsScopeList({
ordering: "scope_name",
});
this.oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
ordering: "name",
hasJwks: true,
});
}
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
@ -287,36 +300,27 @@ ${this.instance?.redirectUris}</textarea
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Scopes`} name="propertyMappings"> <ak-form-element-horizontal label=${t`Scopes`} name="propertyMappings">
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.propertyMappings?.results.map((scope) => {
new PropertymappingsApi(DEFAULT_CONFIG) let selected = false;
.propertymappingsScopeList({ if (!this.instance?.propertyMappings) {
ordering: "scope_name", selected =
}) scope.managed?.startsWith(
.then((scopes) => { "goauthentik.io/providers/oauth2/scope-",
return scopes.results.map((scope) => { ) || false;
let selected = false; } else {
if (!this.instance?.propertyMappings) { selected = Array.from(this.instance?.propertyMappings).some(
selected = (su) => {
scope.managed?.startsWith( return su == scope.pk;
"goauthentik.io/providers/oauth2/scope-", },
) || false; );
} else { }
selected = Array.from( return html`<option
this.instance?.propertyMappings, value=${ifDefined(scope.pk)}
).some((su) => { ?selected=${selected}
return su == scope.pk; >
}); ${scope.name}
} </option>`;
return html`<option })}
value=${ifDefined(scope.pk)}
?selected=${selected}
>
${scope.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Select which scopes can be used by the client. The client still has to specify the scope to access the data.`} ${t`Select which scopes can be used by the client. The client still has to specify the scope to access the data.`}
@ -413,29 +417,14 @@ ${this.instance?.redirectUris}</textarea
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Trusted OIDC Sources`} name="jwksSources"> <ak-form-element-horizontal label=${t`Trusted OIDC Sources`} name="jwksSources">
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.oauthSources?.results.map((source) => {
new SourcesApi(DEFAULT_CONFIG) const selected = (this.instance?.jwksSources || []).some((su) => {
.sourcesOauthList({ return su == source.pk;
ordering: "name", });
hasJwks: true, return html`<option value=${source.pk} ?selected=${selected}>
}) ${source.name} (${source.slug})
.then((sources) => { </option>`;
return sources.results.map((source) => { })}
const selected = (
this.instance?.jwksSources || []
).some((su) => {
return su == source.pk;
});
return html`<option
value=${source.pk}
?selected=${selected}
>
${source.name} (${source.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.`} ${t`JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.`}

View File

@ -13,7 +13,6 @@ import { CSSResult, css } from "lit";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css"; import PFList from "@patternfly/patternfly/components/List/list.css";
@ -28,6 +27,8 @@ import {
FlowsApi, FlowsApi,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest, FlowsInstancesListRequest,
PaginatedOAuthSourceList,
PaginatedScopeMappingList,
PropertymappingsApi, PropertymappingsApi,
ProvidersApi, ProvidersApi,
ProxyMode, ProxyMode,
@ -51,18 +52,30 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
); );
} }
loadInstance(pk: number): Promise<ProxyProvider> { async loadInstance(pk: number): Promise<ProxyProvider> {
return new ProvidersApi(DEFAULT_CONFIG) const provider = await new ProvidersApi(DEFAULT_CONFIG).providersProxyRetrieve({
.providersProxyRetrieve({ id: pk,
id: pk, });
}) this.showHttpBasic = first(provider.basicAuthEnabled, true);
.then((provider) => { this.mode = first(provider.mode, ProxyMode.Proxy);
this.showHttpBasic = first(provider.basicAuthEnabled, true); return provider;
this.mode = first(provider.mode, ProxyMode.Proxy);
return provider;
});
} }
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsScopeList({
ordering: "scope_name",
});
this.oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
ordering: "name",
hasJwks: true,
});
}
propertyMappings?: PaginatedScopeMappingList;
oauthSources?: PaginatedOAuthSourceList;
@state() @state()
showHttpBasic = true; showHttpBasic = true;
@ -392,34 +405,23 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.propertyMappings?.results
new PropertymappingsApi(DEFAULT_CONFIG) .filter((scope) => {
.propertymappingsScopeList({ return !scope.managed?.startsWith("goauthentik.io/providers");
ordering: "scope_name", })
}) .map((scope) => {
.then((scopes) => { const selected = (this.instance?.propertyMappings || []).some(
return scopes.results (su) => {
.filter((scope) => { return su == scope.pk;
return !scope.managed?.startsWith( },
"goauthentik.io/providers", );
); return html`<option
}) value=${ifDefined(scope.pk)}
.map((scope) => { ?selected=${selected}
const selected = ( >
this.instance?.propertyMappings || [] ${scope.name}
).some((su) => { </option>`;
return su == scope.pk; })}
});
return html`<option
value=${ifDefined(scope.pk)}
?selected=${selected}
>
${scope.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Additional scope mappings, which are passed to the proxy.`} ${t`Additional scope mappings, which are passed to the proxy.`}
@ -497,29 +499,14 @@ ${this.instance?.skipPathRegex}</textarea
${this.showHttpBasic ? this.renderHttpBasic() : html``} ${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal label=${t`Trusted OIDC Sources`} name="jwksSources"> <ak-form-element-horizontal label=${t`Trusted OIDC Sources`} name="jwksSources">
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.oauthSources?.results.map((source) => {
new SourcesApi(DEFAULT_CONFIG) const selected = (this.instance?.jwksSources || []).some((su) => {
.sourcesOauthList({ return su == source.pk;
ordering: "name", });
hasJwks: true, return html`<option value=${source.pk} ?selected=${selected}>
}) ${source.name} (${source.slug})
.then((sources) => { </option>`;
return sources.results.map((source) => { })}
const selected = (
this.instance?.jwksSources || []
).some((su) => {
return su == source.pk;
});
return html`<option
value=${source.pk}
?selected=${selected}
>
${source.name} (${source.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.`} ${t`JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.`}

View File

@ -9,9 +9,9 @@ import "@goauthentik/elements/forms/SearchSelect";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { customElement } from "lit-element"; import { TemplateResult, html } from "lit";
import { TemplateResult, html } from "lit-html";
import { ifDefined } from "lit-html/directives/if-defined.js"; import { ifDefined } from "lit-html/directives/if-defined.js";
import { customElement } from "lit/decorators.js";
import { import {
Flow, Flow,

View File

@ -11,7 +11,8 @@ import "@goauthentik/elements/events/ObjectChangelog";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, customElement, html, property } from "lit-element"; import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css";

View File

@ -12,7 +12,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CertificateKeyPair, CertificateKeyPair,
@ -23,6 +22,7 @@ import {
FlowsApi, FlowsApi,
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest, FlowsInstancesListRequest,
PaginatedSAMLPropertyMappingList,
PropertymappingsApi, PropertymappingsApi,
PropertymappingsSamlListRequest, PropertymappingsSamlListRequest,
ProvidersApi, ProvidersApi,
@ -40,6 +40,16 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
}); });
} }
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsSamlList({
ordering: "saml_name",
});
}
propertyMappings?: PaginatedSAMLPropertyMappingList;
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated provider.`; return t`Successfully updated provider.`;
@ -241,36 +251,27 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.propertyMappings?.results.map((mapping) => {
new PropertymappingsApi(DEFAULT_CONFIG) let selected = false;
.propertymappingsSamlList({ if (!this.instance?.propertyMappings) {
ordering: "saml_name", selected =
}) mapping.managed?.startsWith(
.then((mappings) => { "goauthentik.io/providers/saml",
return mappings.results.map((mapping) => { ) || false;
let selected = false; } else {
if (!this.instance?.propertyMappings) { selected = Array.from(this.instance?.propertyMappings).some(
selected = (su) => {
mapping.managed?.startsWith( return su == mapping.pk;
"goauthentik.io/providers/saml", },
) || false; );
} else { }
selected = Array.from( return html`<option
this.instance?.propertyMappings, value=${ifDefined(mapping.pk)}
).some((su) => { ?selected=${selected}
return su == mapping.pk; >
}); ${mapping.name}
} </option>`;
return html`<option })}
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Hold control/command to select multiple items.`} ${t`Hold control/command to select multiple items.`}

View File

@ -11,12 +11,12 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CoreApi, CoreApi,
CoreGroupsListRequest, CoreGroupsListRequest,
Group, Group,
PaginatedSCIMMappingList,
PropertymappingsApi, PropertymappingsApi,
ProvidersApi, ProvidersApi,
SCIMProvider, SCIMProvider,
@ -30,6 +30,16 @@ export class SCIMProviderFormPage extends ModelForm<SCIMProvider, number> {
}); });
} }
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsScimList({
ordering: "managed",
});
}
propertyMappings?: PaginatedSCIMMappingList;
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated provider.`; return t`Successfully updated provider.`;
@ -147,36 +157,26 @@ export class SCIMProviderFormPage extends ModelForm<SCIMProvider, number> {
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.propertyMappings?.results.map((mapping) => {
new PropertymappingsApi(DEFAULT_CONFIG) let selected = false;
.propertymappingsScimList({ if (!this.instance?.propertyMappings) {
ordering: "managed", selected =
}) mapping.managed === "goauthentik.io/providers/scim/user" ||
.then((mappings) => { false;
return mappings.results.map((mapping) => { } else {
let selected = false; selected = Array.from(this.instance?.propertyMappings).some(
if (!this.instance?.propertyMappings) { (su) => {
selected = return su == mapping.pk;
mapping.managed === },
"goauthentik.io/providers/scim/user" || );
false; }
} else { return html`<option
selected = Array.from( value=${ifDefined(mapping.pk)}
this.instance?.propertyMappings, ?selected=${selected}
).some((su) => { >
return su == mapping.pk; ${mapping.name}
}); </option>`;
} })}
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Property mappings used to user mapping.`} ${t`Property mappings used to user mapping.`}
@ -191,35 +191,25 @@ export class SCIMProviderFormPage extends ModelForm<SCIMProvider, number> {
name="propertyMappingsGroup" name="propertyMappingsGroup"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.propertyMappings?.results.map((mapping) => {
new PropertymappingsApi(DEFAULT_CONFIG) let selected = false;
.propertymappingsScimList({ if (!this.instance?.propertyMappingsGroup) {
ordering: "managed", selected =
}) mapping.managed === "goauthentik.io/providers/scim/group";
.then((mappings) => { } else {
return mappings.results.map((mapping) => { selected = Array.from(
let selected = false; this.instance?.propertyMappingsGroup,
if (!this.instance?.propertyMappingsGroup) { ).some((su) => {
selected = return su == mapping.pk;
mapping.managed === });
"goauthentik.io/providers/scim/group"; }
} else { return html`<option
selected = Array.from( value=${ifDefined(mapping.pk)}
this.instance?.propertyMappingsGroup, ?selected=${selected}
).some((su) => { >
return su == mapping.pk; ${mapping.name}
}); </option>`;
} })}
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Property mappings used to group creation.`} ${t`Property mappings used to group creation.`}

View File

@ -10,7 +10,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CertificateKeyPair, CertificateKeyPair,
@ -21,6 +20,7 @@ import {
Group, Group,
LDAPSource, LDAPSource,
LDAPSourceRequest, LDAPSourceRequest,
PaginatedLDAPPropertyMappingList,
PropertymappingsApi, PropertymappingsApi,
SourcesApi, SourcesApi,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -33,6 +33,16 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
}); });
} }
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsLdapList({
ordering: "managed,object_field",
});
}
propertyMappings?: PaginatedLDAPPropertyMappingList;
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated source.`; return t`Successfully updated source.`;
@ -241,40 +251,31 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.propertyMappings?.results.map((mapping) => {
new PropertymappingsApi(DEFAULT_CONFIG) let selected = false;
.propertymappingsLdapList({ if (!this.instance?.propertyMappings) {
ordering: "managed,object_field", selected =
}) mapping.managed?.startsWith(
.then((mappings) => { "goauthentik.io/sources/ldap/default",
return mappings.results.map((mapping) => { ) ||
let selected = false; mapping.managed?.startsWith(
if (!this.instance?.propertyMappings) { "goauthentik.io/sources/ldap/ms",
selected = ) ||
mapping.managed?.startsWith( false;
"goauthentik.io/sources/ldap/default", } else {
) || selected = Array.from(this.instance?.propertyMappings).some(
mapping.managed?.startsWith( (su) => {
"goauthentik.io/sources/ldap/ms", return su == mapping.pk;
) || },
false; );
} else { }
selected = Array.from( return html`<option
this.instance?.propertyMappings, value=${ifDefined(mapping.pk)}
).some((su) => { ?selected=${selected}
return su == mapping.pk; >
}); ${mapping.name}
} </option>`;
return html`<option })}
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Property mappings used to user creation.`} ${t`Property mappings used to user creation.`}
@ -289,35 +290,26 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
name="propertyMappingsGroup" name="propertyMappingsGroup"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.propertyMappings?.results.map((mapping) => {
new PropertymappingsApi(DEFAULT_CONFIG) let selected = false;
.propertymappingsLdapList({ if (!this.instance?.propertyMappingsGroup) {
ordering: "managed,object_field", selected =
}) mapping.managed ===
.then((mappings) => { "goauthentik.io/sources/ldap/default-name";
return mappings.results.map((mapping) => { } else {
let selected = false; selected = Array.from(
if (!this.instance?.propertyMappingsGroup) { this.instance?.propertyMappingsGroup,
selected = ).some((su) => {
mapping.managed === return su == mapping.pk;
"goauthentik.io/sources/ldap/default-name"; });
} else { }
selected = Array.from( return html`<option
this.instance?.propertyMappingsGroup, value=${ifDefined(mapping.pk)}
).some((su) => { ?selected=${selected}
return su == mapping.pk; >
}); ${mapping.name}
} </option>`;
return html`<option })}
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Property mappings used to group creation.`} ${t`Property mappings used to group creation.`}

View File

@ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -13,7 +14,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CapabilitiesEnum, CapabilitiesEnum,
@ -315,63 +315,52 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`} ${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${until( ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
config().then((c) => { ? html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { <input type="file" value="" class="pf-c-form-control" />
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon"> ${this.instance?.icon
<input type="file" value="" class="pf-c-form-control" /> ? html`
${this.instance?.icon <p class="pf-c-form__helper-text">
? html` ${t`Currently set to:`} ${this.instance?.icon}
<p class="pf-c-form__helper-text"> </p>
${t`Currently set to:`} ${this.instance?.icon} `
</p> : html``}
` </ak-form-element-horizontal>
: html``} ${this.instance?.icon
</ak-form-element-horizontal> ? html`
${this.instance?.icon <ak-form-element-horizontal>
? html` <label class="pf-c-switch">
<ak-form-element-horizontal> <input
<label class="pf-c-switch"> class="pf-c-switch__input"
<input type="checkbox"
class="pf-c-switch__input" @change=${(ev: Event) => {
type="checkbox" const target = ev.target as HTMLInputElement;
@change=${(ev: Event) => { this.clearIcon = target.checked;
const target = ev.target as HTMLInputElement; }}
this.clearIcon = target.checked; />
}} <span class="pf-c-switch__toggle">
/> <span class="pf-c-switch__toggle-icon">
<span class="pf-c-switch__toggle"> <i class="fas fa-check" aria-hidden="true"></i>
<span class="pf-c-switch__toggle-icon"> </span>
<i </span>
class="fas fa-check" <span class="pf-c-switch__label"> ${t`Clear icon`} </span>
aria-hidden="true" </label>
></i> <p class="pf-c-form__helper-text">
</span> ${t`Delete currently set icon.`}
</span> </p>
<span class="pf-c-switch__label"> </ak-form-element-horizontal>
${t`Clear icon`} `
</span> : html``}`
</label> : html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<input
<p class="pf-c-form__helper-text"> type="text"
${t`Delete currently set icon.`} value="${first(this.instance?.icon, "")}"
</p> class="pf-c-form-control"
</ak-form-element-horizontal> />
` <p class="pf-c-form__helper-text">
: html``}`; ${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
} </p>
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon"> </ak-form-element-horizontal>`}
<input
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}> <ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span> <span slot="header"> ${t`Protocol settings`} </span>

View File

@ -3,6 +3,7 @@ import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex"; import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
import { first, randomString } from "@goauthentik/common/utils"; import { first, randomString } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -13,7 +14,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
CapabilitiesEnum, CapabilitiesEnum,
@ -267,63 +267,52 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`} ${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${until( ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
config().then((c) => { ? html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { <input type="file" value="" class="pf-c-form-control" />
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon"> ${this.instance?.icon
<input type="file" value="" class="pf-c-form-control" /> ? html`
${this.instance?.icon <p class="pf-c-form__helper-text">
? html` ${t`Currently set to:`} ${this.instance?.icon}
<p class="pf-c-form__helper-text"> </p>
${t`Currently set to:`} ${this.instance?.icon} `
</p> : html``}
` </ak-form-element-horizontal>
: html``} ${this.instance?.icon
</ak-form-element-horizontal> ? html`
${this.instance?.icon <ak-form-element-horizontal>
? html` <label class="pf-c-switch">
<ak-form-element-horizontal> <input
<label class="pf-c-switch"> class="pf-c-switch__input"
<input type="checkbox"
class="pf-c-switch__input" @change=${(ev: Event) => {
type="checkbox" const target = ev.target as HTMLInputElement;
@change=${(ev: Event) => { this.clearIcon = target.checked;
const target = ev.target as HTMLInputElement; }}
this.clearIcon = target.checked; />
}} <span class="pf-c-switch__toggle">
/> <span class="pf-c-switch__toggle-icon">
<span class="pf-c-switch__toggle"> <i class="fas fa-check" aria-hidden="true"></i>
<span class="pf-c-switch__toggle-icon"> </span>
<i </span>
class="fas fa-check" <span class="pf-c-switch__label"> ${t`Clear icon`} </span>
aria-hidden="true" </label>
></i> <p class="pf-c-form__helper-text">
</span> ${t`Delete currently set icon.`}
</span> </p>
<span class="pf-c-switch__label"> </ak-form-element-horizontal>
${t`Clear icon`} `
</span> : html``}`
</label> : html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<p class="pf-c-form__helper-text"> <input
${t`Delete currently set icon.`} type="text"
</p> value="${first(this.instance?.icon, "")}"
</ak-form-element-horizontal> class="pf-c-form-control"
` />
: html``}`; <p class="pf-c-form__helper-text">
} ${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon"> </p>
<input </ak-form-element-horizontal>`}
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}> <ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span> <span slot="header"> ${t`Protocol settings`} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">

View File

@ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -13,7 +14,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
BindingTypeEnum, BindingTypeEnum,
@ -161,62 +161,52 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
</option> </option>
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
${until( ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.SaveMedia)
config().then((c) => { ? html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
if (c.capabilities.includes(CapabilitiesEnum.SaveMedia)) { <input type="file" value="" class="pf-c-form-control" />
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon"> ${this.instance?.icon
<input type="file" value="" class="pf-c-form-control" /> ? html`
${this.instance?.icon <p class="pf-c-form__helper-text">
? html` ${t`Currently set to:`} ${this.instance?.icon}
<p class="pf-c-form__helper-text"> </p>
${t`Currently set to:`} ${this.instance?.icon} `
</p> : html``}
` </ak-form-element-horizontal>
: html``} ${this.instance?.icon
</ak-form-element-horizontal> ? html`
${this.instance?.icon <ak-form-element-horizontal>
? html` <label class="pf-c-switch">
<ak-form-element-horizontal> <input
<label class="pf-c-switch"> class="pf-c-switch__input"
<input type="checkbox"
class="pf-c-switch__input" @change=${(ev: Event) => {
type="checkbox" const target = ev.target as HTMLInputElement;
@change=${(ev: Event) => { this.clearIcon = target.checked;
const target = ev.target as HTMLInputElement; }}
this.clearIcon = target.checked; />
}} <span class="pf-c-switch__toggle">
/> <span class="pf-c-switch__toggle-icon">
<span class="pf-c-switch__toggle"> <i class="fas fa-check" aria-hidden="true"></i>
<span class="pf-c-switch__toggle-icon"> </span>
<i </span>
class="fas fa-check" <span class="pf-c-switch__label"> ${t`Clear icon`} </span>
aria-hidden="true" </label>
></i> <p class="pf-c-form__helper-text">
</span> ${t`Delete currently set icon.`}
</span> </p>
<span class="pf-c-switch__label"> </ak-form-element-horizontal>
${t`Clear icon`} `
</span> : html``}`
</label> : html`<ak-form-element-horizontal label=${t`Icon`} name="icon">
<p class="pf-c-form__helper-text"> <input
${t`Delete currently set icon.`} type="text"
</p> value="${first(this.instance?.icon, "")}"
</ak-form-element-horizontal> class="pf-c-form-control"
` />
: html``}`; <p class="pf-c-form__helper-text">
} ${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
return html`<ak-form-element-horizontal label=${t`Icon`} name="icon"> </p>
<input </ak-form-element-horizontal>`}
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".`}
</p>
</ak-form-element-horizontal>`;
}),
)}
<ak-form-group .expanded=${true}> <ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span> <span slot="header"> ${t`Protocol settings`} </span>

View File

@ -10,30 +10,35 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
AuthenticatorValidateStage, AuthenticatorValidateStage,
DeviceClassesEnum, DeviceClassesEnum,
NotConfiguredActionEnum, NotConfiguredActionEnum,
PaginatedStageList,
StagesApi, StagesApi,
UserVerificationEnum, UserVerificationEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate-form") @customElement("ak-stage-authenticator-validate-form")
export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValidateStage, string> { export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValidateStage, string> {
loadInstance(pk: string): Promise<AuthenticatorValidateStage> { async loadInstance(pk: string): Promise<AuthenticatorValidateStage> {
return new StagesApi(DEFAULT_CONFIG) const stage = await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorValidateRetrieve({
.stagesAuthenticatorValidateRetrieve({ stageUuid: pk,
stageUuid: pk, });
}) this.showConfigurationStages =
.then((stage) => { stage.notConfiguredAction === NotConfiguredActionEnum.Configure;
this.showConfigurationStages = return stage;
stage.notConfiguredAction === NotConfiguredActionEnum.Configure;
return stage;
});
} }
async load(): Promise<void> {
this.stages = await new StagesApi(DEFAULT_CONFIG).stagesAllList({
ordering: "name",
});
}
stages?: PaginatedStageList;
@property({ type: Boolean }) @property({ type: Boolean })
showConfigurationStages = true; showConfigurationStages = true;
@ -216,28 +221,19 @@ export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValid
name="configurationStages" name="configurationStages"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.stages?.results.map((stage) => {
new StagesApi(DEFAULT_CONFIG) const selected = Array.from(
.stagesAllList({ this.instance?.configurationStages || [],
ordering: "name", ).some((su) => {
}) return su == stage.pk;
.then((stages) => { });
return stages.results.map((stage) => { return html`<option
const selected = Array.from( value=${ifDefined(stage.pk)}
this.instance?.configurationStages || [], ?selected=${selected}
).some((su) => { >
return su == stage.pk; ${stage.name} (${stage.verboseName})
}); </option>`;
return html`<option })}
value=${ifDefined(stage.pk)}
?selected=${selected}
>
${stage.name} (${stage.verboseName})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.`} ${t`Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.`}

View File

@ -9,23 +9,25 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { EmailStage, StagesApi } from "@goauthentik/api"; import { EmailStage, StagesApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-stage-email-form") @customElement("ak-stage-email-form")
export class EmailStageForm extends ModelForm<EmailStage, string> { export class EmailStageForm extends ModelForm<EmailStage, string> {
loadInstance(pk: string): Promise<EmailStage> { async loadInstance(pk: string): Promise<EmailStage> {
return new StagesApi(DEFAULT_CONFIG) const stage = await new StagesApi(DEFAULT_CONFIG).stagesEmailRetrieve({
.stagesEmailRetrieve({ stageUuid: pk,
stageUuid: pk, });
}) this.showConnectionSettings = !stage.useGlobalSettings;
.then((stage) => { return stage;
this.showConnectionSettings = !stage.useGlobalSettings;
return stage;
});
} }
async load(): Promise<void> {
this.templates = await new StagesApi(DEFAULT_CONFIG).stagesEmailTemplatesList();
}
templates?: TypeCreate[];
@property({ type: Boolean }) @property({ type: Boolean })
showConnectionSettings = false; showConnectionSettings = false;
@ -232,23 +234,15 @@ export class EmailStageForm extends ModelForm<EmailStage, string> {
name="template" name="template"
> >
<select name="users" class="pf-c-form-control"> <select name="users" class="pf-c-form-control">
${until( ${this.templates?.map((template) => {
new StagesApi(DEFAULT_CONFIG) const selected = this.instance?.template === template.name;
.stagesEmailTemplatesList() return html`<option
.then((templates) => { value=${ifDefined(template.name)}
return templates.map((template) => { ?selected=${selected}
const selected = >
this.instance?.template === template.name; ${template.description}
return html`<option </option>`;
value=${ifDefined(template.name)} })}
?selected=${selected}
>
${template.description}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>

View File

@ -11,7 +11,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { import {
Flow, Flow,
@ -19,6 +18,7 @@ import {
FlowsInstancesListDesignationEnum, FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest, FlowsInstancesListRequest,
IdentificationStage, IdentificationStage,
PaginatedSourceList,
SourcesApi, SourcesApi,
Stage, Stage,
StagesApi, StagesApi,
@ -34,6 +34,14 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
}); });
} }
async load(): Promise<void> {
this.sources = await new SourcesApi(DEFAULT_CONFIG).sourcesAllList({
ordering: "slug",
});
}
sources?: PaginatedSourceList;
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated stage.`; return t`Successfully updated stage.`;
@ -80,7 +88,7 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
<span slot="header"> ${t`Stage-specific settings`} </span> <span slot="header"> ${t`Stage-specific settings`} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`User fields`} name="userFields"> <ak-form-element-horizontal label=${t`User fields`} name="userFields">
<select name="users" class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
<option <option
value=${UserFieldsEnum.Username} value=${UserFieldsEnum.Username}
?selected=${this.isUserFieldSelected(UserFieldsEnum.Username)} ?selected=${this.isUserFieldSelected(UserFieldsEnum.Username)}
@ -187,35 +195,28 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
name="sources" name="sources"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${until( ${this.sources?.results.map((source) => {
new SourcesApi(DEFAULT_CONFIG) let selected = Array.from(this.instance?.sources || []).some(
.sourcesAllList({}) (su) => {
.then((sources) => { return su == source.pk;
return sources.results.map((source) => { },
let selected = Array.from( );
this.instance?.sources || [], // Creating a new instance, auto-select built-in source
).some((su) => { // Only when no other sources exist
return su == source.pk; if (
}); !this.instance &&
// Creating a new instance, auto-select built-in source source.component === "" &&
// Only when no other sources exist (this.sources?.results || []).length < 2
if ( ) {
!this.instance && selected = true;
source.component === "" && }
sources.results.length < 2 return html`<option
) { value=${ifDefined(source.pk)}
selected = true; ?selected=${selected}
} >
return html`<option ${source.name}
value=${ifDefined(source.pk)} </option>`;
?selected=${selected} })}
>
${source.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.`} ${t`Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.`}

View File

@ -10,9 +10,14 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { PoliciesApi, PromptStage, StagesApi } from "@goauthentik/api"; import {
PaginatedPolicyList,
PaginatedPromptList,
PoliciesApi,
PromptStage,
StagesApi,
} from "@goauthentik/api";
@customElement("ak-stage-prompt-form") @customElement("ak-stage-prompt-form")
export class PromptStageForm extends ModelForm<PromptStage, string> { export class PromptStageForm extends ModelForm<PromptStage, string> {
@ -22,6 +27,18 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
}); });
} }
async load(): Promise<void> {
this.prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({
ordering: "field_name",
});
this.policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList({
ordering: "name",
});
}
prompts?: PaginatedPromptList;
policies?: PaginatedPolicyList;
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance) { if (this.instance) {
return t`Successfully updated stage.`; return t`Successfully updated stage.`;
@ -61,28 +78,19 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Fields`} ?required=${true} name="fields"> <ak-form-element-horizontal label=${t`Fields`} ?required=${true} name="fields">
<select name="users" class="pf-c-form-control" multiple> <select name="users" class="pf-c-form-control" multiple>
${until( ${this.prompts?.results.map((prompt) => {
new StagesApi(DEFAULT_CONFIG) const selected = Array.from(this.instance?.fields || []).some(
.stagesPromptPromptsList({ (su) => {
ordering: "field_name", return su == prompt.pk;
}) },
.then((prompts) => { );
return prompts.results.map((prompt) => { return html`<option
const selected = Array.from( value=${ifDefined(prompt.pk)}
this.instance?.fields || [], ?selected=${selected}
).some((su) => { >
return su == prompt.pk; ${t`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`}
}); </option>`;
return html`<option })}
value=${ifDefined(prompt.pk)}
?selected=${selected}
>
${t`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Hold control/command to select multiple items.`} ${t`Hold control/command to select multiple items.`}
@ -101,28 +109,19 @@ export class PromptStageForm extends ModelForm<PromptStage, string> {
name="validationPolicies" name="validationPolicies"
> >
<select name="users" class="pf-c-form-control" multiple> <select name="users" class="pf-c-form-control" multiple>
${until( ${this.policies?.results.map((policy) => {
new PoliciesApi(DEFAULT_CONFIG) const selected = Array.from(
.policiesAllList({ this.instance?.validationPolicies || [],
ordering: "name", ).some((su) => {
}) return su == policy.pk;
.then((policies) => { });
return policies.results.map((policy) => { return html`<option
const selected = Array.from( value=${ifDefined(policy.pk)}
this.instance?.validationPolicies || [], ?selected=${selected}
).some((su) => { >
return su == policy.pk; ${t`${policy.name} (${policy.verboseName})`}
}); </option>`;
return html`<option })}
value=${ifDefined(policy.pk)}
?selected=${selected}
>
${t`${policy.name} (${policy.verboseName})`}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select> </select>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Selected policies are executed when the stage is submitted to validate the data.`} ${t`Selected policies are executed when the stage is submitted to validate the data.`}

View File

@ -1,6 +1,6 @@
import { tenant } from "@goauthentik/common/api/config"; import { config, tenant } from "@goauthentik/common/api/config";
import { EVENT_LOCALE_CHANGE, EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { EVENT_LOCALE_CHANGE, EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { uiConfig } from "@goauthentik/common/ui/config"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { LitElement } from "lit"; import { LitElement } from "lit";
import { state } from "lit/decorators.js"; import { state } from "lit/decorators.js";
@ -9,13 +9,13 @@ import AKGlobal from "@goauthentik/common/styles/authentik.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css"; import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CurrentTenant, UiThemeEnum } from "@goauthentik/api"; import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api";
export function rootInterface(): Interface | undefined { export function rootInterface<T extends Interface>(): T | undefined {
const el = Array.from(document.body.querySelectorAll("*")).filter( const el = Array.from(document.body.querySelectorAll("*")).filter(
(el) => el instanceof Interface, (el) => el instanceof Interface,
); );
return el[0] as Interface; return el[0] as T;
} }
let css: Promise<string[]> | undefined; let css: Promise<string[]> | undefined;
@ -171,10 +171,17 @@ export class Interface extends AKElement {
@state() @state()
tenant?: CurrentTenant; tenant?: CurrentTenant;
@state()
uiConfig?: UIConfig;
@state()
config?: Config;
constructor() { constructor() {
super(); super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, PFBase]; document.adoptedStyleSheets = [...document.adoptedStyleSheets, PFBase];
tenant().then((tenant) => (this.tenant = tenant)); tenant().then((tenant) => (this.tenant = tenant));
config().then((config) => (this.config = config));
} }
_activateTheme(root: AdoptedStyleSheetsElement, theme: UiThemeEnum): void { _activateTheme(root: AdoptedStyleSheetsElement, theme: UiThemeEnum): void {
@ -183,7 +190,9 @@ export class Interface extends AKElement {
} }
async getTheme(): Promise<UiThemeEnum> { async getTheme(): Promise<UiThemeEnum> {
const config = await uiConfig(); if (!this.uiConfig) {
return config.theme?.base || UiThemeEnum.Automatic; this.uiConfig = await uiConfig();
}
return this.uiConfig.theme?.base || UiThemeEnum.Automatic;
} }
} }

View File

@ -7,7 +7,7 @@ import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -22,7 +22,7 @@ import { ResponseError, ValidationError } from "@goauthentik/api";
export class PreventFormSubmit { export class PreventFormSubmit {
// Stub class which can be returned by form elements to prevent the form from submitting // Stub class which can be returned by form elements to prevent the form from submitting
constructor(public message: string) {} constructor(public message: string, public element?: HorizontalFormElement) {}
} }
export class APIError extends Error { export class APIError extends Error {
@ -36,16 +36,15 @@ export interface KeyUnknown {
} }
@customElement("ak-form") @customElement("ak-form")
export class Form<T> extends AKElement { export abstract class Form<T> extends AKElement {
abstract send(data: T): Promise<unknown>;
viewportCheck = true; viewportCheck = true;
@property() @property()
successMessage = ""; successMessage = "";
@property() @state()
send!: (data: T) => Promise<unknown>;
@property({ attribute: false })
nonFieldErrors?: string[]; nonFieldErrors?: string[];
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -177,17 +176,15 @@ export class Form<T> extends AKElement {
json[element.name] = inputElement.checked; json[element.name] = inputElement.checked;
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") { } else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
const select = inputElement as unknown as SearchSelect<unknown>; const select = inputElement as unknown as SearchSelect<unknown>;
let value: unknown;
try { try {
value = select.toForm(); const value = select.toForm();
} catch { json[element.name] = value;
console.debug("authentik/form: SearchSelect.value error"); } catch (exc) {
return; if (exc instanceof PreventFormSubmit) {
throw new PreventFormSubmit(exc.message, element);
}
throw exc;
} }
if (value instanceof PreventFormSubmit) {
throw new Error(value.message);
}
json[element.name] = value;
} else { } else {
this.serializeFieldRecursive(inputElement, inputElement.value, json); this.serializeFieldRecursive(inputElement, inputElement.value, json);
} }
@ -215,30 +212,27 @@ export class Form<T> extends AKElement {
parent[nameElements[nameElements.length - 1]] = value; parent[nameElements[nameElements.length - 1]] = value;
} }
submit(ev: Event): Promise<unknown> | undefined { async submit(ev: Event): Promise<unknown | undefined> {
ev.preventDefault(); ev.preventDefault();
const data = this.serializeForm(); try {
if (!data) { const data = this.serializeForm();
return; if (!data) {
} return;
return this.send(data) }
.then((r) => { const response = await this.send(data);
showMessage({ showMessage({
level: MessageLevel.success, level: MessageLevel.success,
message: this.getSuccessMessage(), message: this.getSuccessMessage(),
}); });
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, { new CustomEvent(EVENT_REFRESH, {
bubbles: true, bubbles: true,
composed: true, composed: true,
}), }),
); );
return r; return response;
}) } catch (ex) {
.catch(async (ex: Error | ResponseError) => { if (ex instanceof ResponseError) {
if (!(ex instanceof ResponseError)) {
throw ex;
}
let msg = ex.response.statusText; let msg = ex.response.statusText;
if (ex.response.status > 399 && ex.response.status < 500) { if (ex.response.status > 399 && ex.response.status < 500) {
const errorMessage: ValidationError = await ex.response.json(); const errorMessage: ValidationError = await ex.response.json();
@ -277,9 +271,14 @@ export class Form<T> extends AKElement {
message: msg, message: msg,
level: MessageLevel.error, level: MessageLevel.error,
}); });
// rethrow the error so the form doesn't close }
throw ex; if (ex instanceof PreventFormSubmit && ex.element) {
}); ex.element.errorMessages = [ex.message];
ex.element.invalid = true;
}
// rethrow the error so the form doesn't close
throw ex;
}
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {

View File

@ -1,27 +1,44 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/elements/EmptyState";
import { Form } from "@goauthentik/elements/forms/Form"; import { Form } from "@goauthentik/elements/forms/Form";
import { TemplateResult } from "lit"; import { TemplateResult, html } from "lit";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.js";
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> { export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
abstract loadInstance(pk: PKT): Promise<T>; abstract loadInstance(pk: PKT): Promise<T>;
async load(): Promise<void> {
return Promise.resolve();
}
@property({ attribute: false }) @property({ attribute: false })
set instancePk(value: PKT) { set instancePk(value: PKT) {
this._instancePk = value; this._instancePk = value;
if (this.viewportCheck && !this.isInViewport) { if (this.viewportCheck && !this.isInViewport) {
return; return;
} }
this.loadInstance(value).then((instance) => { if (this._isLoading) {
this.instance = instance; return;
this.requestUpdate(); }
this._isLoading = true;
this.load().then(() => {
this.loadInstance(value).then((instance) => {
this.instance = instance;
this._isLoading = false;
this.requestUpdate();
});
}); });
} }
private _instancePk?: PKT; private _instancePk?: PKT;
// Keep track if we've loaded the model instance
private _initialLoad = false; private _initialLoad = false;
// Keep track if we've done the general data loading of load()
private _initialDataLoad = false;
private _isLoading = false;
@property({ attribute: false }) @property({ attribute: false })
instance?: T = this.defaultInstance; instance?: T = this.defaultInstance;
@ -45,17 +62,29 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
this._initialLoad = false; this._initialLoad = false;
} }
renderVisible(): TemplateResult {
if ((this._instancePk && !this.instance) || !this._initialDataLoad) {
return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
}
return super.renderVisible();
}
render(): TemplateResult { render(): TemplateResult {
if (this._instancePk && !this._initialLoad) { // if we're in viewport now and haven't loaded AND have a PK set, load now
if ( // Or if we don't check for viewport in some cases
// if we're in viewport now and haven't loaded AND have a PK set, load now const viewportVisible = this.isInViewport || !this.viewportCheck;
this.isInViewport || if (this._instancePk && !this._initialLoad && viewportVisible) {
// Or if we don't check for viewport in some cases this.instancePk = this._instancePk;
!this.viewportCheck this._initialLoad = true;
) { } else if (!this._initialDataLoad && viewportVisible) {
this.instancePk = this._instancePk; // else if since if the above case triggered that will also call this.load(), so
this._initialLoad = true; // ensure we don't load again
} this.load().then(() => {
this._initialDataLoad = true;
// Class attributes changed in this.load() might not be @property()
// or @state() so let's trigger a re-render to be sure we get updated
this.requestUpdate();
});
} }
return super.render(); return super.render();
} }

View File

@ -4,7 +4,7 @@ import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
@customElement("ak-proxy-form") @customElement("ak-proxy-form")
export class ProxyForm extends Form<unknown> { export abstract class ProxyForm extends Form<unknown> {
@property() @property()
type!: string; type!: string;
@ -16,7 +16,7 @@ export class ProxyForm extends Form<unknown> {
innerElement?: Form<unknown>; innerElement?: Form<unknown>;
submit(ev: Event): Promise<unknown> | undefined { async submit(ev: Event): Promise<unknown | undefined> {
return this.innerElement?.submit(ev); return this.innerElement?.submit(ev);
} }
@ -39,7 +39,9 @@ export class ProxyForm extends Form<unknown> {
if (this.type in this.typeMap) { if (this.type in this.typeMap) {
elementName = this.typeMap[this.type]; elementName = this.typeMap[this.type];
} }
this.innerElement = document.createElement(elementName) as Form<unknown>; if (!this.innerElement) {
this.innerElement = document.createElement(elementName) as Form<unknown>;
}
this.innerElement.viewportCheck = this.viewportCheck; this.innerElement.viewportCheck = this.viewportCheck;
for (const k in this.args) { for (const k in this.args) {
this.innerElement.setAttribute(k, this.args[k] as string); this.innerElement.setAttribute(k, this.args[k] as string);

View File

@ -25,11 +25,6 @@ export class Radio<T> extends AKElement {
@property() @property()
value?: T; value?: T;
@property({ attribute: false })
onChange: (value: T) => void = () => {
return;
};
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -63,7 +58,13 @@ export class Radio<T> extends AKElement {
id=${elId} id=${elId}
@change=${() => { @change=${() => {
this.value = opt.value; this.value = opt.value;
this.onChange(opt.value); this.dispatchEvent(
new CustomEvent("change", {
bubbles: true,
composed: true,
detail: opt.value,
}),
);
}} }}
.checked=${opt.value === this.value} .checked=${opt.value === this.value}
/> />

View File

@ -94,7 +94,7 @@ export class SearchSelect<T> extends AKElement {
toForm(): unknown { toForm(): unknown {
if (!this.objects) { if (!this.objects) {
return new PreventFormSubmit(t`Loading options...`); throw new PreventFormSubmit(t`Loading options...`);
} }
return this.value(this.selectedObject) || ""; return this.value(this.selectedObject) || "";
} }

View File

@ -13,13 +13,13 @@ import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-gro
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-wizard-form") @customElement("ak-wizard-form")
export class WizardForm extends Form<KeyUnknown> { export abstract class WizardForm extends Form<KeyUnknown> {
viewportCheck = false; viewportCheck = false;
@property({ attribute: false }) @property({ attribute: false })
nextDataCallback!: (data: KeyUnknown) => Promise<boolean>; nextDataCallback!: (data: KeyUnknown) => Promise<boolean>;
submit(): Promise<boolean> | undefined { async submit(): Promise<boolean | undefined> {
const data = this.serializeForm(); const data = this.serializeForm();
if (!data) { if (!data) {
return; return;

View File

@ -5,7 +5,7 @@ import {
EVENT_WS_MESSAGE, EVENT_WS_MESSAGE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config"; import { UserDisplay } from "@goauthentik/common/ui/config";
import { autoDetectLanguage } from "@goauthentik/common/ui/locale"; import { autoDetectLanguage } from "@goauthentik/common/ui/locale";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
@ -56,9 +56,6 @@ export class UserInterface extends Interface {
@state() @state()
me?: SessionUser; me?: SessionUser;
@state()
config?: UIConfig;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -126,7 +123,6 @@ export class UserInterface extends Interface {
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
this.me = await me(); this.me = await me();
this.config = await uiConfig();
const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({ const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
seen: false, seen: false,
ordering: "-created", ordering: "-created",
@ -137,11 +133,11 @@ export class UserInterface extends Interface {
} }
render(): TemplateResult { render(): TemplateResult {
if (!this.config || !this.me) { if (!this.uiConfig || !this.me) {
return html``; return html``;
} }
let userDisplay = ""; let userDisplay = "";
switch (this.config.navbar.userDisplay) { switch (this.uiConfig.navbar.userDisplay) {
case UserDisplay.username: case UserDisplay.username:
userDisplay = this.me.user.username; userDisplay = this.me.user.username;
break; break;
@ -155,7 +151,7 @@ export class UserInterface extends Interface {
userDisplay = this.me.user.username; userDisplay = this.me.user.username;
} }
return html`<div class="pf-c-page"> return html`<div class="pf-c-page">
<div class="background-wrapper" style="${this.config.theme.background}"></div> <div class="background-wrapper" style="${this.uiConfig.theme.background}"></div>
<header class="pf-c-page__header"> <header class="pf-c-page__header">
<div class="pf-c-page__header-brand"> <div class="pf-c-page__header-brand">
<a href="#/" class="pf-c-page__header-brand-link"> <a href="#/" class="pf-c-page__header-brand-link">
@ -168,7 +164,7 @@ export class UserInterface extends Interface {
</div> </div>
<div class="pf-c-page__header-tools"> <div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group"> <div class="pf-c-page__header-tools-group">
${this.config.enabledFeatures.apiDrawer ${this.uiConfig.enabledFeatures.apiDrawer
? html`<div ? html`<div
class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg" class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"
> >
@ -186,7 +182,7 @@ export class UserInterface extends Interface {
</button> </button>
</div>` </div>`
: html``} : html``}
${this.config.enabledFeatures.notificationDrawer ${this.uiConfig.enabledFeatures.notificationDrawer
? html`<div ? html`<div
class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg" class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"
> >
@ -216,7 +212,7 @@ export class UserInterface extends Interface {
</button> </button>
</div> ` </div> `
: html``} : html``}
${this.config.enabledFeatures.settings ${this.uiConfig.enabledFeatures.settings
? html` <div class="pf-c-page__header-tools-item"> ? html` <div class="pf-c-page__header-tools-item">
<a class="pf-c-button pf-m-plain" type="button" href="#/settings"> <a class="pf-c-button pf-m-plain" type="button" href="#/settings">
<i class="fas fa-cog" aria-hidden="true"></i> <i class="fas fa-cog" aria-hidden="true"></i>