web: admin interface: faster card load (#13331)

* wip

* wip

* try to make this work with ken's writeup

Signed-off-by: Dominic R <dominic@sdko.org>

* wip

---------

Signed-off-by: Dominic R <dominic@sdko.org>
This commit is contained in:
Dominic R
2025-03-10 11:34:51 -04:00
committed by GitHub
parent 985d491073
commit ef4d532b9c

View File

@ -3,8 +3,8 @@ import { PFSize } from "@goauthentik/common/enums.js";
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { until } from "lit/directives/until.js";
import { PropertyValues, TemplateResult, html, nothing } from "lit";
import { state } from "lit/decorators.js";
import { ResponseError } from "@goauthentik/api";
@ -13,46 +13,142 @@ export interface AdminStatus {
message?: TemplateResult;
}
/**
* Abstract base class for admin status cards with robust state management
*
* @template T - Type of the primary data value used in the card
*/
export abstract class AdminStatusCard<T> extends AggregateCard {
abstract getPrimaryValue(): Promise<T>;
abstract getStatus(value: T): Promise<AdminStatus>;
// Current data value state
@state()
value?: T;
// Current status state derived from value
@state()
protected status?: AdminStatus;
// Current error state if any request fails
@state()
protected error?: string;
// Abstract methods to be implemented by subclasses
abstract getPrimaryValue(): Promise<T>;
abstract getStatus(value: T): Promise<AdminStatus>;
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
this.requestUpdate();
});
// Proper binding for event handler
this.fetchData = this.fetchData.bind(this);
// Register refresh event listener
this.addEventListener(EVENT_REFRESH, this.fetchData);
}
renderValue(): TemplateResult {
// Lifecycle method: Called when component is added to DOM
connectedCallback(): void {
super.connectedCallback();
// Initial data fetch
this.fetchData();
}
/**
* Fetch primary data and handle errors
*/
private fetchData() {
this.getPrimaryValue()
.then((value: T) => {
this.value = value; // Triggers shouldUpdate
this.error = undefined;
})
.catch((err: ResponseError) => {
this.status = undefined;
this.error = err?.response?.statusText ?? msg("Unknown error");
});
}
/**
* Lit lifecycle method: Determine if component should update
*
* @param changed - Map of changed properties
* @returns boolean indicating if update should proceed
*/
shouldUpdate(changed: PropertyValues<this>) {
if (changed.has("value") && this.value !== undefined) {
// When value changes, fetch new status
this.getStatus(this.value)
.then((status) => {
this.status = status;
this.error = undefined;
})
.catch((err: ResponseError) => {
this.status = undefined;
this.error = err?.response?.statusText ?? msg("Unknown error");
});
// Prevent immediate re-render if only value changed
if (changed.size === 1) return false;
}
return true;
}
/**
* Render the primary value display
*
* @returns TemplateResult displaying the value
*/
protected renderValue(): TemplateResult {
return html`${this.value}`;
}
/**
* Render status state
*
* @param status - AdminStatus object containing icon and message
* @returns TemplateResult for status display
*/
private renderStatus(status: AdminStatus): TemplateResult {
return html`
<p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p>
${status.message ? html`<p class="subtext">${status.message}</p>` : nothing}
`;
}
/**
* Render error state
*
* @param error - Error message to display
* @returns TemplateResult for error display
*/
private renderError(error: string): TemplateResult {
return html`
<p><i class="fa fa-times"></i>&nbsp;${error}</p>
<p class="subtext">${msg("Failed to fetch")}</p>
`;
}
/**
* Render loading state
*
* @returns TemplateResult for loading spinner
*/
private renderLoading(): TemplateResult {
return html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`;
}
/**
* Main render method that selects appropriate state display
*
* @returns TemplateResult for current component state
*/
renderInner(): TemplateResult {
return html`<p class="center-value">
${until(
this.getPrimaryValue()
.then((v) => {
this.value = v;
return this.getStatus(v);
})
.then((status) => {
return html`<p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p>
${status.message
? html`<p class="subtext">${status.message}</p>`
: html``}`;
})
.catch((exc: ResponseError) => {
return html` <p>
<i class="fa fa-times"></i>&nbsp;${exc.response.statusText}
</p>
<p class="subtext">${msg("Failed to fetch")}</p>`;
}),
html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`,
)}
</p>`;
return html`
<p class="center-value">
${this.status
? this.renderStatus(this.status) // Status available
: this.error
? this.renderError(this.error) // Error state
: this.renderLoading()}
// Loading state
</p>
`;
}
}