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:
parent
51e89ce916
commit
10999630e5
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
Reference in New Issue