Files
authentik/web/src/admin/admin-overview/cards/AdminStatusCard.ts
Teffen Ellis 363d655378 web: Normalize client-side error handling (#13595)
web: Clean up error handling. Prep for permission checks.

- Add clearer reporting for API and network errors.
- Tidy error checking.
- Partial type safety for events.
2025-04-07 19:50:41 +02:00

159 lines
4.7 KiB
TypeScript

import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { PFSize } from "@goauthentik/common/enums.js";
import {
APIError,
parseAPIResponseError,
pluckErrorDetail,
} from "@goauthentik/common/errors/network";
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
import { msg } from "@lit/localize";
import { PropertyValues, TemplateResult, html, nothing } from "lit";
import { state } from "lit/decorators.js";
export interface AdminStatus {
icon: string;
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 {
// 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?: APIError;
// Abstract methods to be implemented by subclasses
abstract getPrimaryValue(): Promise<T>;
abstract getStatus(value: T): Promise<AdminStatus>;
constructor() {
super();
// Proper binding for event handler
this.fetchData = this.fetchData.bind(this);
// Register refresh event listener
this.addEventListener(EVENT_REFRESH, this.fetchData);
}
// 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(async (error: unknown) => {
this.status = undefined;
this.error = await parseAPIResponseError(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(async (error: unknown) => {
this.status = undefined;
this.error = await parseAPIResponseError(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;${msg("Failed to fetch")}</p>
<p class="subtext">${error}</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">
${
this.status
? this.renderStatus(this.status) // Status available
: this.error
? this.renderError(pluckErrorDetail(this.error)) // Error state
: this.renderLoading() // Loading state
}
</p>
`;
}
}