| // 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 } 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 = ''; |
| |
| /** The field keys (column values) for the incoming log entries. */ |
| @property({ type: Array }) |
| fieldKeys: string[] = []; |
| |
| /** Indicates whether to enable the button for closing the current log view. */ |
| @property({ type: Boolean }) |
| hideCloseButton = false; |
| |
| @property({ type: Array }) |
| colsHidden: (boolean | undefined)[] = []; |
| |
| /** A StateStore object that stores state of views */ |
| @state() |
| _stateStore: StateStore = new LocalStorageState(); |
| |
| @state() |
| _state: State; |
| |
| @state() |
| _viewTitle = 'Log View'; |
| |
| @state() |
| _settingsMenuOpen = false; |
| |
| @query('.field-menu') _fieldMenu!: HTMLMenuElement; |
| |
| @query('#search-field') _searchField!: HTMLInputElement; |
| |
| @query('.input-facade') _inputFacade!: HTMLDivElement; |
| |
| @queryAll('.item-checkboxes') _itemCheckboxes!: HTMLCollection[]; |
| |
| private firstCheckboxLoad = false; |
| |
| /** 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('.settings-menu-button') settingsMenuButtonEl!: 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')); |
| } |
| |
| protected updated(): void { |
| const checkboxItems = Array.from(this._itemCheckboxes); |
| if (checkboxItems.length > 0 && !this.firstCheckboxLoad) { |
| for (const i in checkboxItems) { |
| const checkboxEl = checkboxItems[i] as unknown as HTMLInputElement; |
| checkboxEl.checked = !this.colsHidden[Number(i) + 1]; |
| } |
| this.firstCheckboxLoad = !this.firstCheckboxLoad; |
| } |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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 Settings menu. |
| */ |
| private toggleSettingsMenu() { |
| this._settingsMenuOpen = !this._settingsMenuOpen; |
| } |
| |
| 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> |
| ${Array.from(this.fieldKeys).map( |
| (field) => html` |
| <li class="field-menu-item"> |
| <input |
| class="item-checkboxes" |
| @click=${this.handleColumnToggle} |
| checked |
| type="checkbox" |
| value=${field} |
| id=${field} |
| /> |
| <label for=${field}>${field}</label> |
| </li> |
| `, |
| )} |
| </menu> |
| </span> |
| |
| <span class="action-button" title="Toggle fields"> |
| <md-icon-button @click=${ |
| this.toggleSettingsMenu |
| } class="settings-menu-button"> |
| <md-icon >more_vert</md-icon> |
| </md-icon-button> |
| |
| <md-menu quick fixed |
| ?open=${this._settingsMenuOpen} |
| .anchor=${this.settingsMenuButtonEl} |
| @closed=${() => { |
| this._settingsMenuOpen = false; |
| }}> |
| <md-menu-item headline="Download logs (.txt)" @click=${ |
| this.handleDownloadLogs |
| } role="button"> |
| <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> |
| `; |
| } |
| } |