web/admin: use searchable select field for users and groups in policy binding form

closes #2285

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-02-26 17:49:04 +01:00
parent ef7952cab3
commit 61f7db314a
5 changed files with 167 additions and 46 deletions

View file

@ -3,7 +3,7 @@ import { t } from "@lingui/macro";
import { CSSResult, LitElement, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFExpandableSection from "../../node_modules/@patternfly/patternfly/components/ExpandableSection/expandable-section.css";
import PFExpandableSection from "@patternfly/patternfly/components/ExpandableSection/expandable-section.css";
@customElement("ak-expand")
export class Expand extends LitElement {

View file

@ -0,0 +1,104 @@
import { t } from "@lingui/macro";
import { CSSResult, LitElement, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import AKGlobal from "../authentik.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFSelect from "@patternfly/patternfly/components/Select/select.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-search-select")
export class SearchSelect<T> extends LitElement {
@property()
query?: string;
@property({ attribute: false })
objects: T[] = [];
@property({ attribute: false })
selectedObject?: T;
@property()
name?: string;
@property({ type: Boolean })
open = false;
@property()
placeholder: string = t`Select an object.`;
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFFormControl, PFSelect, AKGlobal];
}
@property({ attribute: false })
fetchObjects!: (query?: string) => Promise<T[]>;
@property({ attribute: false })
renderElement!: (element: T) => string;
@property({ attribute: false })
value!: (element: T) => unknown;
@property({ attribute: false })
selected!: (element: T) => boolean;
firstUpdated(): void {
this.fetchObjects(this.query).then((objects) => {
this.objects = objects;
this.objects.forEach((obj) => {
if (this.selected(obj)) {
this.selectedObject = obj;
}
});
});
}
render(): TemplateResult {
return html`<div class="pf-c-select">
<div class="pf-c-select__toggle pf-m-typeahead">
<div class="pf-c-select__toggle-wrapper">
<input
class="pf-c-form-control pf-c-select__toggle-typeahead"
type="text"
placeholder=${this.placeholder}
@input=${(ev: InputEvent) => {
this.query = (ev.target as HTMLInputElement).value;
this.firstUpdated();
}}
@focus=${() => {
this.open = true;
}}
@blur=${() => {
setTimeout(() => {
this.open = false;
}, 200);
}}
.value=${this.selectedObject ? this.renderElement(this.selectedObject) : ""}
/>
</div>
</div>
<ul class="pf-c-select__menu" role="listbox" ?hidden="${!this.open}">
${this.objects.map((obj) => {
return html`
<li role="presentation">
<button
class="pf-c-select__menu-item"
role="option"
@click=${() => {
this.selectedObject = obj;
this.open = false;
}}
>
${this.renderElement(obj)}
</button>
</li>
`;
})}
</ul>
</div>`;
}
}

View file

@ -20,6 +20,7 @@ import { ValidationError } from "@goauthentik/api";
import { EVENT_REFRESH } from "../../constants";
import { showMessage } from "../../elements/messages/MessageContainer";
import { camelToSnake, convertToSlug } from "../../utils";
import { SearchSelect } from "../SearchSelect";
import { MessageLevel } from "../messages/Message";
export class APIError extends Error {
@ -150,6 +151,9 @@ export class Form<T> extends LitElement {
json[element.name] = new Date(element.value);
} else if (element.tagName.toLowerCase() === "input" && element.type === "checkbox") {
json[element.name] = element.checked;
} else if (element.tagName.toLowerCase() === "ak-search-select") {
const select = element as unknown as SearchSelect<unknown>;
json[element.name] = select.value(select.selectedObject);
} else {
for (let v = 0; v < values.length; v++) {
this.serializeFieldRecursive(element, values[v], json);

View file

@ -77,6 +77,7 @@ export class HorizontalFormElement extends LitElement {
case "select":
case "ak-codemirror":
case "ak-chip-group":
case "ak-search-select":
(input as HTMLInputElement).name = this.name;
break;
default:

View file

@ -9,9 +9,19 @@ import { until } from "lit/directives/until.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
import { CoreApi, PoliciesApi, Policy, PolicyBinding } from "@goauthentik/api";
import {
CoreApi,
CoreGroupsListRequest,
CoreUsersListRequest,
Group,
PoliciesApi,
Policy,
PolicyBinding,
User,
} from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../api/Config";
import "../../elements/SearchSelect";
import "../../elements/forms/HorizontalFormElement";
import { ModelForm } from "../../elements/forms/ModelForm";
import { UserOption } from "../../elements/user/utils";
@ -195,28 +205,29 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
name="group"
?hidden=${this.policyGroupUser !== target.group}
>
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.group === undefined}>
---------
</option>
${until(
new CoreApi(DEFAULT_CONFIG)
.coreGroupsList({
ordering: "name",
})
.then((groups) => {
return groups.results.map((group) => {
return html`<option
value=${ifDefined(group.pk)}
?selected=${group.pk === this.instance?.group}
>
${group.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<!-- @ts-ignore -->
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "username",
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group): string => {
return group.pk;
}}
.selected=${(group: Group): boolean => {
return group.pk === this.instance?.group;
}}
>
</ak-search-select>
${this.policyOnly
? html`<p class="pf-c-form__helper-text">
${t`Group mappings can only be checked if a user is already logged in when trying to access this source.`}
@ -228,28 +239,29 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
name="user"
?hidden=${this.policyGroupUser !== target.user}
>
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.user === undefined}>
---------
</option>
${until(
new CoreApi(DEFAULT_CONFIG)
.coreUsersList({
ordering: "username",
})
.then((users) => {
return users.results.map((user) => {
return html`<option
value=${ifDefined(user.pk)}
?selected=${user.pk === this.instance?.user}
>
${UserOption(user)}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<!-- @ts-ignore -->
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
ordering: "username",
};
if (query !== undefined) {
args.search = query;
}
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args);
return users.results;
}}
.renderElement=${(user: User): string => {
return UserOption(user);
}}
.value=${(user: User): number => {
return user.pk;
}}
.selected=${(user: User): boolean => {
return user.pk === this.instance?.user;
}}
>
</ak-search-select>
${this.policyOnly
? html`<p class="pf-c-form__helper-text">
${t`User mappings can only be checked if a user is already logged in when trying to access this source.`}