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.
This commit is contained in:
@ -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<V extends EventSlotValueType = EventSlotValueType> = [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` <div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${term}</span>
|
||||
@ -120,7 +134,7 @@ export class EventInfo extends AKElement {
|
||||
return html`<span>-</span>`;
|
||||
}
|
||||
|
||||
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 {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
getEmailInfo(context: EventContext): TemplateResult {
|
||||
getEmailInfo(context: EventContext): SlottedTemplateResult {
|
||||
if (context === null) {
|
||||
return html`<span>-</span>`;
|
||||
}
|
||||
|
||||
// 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`<li>${to}</li>`;
|
||||
})}`],
|
||||
];
|
||||
})}`,
|
||||
],
|
||||
] satisfies FieldLabelTuple<EventSlotValueType>[];
|
||||
|
||||
return html`<dl class="pf-c-description-list pf-m-horizontal">
|
||||
${map(emailFields, this.renderDescriptionGroup)}
|
||||
|
||||
@ -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 {
|
||||
<button
|
||||
class=${classMap(buttonClasses)}
|
||||
?disabled=${!step.enabled}
|
||||
@click=${this.onSidebarNav}
|
||||
@click=${WizardNavigationEvent.toListener(this, step.id)}
|
||||
value=${step.id}
|
||||
>
|
||||
${step.label}
|
||||
|
||||
@ -7,7 +7,7 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { wizardStepContext } from "./WizardContexts";
|
||||
import { type WizardStep } from "./WizardStep";
|
||||
import { NavigationUpdate, WizardNavigationEvent } from "./events";
|
||||
import { NavigationEventInit, WizardNavigationEvent } from "./events";
|
||||
import { WizardStepState } from "./types";
|
||||
|
||||
/**
|
||||
@ -108,7 +108,7 @@ export class WizardStepsManager extends AKElement {
|
||||
// through the entire wizard," but since the user invalidated a prior, that shouldn't be
|
||||
// unexpected. None of the data will have been lost.
|
||||
|
||||
updateStepAvailability(details: NavigationUpdate) {
|
||||
updateStepAvailability(details: NavigationEventInit) {
|
||||
const asArr = (v?: string[] | string) =>
|
||||
v === undefined ? [] : Array.isArray(v) ? v : [v];
|
||||
const enabled = asArr(details.enable);
|
||||
|
||||
@ -1,26 +1,49 @@
|
||||
export type NavigationUpdate = {
|
||||
/**
|
||||
* Initialization options for a wizard navigation event.
|
||||
*/
|
||||
export interface NavigationEventInit {
|
||||
disabled?: string[];
|
||||
enable?: string | string[];
|
||||
hidden?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export class WizardNavigationEvent extends Event {
|
||||
/**
|
||||
* Event dispatched when the wizard navigation is updated.
|
||||
*/
|
||||
export class WizardNavigationEvent<D extends string = string> extends Event {
|
||||
static readonly eventName = "ak-wizard-navigation";
|
||||
|
||||
destination?: string;
|
||||
details?: NavigationUpdate;
|
||||
public readonly destination?: D;
|
||||
public readonly details?: NavigationEventInit;
|
||||
|
||||
constructor(destination?: string, details?: NavigationUpdate) {
|
||||
constructor(destination?: D, init?: NavigationEventInit) {
|
||||
super(WizardNavigationEvent.eventName, { bubbles: true, composed: true });
|
||||
this.destination = destination;
|
||||
this.details = details;
|
||||
this.details = init;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event target, bind the destination and details for dispatching.
|
||||
*/
|
||||
static toListener<D extends string = string>(
|
||||
target: EventTarget,
|
||||
destination: D,
|
||||
init?: NavigationEventInit,
|
||||
) {
|
||||
const wizardNavigationListener = (event?: Event) => {
|
||||
event?.preventDefault?.();
|
||||
|
||||
return target.dispatchEvent(new this(destination, init));
|
||||
};
|
||||
|
||||
return wizardNavigationListener;
|
||||
}
|
||||
}
|
||||
|
||||
export class WizardUpdateEvent<T> extends Event {
|
||||
static readonly eventName = "ak-wizard-update";
|
||||
|
||||
content: T;
|
||||
public readonly content: T;
|
||||
|
||||
constructor(content: T) {
|
||||
super(WizardUpdateEvent.eventName, { bubbles: true, composed: true });
|
||||
@ -39,8 +62,7 @@ export class WizardCloseEvent extends Event {
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
[WizardNavigationEvent.eventName]: WizardNavigationEvent;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[WizardUpdateEvent.eventName]: WizardUpdateEvent<any>;
|
||||
[WizardUpdateEvent.eventName]: WizardUpdateEvent<never>;
|
||||
[WizardCloseEvent.eventName]: WizardCloseEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 { PropertyValues, TemplateResult, html } from "lit";
|
||||
@ -68,7 +69,7 @@ export class ObjectChangelog extends Table<Event> {
|
||||
}
|
||||
}
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
row(item: EventWithContext): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`${actionToLabel(item.action)}`,
|
||||
EventUser(item),
|
||||
|
||||
@ -9,6 +9,7 @@ import "@goauthentik/elements/buttons/Dropdown";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
@ -42,7 +43,7 @@ export class UserEvents extends Table<Event> {
|
||||
];
|
||||
}
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
row(item: EventWithContext): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`${actionToLabel(item.action)}`,
|
||||
EventUser(item),
|
||||
|
||||
Reference in New Issue
Block a user