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:
		| @ -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 { | ||||
|  | ||||
							
								
								
									
										104
									
								
								web/src/elements/SearchSelect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								web/src/elements/SearchSelect.ts
									
									
									
									
									
										Normal 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>`; | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|  | ||||
| @ -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: | ||||
|  | ||||
| @ -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} | ||||
|                         <!-- @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; | ||||
|                             }} | ||||
|                         > | ||||
|                                                 ${group.name} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         </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({ | ||||
|                         <!-- @ts-ignore --> | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||
|                                 const args: CoreUsersListRequest = { | ||||
|                                     ordering: "username", | ||||
|                                     }) | ||||
|                                     .then((users) => { | ||||
|                                         return users.results.map((user) => { | ||||
|                                             return html`<option | ||||
|                                                 value=${ifDefined(user.pk)} | ||||
|                                                 ?selected=${user.pk === this.instance?.user} | ||||
|                                 }; | ||||
|                                 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; | ||||
|                             }} | ||||
|                         > | ||||
|                                                 ${UserOption(user)} | ||||
|                                             </option>`; | ||||
|                                         }); | ||||
|                                     }), | ||||
|                                 html`<option>${t`Loading...`}</option>`, | ||||
|                             )} | ||||
|                         </select> | ||||
|                         </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.`} | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer