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:
Jacob Overgaard
2025-11-24 09:44:45 +01:00
committed by GitHub
parent bb88be9d2e
commit 04f98a758d
4 changed files with 44 additions and 36 deletions

View File

@@ -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',
);

View File

@@ -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() {

View File

@@ -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<string>();
// 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`<div id="loader-container" slot="append">
<uui-loader-circle></uui-loader-circle>
</div>`
: ''}
${this._inputQuery
? html`${!this._isQuerySaved
? html`<uui-button compact slot="append" label="Save search" @click=${this.#openSaveSearchDialog}
><uui-icon name="icon-favorite"></uui-icon
></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-button>`
: 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;

View File

@@ -34,7 +34,7 @@ export class UmbLogViewerSearchViewElement extends UmbLitElement {
override render() {
return html`
<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">
<umb-log-viewer-log-level-filter-menu></umb-log-viewer-log-level-filter-menu>
<div id="dates-polling-container">