From 10999630e5ad66e4558310caf1327baef292e048 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Thu, 4 Jan 2024 13:12:13 -0800 Subject: [PATCH] web: provide a search for the dual list multiselect **This commit** - Includes a new widget that represents the basic, Patternfly-designed search bar. It just emits events of search request updates. - Changes the definition of a data provider to take an optional search string. - Changes the handler in the *independent* layer so that it catches search requests and those requests work on the "selected" collection. - Changes the handler of the `authentik` interface layer so that it catches search requests and those requests are sent to the data provider. - Provides a debounce function for the `authentik` interface layer to not hammer the Django instance too much. - Updates the data providers in the example for `OutpostForm` to handle search requests. - Provides a property in the `authentik` interface layer so that the debounce can be tuned. --- web/src/admin/outposts/OutpostForm.ts | 20 +++++------ .../ak-dual-select/ak-dual-select-provider.ts | 36 +++++++++++++++---- .../elements/ak-dual-select/ak-dual-select.ts | 6 ++-- web/src/elements/ak-dual-select/types.ts | 2 +- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/web/src/admin/outposts/OutpostForm.ts b/web/src/admin/outposts/OutpostForm.ts index 93c2167bb..ded4a088b 100644 --- a/web/src/admin/outposts/OutpostForm.ts +++ b/web/src/admin/outposts/OutpostForm.ts @@ -39,11 +39,11 @@ interface ProviderData { } const api = () => new ProvidersApi(DEFAULT_CONFIG); -const providerListArgs = (page: number) => ({ +const providerListArgs = (page: number, search = "") => ({ ordering: "name", applicationIsnull: false, pageSize: 20, - search: "", + search: search, page, }); @@ -64,17 +64,17 @@ const provisionMaker = (results: ProviderData) => ({ options: results.results.map(dualSelectPairMaker), }); -const proxyListFetch = async (page: number) => - provisionMaker(await api().providersProxyList(providerListArgs(page))); +const proxyListFetch = async (page: number, search = "") => + provisionMaker(await api().providersProxyList(providerListArgs(page, search))); -const ldapListFetch = async (page: number) => - provisionMaker(await api().providersLdapList(providerListArgs(page))); +const ldapListFetch = async (page: number, search = "") => + provisionMaker(await api().providersLdapList(providerListArgs(page, search))); -const radiusListFetch = async (page: number) => - provisionMaker(await api().providersRadiusList(providerListArgs(page))); +const radiusListFetch = async (page: number, search = "") => + provisionMaker(await api().providersRadiusList(providerListArgs(page, search))); -const racListProvider = async (page: number) => - provisionMaker(await api().providersRacList(providerListArgs(page))); +const racListProvider = async (page: number, search = "") => + provisionMaker(await api().providersRacList(providerListArgs(page, search))); function providerProvider(type: OutpostTypeEnum): DataProvider { switch (type) { diff --git a/web/src/elements/ak-dual-select/ak-dual-select-provider.ts b/web/src/elements/ak-dual-select/ak-dual-select-provider.ts index 97a93b9cc..d46883538 100644 --- a/web/src/elements/ak-dual-select/ak-dual-select-provider.ts +++ b/web/src/elements/ak-dual-select/ak-dual-select-provider.ts @@ -6,6 +6,7 @@ import { PropertyValues, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { createRef, ref } from "lit/directives/ref.js"; import type { Ref } from "lit/directives/ref.js"; +import { debounce } from "@goauthentik/elements/utils/debounce"; import type { Pagination } from "@goauthentik/api"; @@ -26,8 +27,9 @@ import type { DataProvider, DualSelectPair } from "./types"; @customElement("ak-dual-select-provider") export class AkDualSelectProvider extends CustomListenerElement(AKElement) { - // A function that takes a page and returns the DualSelectPair[] collection with which to update - // the "Available" pane. + /** A function that takes a page and returns the DualSelectPair[] collection with which to update + * the "Available" pane. + */ @property({ type: Object }) provider!: DataProvider; @@ -40,6 +42,10 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) { @property({ attribute: "selected-label" }) selectedLabel = msg("Selected options"); + /** The remote lists are debounced by definition. This is the interval for the debounce. */ + @property({ attribute: "search-delay", type: Number }) + searchDelay = 250; + @state() private options: DualSelectPair[] = []; @@ -58,12 +64,14 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) { constructor() { super(); setTimeout(() => this.fetch(1), 0); - this.onNav = this.onNav.bind(this); - this.onChange = this.onChange.bind(this); // Notify AkForElementHorizontal how to handle this thing. this.dataset.akControl = "true"; + this.onNav = this.onNav.bind(this); + this.onChange = this.onChange.bind(this); + this.onSearch = this.onSearch.bind(this); this.addCustomListener("ak-pagination-nav-to", this.onNav); this.addCustomListener("ak-dual-select-change", this.onChange); + this.addCustomListener("ak-dual-select-search", this.onSearch); } onNav(event: Event) { @@ -80,7 +88,23 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) { this.selected = event.detail.value; } + doSearch(search: string) { + this.pagination = undefined; + this.fetch(undefined, search); + } + + onSearch(event: Event) { + if (!(event instanceof CustomEvent)) { + throw new Error(`Expecting a CustomEvent for change, received ${event} instead`); + } + this.doSearch(event.detail); + } + willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has("searchDelay")) { + this.doSearch = debounce(this.doSearch.bind(this), this.searchDelay); + } + if (changedProperties.has("provider")) { this.pagination = undefined; if (changedProperties.get("provider")) { @@ -91,13 +115,13 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) { } } - async fetch(page?: number) { + async fetch(page?: number, search = "") { if (this.isLoading) { return; } this.isLoading = true; const goto = page ?? this.pagination?.current ?? 1; - const data = await this.provider(goto); + const data = await this.provider(goto, search); this.pagination = data.pagination; this.options = data.options; this.isLoading = false; diff --git a/web/src/elements/ak-dual-select/ak-dual-select.ts b/web/src/elements/ak-dual-select/ak-dual-select.ts index 6fdb93322..53fe02dbd 100644 --- a/web/src/elements/ak-dual-select/ak-dual-select.ts +++ b/web/src/elements/ak-dual-select/ak-dual-select.ts @@ -234,11 +234,11 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE case "ak-dual-list-selected-search": return this.handleSelectedSearch(event.detail.value); } - + event.stopPropagation(); } - handleAvailbleSearch(value: string) { - console.log(value); + handleAvailableSearch(value: string) { + this.dispatchCustomEvent("ak-dual-select-search", value); } handleSelectedSearch(value: string) { diff --git a/web/src/elements/ak-dual-select/types.ts b/web/src/elements/ak-dual-select/types.ts index c873db62e..92afe1bf3 100644 --- a/web/src/elements/ak-dual-select/types.ts +++ b/web/src/elements/ak-dual-select/types.ts @@ -16,7 +16,7 @@ export type DataProvision = { options: DualSelectPair[]; }; -export type DataProvider = (page: number) => Promise; +export type DataProvider = (page: number, search?: string) => Promise; export interface SearchbarEvent extends CustomEvent { detail: {