Extensions: Adds @provideContext and @consumeContext decorators for a better developer experience (#20510)
* feat: adds first draft of a context consume decorator * feat: uses an options pattern * feat: changes approach to use `addInitializer` and `queueMicroTask` instead * feat: adds extra warning if context is consumed on disconnected controllers * feat: example implementation of consume decorator * feat: adds support for 'subscribe' * feat: initial work on provide decorator * docs: adds license to consume decorator * feat: adds support for umbraco controllers with `hostConnected` * feat: uses asPromise to handle one-time subscription instead * test: adds unit tests for consume decorator * feat: adds support for controllers through hostConnected injection * feat: adds support for controllers through hostConnected injection * test: adds unit tests for provide decorator * docs: adds more documentation around usage and adds a few warnings in console when it detects wrong usage * feat: removes unused controllerMap * docs: adds wording on standard vs legacy decorators * docs: clarifies usage around internal state * feat: adds proper return types for decorators * docs: adds more types * feat: makes element optional * feat: makes element optional * feat: uses @consume in the log viewer to showcase * chore: cleans up debug info * feat: renames to `consumeContext` and `provideContext` to stay inline with our own methods * chore: removes unneeded typings * chore: removes not needed check * chore: removes not needed check * test: adds test for rendered value * feat: splits up code into several smaller functions * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * docs: augments code example for creating a context * Update src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
import { UmbContextToken } from '../token/context-token.js';
|
||||||
|
import type { UmbContextMinimal } from '../types.js';
|
||||||
|
import { UmbContextProvider } from '../provide/context-provider.js';
|
||||||
|
import { consumeContext } from './context-consume.decorator.js';
|
||||||
|
import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing';
|
||||||
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
|
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||||
|
import { html, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
|
|
||||||
|
class UmbTestContextConsumerClass implements UmbContextMinimal {
|
||||||
|
public prop: string = 'value from provider';
|
||||||
|
getHostElement() {
|
||||||
|
return undefined as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testToken = new UmbContextToken<UmbTestContextConsumerClass>('my-test-context');
|
||||||
|
|
||||||
|
class MyTestElement extends UmbLitElement {
|
||||||
|
@consumeContext({
|
||||||
|
context: testToken,
|
||||||
|
})
|
||||||
|
@state()
|
||||||
|
contextValue?: UmbTestContextConsumerClass;
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`<div>${this.contextValue?.prop ?? 'no context'}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('my-consume-test-element', MyTestElement);
|
||||||
|
|
||||||
|
describe('@consume decorator', () => {
|
||||||
|
let provider: UmbContextProvider;
|
||||||
|
let element: MyTestElement;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
provider = new UmbContextProvider(document.body, testToken, new UmbTestContextConsumerClass());
|
||||||
|
provider.hostConnected();
|
||||||
|
|
||||||
|
element = await fixture<MyTestElement>(`<my-consume-test-element></my-consume-test-element>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
provider.destroy();
|
||||||
|
(provider as any) = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should receive a context value when provided on the host', () => {
|
||||||
|
expect(element.contextValue).to.equal(provider.providerInstance());
|
||||||
|
expect(element.contextValue?.prop).to.equal('value from provider');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the value from the context', async () => {
|
||||||
|
expect(element).shadowDom.to.equal('<div>value from provider</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work when the decorator is used in a controller', async () => {
|
||||||
|
class MyController extends UmbControllerBase {
|
||||||
|
@consumeContext({ context: testToken })
|
||||||
|
contextValue?: UmbTestContextConsumerClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new MyController(element);
|
||||||
|
|
||||||
|
await elementUpdated(element);
|
||||||
|
|
||||||
|
expect(element.contextValue).to.equal(provider.providerInstance());
|
||||||
|
expect(controller.contextValue).to.equal(provider.providerInstance());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have called the callback first', async () => {
|
||||||
|
let callbackCalled = false;
|
||||||
|
|
||||||
|
class MyCallbackTestElement extends UmbLitElement {
|
||||||
|
@consumeContext({
|
||||||
|
context: testToken,
|
||||||
|
callback: () => {
|
||||||
|
callbackCalled = true;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
contextValue?: UmbTestContextConsumerClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('my-callback-consume-test-element', MyCallbackTestElement);
|
||||||
|
|
||||||
|
const callbackElement = await fixture<MyCallbackTestElement>(
|
||||||
|
`<my-callback-consume-test-element></my-callback-consume-test-element>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await elementUpdated(callbackElement);
|
||||||
|
|
||||||
|
expect(callbackCalled).to.be.true;
|
||||||
|
expect(callbackElement.contextValue).to.equal(provider.providerInstance());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the context value when the provider instance changes', async () => {
|
||||||
|
const newProviderInstance = new UmbTestContextConsumerClass();
|
||||||
|
newProviderInstance.prop = 'new value from provider';
|
||||||
|
|
||||||
|
const newProvider = new UmbContextProvider(element, testToken, newProviderInstance);
|
||||||
|
newProvider.hostConnected();
|
||||||
|
|
||||||
|
await elementUpdated(element);
|
||||||
|
|
||||||
|
expect(element.contextValue).to.equal(newProvider.providerInstance());
|
||||||
|
expect(element.contextValue?.prop).to.equal(newProviderInstance.prop);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to consume without subscribing', async () => {
|
||||||
|
class MyNoSubscribeTestController extends UmbControllerBase {
|
||||||
|
@consumeContext({ context: testToken, subscribe: false })
|
||||||
|
contextValue?: UmbTestContextConsumerClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new MyNoSubscribeTestController(element);
|
||||||
|
await aTimeout(0); // Wait a tick for promise to resolve
|
||||||
|
|
||||||
|
expect(controller.contextValue).to.equal(provider.providerInstance());
|
||||||
|
|
||||||
|
const newProviderInstance = new UmbTestContextConsumerClass();
|
||||||
|
newProviderInstance.prop = 'new value from provider';
|
||||||
|
|
||||||
|
const newProvider = new UmbContextProvider(element, testToken, newProviderInstance);
|
||||||
|
newProvider.hostConnected();
|
||||||
|
|
||||||
|
await aTimeout(0); // Wait a tick for promise to resolve
|
||||||
|
|
||||||
|
// Should still be the old value
|
||||||
|
expect(controller.contextValue).to.not.equal(newProvider.providerInstance());
|
||||||
|
expect(controller.contextValue?.prop).to.equal('value from provider');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
/*
|
||||||
|
* Portions of this code are adapted from @lit/context
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*
|
||||||
|
* Original source: https://github.com/lit/lit/tree/main/packages/context
|
||||||
|
*
|
||||||
|
* @license BSD-3-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import type { UmbContextToken } from '../token/index.js';
|
||||||
|
import type { UmbContextMinimal } from '../types.js';
|
||||||
|
import { UmbContextConsumerController } from './context-consumer.controller.js';
|
||||||
|
import type { UmbContextCallback } from './context-request.event.js';
|
||||||
|
|
||||||
|
export interface UmbConsumeOptions<
|
||||||
|
BaseType extends UmbContextMinimal = UmbContextMinimal,
|
||||||
|
ResultType extends BaseType = BaseType,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The context to consume, either as a string alias or an UmbContextToken.
|
||||||
|
*/
|
||||||
|
context: string | UmbContextToken<BaseType, ResultType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional callback that is invoked when the context value is set or changes.
|
||||||
|
* Note, the class instance is probably not fully constructed when this is first invoked.
|
||||||
|
* If you need to ensure the class is fully constructed, consider using a setter on the property instead.
|
||||||
|
*/
|
||||||
|
callback?: UmbContextCallback<ResultType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the context consumer will stay active and invoke the callback on context changes.
|
||||||
|
* If false, the context consumer will use asPromise() to get the value once and then clean up.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
subscribe?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A property decorator that adds an UmbContextConsumerController to the component
|
||||||
|
* which will try and retrieve a value for the property via the Umbraco Context API.
|
||||||
|
*
|
||||||
|
* This decorator supports both modern "standard" decorators (Stage 3 TC39 proposal) and
|
||||||
|
* legacy TypeScript experimental decorators for backward compatibility.
|
||||||
|
*
|
||||||
|
* @param {UmbConsumeOptions} options Configuration object containing context, callback, and subscribe options
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import {consumeContext} from '@umbraco-cms/backoffice/context-api';
|
||||||
|
* import {UMB_WORKSPACE_CONTEXT} from './workspace.context-token.js';
|
||||||
|
*
|
||||||
|
* class MyElement extends UmbLitElement {
|
||||||
|
* // Standard decorators (with 'accessor' keyword) - Modern approach
|
||||||
|
* @consumeContext({context: UMB_WORKSPACE_CONTEXT})
|
||||||
|
* accessor workspaceContext?: UmbWorkspaceContext;
|
||||||
|
*
|
||||||
|
* // Legacy decorators (without 'accessor') - Works with @state/@property
|
||||||
|
* @consumeContext({context: UMB_USER_CONTEXT, subscribe: false})
|
||||||
|
* @state()
|
||||||
|
* currentUser?: UmbUserContext;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* @returns {ConsumeDecorator<ResultType>} A property decorator function
|
||||||
|
*/
|
||||||
|
export function consumeContext<
|
||||||
|
BaseType extends UmbContextMinimal = UmbContextMinimal,
|
||||||
|
ResultType extends BaseType = BaseType,
|
||||||
|
>(options: UmbConsumeOptions<BaseType, ResultType>): ConsumeDecorator<ResultType> {
|
||||||
|
const { context, callback, subscribe = true } = options;
|
||||||
|
|
||||||
|
return ((protoOrTarget: any, nameOrContext: PropertyKey | ClassAccessorDecoratorContext<any, ResultType>) => {
|
||||||
|
if (typeof nameOrContext === 'object') {
|
||||||
|
setupStandardDecorator(protoOrTarget, nameOrContext, context, callback, subscribe);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLegacyDecorator(protoOrTarget, nameOrContext as string, context, callback, subscribe);
|
||||||
|
}) as ConsumeDecorator<ResultType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a standard decorator (Stage 3 TC39 proposal) for auto-accessors.
|
||||||
|
* This branch is used when decorating with the 'accessor' keyword.
|
||||||
|
* Example: @consumeContext({context: TOKEN}) accessor myProp?: Type;
|
||||||
|
*
|
||||||
|
* The decorator receives a ClassAccessorDecoratorContext object which provides
|
||||||
|
* addInitializer() to run code during class construction.
|
||||||
|
*
|
||||||
|
* This is the modern, standardized decorator API that will be the standard
|
||||||
|
* when Lit 4.x is released.
|
||||||
|
*
|
||||||
|
* Note: Standard decorators currently don't work with @state()/@property()
|
||||||
|
* decorators, which is why we still need the legacy branch.
|
||||||
|
*/
|
||||||
|
function setupStandardDecorator<BaseType extends UmbContextMinimal, ResultType extends BaseType>(
|
||||||
|
protoOrTarget: any,
|
||||||
|
decoratorContext: ClassAccessorDecoratorContext<any, ResultType>,
|
||||||
|
context: string | UmbContextToken<BaseType, ResultType>,
|
||||||
|
callback: UmbContextCallback<ResultType> | undefined,
|
||||||
|
subscribe: boolean,
|
||||||
|
): void {
|
||||||
|
if (!('addInitializer' in decoratorContext)) {
|
||||||
|
console.warn(
|
||||||
|
'@consumeContext decorator: Standard decorator context does not support addInitializer. ' +
|
||||||
|
'This should not happen with modern decorators.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
decoratorContext.addInitializer(function () {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (subscribe) {
|
||||||
|
// Continuous subscription - stays active and updates property on context changes
|
||||||
|
new UmbContextConsumerController(this, context, (value) => {
|
||||||
|
protoOrTarget.set.call(this, value);
|
||||||
|
callback?.(value);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// One-time consumption - uses asPromise() to get the value once and then cleans up
|
||||||
|
const controller = new UmbContextConsumerController(this, context, callback);
|
||||||
|
controller.asPromise().then((value) => {
|
||||||
|
protoOrTarget.set.call(this, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a legacy decorator (TypeScript experimental) for regular properties.
|
||||||
|
* This branch is used when decorating without the 'accessor' keyword.
|
||||||
|
* Example: @consumeContext({context: TOKEN}) @state() myProp?: Type;
|
||||||
|
*
|
||||||
|
* The decorator receives:
|
||||||
|
* - protoOrTarget: The class prototype
|
||||||
|
* - propertyKey: The property name (string)
|
||||||
|
*
|
||||||
|
* This is the older TypeScript experimental decorator API, still widely used
|
||||||
|
* in Umbraco because it works with @state() and @property() decorators.
|
||||||
|
* The 'accessor' keyword is not compatible with these decorators yet.
|
||||||
|
*
|
||||||
|
* We support three initialization strategies:
|
||||||
|
* 1. addInitializer (if available, e.g., on LitElement classes)
|
||||||
|
* 2. hostConnected wrapper (for UmbController classes)
|
||||||
|
* 3. Warning (if neither is available)
|
||||||
|
*/
|
||||||
|
function setupLegacyDecorator<BaseType extends UmbContextMinimal, ResultType extends BaseType>(
|
||||||
|
protoOrTarget: any,
|
||||||
|
propertyKey: string,
|
||||||
|
context: string | UmbContextToken<BaseType, ResultType>,
|
||||||
|
callback: UmbContextCallback<ResultType> | undefined,
|
||||||
|
subscribe: boolean,
|
||||||
|
): void {
|
||||||
|
const constructor = protoOrTarget.constructor as any;
|
||||||
|
|
||||||
|
// Strategy 1: Use addInitializer if available (LitElement classes)
|
||||||
|
if (constructor.addInitializer) {
|
||||||
|
constructor.addInitializer((element: any): void => {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (subscribe) {
|
||||||
|
// Continuous subscription
|
||||||
|
new UmbContextConsumerController(element, context, (value) => {
|
||||||
|
element[propertyKey] = value;
|
||||||
|
callback?.(value);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// One-time consumption using asPromise()
|
||||||
|
const controller = new UmbContextConsumerController(element, context, callback);
|
||||||
|
controller.asPromise().then((value) => {
|
||||||
|
element[propertyKey] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Wrap hostConnected for UmbController classes without addInitializer
|
||||||
|
if ('hostConnected' in protoOrTarget && typeof protoOrTarget.hostConnected === 'function') {
|
||||||
|
const originalHostConnected = protoOrTarget.hostConnected;
|
||||||
|
|
||||||
|
protoOrTarget.hostConnected = function (this: any) {
|
||||||
|
// Set up consumer once, using a flag to prevent multiple setups
|
||||||
|
if (!this.__consumeControllers) {
|
||||||
|
this.__consumeControllers = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.__consumeControllers.has(propertyKey)) {
|
||||||
|
if (subscribe) {
|
||||||
|
// Continuous subscription
|
||||||
|
const controller = new UmbContextConsumerController(this, context, (value) => {
|
||||||
|
this[propertyKey] = value;
|
||||||
|
callback?.(value);
|
||||||
|
});
|
||||||
|
this.__consumeControllers.set(propertyKey, controller);
|
||||||
|
} else {
|
||||||
|
// One-time consumption using asPromise()
|
||||||
|
const controller = new UmbContextConsumerController(this, context, callback);
|
||||||
|
controller.asPromise().then((value) => {
|
||||||
|
this[propertyKey] = value;
|
||||||
|
});
|
||||||
|
// Don't store in map since it cleans itself up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call original hostConnected if it exists
|
||||||
|
originalHostConnected?.call(this);
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: No supported initialization method available
|
||||||
|
console.warn(
|
||||||
|
`@consumeContext applied to ${constructor.name}.${propertyKey} but neither addInitializer nor hostConnected is available. ` +
|
||||||
|
`Make sure the class extends UmbLitElement, UmbControllerBase, or implements UmbController with hostConnected.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a public interface type that removes private and protected fields.
|
||||||
|
* This allows accepting otherwise incompatible versions of the type (e.g. from
|
||||||
|
* multiple copies of the same package in `node_modules`).
|
||||||
|
*/
|
||||||
|
type Interface<T> = {
|
||||||
|
[K in keyof T]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
declare class ReactiveElement {
|
||||||
|
static addInitializer?: (initializer: (instance: any) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ReactiveController {
|
||||||
|
hostConnected?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type representing the base class of which the decorator should work
|
||||||
|
* requiring either addInitializer (UmbLitElement) or hostConnected (UmbController).
|
||||||
|
*/
|
||||||
|
type ReactiveEntity = ReactiveElement | ReactiveController;
|
||||||
|
|
||||||
|
type ConsumeDecorator<ValueType> = {
|
||||||
|
// legacy
|
||||||
|
<K extends PropertyKey, Proto extends Interface<ReactiveEntity>>(
|
||||||
|
protoOrDescriptor: Proto,
|
||||||
|
name?: K,
|
||||||
|
): FieldMustMatchProvidedType<Proto, K, ValueType>;
|
||||||
|
|
||||||
|
// standard
|
||||||
|
<C extends Interface<ReactiveEntity>, V extends ValueType>(
|
||||||
|
value: ClassAccessorDecoratorTarget<C, V>,
|
||||||
|
context: ClassAccessorDecoratorContext<C, V>,
|
||||||
|
): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note TypeScript requires the return type of a decorator to be `void | any`
|
||||||
|
type DecoratorReturn = void | any;
|
||||||
|
|
||||||
|
type FieldMustMatchProvidedType<Obj, Key extends PropertyKey, ProvidedType> =
|
||||||
|
// First we check whether the object has the property as a required field
|
||||||
|
Obj extends Record<Key, infer ConsumingType>
|
||||||
|
? // Ok, it does, just check whether it's ok to assign the
|
||||||
|
// provided type to the consuming field
|
||||||
|
[ProvidedType] extends [ConsumingType]
|
||||||
|
? DecoratorReturn
|
||||||
|
: {
|
||||||
|
message: 'provided type not assignable to consuming field';
|
||||||
|
provided: ProvidedType;
|
||||||
|
consuming: ConsumingType;
|
||||||
|
}
|
||||||
|
: // Next we check whether the object has the property as an optional field
|
||||||
|
Obj extends Partial<Record<Key, infer ConsumingType>>
|
||||||
|
? // Check assignability again. Note that we have to include undefined
|
||||||
|
// here on the consuming type because it's optional.
|
||||||
|
[ProvidedType] extends [ConsumingType | undefined]
|
||||||
|
? DecoratorReturn
|
||||||
|
: {
|
||||||
|
message: 'provided type not assignable to consuming field';
|
||||||
|
provided: ProvidedType;
|
||||||
|
consuming: ConsumingType | undefined;
|
||||||
|
}
|
||||||
|
: // Ok, the field isn't present, so either someone's using consume
|
||||||
|
// manually, i.e. not as a decorator (maybe don't do that! but if you do,
|
||||||
|
// you're on your own for your type checking, sorry), or the field is
|
||||||
|
// private, in which case we can't check it.
|
||||||
|
DecoratorReturn;
|
||||||
@@ -191,13 +191,21 @@ export class UmbContextConsumer<
|
|||||||
cancelAnimationFrame(this.#raf);
|
cancelAnimationFrame(this.#raf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hostElement = this._retrieveHost();
|
||||||
|
|
||||||
|
// Add connection check to prevent requesting on disconnected elements
|
||||||
|
if (hostElement && !hostElement.isConnected) {
|
||||||
|
console.warn('UmbContextConsumer: Attempting to request context on disconnected element', hostElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const event = new UmbContextRequestEventImplementation(
|
const event = new UmbContextRequestEventImplementation(
|
||||||
this.#contextAlias,
|
this.#contextAlias,
|
||||||
this.#apiAlias,
|
this.#apiAlias,
|
||||||
this._onResponse,
|
this._onResponse,
|
||||||
this.#stopAtContextMatch,
|
this.#stopAtContextMatch,
|
||||||
);
|
);
|
||||||
(this.#skipHost ? this._retrieveHost()?.parentNode : this._retrieveHost())?.dispatchEvent(event);
|
(this.#skipHost ? hostElement?.parentNode : hostElement)?.dispatchEvent(event);
|
||||||
|
|
||||||
if (this.#promiseResolver && this.#promiseOptions?.preventTimeout !== true) {
|
if (this.#promiseResolver && this.#promiseOptions?.preventTimeout !== true) {
|
||||||
this.#raf = requestAnimationFrame(() => {
|
this.#raf = requestAnimationFrame(() => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './context-consume.decorator.js';
|
||||||
export * from './context-consumer.controller.js';
|
export * from './context-consumer.controller.js';
|
||||||
export * from './context-consumer.js';
|
export * from './context-consumer.js';
|
||||||
export * from './context-request.event.js';
|
export * from './context-request.event.js';
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { UmbContextToken } from '../token/context-token.js';
|
||||||
|
import type { UmbContextMinimal } from '../types.js';
|
||||||
|
import { provideContext } from './context-provide.decorator.js';
|
||||||
|
import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing';
|
||||||
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
|
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||||
|
|
||||||
|
class UmbTestContextConsumerClass implements UmbContextMinimal {
|
||||||
|
public prop: string;
|
||||||
|
|
||||||
|
constructor(initialValue = 'value from provider') {
|
||||||
|
this.prop = initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHostElement() {
|
||||||
|
return document.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testToken = new UmbContextToken<UmbTestContextConsumerClass>('my-test-context', 'testApi');
|
||||||
|
|
||||||
|
class MyTestRootElement extends UmbLitElement {
|
||||||
|
@provideContext({ context: testToken })
|
||||||
|
providerInstance = new UmbTestContextConsumerClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('my-provide-test-element', MyTestRootElement);
|
||||||
|
|
||||||
|
class MyTestElement extends UmbLitElement {
|
||||||
|
contextValue?: UmbTestContextConsumerClass;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.consumeContext(testToken, (value) => {
|
||||||
|
this.contextValue = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('my-consume-test-element', MyTestElement);
|
||||||
|
|
||||||
|
describe('@provide decorator', () => {
|
||||||
|
let rootElement: MyTestRootElement;
|
||||||
|
let element: MyTestElement;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
rootElement = await fixture<MyTestRootElement>(
|
||||||
|
`<my-provide-test-element><my-consume-test-element></my-consume-test-element></my-provide-test-element>`,
|
||||||
|
);
|
||||||
|
element = rootElement.querySelector('my-consume-test-element') as MyTestElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {});
|
||||||
|
|
||||||
|
it('should receive a context value when provided on the host', () => {
|
||||||
|
expect(element.contextValue).to.equal(rootElement.providerInstance);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work when the decorator is used in a controller', async () => {
|
||||||
|
class MyController extends UmbControllerBase {
|
||||||
|
@provideContext({ context: testToken })
|
||||||
|
providerInstance = new UmbTestContextConsumerClass('new value');
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new MyController(element);
|
||||||
|
|
||||||
|
await elementUpdated(element);
|
||||||
|
|
||||||
|
expect(element.contextValue).to.equal(controller.providerInstance);
|
||||||
|
expect(controller.providerInstance.prop).to.equal('new value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update the instance when the property changes', async () => {
|
||||||
|
// we do not support setting a new value on a provided property
|
||||||
|
// as it would require a lot more logic to handle updating the context consumers
|
||||||
|
// So for now we just warn the user that this is not supported
|
||||||
|
// This might be revisited in the future if there is a need for it
|
||||||
|
|
||||||
|
const originalProviderInstance = rootElement.providerInstance;
|
||||||
|
|
||||||
|
const newProviderInstance = new UmbTestContextConsumerClass('new value from provider');
|
||||||
|
rootElement.providerInstance = newProviderInstance;
|
||||||
|
|
||||||
|
await aTimeout(0);
|
||||||
|
|
||||||
|
expect(element.contextValue).to.equal(originalProviderInstance);
|
||||||
|
expect(element.contextValue?.prop).to.equal(originalProviderInstance.prop);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the context value when the provider instance is replaced', async () => {
|
||||||
|
const newProviderInstance = new UmbTestContextConsumerClass();
|
||||||
|
newProviderInstance.prop = 'new value from provider';
|
||||||
|
|
||||||
|
class MyUpdateTestElement extends UmbLitElement {
|
||||||
|
@provideContext({ context: testToken })
|
||||||
|
providerInstance = newProviderInstance;
|
||||||
|
}
|
||||||
|
customElements.define('my-update-provide-test-element', MyUpdateTestElement);
|
||||||
|
|
||||||
|
const newProvider = await fixture<MyUpdateTestElement>(
|
||||||
|
`<my-update-provide-test-element><my-consume-test-element></my-consume-test-element></my-update-provide-test-element>`,
|
||||||
|
);
|
||||||
|
const element = newProvider.querySelector('my-consume-test-element') as MyTestElement;
|
||||||
|
|
||||||
|
await elementUpdated(element);
|
||||||
|
|
||||||
|
expect(element.contextValue).to.equal(newProviderInstance);
|
||||||
|
expect(element.contextValue?.prop).to.equal(newProviderInstance.prop);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* Portions of this code are adapted from @lit/context
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*
|
||||||
|
* Original source: https://github.com/lit/lit/tree/main/packages/context
|
||||||
|
*
|
||||||
|
* @license BSD-3-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import type { UmbContextToken } from '../token/index.js';
|
||||||
|
import type { UmbContextMinimal } from '../types.js';
|
||||||
|
import { UmbContextProviderController } from './context-provider.controller.js';
|
||||||
|
|
||||||
|
export interface UmbProvideOptions<BaseType extends UmbContextMinimal, ResultType extends BaseType> {
|
||||||
|
context: string | UmbContextToken<BaseType, ResultType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A property decorator that creates an UmbContextProviderController to provide
|
||||||
|
* a context value to child elements via the Umbraco Context API.
|
||||||
|
*
|
||||||
|
* This decorator supports both modern "standard" decorators (Stage 3 TC39 proposal) and
|
||||||
|
* legacy TypeScript experimental decorators for backward compatibility.
|
||||||
|
*
|
||||||
|
* The provider is created once during initialization with the property's initial value.
|
||||||
|
* To update the provided value dynamically, keep a state inside the provided context instance
|
||||||
|
* and update that state as needed. The context instance itself should remain the same.
|
||||||
|
* You can use any of the Umb{*}State classes.
|
||||||
|
*
|
||||||
|
* @param {UmbProvideOptions} options Configuration object containing the context token
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import {provideContext} from '@umbraco-cms/backoffice/context-api';
|
||||||
|
* import {UMB_WORKSPACE_CONTEXT} from './workspace.context-token.js';
|
||||||
|
*
|
||||||
|
* class MyWorkspaceElement extends UmbLitElement {
|
||||||
|
* // Standard decorators - requires 'accessor' keyword
|
||||||
|
* @provideContext({context: UMB_WORKSPACE_CONTEXT})
|
||||||
|
* accessor workspaceContext = new UmbWorkspaceContext(this);
|
||||||
|
*
|
||||||
|
* // Legacy decorators - works without 'accessor'
|
||||||
|
* @provideContext({context: UMB_WORKSPACE_CONTEXT})
|
||||||
|
* workspaceContext = new UmbWorkspaceContext(this);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // For dynamic updates, store the state inside the context instance
|
||||||
|
* class MyContext extends UmbControllerBase {
|
||||||
|
* someProperty = new UmbStringState('initial value');
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* class MyElement extends UmbLitElement {
|
||||||
|
* @provideContext({context: MY_CONTEXT})
|
||||||
|
* private _myContext = new MyContext(this);
|
||||||
|
*
|
||||||
|
* updateValue(newValue: string) {
|
||||||
|
* this._myContext.someProperty.setValue(newValue);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @returns {ProvideDecorator<InstanceType>} A property decorator function
|
||||||
|
*/
|
||||||
|
export function provideContext<
|
||||||
|
BaseType extends UmbContextMinimal = UmbContextMinimal,
|
||||||
|
ResultType extends BaseType = BaseType,
|
||||||
|
InstanceType extends ResultType = ResultType,
|
||||||
|
>(options: UmbProvideOptions<BaseType, ResultType>): ProvideDecorator<InstanceType> {
|
||||||
|
const { context } = options;
|
||||||
|
|
||||||
|
return ((
|
||||||
|
protoOrTarget: any,
|
||||||
|
nameOrContext: PropertyKey | ClassAccessorDecoratorContext<any, InstanceType>,
|
||||||
|
): void | any => {
|
||||||
|
if (typeof nameOrContext === 'object') {
|
||||||
|
return setupStandardDecorator(protoOrTarget, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLegacyDecorator(protoOrTarget, nameOrContext as string, context);
|
||||||
|
}) as ProvideDecorator<ResultType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a standard decorator (Stage 3 TC39 proposal) for auto-accessors.
|
||||||
|
* This branch is used when decorating with the 'accessor' keyword.
|
||||||
|
* Example: @provideContext({context: TOKEN}) accessor myProp = new MyContext();
|
||||||
|
*
|
||||||
|
* The decorator receives a ClassAccessorDecoratorContext object and returns
|
||||||
|
* an accessor descriptor that intercepts the property initialization.
|
||||||
|
*
|
||||||
|
* This is the modern, standardized decorator API that will be the standard
|
||||||
|
* when Lit 4.x is released.
|
||||||
|
*
|
||||||
|
* Note: Standard decorators currently don't work with @state()/@property()
|
||||||
|
* decorators, which is why we still need the legacy branch.
|
||||||
|
*/
|
||||||
|
function setupStandardDecorator<
|
||||||
|
BaseType extends UmbContextMinimal,
|
||||||
|
ResultType extends BaseType,
|
||||||
|
InstanceType extends ResultType,
|
||||||
|
>(protoOrTarget: any, context: string | UmbContextToken<BaseType, ResultType>) {
|
||||||
|
return {
|
||||||
|
get(this: any) {
|
||||||
|
return protoOrTarget.get.call(this);
|
||||||
|
},
|
||||||
|
set(this: any, value: InstanceType) {
|
||||||
|
return protoOrTarget.set.call(this, value);
|
||||||
|
},
|
||||||
|
init(this: any, value: InstanceType) {
|
||||||
|
// Defer controller creation to avoid timing issues with private fields
|
||||||
|
queueMicrotask(() => {
|
||||||
|
new UmbContextProviderController<BaseType, ResultType, InstanceType>(this, context, value);
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a legacy decorator (TypeScript experimental) for regular properties.
|
||||||
|
* This branch is used when decorating without the 'accessor' keyword.
|
||||||
|
* Example: @provideContext({context: TOKEN}) myProp = new MyContext();
|
||||||
|
*
|
||||||
|
* The decorator receives:
|
||||||
|
* - protoOrTarget: The class prototype
|
||||||
|
* - propertyKey: The property name (string)
|
||||||
|
*
|
||||||
|
* This is the older TypeScript experimental decorator API, still widely used
|
||||||
|
* in Umbraco because it works with @state() and @property() decorators.
|
||||||
|
* The 'accessor' keyword is not compatible with these decorators yet.
|
||||||
|
*
|
||||||
|
* We support three initialization strategies:
|
||||||
|
* 1. addInitializer (if available, e.g., on LitElement classes)
|
||||||
|
* 2. hostConnected wrapper (for UmbController classes)
|
||||||
|
* 3. Warning (if neither is available)
|
||||||
|
*/
|
||||||
|
function setupLegacyDecorator<
|
||||||
|
BaseType extends UmbContextMinimal,
|
||||||
|
ResultType extends BaseType,
|
||||||
|
InstanceType extends ResultType,
|
||||||
|
>(protoOrTarget: any, propertyKey: string, context: string | UmbContextToken<BaseType, ResultType>): void {
|
||||||
|
const constructor = protoOrTarget.constructor as any;
|
||||||
|
|
||||||
|
// Strategy 1: Use addInitializer if available (LitElement classes)
|
||||||
|
if (constructor.addInitializer) {
|
||||||
|
constructor.addInitializer((element: any): void => {
|
||||||
|
// Defer controller creation to avoid timing issues with private fields
|
||||||
|
queueMicrotask(() => {
|
||||||
|
const initialValue = element[propertyKey];
|
||||||
|
new UmbContextProviderController<BaseType, ResultType, InstanceType>(element, context, initialValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Wrap hostConnected for UmbController classes without addInitializer
|
||||||
|
if ('hostConnected' in protoOrTarget && typeof protoOrTarget.hostConnected === 'function') {
|
||||||
|
const originalHostConnected = protoOrTarget.hostConnected;
|
||||||
|
|
||||||
|
protoOrTarget.hostConnected = function (this: any) {
|
||||||
|
// Set up provider once, using a flag to prevent multiple setups
|
||||||
|
if (!this.__provideControllers) {
|
||||||
|
this.__provideControllers = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.__provideControllers.has(propertyKey)) {
|
||||||
|
const initialValue = this[propertyKey];
|
||||||
|
new UmbContextProviderController<BaseType, ResultType, InstanceType>(this, context, initialValue);
|
||||||
|
// Mark as set up to prevent duplicate providers
|
||||||
|
this.__provideControllers.set(propertyKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call original hostConnected if it exists
|
||||||
|
originalHostConnected?.call(this);
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: No supported initialization method available
|
||||||
|
console.warn(
|
||||||
|
`@provideContext applied to ${constructor.name}.${propertyKey} but neither addInitializer nor hostConnected is available. ` +
|
||||||
|
`Make sure the class extends UmbLitElement, UmbControllerBase, or implements UmbController with hostConnected.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a public interface type that removes private and protected fields.
|
||||||
|
* This allows accepting otherwise compatible versions of the type (e.g. from
|
||||||
|
* multiple copies of the same package in `node_modules`).
|
||||||
|
*/
|
||||||
|
type Interface<T> = {
|
||||||
|
[K in keyof T]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
declare class ReactiveElement {
|
||||||
|
static addInitializer?: (initializer: (instance: any) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class ReactiveController {
|
||||||
|
hostConnected?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type representing the base class of which the decorator should work
|
||||||
|
* requiring either addInitializer (UmbLitElement) or hostConnected (UmbController).
|
||||||
|
*/
|
||||||
|
type ReactiveEntity = ReactiveElement | ReactiveController;
|
||||||
|
|
||||||
|
type ProvideDecorator<ContextType> = {
|
||||||
|
// legacy
|
||||||
|
<K extends PropertyKey, Proto extends Interface<ReactiveEntity>>(
|
||||||
|
protoOrDescriptor: Proto,
|
||||||
|
name?: K,
|
||||||
|
): FieldMustMatchContextType<Proto, K, ContextType>;
|
||||||
|
|
||||||
|
// standard
|
||||||
|
<C extends Interface<ReactiveEntity>, V extends ContextType>(
|
||||||
|
value: ClassAccessorDecoratorTarget<C, V>,
|
||||||
|
context: ClassAccessorDecoratorContext<C, V>,
|
||||||
|
): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note TypeScript requires the return type of a decorator to be `void | any`
|
||||||
|
type DecoratorReturn = void | any;
|
||||||
|
|
||||||
|
type FieldMustMatchContextType<Obj, Key extends PropertyKey, ContextType> =
|
||||||
|
// First we check whether the object has the property as a required field
|
||||||
|
Obj extends Record<Key, infer ProvidingType>
|
||||||
|
? // Ok, it does, just check whether it's ok to assign the
|
||||||
|
// provided type to the consuming field
|
||||||
|
[ProvidingType] extends [ContextType]
|
||||||
|
? DecoratorReturn
|
||||||
|
: {
|
||||||
|
message: 'providing field not assignable to context';
|
||||||
|
context: ContextType;
|
||||||
|
provided: ProvidingType;
|
||||||
|
}
|
||||||
|
: // Next we check whether the object has the property as an optional field
|
||||||
|
Obj extends Partial<Record<Key, infer Providing>>
|
||||||
|
? // Check assignability again. Note that we have to include undefined
|
||||||
|
// here on the providing type because it's optional.
|
||||||
|
[Providing | undefined] extends [ContextType]
|
||||||
|
? DecoratorReturn
|
||||||
|
: {
|
||||||
|
message: 'providing field not assignable to context';
|
||||||
|
context: ContextType;
|
||||||
|
consuming: Providing | undefined;
|
||||||
|
}
|
||||||
|
: // Ok, the field isn't present, so either someone's using provide
|
||||||
|
// manually, i.e. not as a decorator (maybe don't do that! but if you do,
|
||||||
|
// you're on your own for your type checking, sorry), or the field is
|
||||||
|
// private, in which case we can't check it.
|
||||||
|
DecoratorReturn;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './context-provide.decorator.js';
|
||||||
export * from './context-boundary.js';
|
export * from './context-boundary.js';
|
||||||
export * from './context-boundary.controller.js';
|
export * from './context-boundary.controller.js';
|
||||||
export * from './context-provide.event.js';
|
export * from './context-provide.event.js';
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class UmbLogViewerMessagesData extends UmbMockDBBase<LogMessageResponseModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLevelCount() {
|
getLevelCount() {
|
||||||
const levels = this.data.map((log) => log.level ?? 'unknown');
|
const levels = this.data.map((log) => log.level?.toLowerCase() ?? 'unknown');
|
||||||
const counts = {};
|
const counts = {};
|
||||||
levels.forEach((level: string) => {
|
levels.forEach((level: string) => {
|
||||||
//eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
//eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { UmbLogViewerDateRange, UmbLogViewerWorkspaceContext } from '../workspace/logviewer-workspace.context.js';
|
import type { UmbLogViewerDateRange } from '../workspace/logviewer-workspace.context.js';
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../workspace/logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../workspace/logviewer-workspace.context-token.js';
|
||||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||||
import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import { query as getQuery, path, toQueryString } from '@umbraco-cms/backoffice/router';
|
import { query as getQuery, path, toQueryString } from '@umbraco-cms/backoffice/router';
|
||||||
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-date-range-selector')
|
@customElement('umb-log-viewer-date-range-selector')
|
||||||
export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
||||||
@@ -17,20 +18,20 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
|||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
horizontal = false;
|
horizontal = false;
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
|
|
||||||
constructor() {
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
super();
|
private set _logViewerContext(value) {
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext = instance;
|
this.#observeStuff();
|
||||||
this.#observeStuff();
|
}
|
||||||
});
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeStuff() {
|
#observeStuff() {
|
||||||
if (!this.#logViewerContext) return;
|
|
||||||
this.observe(
|
this.observe(
|
||||||
this.#logViewerContext.dateRange,
|
this._logViewerContext?.dateRange,
|
||||||
(dateRange: UmbLogViewerDateRange) => {
|
(dateRange: UmbLogViewerDateRange) => {
|
||||||
this._startDate = dateRange.startDate;
|
this._startDate = dateRange.startDate;
|
||||||
this._endDate = dateRange.endDate;
|
this._endDate = dateRange.endDate;
|
||||||
@@ -50,7 +51,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#updateFiltered() {
|
#updateFiltered() {
|
||||||
this.#logViewerContext?.setDateRange({ startDate: this._startDate, endDate: this._endDate });
|
this._logViewerContext?.setDateRange({ startDate: this._startDate, endDate: this._endDate });
|
||||||
|
|
||||||
const query = getQuery();
|
const query = getQuery();
|
||||||
const qs = toQueryString({
|
const qs = toQueryString({
|
||||||
@@ -71,7 +72,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
|||||||
id="start-date"
|
id="start-date"
|
||||||
type="date"
|
type="date"
|
||||||
label="From"
|
label="From"
|
||||||
.max=${this.#logViewerContext?.today ?? ''}
|
.max=${this._logViewerContext?.today ?? ''}
|
||||||
.value=${this._startDate}></umb-input-date>
|
.value=${this._startDate}></umb-input-date>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
@@ -82,7 +83,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
|||||||
type="date"
|
type="date"
|
||||||
label="To"
|
label="To"
|
||||||
.min=${this._startDate}
|
.min=${this._startDate}
|
||||||
.max=${this.#logViewerContext?.today ?? ''}
|
.max=${this._logViewerContext?.today ?? ''}
|
||||||
.value=${this._endDate}></umb-input-date>
|
.value=${this._endDate}></umb-input-date>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||||
import { html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
import { html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import type { LoggerResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
import type { LoggerResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-log-level-overview')
|
@customElement('umb-log-viewer-log-level-overview')
|
||||||
export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement {
|
export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement {
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
constructor() {
|
|
||||||
super();
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
private set _logViewerContext(value) {
|
||||||
this.#logViewerContext = instance;
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext?.getSavedSearches();
|
this.#logViewerContext?.getSavedSearches();
|
||||||
this.#observeLogLevels();
|
this.#observeLogLevels();
|
||||||
});
|
}
|
||||||
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _loggers: LoggerResponseModel[] = [];
|
private _loggers: LoggerResponseModel[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the logger to get the level for. Defaults to 'Global'.
|
* The name of the logger to get the level for. Defaults to 'Global'.
|
||||||
* @memberof UmbLogViewerLogLevelOverviewElement
|
* @memberof UmbLogViewerLogLevelOverviewElement
|
||||||
@@ -26,8 +29,7 @@ export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement {
|
|||||||
loggerName = 'Global';
|
loggerName = 'Global';
|
||||||
|
|
||||||
#observeLogLevels() {
|
#observeLogLevels() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.loggers, (loggers) => {
|
||||||
this.observe(this.#logViewerContext.loggers, (loggers) => {
|
|
||||||
this._loggers = loggers ?? [];
|
this._loggers = loggers ?? [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import type { LogLevelCountsReponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
import type { LogLevelCountsReponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-log-types-chart')
|
@customElement('umb-log-viewer-log-types-chart')
|
||||||
export class UmbLogViewerLogTypesChartElement extends UmbLitElement {
|
export class UmbLogViewerLogTypesChartElement extends UmbLitElement {
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
constructor() {
|
|
||||||
super();
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
private set _logViewerContext(value) {
|
||||||
this.#logViewerContext = instance;
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext?.getLogCount();
|
this.#logViewerContext?.getLogCount();
|
||||||
this.#observeStuff();
|
this.#observeStuff();
|
||||||
});
|
}
|
||||||
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@@ -47,8 +49,7 @@ export class UmbLogViewerLogTypesChartElement extends UmbLitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#observeStuff() {
|
#observeStuff() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.logCount, (logLevel) => {
|
||||||
this.observe(this.#logViewerContext.logCount, (logLevel) => {
|
|
||||||
this._logLevelCountResponse = logLevel ?? null;
|
this._logLevelCountResponse = logLevel ?? null;
|
||||||
this.setLogLevelCount();
|
this.setLogLevelCount();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||||
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import type { LogTemplateResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
import type { LogTemplateResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||||
import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
|
import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-message-templates-overview')
|
@customElement('umb-log-viewer-message-templates-overview')
|
||||||
export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement {
|
export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement {
|
||||||
@@ -17,19 +17,20 @@ export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement {
|
|||||||
@state()
|
@state()
|
||||||
private _messageTemplates: Array<LogTemplateResponseModel> = [];
|
private _messageTemplates: Array<LogTemplateResponseModel> = [];
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
constructor() {
|
|
||||||
super();
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
private set _logViewerContext(value) {
|
||||||
this.#logViewerContext = instance;
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext?.getMessageTemplates(0, this.#itemsPerPage);
|
this.#getMessageTemplates();
|
||||||
this.#observeStuff();
|
this.#observeStuff();
|
||||||
});
|
}
|
||||||
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeStuff() {
|
#observeStuff() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.messageTemplates, (templates) => {
|
||||||
this.observe(this.#logViewerContext.messageTemplates, (templates) => {
|
|
||||||
this._messageTemplates = templates?.items ?? [];
|
this._messageTemplates = templates?.items ?? [];
|
||||||
this._total = templates?.total ?? 0;
|
this._total = templates?.total ?? 0;
|
||||||
});
|
});
|
||||||
@@ -37,7 +38,7 @@ export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement {
|
|||||||
|
|
||||||
#getMessageTemplates() {
|
#getMessageTemplates() {
|
||||||
const skip = this.#currentPage * this.#itemsPerPage - this.#itemsPerPage;
|
const skip = this.#currentPage * this.#itemsPerPage - this.#itemsPerPage;
|
||||||
this.#logViewerContext?.getMessageTemplates(skip, this.#itemsPerPage);
|
this._logViewerContext?.getMessageTemplates(skip, this.#itemsPerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
#onChangePage(event: UUIPaginationEvent) {
|
#onChangePage(event: UUIPaginationEvent) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||||
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import type { SavedLogSearchResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
import type { SavedLogSearchResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||||
import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
|
import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-saved-searches-overview')
|
@customElement('umb-log-viewer-saved-searches-overview')
|
||||||
export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement {
|
export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement {
|
||||||
@@ -17,20 +17,20 @@ export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement {
|
|||||||
@state()
|
@state()
|
||||||
private _total = 0;
|
private _total = 0;
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
|
|
||||||
constructor() {
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
super();
|
private set _logViewerContext(value) {
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext = instance;
|
this.#getSavedSearches();
|
||||||
this.#logViewerContext?.getSavedSearches({ skip: 0, take: this.#itemsPerPage });
|
this.#observeStuff();
|
||||||
this.#observeStuff();
|
}
|
||||||
});
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeStuff() {
|
#observeStuff() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.savedSearches, (savedSearches) => {
|
||||||
this.observe(this.#logViewerContext.savedSearches, (savedSearches) => {
|
|
||||||
this._savedSearches = savedSearches?.items ?? [];
|
this._savedSearches = savedSearches?.items ?? [];
|
||||||
this._total = savedSearches?.total ?? 0;
|
this._total = savedSearches?.total ?? 0;
|
||||||
});
|
});
|
||||||
@@ -38,7 +38,7 @@ export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement {
|
|||||||
|
|
||||||
#getSavedSearches() {
|
#getSavedSearches() {
|
||||||
const skip = this.#currentPage * this.#itemsPerPage - this.#itemsPerPage;
|
const skip = this.#currentPage * this.#itemsPerPage - this.#itemsPerPage;
|
||||||
this.#logViewerContext?.getSavedSearches({ skip, take: this.#itemsPerPage });
|
this._logViewerContext?.getSavedSearches({ skip, take: this.#itemsPerPage });
|
||||||
}
|
}
|
||||||
|
|
||||||
#onChangePage(event: UUIPaginationEvent) {
|
#onChangePage(event: UUIPaginationEvent) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../logviewer-workspace.context-token.js';
|
||||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
//TODO: add a disabled attribute to the show more button when the total number of items is correctly returned from the endpoint
|
//TODO: add a disabled attribute to the show more button when the total number of items is correctly returned from the endpoint
|
||||||
@customElement('umb-log-viewer-overview-view')
|
@customElement('umb-log-viewer-overview-view')
|
||||||
@@ -12,28 +12,29 @@ export class UmbLogViewerOverviewViewElement extends UmbLitElement {
|
|||||||
@state()
|
@state()
|
||||||
private _canShowLogs = false;
|
private _canShowLogs = false;
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
constructor() {
|
|
||||||
super();
|
@consumeContext({
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
context: UMB_APP_LOG_VIEWER_CONTEXT,
|
||||||
this.#logViewerContext = instance;
|
})
|
||||||
this.#observeErrorCount();
|
private set _logViewerContext(value) {
|
||||||
this.#observeCanShowLogs();
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext?.getLogLevels(0, 100);
|
this.#observeErrorCount();
|
||||||
});
|
this.#observeCanShowLogs();
|
||||||
|
value?.getLogLevels(0, 100);
|
||||||
|
}
|
||||||
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeErrorCount() {
|
#observeErrorCount() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.logCount, (logLevelCount) => {
|
||||||
|
|
||||||
this.observe(this.#logViewerContext.logCount, (logLevelCount) => {
|
|
||||||
this._errorCount = logLevelCount?.error;
|
this._errorCount = logLevelCount?.error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeCanShowLogs() {
|
#observeCanShowLogs() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.canShowLogs, (canShowLogs) => {
|
||||||
this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => {
|
|
||||||
this._canShowLogs = canShowLogs ?? false;
|
this._canShowLogs = canShowLogs ?? false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||||
import type { UUICheckboxElement } from '@umbraco-cms/backoffice/external/uui';
|
import type { UUICheckboxElement } from '@umbraco-cms/backoffice/external/uui';
|
||||||
import { css, html, customElement, queryAll, state } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, queryAll, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
@@ -6,6 +5,7 @@ import { debounce } from '@umbraco-cms/backoffice/utils';
|
|||||||
import { LogLevelModel } from '@umbraco-cms/backoffice/external/backend-api';
|
import { LogLevelModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import { path, query, toQueryString } from '@umbraco-cms/backoffice/router';
|
import { path, query, toQueryString } from '@umbraco-cms/backoffice/router';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-log-level-filter-menu')
|
@customElement('umb-log-viewer-log-level-filter-menu')
|
||||||
export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement {
|
export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement {
|
||||||
@@ -15,27 +15,24 @@ export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement {
|
|||||||
@state()
|
@state()
|
||||||
private _logLevelFilter: LogLevelModel[] = [];
|
private _logLevelFilter: LogLevelModel[] = [];
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
|
|
||||||
constructor() {
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
super();
|
private set _logViewerContext(value) {
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext = instance;
|
this.#observeLogLevelFilter();
|
||||||
this.#observeLogLevelFilter();
|
}
|
||||||
});
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeLogLevelFilter() {
|
#observeLogLevelFilter() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.logLevelsFilter, (levelsFilter) => {
|
||||||
|
|
||||||
this.observe(this.#logViewerContext.logLevelsFilter, (levelsFilter) => {
|
|
||||||
this._logLevelFilter = levelsFilter ?? [];
|
this._logLevelFilter = levelsFilter ?? [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#setLogLevel() {
|
#setLogLevel() {
|
||||||
if (!this.#logViewerContext) return;
|
|
||||||
|
|
||||||
const logLevels = Array.from(this._logLevelSelectorCheckboxes)
|
const logLevels = Array.from(this._logLevelSelectorCheckboxes)
|
||||||
.filter((checkbox) => checkbox.checked)
|
.filter((checkbox) => checkbox.checked)
|
||||||
.map((checkbox) => checkbox.value as LogLevelModel);
|
.map((checkbox) => checkbox.value as LogLevelModel);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||||
import type { UUIScrollContainerElement, UUIPaginationElement } from '@umbraco-cms/backoffice/external/uui';
|
import type { UUIScrollContainerElement, UUIPaginationElement } from '@umbraco-cms/backoffice/external/uui';
|
||||||
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import type { LogMessageResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
import type { LogMessageResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||||
import { DirectionModel } from '@umbraco-cms/backoffice/external/backend-api';
|
import { DirectionModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-messages-list')
|
@customElement('umb-log-viewer-messages-list')
|
||||||
export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
||||||
@@ -23,46 +23,45 @@ export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
|||||||
@state()
|
@state()
|
||||||
private _isLoading = true;
|
private _isLoading = true;
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
|
|
||||||
constructor() {
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
super();
|
private set _logViewerContext(value) {
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext = instance;
|
this.#observeLogs();
|
||||||
this.#observeLogs();
|
}
|
||||||
});
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeLogs() {
|
#observeLogs() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.logs, (logs) => {
|
||||||
|
|
||||||
this.observe(this.#logViewerContext.logs, (logs) => {
|
|
||||||
this._logs = logs ?? [];
|
this._logs = logs ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
this.observe(this.#logViewerContext.isLoadingLogs, (isLoading) => {
|
this.observe(this._logViewerContext?.isLoadingLogs, (isLoading) => {
|
||||||
this._isLoading = isLoading === null ? this._isLoading : isLoading;
|
this._isLoading = isLoading ?? this._isLoading;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.observe(this.#logViewerContext.logsTotal, (total) => {
|
this.observe(this._logViewerContext?.logsTotal, (total) => {
|
||||||
this._logsTotal = total ?? 0;
|
this._logsTotal = total ?? 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.observe(this.#logViewerContext.sortingDirection, (direction) => {
|
this.observe(this._logViewerContext?.sortingDirection, (direction) => {
|
||||||
this._sortingDirection = direction;
|
this._sortingDirection = direction ?? this._sortingDirection;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#sortLogs() {
|
#sortLogs() {
|
||||||
this.#logViewerContext?.toggleSortOrder();
|
this._logViewerContext?.toggleSortOrder();
|
||||||
this.#logViewerContext?.setCurrentPage(1);
|
this._logViewerContext?.setCurrentPage(1);
|
||||||
this.#logViewerContext?.getLogs();
|
this._logViewerContext?.getLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
#onPageChange(event: Event): void {
|
#onPageChange(event: Event): void {
|
||||||
const current = (event.target as UUIPaginationElement).current;
|
const current = (event.target as UUIPaginationElement).current;
|
||||||
this.#logViewerContext?.setCurrentPage(current);
|
this._logViewerContext?.setCurrentPage(current);
|
||||||
this.#logViewerContext?.getLogs();
|
this._logViewerContext?.getLogs();
|
||||||
this._logsScrollContainer.scrollTop = 0;
|
this._logsScrollContainer.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type {
|
import type { UmbPoolingConfig, UmbPoolingInterval } from '../../../logviewer-workspace.context.js';
|
||||||
UmbPoolingConfig,
|
|
||||||
UmbPoolingInterval,
|
|
||||||
UmbLogViewerWorkspaceContext,
|
|
||||||
} from '../../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||||
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components';
|
import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-polling-button')
|
@customElement('umb-log-viewer-polling-button')
|
||||||
export class UmbLogViewerPollingButtonElement extends UmbLitElement {
|
export class UmbLogViewerPollingButtonElement extends UmbLitElement {
|
||||||
@@ -18,30 +15,29 @@ export class UmbLogViewerPollingButtonElement extends UmbLitElement {
|
|||||||
|
|
||||||
#pollingIntervals: UmbPoolingInterval[] = [2000, 5000, 10000, 20000, 30000];
|
#pollingIntervals: UmbPoolingInterval[] = [2000, 5000, 10000, 20000, 30000];
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
|
|
||||||
constructor() {
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
super();
|
private set _logViewerContext(value) {
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
this.#logViewerContext = value;
|
||||||
this.#logViewerContext = instance;
|
this.#observePoolingConfig();
|
||||||
this.#observePoolingConfig();
|
}
|
||||||
});
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
#observePoolingConfig() {
|
#observePoolingConfig() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.polling, (poolingConfig) => {
|
||||||
|
this._poolingConfig = poolingConfig ? { ...poolingConfig } : { enabled: false, interval: 0 };
|
||||||
this.observe(this.#logViewerContext.polling, (poolingConfig) => {
|
|
||||||
this._poolingConfig = { ...poolingConfig };
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#togglePolling() {
|
#togglePolling() {
|
||||||
this.#logViewerContext?.togglePolling();
|
this._logViewerContext?.togglePolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
#setPolingInterval = (interval: UmbPoolingInterval) => {
|
#setPolingInterval = (interval: UmbPoolingInterval) => {
|
||||||
this.#logViewerContext?.setPollingInterval(interval);
|
this._logViewerContext?.setPollingInterval(interval);
|
||||||
|
|
||||||
this.#closePoolingPopover();
|
this.#closePoolingPopover();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||||
import { UMB_LOG_VIEWER_SAVE_SEARCH_MODAL } from './log-viewer-search-input-modal.modal-token.js';
|
import { UMB_LOG_VIEWER_SAVE_SEARCH_MODAL } from './log-viewer-search-input-modal.modal-token.js';
|
||||||
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
@@ -12,6 +11,7 @@ import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components';
|
|||||||
import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
|
import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
|
||||||
|
|
||||||
import './log-viewer-search-input-modal.element.js';
|
import './log-viewer-search-input-modal.element.js';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-search-input')
|
@customElement('umb-log-viewer-search-input')
|
||||||
export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
||||||
@@ -33,15 +33,20 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
// TODO: Revisit this code, to not use RxJS directly:
|
// TODO: Revisit this code, to not use RxJS directly:
|
||||||
#inputQuery$ = new Subject<string>();
|
#inputQuery$ = new Subject<string>();
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
|
|
||||||
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
|
private set _logViewerContext(value) {
|
||||||
|
this.#logViewerContext = value;
|
||||||
|
this.#observeStuff();
|
||||||
|
this.#logViewerContext?.getSavedSearches();
|
||||||
|
}
|
||||||
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
|
||||||
this.#logViewerContext = instance;
|
|
||||||
this.#observeStuff();
|
|
||||||
this.#logViewerContext?.getSavedSearches();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#inputQuery$
|
this.#inputQuery$
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -49,7 +54,7 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
debounceTime(250),
|
debounceTime(250),
|
||||||
)
|
)
|
||||||
.subscribe((query) => {
|
.subscribe((query) => {
|
||||||
this.#logViewerContext?.setFilterExpression(query);
|
this._logViewerContext?.setFilterExpression(query);
|
||||||
this.#persist(query);
|
this.#persist(query);
|
||||||
this._isQuerySaved = this._savedSearches.some((search) => search.query === query);
|
this._isQuerySaved = this._savedSearches.some((search) => search.query === query);
|
||||||
this._showLoader = false;
|
this._showLoader = false;
|
||||||
@@ -57,15 +62,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#observeStuff() {
|
#observeStuff() {
|
||||||
if (!this.#logViewerContext) return;
|
this.observe(this._logViewerContext?.savedSearches, (savedSearches) => {
|
||||||
this.observe(this.#logViewerContext.savedSearches, (savedSearches) => {
|
|
||||||
this._savedSearches = savedSearches?.items ?? [];
|
this._savedSearches = savedSearches?.items ?? [];
|
||||||
this._isQuerySaved = this._savedSearches.some((search) => search.query === this._inputQuery);
|
this._isQuerySaved = this._savedSearches.some((search) => search.query === this._inputQuery);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.observe(this.#logViewerContext.filterExpression, (query) => {
|
this.observe(this._logViewerContext?.filterExpression, (query) => {
|
||||||
this._inputQuery = query;
|
this._inputQuery = query ?? '';
|
||||||
this._isQuerySaved = this._savedSearches.some((search) => search.query === query);
|
this._isQuerySaved = this._savedSearches.some((search) => search.query === this._inputQuery);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,11 +96,11 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
|
|
||||||
#clearQuery() {
|
#clearQuery() {
|
||||||
this.#inputQuery$.next('');
|
this.#inputQuery$.next('');
|
||||||
this.#logViewerContext?.setFilterExpression('');
|
this._logViewerContext?.setFilterExpression('');
|
||||||
}
|
}
|
||||||
|
|
||||||
#saveSearch(savedSearch: SavedLogSearchResponseModel) {
|
#saveSearch(savedSearch: SavedLogSearchResponseModel) {
|
||||||
this.#logViewerContext?.saveSearch(savedSearch);
|
this._logViewerContext?.saveSearch(savedSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #removeSearch(name: string) {
|
async #removeSearch(name: string) {
|
||||||
@@ -107,7 +111,7 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
confirmLabel: 'Delete',
|
confirmLabel: 'Delete',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#logViewerContext?.removeSearch({ name });
|
this._logViewerContext?.removeSearch({ name });
|
||||||
//this.dispatchEvent(new UmbDeleteEvent());
|
//this.dispatchEvent(new UmbDeleteEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import type { UmbLogViewerWorkspaceContext } from '../../logviewer-workspace.context.js';
|
|
||||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../logviewer-workspace.context-token.js';
|
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../logviewer-workspace.context-token.js';
|
||||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||||
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
||||||
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-search-view')
|
@customElement('umb-log-viewer-search-view')
|
||||||
export class UmbLogViewerSearchViewElement extends UmbLitElement {
|
export class UmbLogViewerSearchViewElement extends UmbLitElement {
|
||||||
@state()
|
@state()
|
||||||
private _canShowLogs = true;
|
private _canShowLogs = true;
|
||||||
|
|
||||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
|
|
||||||
|
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||||
|
private set _logViewerContext(value) {
|
||||||
|
this.#logViewerContext = value;
|
||||||
|
this.#observeCanShowLogs();
|
||||||
|
}
|
||||||
|
private get _logViewerContext() {
|
||||||
|
return this.#logViewerContext;
|
||||||
|
}
|
||||||
|
|
||||||
#canShowLogsObserver?: UmbObserverController<boolean | null>;
|
#canShowLogsObserver?: UmbObserverController<boolean | null>;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
|
||||||
this.#logViewerContext = instance;
|
|
||||||
this.#observeCanShowLogs();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#observeCanShowLogs() {
|
#observeCanShowLogs() {
|
||||||
if (this.#canShowLogsObserver) this.#canShowLogsObserver.destroy();
|
if (this.#canShowLogsObserver) this.#canShowLogsObserver.destroy();
|
||||||
if (!this.#logViewerContext) return;
|
|
||||||
|
|
||||||
this.#canShowLogsObserver = this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => {
|
this.#canShowLogsObserver = this.observe(this._logViewerContext?.canShowLogs, (canShowLogs) => {
|
||||||
this._canShowLogs = canShowLogs ?? this._canShowLogs;
|
this._canShowLogs = canShowLogs ?? this._canShowLogs;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user