import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { groupBy } from "@goauthentik/common/utils"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/chips/Chip"; import "@goauthentik/elements/chips/ChipGroup"; import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; import "@goauthentik/elements/table/TablePagination"; import { Pagination } from "@goauthentik/elements/table/TablePagination"; import "@goauthentik/elements/table/TableSearch"; import { t } from "@lingui/macro"; import { CSSResult, TemplateResult, css, html } from "lit"; import { property, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import AKGlobal from "@goauthentik/common/styles/authentik.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css"; import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css"; import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css"; import PFTable from "@patternfly/patternfly/components/Table/table.css"; import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css"; import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; export class TableColumn { title: string; orderBy?: string; onClick?: () => void; constructor(title: string, orderBy?: string) { this.title = title; this.orderBy = orderBy; } headerClickHandler(table: Table): void { if (!this.orderBy) { return; } if (table.order === this.orderBy) { table.order = `-${this.orderBy}`; } else { table.order = this.orderBy; } table.fetch(); } private getSortIndicator(table: Table): string { switch (table.order) { case this.orderBy: return "fa-long-arrow-alt-down"; case `-${this.orderBy}`: return "fa-long-arrow-alt-up"; default: return "fa-arrows-alt-v"; } } renderSortable(table: Table): TemplateResult { return html` `; } render(table: Table): TemplateResult { return html` ${this.orderBy ? this.renderSortable(table) : html`${this.title}`} `; } } export interface PaginatedResponse { pagination: Pagination; results: Array; } export abstract class Table extends AKElement { abstract apiEndpoint(page: number): Promise>; abstract columns(): TableColumn[]; abstract row(item: T): TemplateResult[]; private isLoading = false; searchEnabled(): boolean { return false; } // eslint-disable-next-line @typescript-eslint/no-unused-vars renderExpanded(item: T): TemplateResult { if (this.expandable) { throw new Error("Expandable is enabled but renderExpanded is not overridden!"); } return html``; } @property({ attribute: false }) data?: PaginatedResponse; @property({ type: Number }) page = getURLParam("page", 1); @property({ type: String }) order?: string; @property({ type: String }) search: string = getURLParam("search", ""); @property({ type: Boolean }) checkbox = false; @property({ type: Boolean }) checkboxChip = false; @property({ attribute: false }) selectedElements: T[] = []; @property({ type: Boolean }) paginated = true; @property({ type: Boolean }) expandable = false; @property({ attribute: false }) expandedElements: T[] = []; @state() hasError?: Error; static get styles(): CSSResult[] { return [ PFBase, PFTable, PFBullseye, PFButton, PFSwitch, PFToolbar, PFDropdown, PFPagination, AKGlobal, css` .pf-c-table thead .pf-c-table__check { min-width: 3rem; } .pf-c-table tbody .pf-c-table__check input { margin-top: calc(var(--pf-c-table__check--input--MarginTop) + 1px); } `, ]; } constructor() { super(); this.addEventListener(EVENT_REFRESH, () => { this.fetch(); }); } public groupBy(items: T[]): [string, T[]][] { return groupBy(items, () => { return ""; }); } public async fetch(): Promise { if (this.isLoading) { return; } this.isLoading = true; try { this.data = await this.apiEndpoint(this.page); this.hasError = undefined; this.page = this.data.pagination.current; const newSelected: T[] = []; const newExpanded: T[] = []; this.data.results.forEach((res) => { const jsonRes = JSON.stringify(res); // So because we're dealing with complex objects here, we can't use indexOf // since it checks strict equality, and we also can't easily check in findIndex() // Instead we default to comparing the JSON of both objects, which is quite slow // Hence we check if the objects have `pk` attributes set (as most models do) // and compare that instead, which will be much faster. let comp = (item: T) => { return JSON.stringify(item) === jsonRes; }; if (Object.hasOwn(res as object, "pk")) { comp = (item: T) => { return ( (item as unknown as { pk: string | number }).pk === (res as unknown as { pk: string | number }).pk ); }; } const selectedIndex = this.selectedElements.findIndex(comp); if (selectedIndex > -1) { newSelected.push(res); } const expandedIndex = this.expandedElements.findIndex(comp); if (expandedIndex > -1) { newExpanded.push(res); } }); this.isLoading = false; this.selectedElements = newSelected; this.expandedElements = newExpanded; } catch (ex) { this.isLoading = false; this.hasError = ex as Error; } } private renderLoading(): TemplateResult { return html`
`; } renderEmpty(inner?: TemplateResult): TemplateResult { return html`
${inner ? inner : html``}
`; } renderError(): TemplateResult { return html`
${this.hasError?.toString()}
`; } private renderRows(): TemplateResult[] | undefined { if (this.hasError) { return [this.renderEmpty(this.renderError())]; } if (!this.data) { return; } if (this.data.pagination.count === 0) { return [this.renderEmpty()]; } const groupedResults = this.groupBy(this.data.results); if (groupedResults.length === 1) { return this.renderRowGroup(groupedResults[0][1]); } return groupedResults.map(([group, items]) => { return html` ${group} ${this.renderRowGroup(items)}`; }); } private renderRowGroup(items: T[]): TemplateResult[] { return items.map((item) => { return html` ${this.checkbox ? html` ` : html``} ${this.expandable ? html` ` : html``} ${this.row(item).map((col) => { return html`${col}`; })} ${this.expandedElements.indexOf(item) > -1 ? this.renderExpanded(item) : html``} `; }); } renderToolbar(): TemplateResult { return html` { return this.fetch(); }} class="pf-m-secondary" > ${t`Refresh`}`; } renderToolbarSelected(): TemplateResult { return html``; } renderToolbarAfter(): TemplateResult { return html``; } renderSearch(): TemplateResult { if (!this.searchEnabled()) { return html``; } return html` { this.search = value; this.fetch(); updateURLParams({ search: value, }); }} > `; } // eslint-disable-next-line @typescript-eslint/no-unused-vars renderSelectedChip(item: T): TemplateResult { return html``; } renderToolbarContainer(): TemplateResult { return html`
${this.renderSearch()}
${this.renderToolbar()}
${this.renderToolbarAfter()}
${this.renderToolbarSelected()}
${this.paginated ? html` { this.page = page; updateURLParams({ page: page }); this.fetch(); }} > ` : html``}
`; } firstUpdated(): void { this.fetch(); } renderTable(): TemplateResult { return html` ${this.checkbox && this.checkboxChip ? html` ${this.selectedElements.map((el) => { return html`${this.renderSelectedChip(el)}`; })} ` : html``} ${this.renderToolbarContainer()} ${this.checkbox ? html`` : html``} ${this.expandable ? html`` : html``} ${this.columns().map((col) => col.render(this))} ${this.isLoading || !this.data ? this.renderLoading() : this.renderRows()}
0} @input=${(ev: InputEvent) => { if ((ev.target as HTMLInputElement).checked) { this.selectedElements = this.data?.results.slice(0) || []; } else { this.selectedElements = []; } }} />
${this.paginated ? html`
{ this.page = page; this.fetch(); }} >
` : html``}`; } render(): TemplateResult { return this.renderTable(); } }