web: provide tier-3 click capability.

This commit enables the search function to work when changing from one tier 3 link to another in
the tier 2 category (Providers, Events, Stages, etc.).  Because the router ignores such changes,
we must bring those changes to the attention of the receiver by hand.

This commit makes the recepient Table object locatable with a data attribute tag and a painstakingly
researched path to its most common location in our code.

It then checks any `anchor:click` event from the Sidebar to see if its likely to be elided
by the router.  If it is, we take over, identify the Table object, set or clear its search
state to the desired state, and call the `.fetch()` method on the Table to initiate a new
search with that state.

This is, frankly, _gross_.  The URL and internal states mirror the actual desired state more
or less by accident.  But it's what we've got to work with at the moment.
This commit is contained in:
Ken Sternberg 2023-11-27 15:48:24 -08:00
parent 5b898bef01
commit f2834cc7e2
3 changed files with 72 additions and 4 deletions

View file

@ -160,14 +160,14 @@ export class AkAdminSidebar extends AKElement {
[ [
reload, reload,
msg( msg(
str`You're currently impersonating ${this.impersonation}. Click to stop.` str`You're currently impersonating ${this.impersonation}. Click to stop.`,
), ),
], ],
] ]
: []; : [];
const enterpriseMenu: LocalSidebarEntry[] = this.config?.capabilities.includes( const enterpriseMenu: LocalSidebarEntry[] = this.config?.capabilities.includes(
CapabilitiesEnum.IsEnterprise CapabilitiesEnum.IsEnterprise,
) )
? [[null, msg("Enterprise"), null, [["/enterprise/licenses", msg("Licenses")]]]] ? [[null, msg("Enterprise"), null, [["/enterprise/licenses", msg("Licenses")]]]]
: []; : [];

View file

@ -1,4 +1,6 @@
import { ROUTE_SEPARATOR } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { findTable } from "@goauthentik/elements/table/TablePage";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
@ -113,6 +115,8 @@ export class SidebarItems extends AKElement {
super(); super();
this.renderItem = this.renderItem.bind(this); this.renderItem = this.renderItem.bind(this);
this.toggleExpand = this.toggleExpand.bind(this); this.toggleExpand = this.toggleExpand.bind(this);
this.onHashChange = this.onHashChange.bind(this);
this.reclick = this.reclick.bind(this);
} }
connectedCallback() { connectedCallback() {
@ -153,6 +157,46 @@ export class SidebarItems extends AKElement {
this.requestUpdate(); this.requestUpdate();
} }
// This is gross and feels like 2007: using a path from the root through the shadowDoms (see
// `TablePage:findTable()`), this code finds the element that *should* be triggered by an event
// on the URL, and forcibly injects the text of the search and the click of the search button.
reclick(ev: Event, path: string) {
const oldPath = window.location.hash.split(ROUTE_SEPARATOR)[0];
const [curPath, ...curSearchComponents] = path.split(ROUTE_SEPARATOR);
const curSearch: string =
curSearchComponents.length > 0 ? curSearchComponents.join(ROUTE_SEPARATOR) : "";
if (curPath !== oldPath) {
// A Tier 1 or Tier 2 change should be handled by the router. (So should a Tier 3
// change, but... here we are.)
return;
}
const table = findTable();
if (!table) {
return;
}
// Always wrap the minimal exceptional code possible in an IIFE and supply the failure
// alternative. Turn exceptions into expressions with the smallest functional rewind
// whenever possible.
const search = (() => {
try {
return curSearch ? JSON.parse(decodeURIComponent(curSearch)) : { search: "" };
} catch {
return { search: "" };
}
})();
if ("search" in search) {
ev.preventDefault();
ev.stopPropagation();
table.search = search.search;
table.fetch();
}
}
render(): TemplateResult { render(): TemplateResult {
const lightThemed = { "pf-m-light": this.activeTheme === UiThemeEnum.Light }; const lightThemed = { "pf-m-light": this.activeTheme === UiThemeEnum.Light };
@ -213,8 +257,10 @@ export class SidebarItems extends AKElement {
${entry.label} ${entry.label}
</a>`; </a>`;
} }
const path = `${entry.attributes?.isAbsoluteLink ? "" : "#"}${entry.path}`;
return html` <a return html` <a
href="${entry.attributes?.isAbsoluteLink ? "" : "#"}${entry.path}" href=${path}
@click=${(ev: Event) => this.reclick(ev, path)}
class=${classMap(this.getLinkClasses(entry))} class=${classMap(this.getLinkClasses(entry))}
> >
${entry.label} ${entry.label}
@ -246,9 +292,11 @@ export class SidebarItems extends AKElement {
renderLinkAndChildren(entry: SidebarEntry): TemplateResult { renderLinkAndChildren(entry: SidebarEntry): TemplateResult {
const handler = () => this.toggleExpand(entry); const handler = () => this.toggleExpand(entry);
const path = `${entry.attributes?.isAbsoluteLink ? "" : "#"}${entry.path}`;
return html` <div class="pf-c-nav__link"> return html` <div class="pf-c-nav__link">
<a <a
href="${entry.attributes?.isAbsoluteLink ? "" : "#"}${entry.path}" href=${path}
@click=${(ev: Event) => this.reclick(ev, path)}
class="ak-nav__link" class="ak-nav__link"
> >
${entry.label} ${entry.label}

View file

@ -20,6 +20,11 @@ export abstract class TablePage<T> extends Table<T> {
return super.styles.concat(PFPage, PFContent, PFSidebar); return super.styles.concat(PFPage, PFContent, PFSidebar);
} }
constructor() {
super();
this.dataset.akApiTable = "true";
}
renderSidebarBefore(): TemplateResult { renderSidebarBefore(): TemplateResult {
return html``; return html``;
} }
@ -92,3 +97,18 @@ export abstract class TablePage<T> extends Table<T> {
${this.renderSectionAfter()}`; ${this.renderSectionAfter()}`;
} }
} }
// This painstakingly researched path is nonetheless surprisingly robust; it works for every extant
// TablePage, but only because Jens has been utterly consistent in where he puts his TablePage
// elements with respect to the Interface object. If we ever re-arrange this code, we're going
// to have to re-arrange this as well.
export function findTable<T, U extends TablePage<T>>(): U | undefined {
return (
(document.body
?.querySelector("[data-ak-interface-root]")
?.shadowRoot?.querySelector("ak-locale-context")
?.querySelector("ak-router-outlet")
?.shadowRoot?.querySelector("[data-ak-api-table]") as U) ?? undefined
);
}