From 363d6553784cafab88b0dca87b965a3c08e8539f Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:50:41 +0200 Subject: [PATCH] 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. --- web/src/admin/DebugPage.ts | 7 +- .../admin-overview/cards/AdminStatusCard.ts | 23 ++- .../admin-overview/cards/RecentEventsCard.ts | 9 +- .../wizard/ApplicationWizardStep.ts | 8 +- .../ak-application-wizard-submit-step.ts | 100 ++++++---- ...k-application-wizard-provider-for-oauth.ts | 4 +- web/src/admin/applications/wizard/types.ts | 30 ++- web/src/admin/events/EventListPage.ts | 3 +- web/src/admin/events/utils.ts | 42 ++-- web/src/admin/flows/FlowImportForm.ts | 2 +- web/src/admin/flows/FlowViewPage.ts | 24 ++- web/src/admin/groups/RelatedUserList.ts | 28 ++- .../providers/saml/SAMLProviderImportForm.ts | 2 +- web/src/admin/stages/prompt/PromptForm.ts | 26 ++- web/src/admin/users/UserActiveForm.ts | 13 +- web/src/admin/users/UserDevicesTable.ts | 2 +- web/src/admin/users/UserListPage.ts | 16 +- web/src/common/errors.ts | 36 ---- web/src/common/errors/network.ts | 184 ++++++++++++++++++ web/src/common/events.ts | 24 ++- web/src/common/helpers/plex.ts | 2 +- web/src/common/sentry.ts | 6 +- web/src/common/users.ts | 135 ++++++++----- web/src/common/utils.ts | 2 +- web/src/components/ak-event-info.ts | 37 +++- web/src/components/ak-wizard/WizardStep.ts | 15 +- .../components/ak-wizard/ak-wizard-steps.ts | 4 +- web/src/components/ak-wizard/events.ts | 42 +++- web/src/components/events/ObjectChangelog.ts | 3 +- web/src/components/events/UserEvents.ts | 3 +- .../buttons/ActionButton/ak-action-button.ts | 2 +- .../TokenCopyButton/ak-token-copy-button.ts | 2 +- web/src/elements/charts/Chart.ts | 24 ++- web/src/elements/forms/ConfirmationForm.ts | 17 +- web/src/elements/forms/DeleteForm.ts | 21 +- web/src/elements/forms/Form.ts | 105 +++++----- web/src/elements/forms/ModalForm.ts | 5 +- .../forms/SearchSelect/SearchSelect.ts | 33 ++-- web/src/elements/messages/Message.ts | 5 +- web/src/elements/messages/MessageContainer.ts | 66 ++++++- web/src/elements/table/Table.ts | 164 +++++++++------- .../user/sources/SourceSettingsOAuth.ts | 9 +- .../user/sources/SourceSettingsPlex.ts | 8 +- .../user/sources/SourceSettingsSAML.ts | 9 +- web/src/elements/wizard/FormWizardPage.ts | 11 +- web/src/flow/FlowInspector.ts | 17 +- web/src/flow/sources/plex/PlexLoginInit.ts | 25 ++- .../AuthenticatorValidateStageWebAuthn.ts | 7 +- .../WebAuthnAuthenticatorRegisterStage.ts | 5 +- .../details/UserSettingsFlowExecutor.ts | 22 ++- .../user/user-settings/mfa/MFADeviceForm.ts | 2 +- .../user/user-settings/mfa/MFADevicesPage.ts | 2 +- web/tsconfig.base.json | 1 + 53 files changed, 901 insertions(+), 493 deletions(-) delete mode 100644 web/src/common/errors.ts create mode 100644 web/src/common/errors/network.ts diff --git a/web/src/admin/DebugPage.ts b/web/src/admin/DebugPage.ts index 101aab7230..050e242fec 100644 --- a/web/src/admin/DebugPage.ts +++ b/web/src/admin/DebugPage.ts @@ -1,4 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network"; import { MessageLevel } from "@goauthentik/common/messages"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/PageHeader"; @@ -54,10 +55,12 @@ export class DebugPage extends AKElement { message: "Success", }); }) - .catch((exc) => { + .catch(async (error) => { + const parsedError = await parseAPIResponseError(error); + showMessage({ level: MessageLevel.error, - message: exc, + message: pluckErrorDetail(parsedError), }); }); }} diff --git a/web/src/admin/admin-overview/cards/AdminStatusCard.ts b/web/src/admin/admin-overview/cards/AdminStatusCard.ts index 90620d4c92..266c2ec73a 100644 --- a/web/src/admin/admin-overview/cards/AdminStatusCard.ts +++ b/web/src/admin/admin-overview/cards/AdminStatusCard.ts @@ -1,13 +1,16 @@ 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"; -import { ResponseError } from "@goauthentik/api"; - export interface AdminStatus { icon: string; message?: TemplateResult; @@ -29,7 +32,7 @@ export abstract class AdminStatusCard extends AggregateCard { // Current error state if any request fails @state() - protected error?: string; + protected error?: APIError; // Abstract methods to be implemented by subclasses abstract getPrimaryValue(): Promise; @@ -59,9 +62,9 @@ export abstract class AdminStatusCard extends AggregateCard { this.value = value; // Triggers shouldUpdate this.error = undefined; }) - .catch((err: ResponseError) => { + .catch(async (error: unknown) => { this.status = undefined; - this.error = err?.response?.statusText ?? msg("Unknown error"); + this.error = await parseAPIResponseError(error); }); } @@ -79,9 +82,9 @@ export abstract class AdminStatusCard extends AggregateCard { this.status = status; this.error = undefined; }) - .catch((err: ResponseError) => { + .catch(async (error: unknown) => { this.status = undefined; - this.error = err?.response?.statusText ?? msg("Unknown error"); + this.error = await parseAPIResponseError(error); }); // Prevent immediate re-render if only value changed @@ -120,8 +123,8 @@ export abstract class AdminStatusCard extends AggregateCard { */ private renderError(error: string): TemplateResult { return html` -

 ${error}

-

${msg("Failed to fetch")}

+

 ${msg("Failed to fetch")}

+

${error}

`; } @@ -146,7 +149,7 @@ export abstract class AdminStatusCard extends AggregateCard { this.status ? this.renderStatus(this.status) // Status available : this.error - ? this.renderError(this.error) // Error state + ? this.renderError(pluckErrorDetail(this.error)) // Error state : this.renderLoading() // Loading state }

diff --git a/web/src/admin/admin-overview/cards/RecentEventsCard.ts b/web/src/admin/admin-overview/cards/RecentEventsCard.ts index a18419ebcc..b5cd658d62 100644 --- a/web/src/admin/admin-overview/cards/RecentEventsCard.ts +++ b/web/src/admin/admin-overview/cards/RecentEventsCard.ts @@ -10,6 +10,7 @@ import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/SpinnerButton"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table"; +import { SlottedTemplateResult } from "@goauthentik/elements/types"; import { msg } from "@lit/localize"; import { CSSResult, TemplateResult, css, html } from "lit"; @@ -68,7 +69,7 @@ export class RecentEventsCard extends Table { `; } - row(item: EventWithContext): TemplateResult[] { + row(item: EventWithContext): SlottedTemplateResult[] { return [ html` ${item.app}`, @@ -81,7 +82,11 @@ export class RecentEventsCard extends Table { ]; } - renderEmpty(): TemplateResult { + renderEmpty(inner?: SlottedTemplateResult): TemplateResult { + if (this.error) { + return super.renderEmpty(inner); + } + return super.renderEmpty( html`
${msg("No matching events could be found.")}
diff --git a/web/src/admin/applications/wizard/ApplicationWizardStep.ts b/web/src/admin/applications/wizard/ApplicationWizardStep.ts index 71095a6d46..33ff80ccc4 100644 --- a/web/src/admin/applications/wizard/ApplicationWizardStep.ts +++ b/web/src/admin/applications/wizard/ApplicationWizardStep.ts @@ -1,7 +1,7 @@ import { styles } from "@goauthentik/admin/applications/wizard/ApplicationWizardFormStepStyles.css.js"; import { WizardStep } from "@goauthentik/components/ak-wizard/WizardStep.js"; import { - NavigationUpdate, + NavigationEventInit, WizardNavigationEvent, WizardUpdateEvent, } from "@goauthentik/components/ak-wizard/events"; @@ -14,9 +14,9 @@ import { property, query } from "lit/decorators.js"; import { ValidationError } from "@goauthentik/api"; import { + ApplicationTransactionValidationError, type ApplicationWizardState, type ApplicationWizardStateUpdate, - ExtendedValidationError, } from "./types"; export class ApplicationWizardStep extends WizardStep { @@ -48,7 +48,7 @@ export class ApplicationWizardStep extends WizardStep { } protected removeErrors( - keyToDelete: keyof ExtendedValidationError, + keyToDelete: keyof ApplicationTransactionValidationError, ): ValidationError | undefined { if (!this.wizard.errors) { return undefined; @@ -71,7 +71,7 @@ export class ApplicationWizardStep extends WizardStep { public handleUpdate( update?: ApplicationWizardStateUpdate, destination?: string, - enable?: NavigationUpdate, + enable?: NavigationEventInit, ) { // Inform ApplicationWizard of content state if (update) { diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-submit-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-submit-step.ts index d2e2786971..2ca5e6b5d3 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-submit-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-submit-step.ts @@ -1,9 +1,10 @@ import "@goauthentik/admin/applications/wizard/ak-wizard-title.js"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; -import { parseAPIError } from "@goauthentik/common/errors"; +import { parseAPIResponseError } from "@goauthentik/common/errors/network"; import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js"; import { type WizardButton } from "@goauthentik/components/ak-wizard/types"; +import { showAPIErrorMessage } from "@goauthentik/elements/messages/MessageContainer"; import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; import { P, match } from "ts-pattern"; @@ -30,10 +31,11 @@ import { type TransactionApplicationRequest, type TransactionApplicationResponse, type TransactionPolicyBindingRequest, + instanceOfValidationError, } from "@goauthentik/api"; import { ApplicationWizardStep } from "../ApplicationWizardStep.js"; -import { ExtendedValidationError, OneOfProvider } from "../types.js"; +import { OneOfProvider, isApplicationTransactionValidationError } from "../types.js"; import { providerRenderers } from "./SubmitStepOverviewRenderers.js"; const _submitStates = ["reviewing", "running", "submitted"] as const; @@ -131,39 +133,46 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio this.state = "running"; - return ( - new CoreApi(DEFAULT_CONFIG) - .coreTransactionalApplicationsUpdate({ - transactionApplicationRequest: request, - }) - .then((_response: TransactionApplicationResponse) => { - this.dispatchCustomEvent(EVENT_REFRESH); - this.state = "submitted"; - }) + return new CoreApi(DEFAULT_CONFIG) + .coreTransactionalApplicationsUpdate({ + transactionApplicationRequest: request, + }) + .then((_response: TransactionApplicationResponse) => { + this.dispatchCustomEvent(EVENT_REFRESH); + this.state = "submitted"; + }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .catch(async (resolution: any) => { - const errors = (await parseAPIError( - await resolution, - )) as ExtendedValidationError; + .catch(async (error) => { + const parsedError = await parseAPIResponseError(error); - // THIS is a really gross special case; if the user is duplicating the name of - // an existing provider, the error appears on the `app` (!) error object. We - // have to move that to the `provider.name` error field so it shows up in the - // right place. - if (Array.isArray(errors?.app?.provider)) { - const providerError = errors.app.provider; - errors.provider = errors.provider ?? {}; - errors.provider.name = providerError; - delete errors.app.provider; - if (Object.keys(errors.app).length === 0) { - delete errors.app; + if (!instanceOfValidationError(parsedError)) { + showAPIErrorMessage(parsedError); + + return; + } + + if (isApplicationTransactionValidationError(parsedError)) { + // THIS is a really gross special case; if the user is duplicating the name of an existing provider, the error appears on the `app` (!) error object. + // We have to move that to the `provider.name` error field so it shows up in the right place. + if (Array.isArray(parsedError.app?.provider)) { + const providerError = parsedError.app.provider; + + parsedError.provider = { + ...parsedError.provider, + name: providerError, + }; + + delete parsedError.app.provider; + + if (Object.keys(parsedError.app).length === 0) { + delete parsedError.app; } } - this.handleUpdate({ errors }); - this.state = "reviewing"; - }) - ); + } + + this.handleUpdate({ errors: parsedError }); + this.state = "reviewing"; + }); } override handleButton(button: WizardButton) { @@ -225,22 +234,20 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio } renderError() { - if (Object.keys(this.wizard.errors).length === 0) { - return nothing; - } + const { errors } = this.wizard; + + if (Object.keys(errors).length === 0) return nothing; - const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step)); - const errors = this.wizard.errors; return html`
- ${match(errors as ExtendedValidationError) + ${match(errors) .with( { app: P.nonNullable }, () => html`

${msg("There was an error in the application.")}

- ${msg("Review the application.")} + + ${msg("Review the application.")} +

`, ) .with( @@ -248,13 +255,20 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio () => html`

${msg("There was an error in the provider.")}

- ${msg("Review the provider.")} + ${msg("Review the provider.")}

`, ) .with( { detail: P.nonNullable }, () => - `

${msg("There was an error. Please go back and review the application.")}: ${errors.detail}

`, + html`

+ ${msg( + "There was an error. Please go back and review the application.", + )}: + ${errors.detail} +

`, ) .with( { @@ -264,7 +278,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio html`

${msg("There was an error:")}:

    ${(errors.nonFieldErrors ?? []).map( - (e: string) => html`
  • ${e}
  • `, + (reason) => html`
  • ${reason}
  • `, )}

${msg("Please go back and review the application.")}

`, diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth.ts index ff524d65d2..4b23e93950 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth.ts @@ -9,7 +9,7 @@ import { customElement, state } from "lit/decorators.js"; import { OAuth2ProviderRequest, SourcesApi } from "@goauthentik/api"; import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api"; -import { ExtendedValidationError } from "../../types.js"; +import { ApplicationTransactionValidationError } from "../../types.js"; import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; @customElement("ak-application-wizard-provider-for-oauth") @@ -34,7 +34,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid }); } - renderForm(provider: OAuth2Provider, errors: ExtendedValidationError) { + renderForm(provider: OAuth2Provider, errors: ApplicationTransactionValidationError) { const showClientSecretCallback = (show: boolean) => { this.showClientSecret = show; }; diff --git a/web/src/admin/applications/wizard/types.ts b/web/src/admin/applications/wizard/types.ts index 3529d14f3c..5e20ffe612 100644 --- a/web/src/admin/applications/wizard/types.ts +++ b/web/src/admin/applications/wizard/types.ts @@ -25,16 +25,30 @@ export type OneOfProvider = export type ValidationRecord = { [key: string]: string[] }; -// TODO: Elf, extend this type and apply it to every object in the wizard. Then run -// the type-checker again. - -export type ExtendedValidationError = ValidationError & { +/** + * An error that occurs during the creation or modification of an application. + * + * @todo (Elf) Extend this type to include all possible errors that can occur during the creation or modification of an application. + */ +export interface ApplicationTransactionValidationError extends ValidationError { app?: ValidationRecord; provider?: ValidationRecord; bindings?: ValidationRecord; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - detail?: any; -}; + detail?: unknown; +} + +/** + * Type-guard to determine if an API response is shaped like an {@linkcode ApplicationTransactionValidationError}. + */ +export function isApplicationTransactionValidationError( + error: ValidationError, +): error is ApplicationTransactionValidationError { + if ("app" in error) return true; + if ("provider" in error) return true; + if ("bindings" in error) return true; + + return false; +} // We use the PolicyBinding instead of the PolicyBindingRequest here, because that gives us a slot // in which to preserve the retrieved policy, group, or user object from the SearchSelect used to @@ -49,7 +63,7 @@ export interface ApplicationWizardState { proxyMode: ProxyMode; bindings: PolicyBinding[]; currentBinding: number; - errors: ExtendedValidationError; + errors: ValidationError | ApplicationTransactionValidationError; } export interface ApplicationWizardStateUpdate { diff --git a/web/src/admin/events/EventListPage.ts b/web/src/admin/events/EventListPage.ts index b8f34df19b..e62b5767aa 100644 --- a/web/src/admin/events/EventListPage.ts +++ b/web/src/admin/events/EventListPage.ts @@ -8,6 +8,7 @@ import "@goauthentik/components/ak-event-info"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table"; import { TablePage } from "@goauthentik/elements/table/TablePage"; +import { SlottedTemplateResult } from "@goauthentik/elements/types"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { msg } from "@lit/localize"; @@ -72,7 +73,7 @@ export class EventListPage extends TablePage { `; } - row(item: EventWithContext): TemplateResult[] { + row(item: EventWithContext): SlottedTemplateResult[] { return [ html`
${actionToLabel(item.action)}
${item.app}`, diff --git a/web/src/admin/events/utils.ts b/web/src/admin/events/utils.ts index 89999d39d6..3c73c3f390 100644 --- a/web/src/admin/events/utils.ts +++ b/web/src/admin/events/utils.ts @@ -1,27 +1,31 @@ import { EventWithContext } from "@goauthentik/common/events"; import { truncate } from "@goauthentik/common/utils"; -import { KeyUnknown } from "@goauthentik/elements/forms/Form"; +import { SlottedTemplateResult } from "@goauthentik/elements/types"; import { msg, str } from "@lit/localize"; -import { TemplateResult, html } from "lit"; +import { html, nothing } from "lit"; -export function EventGeo(event: EventWithContext): TemplateResult { - let geo: KeyUnknown | undefined = undefined; - if (Object.hasOwn(event.context, "geo")) { - geo = event.context.geo as KeyUnknown; - const parts = [geo.city, geo.country, geo.continent].filter( - (v) => v !== "" && v !== undefined, - ); - return html`${parts.join(", ")}`; - } - return html``; +/** + * Given event with a geographical context, format it into a string for display. + */ +export function EventGeo(event: EventWithContext): SlottedTemplateResult { + if (!event.context.geo) return nothing; + + const { city, country, continent } = event.context.geo; + + const parts = [city, country, continent].filter(Boolean); + + return html`${parts.join(", ")}`; } -export function EventUser(event: EventWithContext, truncateUsername?: number): TemplateResult { - if (!event.user.username) { - return html`-`; - } - let body = html``; +export function EventUser( + event: EventWithContext, + truncateUsername?: number, +): SlottedTemplateResult { + if (!event.user.username) return html`-`; + + let body: SlottedTemplateResult = nothing; + if (event.user.is_anonymous) { body = html`
${msg("Anonymous user")}
`; } else { @@ -33,12 +37,14 @@ export function EventUser(event: EventWithContext, truncateUsername?: number): T > `; } + if (event.user.on_behalf_of) { - body = html`${body} + return html`${body} ${msg(str`On behalf of ${event.user.on_behalf_of.username}`)} `; } + return body; } diff --git a/web/src/admin/flows/FlowImportForm.ts b/web/src/admin/flows/FlowImportForm.ts index 783f51217e..2c7b743e46 100644 --- a/web/src/admin/flows/FlowImportForm.ts +++ b/web/src/admin/flows/FlowImportForm.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { SentryIgnoredError } from "@goauthentik/common/errors"; +import { SentryIgnoredError } from "@goauthentik/common/sentry"; import "@goauthentik/components/ak-status-label"; import "@goauthentik/elements/events/LogViewer"; import { Form } from "@goauthentik/elements/forms/Form"; diff --git a/web/src/admin/flows/FlowViewPage.ts b/web/src/admin/flows/FlowViewPage.ts index 661cc3367c..718235b397 100644 --- a/web/src/admin/flows/FlowViewPage.ts +++ b/web/src/admin/flows/FlowViewPage.ts @@ -5,6 +5,7 @@ import { DesignationToLabel } from "@goauthentik/admin/flows/utils"; import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/rbac/ObjectPermissionsPage"; import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { isResponseErrorLike } from "@goauthentik/common/errors/network"; import "@goauthentik/components/events/ObjectChangelog"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/PageHeader"; @@ -23,12 +24,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; -import { - Flow, - FlowsApi, - RbacPermissionsAssignedByUsersListModelEnum, - ResponseError, -} from "@goauthentik/api"; +import { Flow, FlowsApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api"; @customElement("ak-flow-view") export class FlowViewPage extends AKElement { @@ -195,13 +191,15 @@ export class FlowViewPage extends AKElement { )}`; window.open(finalURL, "_blank"); }) - .catch((exc: ResponseError) => { - // This request can return a HTTP 400 when a flow - // is not applicable. - window.open( - exc.response.url, - "_blank", - ); + .catch(async (error: unknown) => { + if (isResponseErrorLike(error)) { + // This request can return a HTTP 400 when a flow + // is not applicable. + window.open( + error.response.url, + "_blank", + ); + } }); }} > diff --git a/web/src/admin/groups/RelatedUserList.ts b/web/src/admin/groups/RelatedUserList.ts index ed133ede3d..5370dd6b02 100644 --- a/web/src/admin/groups/RelatedUserList.ts +++ b/web/src/admin/groups/RelatedUserList.ts @@ -6,6 +6,7 @@ import "@goauthentik/admin/users/UserPasswordForm"; import "@goauthentik/admin/users/UserResetEmailForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { PFSize } from "@goauthentik/common/enums.js"; +import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network"; import { MessageLevel } from "@goauthentik/common/messages"; import { me } from "@goauthentik/common/users"; import { getRelativeTime } from "@goauthentik/common/utils"; @@ -37,14 +38,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; -import { - CoreApi, - CoreUsersListTypeEnum, - Group, - ResponseError, - SessionUser, - User, -} from "@goauthentik/api"; +import { CoreApi, CoreUsersListTypeEnum, Group, SessionUser, User } from "@goauthentik/api"; @customElement("ak-user-related-add") export class RelatedUserAdd extends Form<{ users: number[] }> { @@ -319,14 +313,16 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl description: rec.link, }); }) - .catch((ex: ResponseError) => { - ex.response.json().then(() => { - showMessage({ - level: MessageLevel.error, - message: msg( - "No recovery flow is configured.", - ), - }); + .catch(async (error: unknown) => { + const parsedError = + await parseAPIResponseError( + error, + ); + + showMessage({ + level: MessageLevel.error, + message: + pluckErrorDetail(parsedError), }); }); }} diff --git a/web/src/admin/providers/saml/SAMLProviderImportForm.ts b/web/src/admin/providers/saml/SAMLProviderImportForm.ts index 3241e24b66..07ec8ffaf0 100644 --- a/web/src/admin/providers/saml/SAMLProviderImportForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderImportForm.ts @@ -1,6 +1,6 @@ import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { SentryIgnoredError } from "@goauthentik/common/errors"; +import { SentryIgnoredError } from "@goauthentik/common/sentry"; import { Form } from "@goauthentik/elements/forms/Form"; import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/SearchSelect"; diff --git a/web/src/admin/stages/prompt/PromptForm.ts b/web/src/admin/stages/prompt/PromptForm.ts index 0a801d6468..cc1d00b9a1 100644 --- a/web/src/admin/stages/prompt/PromptForm.ts +++ b/web/src/admin/stages/prompt/PromptForm.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { parseAPIError } from "@goauthentik/common/errors"; +import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network"; import { first } from "@goauthentik/common/utils"; import "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; @@ -21,9 +21,8 @@ import { Prompt, PromptChallenge, PromptTypeEnum, - ResponseError, StagesApi, - ValidationError, + instanceOfValidationError, } from "@goauthentik/api"; class PreviewStageHost implements StageHost { @@ -78,15 +77,22 @@ export class PromptForm extends ModelForm { return; } } - try { - this.preview = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsPreviewCreate({ + + return new StagesApi(DEFAULT_CONFIG) + .stagesPromptPromptsPreviewCreate({ promptRequest: prompt, + }) + .then((nextPreview) => { + this.preview = nextPreview; + this.previewError = undefined; + }) + .catch(async (error: unknown) => { + const parsedError = await parseAPIResponseError(error); + + this.previewError = instanceOfValidationError(parsedError) + ? parsedError.nonFieldErrors + : [pluckErrorDetail(parsedError, msg("Failed to preview prompt"))]; }); - this.previewError = undefined; - } catch (exc) { - const errorMessage = parseAPIError(exc as ResponseError); - this.previewError = (errorMessage as ValidationError).nonFieldErrors; - } } getSuccessMessage(): string { diff --git a/web/src/admin/users/UserActiveForm.ts b/web/src/admin/users/UserActiveForm.ts index 7eadb5a52b..ed1499a9aa 100644 --- a/web/src/admin/users/UserActiveForm.ts +++ b/web/src/admin/users/UserActiveForm.ts @@ -1,3 +1,4 @@ +import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network"; import { MessageLevel } from "@goauthentik/common/messages"; import "@goauthentik/elements/buttons/SpinnerButton"; import { DeleteForm } from "@goauthentik/elements/forms/DeleteForm"; @@ -16,10 +17,14 @@ export class UserActiveForm extends DeleteForm { }); } - onError(e: Error): void { - showMessage({ - message: msg(str`Failed to update ${this.objectLabel}: ${e.toString()}`), - level: MessageLevel.error, + onError(error: unknown): Promise { + return parseAPIResponseError(error).then((parsedError) => { + showMessage({ + message: msg( + str`Failed to update ${this.objectLabel}: ${pluckErrorDetail(parsedError)}`, + ), + level: MessageLevel.error, + }); }); } diff --git a/web/src/admin/users/UserDevicesTable.ts b/web/src/admin/users/UserDevicesTable.ts index 36e885fcd2..8ab7bb5c22 100644 --- a/web/src/admin/users/UserDevicesTable.ts +++ b/web/src/admin/users/UserDevicesTable.ts @@ -1,6 +1,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { SentryIgnoredError } from "@goauthentik/common/errors"; import { deviceTypeName } from "@goauthentik/common/labels"; +import { SentryIgnoredError } from "@goauthentik/common/sentry"; import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/elements/forms/DeleteBulkForm"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; diff --git a/web/src/admin/users/UserListPage.ts b/web/src/admin/users/UserListPage.ts index fb62d62bfb..c259ec6623 100644 --- a/web/src/admin/users/UserListPage.ts +++ b/web/src/admin/users/UserListPage.ts @@ -7,6 +7,7 @@ import "@goauthentik/admin/users/UserPasswordForm"; import "@goauthentik/admin/users/UserResetEmailForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { PFSize } from "@goauthentik/common/enums.js"; +import { parseAPIResponseError } from "@goauthentik/common/errors/network"; import { userTypeToLabel } from "@goauthentik/common/labels"; import { MessageLevel } from "@goauthentik/common/messages"; import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config"; @@ -23,7 +24,7 @@ import "@goauthentik/elements/TreeView"; import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/ModalForm"; -import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; +import { showAPIErrorMessage, showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table"; @@ -39,7 +40,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; -import { CoreApi, ResponseError, SessionUser, User, UserPath } from "@goauthentik/api"; +import { CoreApi, SessionUser, User, UserPath } from "@goauthentik/api"; export const requestRecoveryLink = (user: User) => new CoreApi(DEFAULT_CONFIG) @@ -57,16 +58,7 @@ export const requestRecoveryLink = (user: User) => }), ), ) - .catch((ex: ResponseError) => - ex.response.json().then(() => - showMessage({ - level: MessageLevel.error, - message: msg( - "The current brand must have a recovery flow configured to use a recovery link", - ), - }), - ), - ); + .catch((error: unknown) => parseAPIResponseError(error).then(showAPIErrorMessage)); export const renderRecoveryEmailRequest = (user: User) => html` diff --git a/web/src/common/errors.ts b/web/src/common/errors.ts deleted file mode 100644 index da1c68561d..0000000000 --- a/web/src/common/errors.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { - GenericError, - GenericErrorFromJSON, - ResponseError, - ValidationError, - ValidationErrorFromJSON, -} from "@goauthentik/api"; - -export class SentryIgnoredError extends Error {} -export class NotFoundError extends Error {} -export class RequestError extends Error {} - -export type APIErrorTypes = ValidationError | GenericError; - -export const HTTP_BAD_REQUEST = 400; -export const HTTP_INTERNAL_SERVICE_ERROR = 500; - -export async function parseAPIError(error: Error): Promise { - if (!(error instanceof ResponseError)) { - return error; - } - if ( - error.response.status < HTTP_BAD_REQUEST || - error.response.status >= HTTP_INTERNAL_SERVICE_ERROR - ) { - return error; - } - const body = await error.response.json(); - if (error.response.status === 400) { - return ValidationErrorFromJSON(body); - } - if (error.response.status === 403) { - return GenericErrorFromJSON(body); - } - return body; -} diff --git a/web/src/common/errors/network.ts b/web/src/common/errors/network.ts new file mode 100644 index 0000000000..f41b4d4db4 --- /dev/null +++ b/web/src/common/errors/network.ts @@ -0,0 +1,184 @@ +import { + GenericError, + GenericErrorFromJSON, + ResponseError, + ValidationError, + ValidationErrorFromJSON, +} from "@goauthentik/api"; + +//#region HTTP + +/** + * Common HTTP status names used in the API and their corresponding codes. + */ +export const HTTPStatusCode = { + BadRequest: 400, + Forbidden: 403, + InternalServiceError: 500, +} as const satisfies Record; + +export type HTTPStatusCode = (typeof HTTPStatusCode)[keyof typeof HTTPStatusCode]; + +export type HTTPErrorJSONTransformer = (json: T) => APIError; + +export const HTTPStatusCodeTransformer: Record = { + [HTTPStatusCode.BadRequest]: ValidationErrorFromJSON, + [HTTPStatusCode.Forbidden]: GenericErrorFromJSON, +} as const; + +/** + * Type guard to check if a response contains a JSON body. + * + * This is useful to guard against parsing errors when attempting to read the response body. + */ +export function isJSONResponse(response: Response): boolean { + return Boolean(response.headers.get("content-type")?.includes("application/json")); +} + +//#endregion + +//#region API + +/** + * An API response error, typically derived from a {@linkcode Response} body. + * + * @see {@linkcode parseAPIResponseError} + */ +export type APIError = ValidationError | GenericError; + +/** + * Given an error-like object, attempts to normalize it into a {@linkcode GenericError} + * suitable for display to the user. + */ +export function createSyntheticGenericError(detail?: string): GenericError { + const syntheticGenericError: GenericError = { + detail: detail || ResponseErrorMessages[HTTPStatusCode.InternalServiceError].reason, + }; + + return syntheticGenericError; +} + +/** + * An error that contains a native response object. + * + * @see {@linkcode isResponseErrorLike} to determine if an error contains a response object. + */ +export type APIErrorWithResponse = Pick; + +/** + * Type guard to check if an error contains a HTTP {@linkcode Response} object. + * + * @see {@linkcode parseAPIResponseError} to parse the response body into a {@linkcode APIError}. + */ +export function isResponseErrorLike(errorLike: unknown): errorLike is APIErrorWithResponse { + if (!errorLike || typeof errorLike !== "object") return false; + + return "response" in errorLike && errorLike.response instanceof Response; +} + +/** + * A descriptor to provide a human readable error message for a given HTTP status code. + * + * @see {@linkcode ResponseErrorMessages} for a list of fallback error messages. + */ +interface ResponseErrorDescriptor { + headline: string; + reason: string; +} + +/** + * Fallback error messages for HTTP status codes used when a more specific error message is not available in the response. + */ +export const ResponseErrorMessages: Record = { + [HTTPStatusCode.BadRequest]: { + headline: "Bad request", + reason: "The server did not understand the request", + }, + [HTTPStatusCode.InternalServiceError]: { + headline: "Internal server error", + reason: "An unexpected error occurred", + }, +} as const; + +/** + * Composes a human readable error message from a {@linkcode ResponseErrorDescriptor}. + * + * Note that this is kept separate from localization to lower the complexity of the error handling code. + */ +export function composeResponseErrorDescriptor(descriptor: ResponseErrorDescriptor): string { + return `${descriptor.headline}: ${descriptor.reason}`; +} + +/** + * Attempts to pluck a human readable error message from a {@linkcode ValidationError}. + */ +export function pluckErrorDetail(validationError: ValidationError, fallback?: string): string; +/** + * Attempts to pluck a human readable error message from a {@linkcode GenericError}. + */ +export function pluckErrorDetail(genericError: GenericError, fallback?: string): string; +/** + * Attempts to pluck a human readable error message from an `Error` object. + */ +export function pluckErrorDetail(error: Error, fallback?: string): string; +/** + * Attempts to pluck a human readable error message from an error-like object. + * + * Prioritizes the `detail` key, then the `message` key. + * + */ +export function pluckErrorDetail(errorLike: unknown, fallback?: string): string; +export function pluckErrorDetail(errorLike: unknown, fallback?: string): string { + fallback ||= composeResponseErrorDescriptor( + ResponseErrorMessages[HTTPStatusCode.InternalServiceError], + ); + + if (!errorLike || typeof errorLike !== "object") { + return fallback; + } + + if ("detail" in errorLike && typeof errorLike.detail === "string") { + return errorLike.detail; + } + + if ("message" in errorLike && typeof errorLike.message === "string") { + return errorLike.message; + } + + return fallback; +} + +/** + * Given API error, parses the response body and transforms it into a {@linkcode APIError}. + */ +export async function parseAPIResponseError( + error: unknown, +): Promise { + if (!isResponseErrorLike(error)) { + const message = error instanceof Error ? error.message : String(error); + + return createSyntheticGenericError(message) as T; + } + + const { response, message } = error; + + if (!isJSONResponse(response)) { + return createSyntheticGenericError(message || response.statusText) as T; + } + + return response + .json() + .then((body) => { + const transformer = HTTPStatusCodeTransformer[response.status]; + const transformedBody = transformer ? transformer(body) : body; + + return transformedBody as unknown as T; + }) + .catch((transformerError: unknown) => { + console.error("Failed to parse response error body", transformerError); + + return createSyntheticGenericError(message || response.statusText) as T; + }); +} + +//#endregion diff --git a/web/src/common/events.ts b/web/src/common/events.ts index aedad64d97..418c9a0a10 100644 --- a/web/src/common/events.ts +++ b/web/src/common/events.ts @@ -8,13 +8,10 @@ export interface EventUser { is_anonymous?: boolean; } -export interface EventContext { - [key: string]: EventContext | EventModel | string | number | string[]; -} - -export interface EventWithContext extends Event { - user: EventUser; - context: EventContext; +export interface EventGeo { + city?: string; + country?: string; + continent?: string; } export interface EventModel { @@ -28,3 +25,16 @@ export interface EventRequest { path: string; method: string; } + +export type EventContextProperty = EventModel | EventGeo | string | number | string[] | undefined; + +// TODO: Events should have more specific types. +export interface EventContext { + [key: string]: EventContext | EventContextProperty; + geo?: EventGeo; +} + +export interface EventWithContext extends Event { + user: EventUser; + context: EventContext; +} diff --git a/web/src/common/helpers/plex.ts b/web/src/common/helpers/plex.ts index c3735af5bd..228a505a07 100644 --- a/web/src/common/helpers/plex.ts +++ b/web/src/common/helpers/plex.ts @@ -1,5 +1,5 @@ import { VERSION } from "@goauthentik/common/constants"; -import { SentryIgnoredError } from "@goauthentik/common/errors"; +import { SentryIgnoredError } from "@goauthentik/common/sentry"; export interface PlexPinResponse { // Only has the fields we care about diff --git a/web/src/common/sentry.ts b/web/src/common/sentry.ts index aa6a8a6eb1..3699ba5d6b 100644 --- a/web/src/common/sentry.ts +++ b/web/src/common/sentry.ts @@ -1,6 +1,5 @@ import { config } from "@goauthentik/common/api/config"; import { VERSION } from "@goauthentik/common/constants"; -import { SentryIgnoredError } from "@goauthentik/common/errors"; import { me } from "@goauthentik/common/users"; import { ErrorEvent, @@ -13,6 +12,11 @@ import { import { CapabilitiesEnum, Config, ResponseError } from "@goauthentik/api"; +/** + * A generic error that can be thrown without triggering Sentry's reporting. + */ +export class SentryIgnoredError extends Error {} + export const TAG_SENTRY_COMPONENT = "authentik.component"; export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities"; diff --git a/web/src/common/users.ts b/web/src/common/users.ts index 29c218a021..067b1ddc68 100644 --- a/web/src/common/users.ts +++ b/web/src/common/users.ts @@ -1,63 +1,96 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; +import { isResponseErrorLike } from "@goauthentik/common/errors/network"; -import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api"; +import { CoreApi, SessionUser } from "@goauthentik/api"; -let globalMePromise: Promise | undefined; +/** + * Create a guest session for unauthenticated users. + * + * @see {@linkcode me} for the actual session retrieval. + */ +function createGuestSession(): SessionUser { + const guest: SessionUser = { + user: { + pk: -1, + isSuperuser: false, + isActive: true, + groups: [], + avatar: "", + uid: "", + username: "", + name: "", + settings: {}, + systemPermissions: [], + }, + }; + return guest; +} + +let memoizedSession: SessionUser | null = null; + +/** + * Refresh the current user session. + */ export function refreshMe(): Promise { - globalMePromise = undefined; + memoizedSession = null; return me(); } -export function me(): Promise { - if (!globalMePromise) { - globalMePromise = new CoreApi(DEFAULT_CONFIG) - .coreUsersMeRetrieve() - .then((user) => { - if (!user.user.settings || !("locale" in user.user.settings)) { - return user; - } - const locale: string | undefined = user.user.settings.locale; - if (locale && locale !== "") { - console.debug( - `authentik/locale: Activating user's configured locale '${locale}'`, - ); - window.dispatchEvent( - new CustomEvent(EVENT_LOCALE_REQUEST, { - composed: true, - bubbles: true, - detail: { locale }, - }), +/** + * Retrieve the current user session. + * + * This is a memoized function, so it will only make one request per page load. + * + * @see {@linkcode refreshMe} to force a refresh. + */ +export async function me(): Promise { + if (memoizedSession) return memoizedSession; + + return new CoreApi(DEFAULT_CONFIG) + .coreUsersMeRetrieve() + .then((nextSession) => { + const locale: string | undefined = nextSession.user.settings.locale; + + if (locale) { + console.debug(`authentik/locale: Activating user's configured locale '${locale}'`); + + window.dispatchEvent( + new CustomEvent(EVENT_LOCALE_REQUEST, { + composed: true, + bubbles: true, + detail: { locale }, + }), + ); + } + + return nextSession; + }) + .catch(async (error: unknown) => { + if (isResponseErrorLike(error)) { + const { response } = error; + + if (response.status === 401 || response.status === 403) { + const { pathname, search, hash } = window.location; + + const authFlowRedirectURL = new URL( + `/flows/-/default/authentication/`, + window.location.origin, ); + + authFlowRedirectURL.searchParams.set("next", `${pathname}${search}${hash}`); + + window.location.assign(authFlowRedirectURL); } - return user; - }) - .catch((ex: ResponseError) => { - const defaultUser: SessionUser = { - user: { - pk: -1, - isSuperuser: false, - isActive: true, - groups: [], - avatar: "", - uid: "", - username: "", - name: "", - settings: {}, - systemPermissions: [], - }, - }; - if (ex.response?.status === 401 || ex.response?.status === 403) { - const relativeUrl = window.location - .toString() - .substring(window.location.origin.length); - window.location.assign( - `/flows/-/default/authentication/?next=${encodeURIComponent(relativeUrl)}`, - ); - } - return defaultUser; - }); - } - return globalMePromise; + } + + console.debug("authentik/users: Failed to retrieve user session", error); + + return createGuestSession(); + }) + .then((nextSession) => { + memoizedSession = nextSession; + return nextSession; + }); } diff --git a/web/src/common/utils.ts b/web/src/common/utils.ts index 1d140c5d05..ccf30f6ab3 100644 --- a/web/src/common/utils.ts +++ b/web/src/common/utils.ts @@ -1,4 +1,4 @@ -import { SentryIgnoredError } from "@goauthentik/common/errors"; +import { SentryIgnoredError } from "@goauthentik/common/sentry"; import { CSSResult, css } from "lit"; diff --git a/web/src/components/ak-event-info.ts b/web/src/components/ak-event-info.ts index e94944d976..2b2af8f284 100644 --- a/web/src/components/ak-event-info.ts +++ b/web/src/components/ak-event-info.ts @@ -1,10 +1,16 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { VERSION } from "@goauthentik/common/constants"; import { PFSize } from "@goauthentik/common/enums.js"; -import { EventContext, EventModel, EventWithContext } from "@goauthentik/common/events"; +import { + EventContext, + EventContextProperty, + EventModel, + EventWithContext, +} from "@goauthentik/common/events"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/Expand"; import "@goauthentik/elements/Spinner"; +import { SlottedTemplateResult } from "@goauthentik/elements/types"; import { msg, str } from "@lit/localize"; import { CSSResult, TemplateResult, css, html } from "lit"; @@ -23,7 +29,15 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { EventActions, FlowsApi } from "@goauthentik/api"; -type Pair = [string, string | number | EventContext | EventModel | string[] | TemplateResult]; +// TODO: Settle these types. It's too hard to make sense of what we're expecting here. +type EventSlotValueType = + | number + | SlottedTemplateResult + | undefined + | EventContext + | EventContextProperty; + +type FieldLabelTuple = [label: string, value: V]; // https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-issues/about-automation-for-issues-and-pull-requests-with-query-parameters @@ -104,7 +118,7 @@ export class EventInfo extends AKElement { ]; } - renderDescriptionGroup([term, description]: Pair) { + renderDescriptionGroup([term, description]: FieldLabelTuple) { return html`
${term} @@ -120,7 +134,7 @@ export class EventInfo extends AKElement { return html`-`; } - const modelFields: Pair[] = [ + const modelFields: FieldLabelTuple[] = [ [msg("UID"), context.pk], [msg("Name"), context.name], [msg("App"), context.app], @@ -134,20 +148,23 @@ export class EventInfo extends AKElement {
`; } - getEmailInfo(context: EventContext): TemplateResult { + getEmailInfo(context: EventContext): SlottedTemplateResult { if (context === null) { return html`-`; } - // prettier-ignore - const emailFields: Pair[] = [ + const emailFields = [ + // --- [msg("Message"), context.message], [msg("Subject"), context.subject], [msg("From"), context.from_email], - [msg("To"), html`${(context.to_email as string[]).map((to) => { + [ + msg("To"), + html`${(context.to_email as string[]).map((to) => { return html`
  • ${to}
  • `; - })}`], - ]; + })}`, + ], + ] satisfies FieldLabelTuple[]; return html`
    ${map(emailFields, this.renderDescriptionGroup)} diff --git a/web/src/components/ak-wizard/WizardStep.ts b/web/src/components/ak-wizard/WizardStep.ts index 3946e253e4..ad99134a82 100644 --- a/web/src/components/ak-wizard/WizardStep.ts +++ b/web/src/components/ak-wizard/WizardStep.ts @@ -14,7 +14,7 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css"; import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css"; import { wizardStepContext } from "./WizardContexts.js"; -import { NavigationUpdate, WizardCloseEvent, WizardNavigationEvent } from "./events.js"; +import { NavigationEventInit, WizardCloseEvent, WizardNavigationEvent } from "./events.js"; import { WizardStepLabel, WizardStepState } from "./types"; import { type ButtonKind, type NavigableButton, type WizardButton } from "./types"; @@ -139,7 +139,7 @@ export class WizardStep extends AKElement { // Override this to intercept 'next' and 'back' events, perform validation, and include enabling // before allowing navigation to continue. - public handleButton(button: WizardButton, details?: NavigationUpdate) { + public handleButton(button: WizardButton, details?: NavigationEventInit) { if (["close", "cancel"].includes(button.kind)) { this.dispatchEvent(new WizardCloseEvent()); return; @@ -153,7 +153,7 @@ export class WizardStep extends AKElement { throw new Error(`Incoherent button passed: ${JSON.stringify(button, null, 2)}`); } - public handleEnabling(details: NavigationUpdate) { + public handleEnabling(details: NavigationEventInit) { this.dispatchEvent(new WizardNavigationEvent(undefined, details)); } @@ -185,13 +185,6 @@ export class WizardStep extends AKElement { this.dispatchEvent(new WizardCloseEvent()); } - @bound - onSidebarNav(ev: PointerEvent) { - ev.stopPropagation(); - const target = (ev.target as HTMLButtonElement).value; - this.dispatchEvent(new WizardNavigationEvent(target)); - } - getButtonLabel(button: WizardButton) { return button.label ?? BUTTON_KIND_TO_LABEL[button.kind]; } @@ -269,7 +262,7 @@ export class WizardStep extends AKElement {