diff --git a/web/src/admin/admin-overview/cards/AdminStatusCard.ts b/web/src/admin/admin-overview/cards/AdminStatusCard.ts index ef9f9882d2..ed2c494009 100644 --- a/web/src/admin/admin-overview/cards/AdminStatusCard.ts +++ b/web/src/admin/admin-overview/cards/AdminStatusCard.ts @@ -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 extends AggregateCard { - abstract getPrimaryValue(): Promise; - - abstract getStatus(value: T): Promise; - + // 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; + abstract getStatus(value: T): Promise; + 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) { + 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` +

 ${this.renderValue()}

+ ${status.message ? html`

${status.message}

` : nothing} + `; + } + + /** + * Render error state + * + * @param error - Error message to display + * @returns TemplateResult for error display + */ + private renderError(error: string): TemplateResult { + return html` +

 ${error}

+

${msg("Failed to fetch")}

+ `; + } + + /** + * Render loading state + * + * @returns TemplateResult for loading spinner + */ + private renderLoading(): TemplateResult { + return html``; + } + + /** + * Main render method that selects appropriate state display + * + * @returns TemplateResult for current component state + */ renderInner(): TemplateResult { - return html`

- ${until( - this.getPrimaryValue() - .then((v) => { - this.value = v; - return this.getStatus(v); - }) - .then((status) => { - return html`

 ${this.renderValue()}

- ${status.message - ? html`

${status.message}

` - : html``}`; - }) - .catch((exc: ResponseError) => { - return html`

-  ${exc.response.statusText} -

-

${msg("Failed to fetch")}

`; - }), - html``, - )} -

`; + return html` +

+ ${this.status + ? this.renderStatus(this.status) // Status available + : this.error + ? this.renderError(this.error) // Error state + : this.renderLoading()} + // Loading state +

+ `; } }