diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/performance-profiling/dashboard-performance-profiling.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/performance-profiling/dashboard-performance-profiling.element.ts index 78e8195064..fa4835f52b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/performance-profiling/dashboard-performance-profiling.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/dashboards/performance-profiling/dashboard-performance-profiling.element.ts @@ -2,12 +2,11 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { ApiError, ProblemDetails, ProfilingResource } from '@umbraco-cms/backend-api'; -import { UmbContextConsumerMixin } from '@umbraco-cms/context-api'; -import { UmbNotificationDefaultData, UmbNotificationService } from '@umbraco-cms/services'; +import { UmbResourceController } from '@umbraco-cms/controllers'; +import { ProfilingResource } from '@umbraco-cms/backend-api'; @customElement('umb-dashboard-performance-profiling') -export class UmbDashboardPerformanceProfilingElement extends UmbContextConsumerMixin(LitElement) { +export class UmbDashboardPerformanceProfilingElement extends LitElement { static styles = [ UUITextStyles, css` @@ -31,27 +30,7 @@ export class UmbDashboardPerformanceProfilingElement extends UmbContextConsumerM @state() private _profilingPerfomance = false; - private _notificationService?: UmbNotificationService; - - private async _getProfilingStatus() { - try { - const status = await ProfilingResource.getProfilingStatus(); - this._profilingStatus = status.enabled; - } catch (e) { - if (e instanceof ApiError) { - const error = e as ProblemDetails; - const data: UmbNotificationDefaultData = { message: error.message ?? 'Something went wrong' }; - this._notificationService?.peek('danger', { data }); - } - } - } - - constructor() { - super(); - this.consumeAllContexts(['umbNotificationService'], (instances) => { - this._notificationService = instances['umbNotificationService']; - }); - } + private _resourceController = new UmbResourceController(this); connectedCallback(): void { super.connectedCallback(); @@ -59,6 +38,15 @@ export class UmbDashboardPerformanceProfilingElement extends UmbContextConsumerM this._profilingPerfomance = localStorage.getItem('profilingPerformance') === 'true'; } + private async _getProfilingStatus() { + const [profilingStatus] = await this._resourceController.tryExecuteAndNotify( + ProfilingResource.getProfilingStatus() + ); + if (profilingStatus) { + this._profilingStatus = profilingStatus.enabled; + } + } + private _changeProfilingPerformance() { this._profilingPerfomance = !this._profilingPerfomance; localStorage.setItem('profilingPerformance', this._profilingPerfomance.toString()); diff --git a/src/Umbraco.Web.UI.Client/src/core/controllers/index.ts b/src/Umbraco.Web.UI.Client/src/core/controllers/index.ts new file mode 100644 index 0000000000..b4dfcb2097 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/controllers/index.ts @@ -0,0 +1 @@ +export * from './resource.controller'; diff --git a/src/Umbraco.Web.UI.Client/src/core/controllers/resource.controller.ts b/src/Umbraco.Web.UI.Client/src/core/controllers/resource.controller.ts new file mode 100644 index 0000000000..bff3e44b7b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/controllers/resource.controller.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ReactiveController, ReactiveControllerHost } from 'lit'; +import { ApiError, CancelablePromise, ProblemDetails } from '@umbraco-cms/backend-api'; +import { UmbNotificationOptions, UmbNotificationDefaultData, UmbNotificationService } from '@umbraco-cms/services'; +import { UmbContextConsumer } from '@umbraco-cms/context-api'; + +export class UmbResourceController implements ReactiveController { + host: ReactiveControllerHost; + + #promises: Promise[] = []; + + #notificationConsumer: UmbContextConsumer; + + #notificationService?: UmbNotificationService; + + constructor(host: ReactiveControllerHost) { + (this.host = host).addController(this); + + this.#notificationConsumer = new UmbContextConsumer( + host as unknown as EventTarget, + 'umbNotificationService', + (_instance: UmbNotificationService) => { + this.#notificationService = _instance; + } + ); + } + + hostConnected() { + this.#promises.length = 0; + this.#notificationConsumer.attach(); + } + + hostDisconnected() { + this.cancelAllResources(); + this.#notificationConsumer.detach(); + } + + addResource(promise: Promise): void { + this.#promises.push(promise); + } + + /** + * Execute a given function and get the result as a promise. + */ + execute(func: Promise): Promise { + this.addResource(func); + return func; + } + + /** + * Wrap the {execute} function in a try/catch block and return a tuple with the result and the error. + */ + async tryExecute(func: Promise): Promise<[T | undefined, ProblemDetails | undefined]> { + try { + return [await this.execute(func), undefined]; + } catch (e) { + return [undefined, this.#toProblemDetails(e)]; + } + } + + /** + * Wrap the {execute} function in a try/catch block and return the result. + * If the executor function throws an error, then show the details in a notification. + */ + async tryExecuteAndNotify( + func: Promise, + options?: UmbNotificationOptions + ): Promise<[T | undefined, ProblemDetails | undefined]> { + const [result, error] = await this.tryExecute(func); + + if (error) { + const data: UmbNotificationDefaultData = { + headline: error.title ?? 'Server Error', + message: error.detail ?? 'Something went wrong', + }; + + if (this.#notificationService) { + this.#notificationService?.peek('danger', { data, ...options }); + } else { + console.group('UmbResourceController'); + console.error(error); + console.groupEnd(); + } + } + + return [result, error]; + } + + /** + * Cancel all resources that are currently being executed by this controller if they are cancelable. + * + * This works by checking if the promise is a CancelablePromise and if so, it will call the cancel method. + * + * This is useful when the controller is being disconnected from the DOM. + * + * @see CancelablePromise + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController + */ + cancelAllResources() { + this.#promises.forEach((promise) => { + if (promise instanceof CancelablePromise) { + promise.cancel(); + } + }); + } + + /** + * Extract the ProblemDetails object from an ApiError. + * + * This assumes that all ApiErrors contain a ProblemDetails object in their body. + */ + #toProblemDetails(error: unknown): ProblemDetails | undefined { + if (error instanceof ApiError) { + const errorDetails = error.body as ProblemDetails; + return errorDetails; + } else if (error instanceof Error) { + return { + title: error.name, + detail: error.message, + }; + } + + return undefined; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/core/services/index.ts b/src/Umbraco.Web.UI.Client/src/core/services/index.ts index 9a44d0ea7a..4aae52e06b 100644 --- a/src/Umbraco.Web.UI.Client/src/core/services/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/services/index.ts @@ -3,5 +3,5 @@ /* eslint-disable */ export * from './modal'; -export { UmbNotificationService } from './notification'; +export * from './notification'; export type { UmbNotificationDefaultData } from './notification/layouts/default'; diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 7b360952af..6763dcdfee 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -28,6 +28,7 @@ "@umbraco-cms/observable-api": ["src/core/observable-api"], "@umbraco-cms/utils": ["src/core/utils"], "@umbraco-cms/test-utils": ["src/core/test-utils"], + "@umbraco-cms/controllers": ["src/core/controllers"], "@umbraco-cms/services": ["src/core/services"], "@umbraco-cms/components/*": ["src/backoffice/components/*"], "@umbraco-cms/stores/*": ["src/core/stores/*"], diff --git a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs index a4480d24f7..2167c55a73 100644 --- a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs +++ b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs @@ -16,8 +16,11 @@ export default { '@umbraco-cms/context-api': './src/core/context-api/index.ts', '@umbraco-cms/extensions-api': './src/core/extensions-api/index.ts', '@umbraco-cms/observable-api': './src/core/observable-api/index.ts', + '@umbraco-cms/resource-api': './src/core/resource-api', '@umbraco-cms/utils': './src/core/utils/index.ts', '@umbraco-cms/test-utils': './src/core/test-utils/index.ts', + '@umbraco-cms/controllers': './src/core/controllers', + '@umbraco-cms/services': './src/core/services', '@umbraco-cms/extensions-registry': './src/core/extensions-registry/index.ts', }, },