From 04f98a758df4a0a23f6c3f92119a629ca5a7df20 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 24 Nov 2025 09:44:45 +0100 Subject: [PATCH] Log viewer: Improves search functionality and code quality (#20913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 * chore: cleans up in docs * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../log-viewer-date-range-selector.element.ts | 7 +-- .../log-viewer-messages-list.element.ts | 12 ++++ .../log-viewer-search-input.element.ts | 59 +++++++++---------- .../views/search/log-search-view.element.ts | 2 +- 4 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts index 71d6c6c844..7318a80b18 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts @@ -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 { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; @@ -32,9 +31,9 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { #observeStuff() { this.observe( this._logViewerContext?.dateRange, - (dateRange: UmbLogViewerDateRange) => { - this._startDate = dateRange.startDate; - this._endDate = dateRange.endDate; + (dateRange) => { + this._startDate = dateRange?.startDate ?? ''; + this._endDate = dateRange?.endDate ?? ''; }, '_observeDateRange', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-messages-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-messages-list.element.ts index 3e1bed642e..322c89fb80 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-messages-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-messages-list.element.ts @@ -5,6 +5,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { LogMessageResponseModel } 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 { skip } from '@umbraco-cms/backoffice/external/rxjs'; @customElement('umb-log-viewer-messages-list') export class UmbLogViewerMessagesListElement extends UmbLitElement { @@ -50,6 +51,17 @@ export class UmbLogViewerMessagesListElement extends UmbLitElement { this.observe(this._logViewerContext?.sortingDirection, (direction) => { 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() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts index e4563d9761..ed1533ab6d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts @@ -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 { escapeHTML } from '@umbraco-cms/backoffice/utils'; 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 { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { SavedLogSearchResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components'; 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 { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-search-input') export class UmbLogViewerSearchInputElement extends UmbLitElement { @@ -24,14 +25,11 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { @state() private _inputQuery = ''; - @state() - private _showLoader = false; - @state() private _isQuerySaved = false; - // TODO: Revisit this code, to not use RxJS directly: - #inputQuery$ = new Subject(); + // Local state for debouncing user input before updating context + #localQueryState = new UmbStringState(''); #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; @@ -48,17 +46,17 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { constructor() { super(); - this.#inputQuery$ - .pipe( - tap(() => (this._showLoader = true)), + // Debounce local input and update context + this.observe( + this.#localQueryState.asObservable().pipe( + skip(1), // Skip initial value debounceTime(250), - ) - .subscribe((query) => { + ), + (query) => { this._logViewerContext?.setFilterExpression(query); this.#persist(query); - this._isQuerySaved = this._savedSearches.some((search) => search.query === query); - this._showLoader = false; - }); + }, + ); } #observeStuff() { @@ -75,11 +73,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { #setQuery(event: Event) { 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) { - this.#inputQuery$.next(query); + this._logViewerContext?.setFilterExpression(query); + this.#persist(query); this._searchDropdownElement.open = false; } @@ -95,8 +96,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { } #clearQuery() { - this.#inputQuery$.next(''); this._logViewerContext?.setFilterExpression(''); + this.#persist(''); + this.#localQueryState.setValue(''); + } + + #refreshSearch() { + // Force immediate search, bypassing debounce + this._logViewerContext?.getLogs(); } #saveSearch(savedSearch: SavedLogSearchResponseModel) { @@ -137,17 +144,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { slot="trigger" @input=${this.#setQuery} .value=${this._inputQuery}> - ${this._showLoader - ? html`
- -
` - : ''} ${this._inputQuery ? html`${!this._isQuerySaved ? html`` - : ''}` : html``} @@ -198,13 +202,6 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { flex: 1; } - #loader-container { - display: flex; - justify-content: center; - align-items: center; - margin: 0 var(--uui-size-space-4); - } - .saved-search-item { display: flex; justify-content: space-between; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/log-search-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/log-search-view.element.ts index 317711c2e5..513dd7ee50 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/log-search-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/log-search-view.element.ts @@ -34,7 +34,7 @@ export class UmbLogViewerSearchViewElement extends UmbLitElement { override render() { return html` -