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 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) {

View file

@ -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<this>) {
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;

View file

@ -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) {

View file

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