Files
authentik/web/src/admin/lifecycle/LifecycleRuleForm.ts
authentik-automation[bot] 43675c2b22 web: fix italic formatting in lifecycle rule help text (cherry-pick #20263 to version-2026.2) (#20267)
web: fix italic formatting in lifecycle rule help text (#20263)

* web: fix italic formatting in lifecycle rule help text

* r

Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-14 21:22:43 +00:00

346 lines
12 KiB
TypeScript

import "#elements/ak-dual-select/ak-dual-select-dynamic-selected-provider";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/Radio";
import "#elements/forms/SearchSelect/index";
import "#elements/ak-list-select/ak-list-select";
import "#elements/utils/TimeDeltaHelp";
import "#components/ak-text-input";
import "#components/ak-radio-input";
import "#components/ak-number-input";
import "#components/ak-switch-input";
import { DEFAULT_CONFIG } from "#common/api/config";
import { DataProvision, DualSelectPair } from "#elements/ak-dual-select/types";
import { ModelForm } from "#elements/forms/ModelForm";
import { RadioChangeEventDetail, RadioOption } from "#elements/forms/Radio";
import type SearchSelect from "#elements/forms/SearchSelect/SearchSelect";
import { SlottedTemplateResult } from "#elements/types";
import { eventTransportsProvider, eventTransportsSelector } from "#admin/events/RuleFormHelpers";
import {
Application,
ContentTypeEnum,
CoreApi,
Group,
LifecycleApi,
LifecycleRule,
RbacApi,
ReviewerGroup,
ReviewerUser,
Role,
} from "@goauthentik/api";
import { match } from "ts-pattern";
import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit-html/directives/if-defined.js";
import { customElement, state } from "lit/decorators.js";
import { keyed } from "lit/directives/keyed.js";
import { createRef, ref } from "lit/directives/ref.js";
type TargetObject = Application | Group | Role;
function userToPair(item: ReviewerUser): DualSelectPair {
return [item.uuid, html`<div class="selection-main">${item.name}</div>`, item.name];
}
function groupToPair(item: ReviewerGroup): DualSelectPair {
return [item.pk, html`<div class="selection-main">${item.name}</div>`, item.name];
}
function createContentTypeOptions(): RadioOption<ContentTypeEnum>[] {
return [
{
value: ContentTypeEnum.AuthentikCoreApplication,
label: msg("Application"),
default: true,
},
{
value: ContentTypeEnum.AuthentikCoreGroup,
label: msg("Group"),
},
{
value: ContentTypeEnum.AuthentikRbacRole,
label: msg("Role"),
},
] satisfies RadioOption<ContentTypeEnum>[];
}
function formatContentTypePlaceholder(contentType: ContentTypeEnum): string {
switch (contentType) {
case ContentTypeEnum.AuthentikCoreApplication:
return msg("Select an application...");
case ContentTypeEnum.AuthentikCoreGroup:
return msg("Select a group...");
case ContentTypeEnum.AuthentikRbacRole:
return msg("Select a role...");
case ContentTypeEnum.UnknownDefaultOpenApi:
return msg("Select an object...");
}
}
@customElement("ak-lifecycle-rule-form")
export class LifecycleRuleForm extends ModelForm<LifecycleRule, string> {
#targetSelectRef = createRef<SearchSelect<TargetObject>>();
#reviewerGroupsSelectRef = createRef<SearchSelect<Group>>();
#reviewerUsersSelectRef = createRef<SearchSelect<Group>>();
#coreApi = new CoreApi(DEFAULT_CONFIG);
#lifecycleApi = new LifecycleApi(DEFAULT_CONFIG);
#rbacApi = new RbacApi(DEFAULT_CONFIG);
@state()
protected selectedContentType: ContentTypeEnum = ContentTypeEnum.AuthentikCoreApplication;
protected async loadInstance(pk: string): Promise<LifecycleRule> {
const rule = await this.#lifecycleApi.lifecycleRulesRetrieve({
id: pk,
});
this.selectedContentType = rule.contentType;
return rule;
}
#fetchGroups = (page: number, search?: string): Promise<DataProvision> => {
return this.#coreApi
.coreGroupsList({
page: page,
search: search,
})
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(groupToPair),
};
});
};
#fetchUsers = (page: number, search?: string): Promise<DataProvision> => {
return this.#coreApi
.coreUsersList({
page: page,
search: search,
})
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(userToPair),
};
});
};
protected override async send(data: LifecycleRule): Promise<LifecycleRule> {
if (this.instance) {
return this.#lifecycleApi.lifecycleRulesUpdate({
id: this.instance.id,
lifecycleRuleRequest: data,
});
}
return this.#lifecycleApi.lifecycleRulesCreate({
lifecycleRuleRequest: data,
});
}
protected override serialize(): LifecycleRule | null {
const result = super.serialize();
if (!result) {
return null;
}
if (!result.objectId) {
result.objectId = null;
}
return result;
}
#loadObjects = async (query?: string): Promise<TargetObject[]> => {
const promise = match(this.selectedContentType)
.with(ContentTypeEnum.AuthentikCoreApplication, () =>
this.#coreApi.coreApplicationsList({
ordering: "name",
search: query,
superuserFullList: true,
}),
)
.with(ContentTypeEnum.AuthentikCoreGroup, () =>
this.#coreApi.coreGroupsList({
ordering: "name",
search: query,
}),
)
.with(ContentTypeEnum.AuthentikRbacRole, () =>
this.#rbacApi.rbacRolesList({
ordering: "name",
search: query,
}),
)
.otherwise(() => null);
if (!promise) {
return [];
}
return promise.then((response) => response.results);
};
#contentTypeChangeListener = async (
event: CustomEvent<RadioChangeEventDetail<ContentTypeEnum>>,
): Promise<void> => {
this.selectedContentType = event.detail.value;
};
protected renderForm(): SlottedTemplateResult {
return html`<ak-text-input
label=${msg("Rule Name")}
name="name"
required
value="${ifDefined(this.instance?.name)}"
placeholder=${msg("Type a name for this lifecycle rule...")}
></ak-text-input>
${this.renderContentTypeOptions()} ${this.renderTargetSelection()}
<ak-text-input
label=${msg("Interval")}
name="interval"
required
value="${this.instance?.interval || "days=60"}"
input-hint="code"
help=${msg("The interval between opening new reviews for matching objects.")}
.bighelp=${html`<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
></ak-text-input>
<ak-text-input
label=${msg("Grace period")}
name="gracePeriod"
required
value="${this.instance?.gracePeriod || "days=30"}"
input-hint="code"
help=${msg("The duration of time before an open review is considered overdue.")}
.bighelp=${html`<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
></ak-text-input>
<ak-form-element-horizontal label=${msg("Reviewer groups")} name="reviewerGroups">
${this.renderReviewerGroupsSelection()}
</ak-form-element-horizontal>
<ak-number-input
label=${msg("Min reviewers")}
min=${1}
name="minReviewers"
value="${this.instance?.minReviewers ?? 1}"
help=${msg(
"Number of users from the selected reviewer groups that must approve the review.",
)}
></ak-number-input>
<ak-switch-input
name="minReviewersIsPerGroup"
?checked=${this.instance?.minReviewersIsPerGroup ?? false}
label=${msg("Min reviewers is per-group")}
.help=${msg(
html`If checked, approving a review will require at least that many users from
<em>each</em> of the selected groups. When disabled, the value is a total
across all groups.`,
)}
>
</ak-switch-input>
<ak-form-element-horizontal label=${msg("Reviewers")} name="reviewers">
${this.renderReviewerUserSelection()}
</ak-form-element-horizontal>
${this.renderTransportsSelection()} `;
}
protected renderContentTypeOptions(): SlottedTemplateResult {
return html`<ak-radio-input
@change=${this.#contentTypeChangeListener}
label=${msg("Object type")}
name="contentType"
required
.value=${this.instance?.contentType}
.options=${createContentTypeOptions()}
></ak-radio-input>`;
}
protected renderTargetSelection() {
return keyed(
this.selectedContentType,
html`<ak-form-element-horizontal label=${msg("Object")} name="objectId">
<ak-search-select
${ref(this.#targetSelectRef)}
placeholder=${formatContentTypePlaceholder(this.selectedContentType)}
.fetchObjects=${this.#loadObjects}
.renderElement=${(obj: TargetObject) => obj.name}
.value=${(obj?: TargetObject) => obj?.pk}
.selected=${(obj: TargetObject): boolean => {
return obj.pk === this.instance?.objectId;
}}
blankable
></ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"When set, the rule will apply to the selected individual object. Otherwise, the rule applies to all objects of the selected type.",
)}
</p>
</ak-form-element-horizontal>`,
);
}
protected renderReviewerGroupsSelection(): SlottedTemplateResult {
return html`<ak-dual-select-provider
${ref(this.#reviewerGroupsSelectRef)}
.provider=${this.#fetchGroups}
.selected=${(this.instance?.reviewerGroupsObj ?? []).map(groupToPair)}
available-label=${msg("Available Groups")}
selected-label=${msg("Selected Groups")}
></ak-dual-select-provider>`;
}
protected renderReviewerUserSelection(): SlottedTemplateResult {
return html`<ak-dual-select-provider
${ref(this.#reviewerUsersSelectRef)}
.provider=${this.#fetchUsers}
.selected=${(this.instance?.reviewersObj ?? []).map(userToPair)}
available-label=${msg("Available Users")}
selected-label=${msg("Selected Users")}
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"A review will require approval from each of the users selected here in addition to group members as per above settings.",
)}
</p>`;
}
protected renderTransportsSelection(): SlottedTemplateResult {
return html`
<ak-form-element-horizontal
label=${msg("Notification transports")}
required
name="notificationTransports"
>
<ak-dual-select-dynamic-selected
.provider=${eventTransportsProvider}
.selector=${eventTransportsSelector(this.instance?.notificationTransports)}
available-label="${msg("Available Transports")}"
selected-label="${msg("Selected Transports")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Select which transports should be used to notify the user.")}
</p>
</ak-form-element-horizontal>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-lifecycle-rule-form": LifecycleRuleForm;
}
}