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
+
+ `;
}
}