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.
This commit is contained in:
Ken Sternberg 2024-01-04 13:12:13 -08:00
parent 51e89ce916
commit 10999630e5
4 changed files with 44 additions and 20 deletions

View File

@ -39,11 +39,11 @@ interface ProviderData {
} }
const api = () => new ProvidersApi(DEFAULT_CONFIG); const api = () => new ProvidersApi(DEFAULT_CONFIG);
const providerListArgs = (page: number) => ({ const providerListArgs = (page: number, search = "") => ({
ordering: "name", ordering: "name",
applicationIsnull: false, applicationIsnull: false,
pageSize: 20, pageSize: 20,
search: "", search: search,
page, page,
}); });
@ -64,17 +64,17 @@ const provisionMaker = (results: ProviderData) => ({
options: results.results.map(dualSelectPairMaker), options: results.results.map(dualSelectPairMaker),
}); });
const proxyListFetch = async (page: number) => const proxyListFetch = async (page: number, search = "") =>
provisionMaker(await api().providersProxyList(providerListArgs(page))); provisionMaker(await api().providersProxyList(providerListArgs(page, search)));
const ldapListFetch = async (page: number) => const ldapListFetch = async (page: number, search = "") =>
provisionMaker(await api().providersLdapList(providerListArgs(page))); provisionMaker(await api().providersLdapList(providerListArgs(page, search)));
const radiusListFetch = async (page: number) => const radiusListFetch = async (page: number, search = "") =>
provisionMaker(await api().providersRadiusList(providerListArgs(page))); provisionMaker(await api().providersRadiusList(providerListArgs(page, search)));
const racListProvider = async (page: number) => const racListProvider = async (page: number, search = "") =>
provisionMaker(await api().providersRacList(providerListArgs(page))); provisionMaker(await api().providersRacList(providerListArgs(page, search)));
function providerProvider(type: OutpostTypeEnum): DataProvider { function providerProvider(type: OutpostTypeEnum): DataProvider {
switch (type) { switch (type) {

View File

@ -6,6 +6,7 @@ import { PropertyValues, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js"; import { createRef, ref } from "lit/directives/ref.js";
import type { 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"; import type { Pagination } from "@goauthentik/api";
@ -26,8 +27,9 @@ import type { DataProvider, DualSelectPair } from "./types";
@customElement("ak-dual-select-provider") @customElement("ak-dual-select-provider")
export class AkDualSelectProvider extends CustomListenerElement(AKElement) { export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
// A function that takes a page and returns the DualSelectPair[] collection with which to update /** A function that takes a page and returns the DualSelectPair[] collection with which to update
// the "Available" pane. * the "Available" pane.
*/
@property({ type: Object }) @property({ type: Object })
provider!: DataProvider; provider!: DataProvider;
@ -40,6 +42,10 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
@property({ attribute: "selected-label" }) @property({ attribute: "selected-label" })
selectedLabel = msg("Selected options"); 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() @state()
private options: DualSelectPair[] = []; private options: DualSelectPair[] = [];
@ -58,12 +64,14 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
constructor() { constructor() {
super(); super();
setTimeout(() => this.fetch(1), 0); setTimeout(() => this.fetch(1), 0);
this.onNav = this.onNav.bind(this);
this.onChange = this.onChange.bind(this);
// Notify AkForElementHorizontal how to handle this thing. // Notify AkForElementHorizontal how to handle this thing.
this.dataset.akControl = "true"; 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-pagination-nav-to", this.onNav);
this.addCustomListener("ak-dual-select-change", this.onChange); this.addCustomListener("ak-dual-select-change", this.onChange);
this.addCustomListener("ak-dual-select-search", this.onSearch);
} }
onNav(event: Event) { onNav(event: Event) {
@ -80,7 +88,23 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
this.selected = event.detail.value; 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<this>) { willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("searchDelay")) {
this.doSearch = debounce(this.doSearch.bind(this), this.searchDelay);
}
if (changedProperties.has("provider")) { if (changedProperties.has("provider")) {
this.pagination = undefined; this.pagination = undefined;
if (changedProperties.get("provider")) { 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) { if (this.isLoading) {
return; return;
} }
this.isLoading = true; this.isLoading = true;
const goto = page ?? this.pagination?.current ?? 1; 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.pagination = data.pagination;
this.options = data.options; this.options = data.options;
this.isLoading = false; this.isLoading = false;

View File

@ -234,11 +234,11 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
case "ak-dual-list-selected-search": case "ak-dual-list-selected-search":
return this.handleSelectedSearch(event.detail.value); return this.handleSelectedSearch(event.detail.value);
} }
event.stopPropagation();
} }
handleAvailbleSearch(value: string) { handleAvailableSearch(value: string) {
console.log(value); this.dispatchCustomEvent("ak-dual-select-search", value);
} }
handleSelectedSearch(value: string) { handleSelectedSearch(value: string) {

View File

@ -16,7 +16,7 @@ export type DataProvision = {
options: DualSelectPair[]; options: DualSelectPair[];
}; };
export type DataProvider = (page: number) => Promise<DataProvision>; export type DataProvider = (page: number, search?: string) => Promise<DataProvision>;
export interface SearchbarEvent extends CustomEvent { export interface SearchbarEvent extends CustomEvent {
detail: { detail: {