diff options
author | Xin Li <delphij@google.com> | 2024-01-17 22:13:58 -0800 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2024-01-17 22:13:58 -0800 |
commit | 28d03a2a1cabbe01d7bcb6cf5166c10e50d3c2c6 (patch) | |
tree | c1643be8ab17fc607cea748a8bb1d621a5964873 /pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts | |
parent | ec2628a6ba2d0ecbe3ac10c8c772f6fc6acc345d (diff) | |
parent | f054515492af5132f685cb23fe11891ee77104c9 (diff) | |
download | pigweed-28d03a2a1cabbe01d7bcb6cf5166c10e50d3c2c6.tar.gz |
Merge Android 24Q1 Release (ab/11220357)temp_319669529
Bug: 319669529
Merged-In: Iba357b308a79d0c8b560acd4f72b5423c9c83294
Change-Id: Icdf552029fb97a34e83c6dd7799433fc473a2506
Diffstat (limited to 'pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts')
-rw-r--r-- | pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts b/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts new file mode 100644 index 000000000..8c0226f7b --- /dev/null +++ b/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts @@ -0,0 +1,354 @@ +// Copyright 2023 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { LitElement, html } from 'lit'; +import { + customElement, + property, + query, + queryAll, + state, +} from 'lit/decorators.js'; +import { styles } from './log-view-controls.styles'; +import { State, TableColumn } from '../../shared/interfaces'; +import { StateStore, LocalStorageState } from '../../shared/state'; + +/** + * A sub-component of the log view with user inputs for managing and customizing + * log entry display and interaction. + * + * @element log-view-controls + */ +@customElement('log-view-controls') +export class LogViewControls extends LitElement { + static styles = styles; + + /** The `id` of the parent view containing this log list. */ + @property({ type: String }) + viewId = ''; + + @property({ type: Array }) + columnData: TableColumn[] = []; + + /** Indicates whether to enable the button for closing the current log view. */ + @property({ type: Boolean }) + hideCloseButton = false; + + /** A StateStore object that stores state of views */ + @state() + _stateStore: StateStore = new LocalStorageState(); + + @state() + _viewTitle = 'Log View'; + + @state() + _moreActionsMenuOpen = false; + + @query('.field-menu') _fieldMenu!: HTMLMenuElement; + + @query('#search-field') _searchField!: HTMLInputElement; + + @query('.input-facade') _inputFacade!: HTMLDivElement; + + @queryAll('.item-checkboxes') _itemCheckboxes!: HTMLCollection[]; + + private _state: State; + + /** The timer identifier for debouncing search input. */ + private _inputDebounceTimer: number | null = null; + + /** The delay (in ms) used for debouncing search input. */ + private readonly INPUT_DEBOUNCE_DELAY = 50; + + @query('.more-actions-button') moreActionsButtonEl!: HTMLElement; + + constructor() { + super(); + this._state = this._stateStore.getState(); + } + + protected firstUpdated(): void { + let searchText = ''; + if (this._state !== null) { + const viewConfigArr = this._state.logViewConfig; + for (const i in viewConfigArr) { + if (viewConfigArr[i].viewID === this.viewId) { + searchText = viewConfigArr[i].search as string; + this._viewTitle = viewConfigArr[i].viewTitle + ? viewConfigArr[i].viewTitle + : this._viewTitle; + } + } + } + + this._inputFacade.textContent = searchText; + this._inputFacade.dispatchEvent(new CustomEvent('input')); + } + + /** + * Called whenever the search field value is changed. Debounces the input + * event and dispatches an event with the input value after a specified + * delay. + * + * @param {Event} event - The input event object. + */ + private handleInput = (event: Event) => { + if (this._inputDebounceTimer) { + clearTimeout(this._inputDebounceTimer); + } + + const inputFacade = event.target as HTMLDivElement; + this.markKeysInText(inputFacade); + this._searchField.value = inputFacade.textContent || ''; + const inputValue = this._searchField.value; + + this._inputDebounceTimer = window.setTimeout(() => { + const customEvent = new CustomEvent('input-change', { + detail: { inputValue }, + bubbles: true, + composed: true, + }); + + this.dispatchEvent(customEvent); + }, this.INPUT_DEBOUNCE_DELAY); + }; + + private markKeysInText(target: HTMLElement) { + const pattern = /\b(\w+):(?=\w)/; + const textContent = target.textContent || ''; + const conditions = textContent.split(/\s+/); + const wordsBeforeColons: string[] = []; + + for (const condition of conditions) { + const match = condition.match(pattern); + if (match) { + wordsBeforeColons.push(match[0]); + } + } + } + + private handleKeydown = (event: KeyboardEvent) => { + if (event.key === 'Enter' || event.key === 'Cmd') { + event.preventDefault(); + } + }; + + /** + * Dispatches a custom event for clearing logs. This event includes a + * `timestamp` object indicating the date/time in which the 'clear-logs' event + * was dispatched. + */ + private handleClearLogsClick() { + const timestamp = new Date(); + + const clearLogs = new CustomEvent('clear-logs', { + detail: { timestamp }, + bubbles: true, + composed: true, + }); + + this.dispatchEvent(clearLogs); + } + + /** Dispatches a custom event for toggling wrapping. */ + private handleWrapToggle() { + const wrapToggle = new CustomEvent('wrap-toggle', { + bubbles: true, + composed: true, + }); + + this.dispatchEvent(wrapToggle); + } + + /** + * Dispatches a custom event for closing the parent view. This event includes + * a `viewId` object indicating the `id` of the parent log view. + */ + private handleCloseViewClick() { + const closeView = new CustomEvent('close-view', { + bubbles: true, + composed: true, + detail: { + viewId: this.viewId, + }, + }); + + this.dispatchEvent(closeView); + } + + /** + * Dispatches a custom event for showing or hiding a column in the table. This + * event includes a `field` string indicating the affected column's field name + * and an `isChecked` boolean indicating whether to show or hide the column. + * + * @param {Event} event - The click event object. + */ + private handleColumnToggle(event: Event) { + const inputEl = event.target as HTMLInputElement; + const columnToggle = new CustomEvent('column-toggle', { + bubbles: true, + composed: true, + detail: { + field: inputEl.value, + isChecked: inputEl.checked, + }, + }); + + this.dispatchEvent(columnToggle); + } + + private handleAddView() { + const addView = new CustomEvent('add-view', { + bubbles: true, + composed: true, + }); + + this.dispatchEvent(addView); + } + + /** + * Dispatches a custom event for downloading a logs file. This event includes + * a `format` string indicating the format of the file to be downloaded and a + * `viewTitle` string which passes the title of the current view for naming + * the file. + * + * @param {Event} event - The click event object. + */ + private handleDownloadLogs() { + const downloadLogs = new CustomEvent('download-logs', { + bubbles: true, + composed: true, + detail: { + format: 'plaintext', + viewTitle: this._viewTitle, + }, + }); + + this.dispatchEvent(downloadLogs); + } + + /** Opens and closes the column visibility dropdown menu. */ + private toggleColumnVisibilityMenu() { + this._fieldMenu.hidden = !this._fieldMenu.hidden; + } + + /** Opens and closes the More Actions menu. */ + private toggleMoreActionsMenu() { + this._moreActionsMenuOpen = !this._moreActionsMenuOpen; + } + + render() { + return html` + <p class="host-name">${this._viewTitle}</p> + + <div class="input-container"> + <div class="input-facade" contenteditable="plaintext-only" @input="${ + this.handleInput + }" @keydown="${this.handleKeydown}"></div> + <input id="search-field" type="text"></input> + </div> + + <div class="actions-container"> + <span class="action-button" hidden> + <md-icon-button> + <md-icon>pause_circle</md-icon> + </md-icon-button> + </span> + + <span class="action-button" hidden> + <md-icon-button> + <md-icon>wrap_text</md-icon> + </md-icon-button> + </span> + + <span class="action-button" title="Clear logs"> + <md-icon-button @click=${this.handleClearLogsClick}> + <md-icon>delete_sweep</md-icon> + </md-icon-button> + </span> + + <span class="action-button" title="Toggle Line Wrapping"> + <md-icon-button @click=${this.handleWrapToggle} toggle> + <md-icon>wrap_text</md-icon> + </md-icon-button> + </span> + + <span class='action-button field-toggle' title="Toggle fields"> + <md-icon-button @click=${this.toggleColumnVisibilityMenu} toggle> + <md-icon>view_column</md-icon> + </md-icon-button> + <menu class='field-menu' hidden> + ${this.columnData.map( + (column) => html` + <li class="field-menu-item"> + <input + class="item-checkboxes" + @click=${this.handleColumnToggle} + ?checked=${column.isVisible} + type="checkbox" + value=${column.fieldName} + id=${column.fieldName} + /> + <label for=${column.fieldName}>${column.fieldName}</label> + </li> + `, + )} + </menu> + </span> + + <span class="action-button" title="Toggle fields"> + <md-icon-button @click=${ + this.toggleMoreActionsMenu + } class="more-actions-button"> + <md-icon >more_vert</md-icon> + </md-icon-button> + + <md-menu quick fixed + ?open=${this._moreActionsMenuOpen} + .anchor=${this.moreActionsButtonEl} + @closed=${() => { + this._moreActionsMenuOpen = false; + }}> + + <md-menu-item headline="Add view" @click=${ + this.handleAddView + } role="button" title="Add a view"> + <md-icon slot="start" data-variant="icon">new_window</md-icon> + </md-menu-item> + + <md-menu-item headline="Download logs (.txt)" @click=${ + this.handleDownloadLogs + } role="button" title="Download current logs as a plaintext file"> + <md-icon slot="start" data-variant="icon">download</md-icon> + </md-menu-item> + </md-menu> + </span> + + <span class="action-button" title="Close view" ?hidden=${ + this.hideCloseButton + }> + <md-icon-button @click=${this.handleCloseViewClick}> + <md-icon>close</md-icon> + </md-icon-button> + </span> + + <span class="action-button" hidden> + <md-icon-button> + <md-icon>more_horiz</md-icon> + </md-icon-button> + </span> + </div> + `; + } +} |