web: provide better feedback on Application Library page about search results (#9386)

* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: improve state management of Fuze application search

This commit rewrites a bit (just a bit, really!) of the relationship between
`ak-library-application-impl` and `ak-library-application-search`.

The "show only apps with launch URLs filter" has been moved up to the retrieval layer; there was no
reason for the renderer to repeatedly call a *required* filter; just call it on the list of
applications once and be done.

The search component exchanges the two-state guesswork and custom events for a concrete three-state
solution and *private* events. The search handler now sends the events "reset," "updated," and the
new "updated and empty," which we could not previously track.

By limiting the Impl layer to only those apps with launchUrls, we can now distinguish between "all
apps," and "filtered apps," and understand that when "all apps" is empty we have no apps, and when
"filtered apps" is empty the search has returned nothing.

I also tried to add a lot more comments.

In keeping with ES2020, I've put `.js` extensions on all the local imports.

In keeping with a variety of [best practice
recommendations](https://webcomponents.today/best-practices/), I've renamed web component files to
match the custom element they deploy:

```
ak-library-application-search-empty.ts
19:@customElement("ak-library-application-search-empty")

ak-library-impl.ts
44:@customElement("ak-library-impl")

ak-library.ts
30:@customElement("ak-library")

ak-library-application-list.ts
34:@customElement("ak-library-application-list")

ak-library-application-empty-list.ts
22:@customElement("ak-library-application-empty-list")

ak-library-application-search.ts
46:@customElement("ak-library-application-search")
```

The only effect(s) external to the changes in this vertical is that the Route() had to be updated,
and I have done that.

* web: updated the improved search to Google's Lit standards for events.
This commit is contained in:
Ken Sternberg
2024-06-26 15:11:49 -07:00
committed by GitHub
parent 0caa8cf0fa
commit eff85e489c
12 changed files with 336 additions and 199 deletions

View File

@ -0,0 +1,118 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { me } from "@goauthentik/common/users";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import { localized, msg } from "@lit/localize";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { Application, CoreApi } from "@goauthentik/api";
import "./ak-library-impl.js";
import type { PageUIConfig } from "./types.js";
/**
* List of Applications available
*
* Properties:
* apps: a list of the applications available to the user.
*
* Aggregates two functions:
* - Display the list of applications available to the user
* - Filter that list using the search bar
*
*/
const coreApi = () => new CoreApi(DEFAULT_CONFIG);
@localized()
@customElement("ak-library")
export class LibraryPage extends AKElement {
@state()
ready = false;
@state()
isAdmin = false;
/**
* The list of applications. This is the *complete* list; the constructor fetches as many pages
* as the server announces when page one is accessed, and then concatenates them all together.
*/
@state()
apps: Application[] = [];
@state()
uiConfig: PageUIConfig;
constructor() {
super();
const uiConfig = rootInterface()?.uiConfig;
if (!uiConfig) {
throw new Error("Could not retrieve uiConfig. Reason: unknown. Check logs.");
}
this.uiConfig = {
layout: uiConfig.layout.type,
background: uiConfig.theme.cardBackground,
searchEnabled: uiConfig.enabledFeatures.search,
};
Promise.all([this.fetchApplications(), me()]).then(([applications, meStatus]) => {
this.isAdmin = meStatus.user.isSuperuser;
this.apps = applications;
this.ready = true;
});
}
async fetchApplications(): Promise<Application[]> {
const applicationListParams = (page = 1) => ({
ordering: "name",
page,
pageSize: 100,
});
const applicationListFetch = await coreApi().coreApplicationsList(applicationListParams(1));
const pageCount = applicationListFetch.pagination.totalPages;
if (pageCount === 1) {
return applicationListFetch.results;
}
const applicationLaterPages = await Promise.allSettled(
Array.from({ length: pageCount - 1 }).map((_a, idx) =>
coreApi().coreApplicationsList(applicationListParams(idx + 2)),
),
);
return applicationLaterPages.reduce(
function (acc, result) {
if (result.status === "rejected") {
const reason = JSON.stringify(result.reason, null, 2);
throw new Error(`Could not retrieve list of applications. Reason: ${reason}`);
}
return [...acc, ...result.value.results];
},
[...applicationListFetch.results],
);
}
pageTitle(): string {
return msg("My Applications");
}
loading() {
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}> </ak-empty-state>`;
}
running() {
return html`<ak-library-impl
?isadmin=${this.isAdmin}
.apps=${this.apps}
.uiConfig=${this.uiConfig}
></ak-library-impl>`;
}
render() {
return this.ready ? this.running() : this.loading();
}
}