web: fix application library list display length and capability (#9094)
* 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: fix application display length and capability The User Application Library only shows the top 100 applications. This patch strips what is passed out of the API fetch down to the bare minimum: the list of applications. No pagination, no search strings, none of the items returned by the API other than the application. It then fetches multiple pages of 100 until the user's Application list is exhausted, presenting the entire list to the user. The fetches are done simultaneously; a user with a thousand applications, if one should exist, would start 9 downloads in parallel. The first fetch analyzes the page count to determine how many *more* must be started, then starts them. This should make an interesting stress-test. Failures at the Django end are not well-handled, but then they have never been well-handled. At best, the page is blank and the browser console will contain a cryptic error message. That isn't fixed this time around, but it probably should be. This patch will have no effect until the [application pagination bug](https://github.com/goauthentik/authentik/issues/9093) is fixed. * Prettier has opinions. * attempt to fix backend pagination Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make page_number optional Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -22,6 +22,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.admin.api.metrics import CoordinateSerializer
|
||||
from authentik.api.pagination import Pagination
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
@ -43,9 +44,12 @@ from authentik.rbac.filters import ObjectFilter
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def user_app_cache_key(user_pk: str) -> str:
|
||||
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
||||
"""Cache key where application list for user is saved"""
|
||||
return f"{CACHE_PREFIX}/app_access/{user_pk}"
|
||||
key = f"{CACHE_PREFIX}/app_access/{user_pk}"
|
||||
if page_number:
|
||||
key += f"/{page_number}"
|
||||
return key
|
||||
|
||||
|
||||
class ApplicationSerializer(ModelSerializer):
|
||||
@ -213,7 +217,8 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
return super().list(request)
|
||||
|
||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
paginated_apps = self.paginate_queryset(queryset)
|
||||
paginator: Pagination = self.paginator
|
||||
paginated_apps = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
if "for_user" in request.query_params:
|
||||
try:
|
||||
@ -235,12 +240,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
if not should_cache:
|
||||
allowed_applications = self._get_allowed_applications(paginated_apps)
|
||||
if should_cache:
|
||||
allowed_applications = cache.get(user_app_cache_key(self.request.user.pk))
|
||||
allowed_applications = cache.get(
|
||||
user_app_cache_key(self.request.user.pk, paginator.page.number)
|
||||
)
|
||||
if not allowed_applications:
|
||||
LOGGER.debug("Caching allowed application list")
|
||||
LOGGER.debug("Caching allowed application list", page=paginator.page.number)
|
||||
allowed_applications = self._get_allowed_applications(paginated_apps)
|
||||
cache.set(
|
||||
user_app_cache_key(self.request.user.pk),
|
||||
user_app_cache_key(self.request.user.pk, paginator.page.number),
|
||||
allowed_applications,
|
||||
timeout=86400,
|
||||
)
|
||||
|
@ -2,7 +2,6 @@ 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 { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
|
||||
import { localized, msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
@ -25,6 +24,8 @@ import type { PageUIConfig } from "./types";
|
||||
*
|
||||
*/
|
||||
|
||||
const coreApi = () => new CoreApi(DEFAULT_CONFIG);
|
||||
|
||||
@localized()
|
||||
@customElement("ak-library")
|
||||
export class LibraryPage extends AKElement {
|
||||
@ -35,15 +36,13 @@ export class LibraryPage extends AKElement {
|
||||
isAdmin = false;
|
||||
|
||||
@state()
|
||||
apps!: PaginatedResponse<Application>;
|
||||
apps: Application[] = [];
|
||||
|
||||
@state()
|
||||
uiConfig: PageUIConfig;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const applicationListFetch = new CoreApi(DEFAULT_CONFIG).coreApplicationsList({});
|
||||
const meFetch = me();
|
||||
const uiConfig = rootInterface()?.uiConfig;
|
||||
if (!uiConfig) {
|
||||
throw new Error("Could not retrieve uiConfig. Reason: unknown. Check logs.");
|
||||
@ -55,22 +54,41 @@ export class LibraryPage extends AKElement {
|
||||
searchEnabled: uiConfig.enabledFeatures.search,
|
||||
};
|
||||
|
||||
Promise.allSettled([applicationListFetch, meFetch]).then(
|
||||
([applicationListStatus, meStatus]) => {
|
||||
if (meStatus.status === "rejected") {
|
||||
throw new Error(
|
||||
`Could not determine status of user. Reason: ${meStatus.reason}`,
|
||||
);
|
||||
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}`);
|
||||
}
|
||||
if (applicationListStatus.status === "rejected") {
|
||||
throw new Error(
|
||||
`Could not retrieve list of applications. Reason: ${applicationListStatus.reason}`,
|
||||
);
|
||||
}
|
||||
this.isAdmin = meStatus.value.user.isSuperuser;
|
||||
this.apps = applicationListStatus.value;
|
||||
this.ready = true;
|
||||
return [...acc, ...result.value.results];
|
||||
},
|
||||
[...applicationListFetch.results],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import "@goauthentik/user/LibraryApplication";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@ -42,8 +41,8 @@ export class LibraryPage extends AKElement {
|
||||
@property({ attribute: "isadmin", type: Boolean })
|
||||
isAdmin = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
apps!: PaginatedResponse<Application>;
|
||||
@property({ attribute: false, type: Array })
|
||||
apps!: Application[];
|
||||
|
||||
@property({ attribute: false })
|
||||
uiConfig!: PageUIConfig;
|
||||
@ -66,7 +65,7 @@ export class LibraryPage extends AKElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.filteredApps = this.apps?.results;
|
||||
this.filteredApps = this.apps;
|
||||
if (this.filteredApps === undefined) {
|
||||
throw new Error(
|
||||
"Application.results should never be undefined when passed to the Library Page.",
|
||||
@ -89,7 +88,7 @@ export class LibraryPage extends AKElement {
|
||||
event.stopPropagation();
|
||||
const apps = event.detail.apps;
|
||||
this.selectedApp = undefined;
|
||||
this.filteredApps = this.apps.results;
|
||||
this.filteredApps = this.apps;
|
||||
if (apps.length > 0) {
|
||||
this.selectedApp = apps[0];
|
||||
this.filteredApps = event.detail.apps;
|
||||
@ -132,7 +131,7 @@ export class LibraryPage extends AKElement {
|
||||
}
|
||||
|
||||
renderSearch() {
|
||||
return html`<ak-library-list-search .apps=${this.apps.results}></ak-library-list-search>`;
|
||||
return html`<ak-library-list-search .apps=${this.apps}></ak-library-list-search>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
Reference in New Issue
Block a user