Log viewer: Improves search functionality and code quality (#20913)
* fix: adds correct fallback for dates to avoid console error * fix: resolves a TODO by using UmbStringState over rxjs Subject * Refactor log viewer search to use UmbStringState and improve architecture - Replace RxJS Subject with UmbStringState to follow Umbraco patterns - Move debounced search observation to messages list component - Only triggers when component is mounted (logs are visible) - Prevents unnecessary API calls on other views - Simplify search input to just update context state - Add semantic form structure with role="search" for accessibility - Add visually-hidden submit button for keyboard navigation - Allow re-running same query via form submission (bypasses debounce) - Follow same architecture pattern as date range selector This resolves the TODO to not use RxJS directly and significantly improves separation of concerns where the data consumer (messages list) owns the fetching logic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add visible refresh button to log viewer search input - Add refresh button with icon-refresh next to save and clear buttons - Allows users to re-run search with same query (bypasses debounce) - Remove form structure that couldn't work due to Shadow DOM boundaries - Simplify parent component by removing form submission logic - Keep role="search" for accessibility The refresh button provides a more discoverable UI than the hidden submit button approach and avoids Shadow DOM event bubbling issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix debouncing by adding local state in search input - Add local UmbStringState to debounce user input (250ms) - Only update context filterExpression after debounce - Remove debouncing from messages list (now handled at input level) - Saved searches and refresh button still bypass debounce for immediate feedback This restores the expected debouncing behavior while maintaining the clean architecture where the messages list triggers searches based on context changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: cleans up in docs * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
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';
|
||||||
@@ -32,9 +31,9 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
|||||||
#observeStuff() {
|
#observeStuff() {
|
||||||
this.observe(
|
this.observe(
|
||||||
this._logViewerContext?.dateRange,
|
this._logViewerContext?.dateRange,
|
||||||
(dateRange: UmbLogViewerDateRange) => {
|
(dateRange) => {
|
||||||
this._startDate = dateRange.startDate;
|
this._startDate = dateRange?.startDate ?? '';
|
||||||
this._endDate = dateRange.endDate;
|
this._endDate = dateRange?.endDate ?? '';
|
||||||
},
|
},
|
||||||
'_observeDateRange',
|
'_observeDateRange',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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';
|
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
import { skip } from '@umbraco-cms/backoffice/external/rxjs';
|
||||||
|
|
||||||
@customElement('umb-log-viewer-messages-list')
|
@customElement('umb-log-viewer-messages-list')
|
||||||
export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
||||||
@@ -50,6 +51,17 @@ export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
|||||||
this.observe(this._logViewerContext?.sortingDirection, (direction) => {
|
this.observe(this._logViewerContext?.sortingDirection, (direction) => {
|
||||||
this._sortingDirection = direction ?? this._sortingDirection;
|
this._sortingDirection = direction ?? this._sortingDirection;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Observe filter expression changes to trigger search
|
||||||
|
// Only observes when this component is mounted (when logs are visible)
|
||||||
|
this.observe(
|
||||||
|
this._logViewerContext?.filterExpression.pipe(
|
||||||
|
skip(1), // Skip initial value to avoid duplicate search on page load
|
||||||
|
),
|
||||||
|
() => {
|
||||||
|
this._logViewerContext?.getLogs();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sortLogs() {
|
#sortLogs() {
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import { UMB_LOG_VIEWER_SAVE_SEARCH_MODAL } from './log-viewer-search-input-moda
|
|||||||
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 { escapeHTML } from '@umbraco-cms/backoffice/utils';
|
import { escapeHTML } from '@umbraco-cms/backoffice/utils';
|
||||||
import { query as getQuery, path, toQueryString } from '@umbraco-cms/backoffice/router';
|
import { query as getQuery, path, toQueryString } from '@umbraco-cms/backoffice/router';
|
||||||
import { Subject, debounceTime, tap } from '@umbraco-cms/backoffice/external/rxjs';
|
|
||||||
import { umbConfirmModal, umbOpenModal } from '@umbraco-cms/backoffice/modal';
|
import { umbConfirmModal, umbOpenModal } from '@umbraco-cms/backoffice/modal';
|
||||||
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 { UmbDropdownElement } from '@umbraco-cms/backoffice/components';
|
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 { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||||
|
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||||
|
import { debounceTime, skip } from '@umbraco-cms/backoffice/external/rxjs';
|
||||||
|
|
||||||
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 {
|
||||||
@@ -24,14 +25,11 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
@state()
|
@state()
|
||||||
private _inputQuery = '';
|
private _inputQuery = '';
|
||||||
|
|
||||||
@state()
|
|
||||||
private _showLoader = false;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _isQuerySaved = false;
|
private _isQuerySaved = false;
|
||||||
|
|
||||||
// TODO: Revisit this code, to not use RxJS directly:
|
// Local state for debouncing user input before updating context
|
||||||
#inputQuery$ = new Subject<string>();
|
#localQueryState = new UmbStringState('');
|
||||||
|
|
||||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||||
|
|
||||||
@@ -48,17 +46,17 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.#inputQuery$
|
// Debounce local input and update context
|
||||||
.pipe(
|
this.observe(
|
||||||
tap(() => (this._showLoader = true)),
|
this.#localQueryState.asObservable().pipe(
|
||||||
|
skip(1), // Skip initial value
|
||||||
debounceTime(250),
|
debounceTime(250),
|
||||||
)
|
),
|
||||||
.subscribe((query) => {
|
(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._showLoader = false;
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeStuff() {
|
#observeStuff() {
|
||||||
@@ -75,11 +73,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
|
|
||||||
#setQuery(event: Event) {
|
#setQuery(event: Event) {
|
||||||
const target = event.target as UUIInputElement;
|
const target = event.target as UUIInputElement;
|
||||||
this.#inputQuery$.next(target.value as string);
|
const query = target.value as string;
|
||||||
|
// Update local state which will debounce before updating context
|
||||||
|
this.#localQueryState.setValue(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
#setQueryFromSavedSearch(query: string) {
|
#setQueryFromSavedSearch(query: string) {
|
||||||
this.#inputQuery$.next(query);
|
this._logViewerContext?.setFilterExpression(query);
|
||||||
|
this.#persist(query);
|
||||||
this._searchDropdownElement.open = false;
|
this._searchDropdownElement.open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +96,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#clearQuery() {
|
#clearQuery() {
|
||||||
this.#inputQuery$.next('');
|
|
||||||
this._logViewerContext?.setFilterExpression('');
|
this._logViewerContext?.setFilterExpression('');
|
||||||
|
this.#persist('');
|
||||||
|
this.#localQueryState.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
#refreshSearch() {
|
||||||
|
// Force immediate search, bypassing debounce
|
||||||
|
this._logViewerContext?.getLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
#saveSearch(savedSearch: SavedLogSearchResponseModel) {
|
#saveSearch(savedSearch: SavedLogSearchResponseModel) {
|
||||||
@@ -137,17 +144,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
slot="trigger"
|
slot="trigger"
|
||||||
@input=${this.#setQuery}
|
@input=${this.#setQuery}
|
||||||
.value=${this._inputQuery}>
|
.value=${this._inputQuery}>
|
||||||
${this._showLoader
|
|
||||||
? html`<div id="loader-container" slot="append">
|
|
||||||
<uui-loader-circle></uui-loader-circle>
|
|
||||||
</div>`
|
|
||||||
: ''}
|
|
||||||
${this._inputQuery
|
${this._inputQuery
|
||||||
? html`${!this._isQuerySaved
|
? html`${!this._isQuerySaved
|
||||||
? html`<uui-button compact slot="append" label="Save search" @click=${this.#openSaveSearchDialog}
|
? html`<uui-button compact slot="append" label="Save search" @click=${this.#openSaveSearchDialog}
|
||||||
><uui-icon name="icon-favorite"></uui-icon
|
><uui-icon name="icon-favorite"></uui-icon
|
||||||
></uui-button>`
|
></uui-button>`
|
||||||
: ''}<uui-button compact slot="append" label="Clear" @click=${this.#clearQuery}
|
: ''}<uui-button compact slot="append" label="Refresh search" @click=${this.#refreshSearch}
|
||||||
|
><uui-icon name="icon-refresh"></uui-icon></uui-button
|
||||||
|
><uui-button compact slot="append" label="Clear" @click=${this.#clearQuery}
|
||||||
><uui-icon name="icon-delete"></uui-icon
|
><uui-icon name="icon-delete"></uui-icon
|
||||||
></uui-button>`
|
></uui-button>`
|
||||||
: html``}
|
: html``}
|
||||||
@@ -198,13 +202,6 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loader-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 var(--uui-size-space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.saved-search-item {
|
.saved-search-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class UmbLogViewerSearchViewElement extends UmbLitElement {
|
|||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<umb-body-layout header-transparent header-fit-height>
|
<umb-body-layout header-transparent header-fit-height>
|
||||||
<div id="header" slot="header">
|
<div id="header" slot="header" role="search" aria-label="Filter logs">
|
||||||
<div id="levels-container">
|
<div id="levels-container">
|
||||||
<umb-log-viewer-log-level-filter-menu></umb-log-viewer-log-level-filter-menu>
|
<umb-log-viewer-log-level-filter-menu></umb-log-viewer-log-level-filter-menu>
|
||||||
<div id="dates-polling-container">
|
<div id="dates-polling-container">
|
||||||
|
|||||||
Reference in New Issue
Block a user