web: Table parity (#427)
* core: fix application API always being sorted by name * web: add sorting to tables * web: add search to TablePage * core: add search to applications API * core: add MetaNameSerializer * *: fix signature for non-modal serializers * providers/*: implement MetaNameSerializer * web: implement full app list page, use as default in sidebar * web: fix linting errors * admin: remove old application list * web: fix default sorting for application list * web: fix spacing for search element in toolbar
This commit is contained in:
@ -1,10 +1,11 @@
|
||||
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
|
||||
import { Provider } from "./Providers";
|
||||
|
||||
export class Application {
|
||||
pk: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
provider: number;
|
||||
provider: Provider;
|
||||
|
||||
launch_url: string;
|
||||
meta_launch_url: string;
|
||||
@ -24,4 +25,8 @@ export class Application {
|
||||
static list(filter?: QueryArguments): Promise<PBResponse<Application>> {
|
||||
return DefaultClient.fetch<PBResponse<Application>>(["core", "applications"], filter);
|
||||
}
|
||||
|
||||
static adminUrl(rest: string): string {
|
||||
return `/administration/applications/${rest}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ export class Provider {
|
||||
pk: number;
|
||||
name: string;
|
||||
authorization_flow: string;
|
||||
verbose_name: string;
|
||||
verbose_name_plural: string;
|
||||
|
||||
constructor() {
|
||||
throw Error();
|
||||
|
||||
@ -136,6 +136,12 @@ select[multiple] {
|
||||
--pf-c-table--BorderColor: var(--ak-dark-background-lighter);
|
||||
--pf-c-table--cell--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-table__text {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
.pf-c-table__sort-indicator i {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
/* class for pagination text */
|
||||
.pf-c-options-menu__toggle {
|
||||
color: var(--ak-dark-foreground);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gettext } from "django";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { PBResponse } from "../../api/Client";
|
||||
import { Table } from "../../elements/table/Table";
|
||||
import { Table, TableColumn } from "../../elements/table/Table";
|
||||
import { PolicyBinding } from "../../api/PolicyBindings";
|
||||
|
||||
import "../../elements/Tabs";
|
||||
@ -22,8 +22,14 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||
});
|
||||
}
|
||||
|
||||
columns(): string[] {
|
||||
return ["Policy", "Enabled", "Order", "Timeout", ""];
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn("Policy"),
|
||||
new TableColumn("Enabled", "enabled"),
|
||||
new TableColumn("Order", "order"),
|
||||
new TableColumn("Timeout", "timeout"),
|
||||
new TableColumn(""),
|
||||
];
|
||||
}
|
||||
|
||||
row(item: PolicyBinding): TemplateResult[] {
|
||||
|
||||
@ -6,10 +6,72 @@ import { COMMON_STYLES } from "../../common/styles";
|
||||
import "./TablePagination";
|
||||
import "../EmptyState";
|
||||
|
||||
|
||||
export class TableColumn {
|
||||
|
||||
title: string;
|
||||
orderBy?: string;
|
||||
|
||||
onClick?: () => void;
|
||||
|
||||
constructor(title: string, orderBy?: string) {
|
||||
this.title = title;
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
headerClickHandler(table: Table<unknown>): 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<unknown>): 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<unknown>): TemplateResult {
|
||||
return html`
|
||||
<button class="pf-c-table__button" @click=${() => this.headerClickHandler(table)}>
|
||||
<div class="pf-c-table__button-content">
|
||||
<span class="pf-c-table__text">${gettext(this.title)}</span>
|
||||
<span class="pf-c-table__sort-indicator">
|
||||
<i class="fas ${this.getSortIndicator(table)}"></i>
|
||||
</span>
|
||||
</div>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
render(table: Table<unknown>): TemplateResult {
|
||||
return html`<th
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
class="
|
||||
${this.orderBy ? "pf-c-table__sort " : " "}
|
||||
${(table.order === this.orderBy || table.order === `-${this.orderBy}`) ? "pf-m-selected " : ""}
|
||||
">
|
||||
${this.orderBy ? this.renderSortable(table) : html`${gettext(this.title)}`}
|
||||
</th>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class Table<T> extends LitElement {
|
||||
abstract apiEndpoint(page: number): Promise<PBResponse<T>>;
|
||||
abstract columns(): Array<string>;
|
||||
abstract row(item: T): Array<TemplateResult>;
|
||||
abstract columns(): TableColumn[];
|
||||
abstract row(item: T): TemplateResult[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
renderExpanded(item: T): TemplateResult {
|
||||
@ -25,6 +87,12 @@ export abstract class Table<T> extends LitElement {
|
||||
@property({type: Number})
|
||||
page = 1;
|
||||
|
||||
@property({type: String})
|
||||
order?: string;
|
||||
|
||||
@property({type: String})
|
||||
search?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
expandable = false;
|
||||
|
||||
@ -43,6 +111,7 @@ export abstract class Table<T> extends LitElement {
|
||||
}
|
||||
|
||||
public fetch(): void {
|
||||
this.data = undefined;
|
||||
this.apiEndpoint(this.page).then((r) => {
|
||||
this.data = r;
|
||||
this.page = r.pagination.current;
|
||||
@ -123,12 +192,17 @@ export abstract class Table<T> extends LitElement {
|
||||
</button>`;
|
||||
}
|
||||
|
||||
renderSearch(): TemplateResult {
|
||||
return html``;
|
||||
}
|
||||
|
||||
renderTable(): TemplateResult {
|
||||
if (!this.data) {
|
||||
this.fetch();
|
||||
}
|
||||
return html`<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
${this.renderSearch()}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
${this.renderToolbar()}
|
||||
</div>
|
||||
@ -143,7 +217,7 @@ export abstract class Table<T> extends LitElement {
|
||||
<thead>
|
||||
<tr role="row">
|
||||
${this.expandable ? html`<td role="cell">` : html``}
|
||||
${this.columns().map((col) => html`<th role="columnheader" scope="col">${gettext(col)}</th>`)}
|
||||
${this.columns().map((col) => col.render(this))}
|
||||
</tr>
|
||||
</thead>
|
||||
${this.data ? this.renderRows() : this.renderLoading()}
|
||||
|
||||
@ -1,19 +1,34 @@
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { Table } from "./Table";
|
||||
import "./TableSearch";
|
||||
|
||||
export abstract class TablePage<T> extends Table<T> {
|
||||
abstract pageTitle(): string;
|
||||
abstract pageDescription(): string;
|
||||
abstract pageDescription(): string | undefined;
|
||||
abstract pageIcon(): string;
|
||||
abstract searchEnabled(): boolean;
|
||||
|
||||
renderSearch(): TemplateResult {
|
||||
if (!this.searchEnabled()) {
|
||||
return super.renderSearch();
|
||||
}
|
||||
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
|
||||
this.search = value;
|
||||
this.fetch();
|
||||
}}>
|
||||
</ak-table-search>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const description = this.pageDescription();
|
||||
return html`<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="${this.pageIcon()}"></i>
|
||||
${this.pageTitle()}
|
||||
</h1>
|
||||
<p>${this.pageDescription()}</p>
|
||||
${description ? html`<p>${description}</p>` : html``}
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
|
||||
41
web/src/elements/table/TableSearch.ts
Normal file
41
web/src/elements/table/TableSearch.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { COMMON_STYLES } from "../../common/styles";
|
||||
|
||||
@customElement("ak-table-search")
|
||||
export class TableSearch extends LitElement {
|
||||
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
@property()
|
||||
onSearch?: (value: string) => void;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return COMMON_STYLES;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-toolbar__group pf-m-filter-group">
|
||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||
<form class="pf-c-input-group" method="GET" @submit=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.onSearch) return;
|
||||
const el = this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]");
|
||||
if (!el) return;
|
||||
if (el.value === "") return;
|
||||
this.onSearch(el?.value);
|
||||
}}>
|
||||
<input class="pf-c-form-control" name="search" type="search" placeholder="Search..." value="${ifDefined(this.value)}" @search=${() => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch("");
|
||||
}}>
|
||||
<button class="pf-c-button pf-m-control" type="submit">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -14,7 +14,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
|
||||
return User.me().then(u => u.is_superuser);
|
||||
}),
|
||||
new SidebarItem("Administration").children(
|
||||
new SidebarItem("Applications", "/administration/applications/").activeWhen(
|
||||
new SidebarItem("Applications", "/applications/").activeWhen(
|
||||
`^/applications/(?<slug>${SLUG_REGEX})/$`
|
||||
),
|
||||
new SidebarItem("Sources", "/administration/sources/").activeWhen(
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import { gettext } from "django";
|
||||
import { customElement, html, TemplateResult } from "lit-element";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { Application } from "../../api/Applications";
|
||||
import { PBResponse } from "../../api/Client";
|
||||
import { TablePage } from "../../elements/table/TablePage";
|
||||
|
||||
import "../../elements/buttons/ModalButton";
|
||||
import "../../elements/buttons/SpinnerButton";
|
||||
import { TableColumn } from "../../elements/table/Table";
|
||||
|
||||
@customElement("ak-application-list")
|
||||
export class ApplicationList extends TablePage<Application> {
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
pageTitle(): string {
|
||||
return gettext("Applications");
|
||||
}
|
||||
@ -19,31 +23,51 @@ export class ApplicationList extends TablePage<Application> {
|
||||
return gettext("pf-icon pf-icon-applications");
|
||||
}
|
||||
|
||||
@property()
|
||||
order = "name";
|
||||
|
||||
apiEndpoint(page: number): Promise<PBResponse<Application>> {
|
||||
return Application.list({
|
||||
ordering: "order",
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
search: this.search || "",
|
||||
});
|
||||
}
|
||||
|
||||
columns(): string[] {
|
||||
return ["Name", "Slug", "Provider", "Provider Type", ""];
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(""),
|
||||
new TableColumn("Name", "name"),
|
||||
new TableColumn("Slug", "slug"),
|
||||
new TableColumn("Provider"),
|
||||
new TableColumn("Provider Type"),
|
||||
new TableColumn(""),
|
||||
];
|
||||
}
|
||||
|
||||
row(item: Application): TemplateResult[] {
|
||||
return [
|
||||
html`${item.name}`,
|
||||
html`${item.slug}`,
|
||||
html`${item.provider}`,
|
||||
html`${item.provider}`,
|
||||
html`
|
||||
<ak-modal-button href="administration/policies/bindings/${item.pk}/update/">
|
||||
${item.meta_icon ?
|
||||
html`<img class="app-icon pf-c-avatar" src="${item.meta_icon}" alt="${gettext("Application Icon")}">` :
|
||||
html`<i class="pf-icon pf-icon-arrow"></i>`}`,
|
||||
html`<a href="#/applications/${item.slug}/">
|
||||
<div>
|
||||
${item.name}
|
||||
</div>
|
||||
${item.meta_publisher ? html`<small>${item.meta_publisher}</small>` : html``}
|
||||
</a>`,
|
||||
html`<code>${item.slug}</code>`,
|
||||
html`${item.provider.name}`,
|
||||
html`${item.provider.verbose_name}`,
|
||||
html`
|
||||
<ak-modal-button href="${Application.adminUrl(`${item.pk}/update/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
Edit
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="administration/policies/bindings/${item.pk}/delete/">
|
||||
<ak-modal-button href="${Application.adminUrl(`${item.pk}/delete/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
Delete
|
||||
</ak-spinner-button>
|
||||
@ -52,4 +76,16 @@ export class ApplicationList extends TablePage<Application> {
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`
|
||||
<ak-modal-button href=${Application.adminUrl("create/")}>
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
${gettext("Create")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
${super.renderToolbar()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gettext } from "django";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { PBResponse } from "../../api/Client";
|
||||
import { Table } from "../../elements/table/Table";
|
||||
import { Table, TableColumn } from "../../elements/table/Table";
|
||||
|
||||
import "../../elements/Tabs";
|
||||
import "../../elements/AdminLoginsChart";
|
||||
@ -25,8 +25,13 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
});
|
||||
}
|
||||
|
||||
columns(): string[] {
|
||||
return ["Order", "Name", "Type", ""];
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn("Order"),
|
||||
new TableColumn("Name"),
|
||||
new TableColumn("Type"),
|
||||
new TableColumn(""),
|
||||
];
|
||||
}
|
||||
|
||||
row(item: FlowStageBinding): TemplateResult[] {
|
||||
|
||||
Reference in New Issue
Block a user