From 61f7db314a4f33f93e4e08d0bcaf86f3b49b1223 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 26 Feb 2022 17:49:04 +0100 Subject: [PATCH] web/admin: use searchable select field for users and groups in policy binding form closes #2285 Signed-off-by: Jens Langhammer --- web/src/elements/Expand.ts | 2 +- web/src/elements/SearchSelect.ts | 104 ++++++++++++++++++ web/src/elements/forms/Form.ts | 4 + .../elements/forms/HorizontalFormElement.ts | 1 + web/src/pages/policies/PolicyBindingForm.ts | 102 +++++++++-------- 5 files changed, 167 insertions(+), 46 deletions(-) create mode 100644 web/src/elements/SearchSelect.ts diff --git a/web/src/elements/Expand.ts b/web/src/elements/Expand.ts index 455ed5f89..f1147bd19 100644 --- a/web/src/elements/Expand.ts +++ b/web/src/elements/Expand.ts @@ -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 { diff --git a/web/src/elements/SearchSelect.ts b/web/src/elements/SearchSelect.ts new file mode 100644 index 000000000..78ce3f8bb --- /dev/null +++ b/web/src/elements/SearchSelect.ts @@ -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 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; + + @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`
+
+
+ { + 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) : ""} + /> +
+
+ +
    + ${this.objects.map((obj) => { + return html` +
  • + +
  • + `; + })} +
+
`; + } +} diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index fa3914d75..ca2ddd6cb 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -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 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; + json[element.name] = select.value(select.selectedObject); } else { for (let v = 0; v < values.length; v++) { this.serializeFieldRecursive(element, values[v], json); diff --git a/web/src/elements/forms/HorizontalFormElement.ts b/web/src/elements/forms/HorizontalFormElement.ts index d387062db..fb719309e 100644 --- a/web/src/elements/forms/HorizontalFormElement.ts +++ b/web/src/elements/forms/HorizontalFormElement.ts @@ -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: diff --git a/web/src/pages/policies/PolicyBindingForm.ts b/web/src/pages/policies/PolicyBindingForm.ts index 2a5d64660..dffefcefc 100644 --- a/web/src/pages/policies/PolicyBindingForm.ts +++ b/web/src/pages/policies/PolicyBindingForm.ts @@ -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 { name="group" ?hidden=${this.policyGroupUser !== target.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; + }} + > + ${this.policyOnly ? html`

${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 { name="user" ?hidden=${this.policyGroupUser !== target.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; + }} + > + ${this.policyOnly ? html`

${t`User mappings can only be checked if a user is already logged in when trying to access this source.`}