diff options
author | Luis Flores <lesprit@google.com> | 2023-09-12 20:22:55 +0000 |
---|---|---|
committer | CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2023-09-12 20:22:55 +0000 |
commit | e0b85213d6493b72d9d93019f8577b8cfb89867c (patch) | |
tree | 3815bc2862a338f0f66c14e0683caf0936156b27 /pw_web | |
parent | 93a418acb4fa8c09abeda07ec40c3a0e53e8c7e1 (diff) | |
download | pigweed-e0b85213d6493b72d9d93019f8577b8cfb89867c.tar.gz |
pw_web: Fix column sizing & toggling, update UI
Table Column Changes
- Fix column toggling based on updated test logs
- Default column size is now calculated based on the character length of
text content
- Introduce `columnData` object to contain field names, char.
lengths, resize widths, and visibility status and replace
`LogColumnState`
- Manually-resized column widths are now saved across sessions
- Prevent autosizing after a certain number of logs have been added to
the table (`_autosizeLocked`)
Misc. UI Changes
- Replace Google Sans usage with Roboto Flex
- Move "Add View" button into the More Actions menu
- Add titles (tooltips) to column headers
- Update filter field to be right-justified
Bugs: b/298096049, b/298096162, b/299502498, b/294598541
Change-Id: I0aaa80abe7ade1081e44456b5af6ac6037eded4e
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169591
Reviewed-by: Asad Memon <asadmemon@google.com>
Commit-Queue: Luis Flores <lesprit@google.com>
Reviewed-by: Amy Hu <amyhu@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
Diffstat (limited to 'pw_web')
-rw-r--r-- | pw_web/log-viewer/package-lock.json | 38 | ||||
-rw-r--r-- | pw_web/log-viewer/package.json | 2 | ||||
-rw-r--r-- | pw_web/log-viewer/src/components/log-list/log-list.styles.ts | 37 | ||||
-rw-r--r-- | pw_web/log-viewer/src/components/log-list/log-list.ts | 335 | ||||
-rw-r--r-- | pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts | 19 | ||||
-rw-r--r-- | pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts | 98 | ||||
-rw-r--r-- | pw_web/log-viewer/src/components/log-view/log-view.ts | 164 | ||||
-rw-r--r-- | pw_web/log-viewer/src/components/log-viewer.styles.ts | 5 | ||||
-rw-r--r-- | pw_web/log-viewer/src/components/log-viewer.ts | 23 | ||||
-rw-r--r-- | pw_web/log-viewer/src/createLogViewer.ts | 4 | ||||
-rw-r--r-- | pw_web/log-viewer/src/events/add-view.ts | 23 | ||||
-rw-r--r-- | pw_web/log-viewer/src/index.css | 13 | ||||
-rw-r--r-- | pw_web/log-viewer/src/shared/interfaces.ts | 11 |
13 files changed, 455 insertions, 317 deletions
diff --git a/pw_web/log-viewer/package-lock.json b/pw_web/log-viewer/package-lock.json index 5f2294340..298ae661f 100644 --- a/pw_web/log-viewer/package-lock.json +++ b/pw_web/log-viewer/package-lock.json @@ -8,7 +8,7 @@ "name": "log-viewer", "version": "0.0.0", "dependencies": { - "@lit-labs/virtualizer": "^2.0.3", + "@lit-labs/virtualizer": "^2.0.7", "@material/web": "^1.0.0-pre.16", "date-fns": "^2.30.0", "lit": "^3.0.0-pre.0", @@ -612,11 +612,11 @@ "integrity": "sha512-3FSKQV90k20guBluMFzd9paVuzZTxeL1vDuqNc8SbwpiCmZkY7CJH7HH4HVD35D4hU8d8JTKKL51eXRWzXBZXQ==" }, "node_modules/@lit-labs/virtualizer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@lit-labs/virtualizer/-/virtualizer-2.0.3.tgz", - "integrity": "sha512-/D8dYN0LmwMwXqPdKGPK7EKJjZiLHGtX1GwyNJX/dpOeztixliPrtG4KGAqzRTbVom8gXbM3N010fe7ssWrpOw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@lit-labs/virtualizer/-/virtualizer-2.0.7.tgz", + "integrity": "sha512-WWkI31QsSVoJZRuFo3sMn28LgSf2MYL7gp6OG2ZNksr2EoA3D8igNGp49cajI8HGHurSK4jGpMN2LHZrmy+Cxw==", "dependencies": { - "lit": "^2.7.0", + "lit": "^2.8.0", "tslib": "^2.0.3" } }, @@ -626,37 +626,37 @@ "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==" }, "node_modules/@lit-labs/virtualizer/node_modules/@lit/reactive-element": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.2.tgz", - "integrity": "sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.0.0" } }, "node_modules/@lit-labs/virtualizer/node_modules/lit": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.5.tgz", - "integrity": "sha512-i/cH7Ye6nBDUASMnfwcictBnsTN91+aBjXoTHF2xARghXScKxpD4F4WYI+VLXg9lqbMinDfvoI7VnZXjyHgdfQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", "dependencies": { "@lit/reactive-element": "^1.6.0", "lit-element": "^3.3.0", - "lit-html": "^2.7.0" + "lit-html": "^2.8.0" } }, "node_modules/@lit-labs/virtualizer/node_modules/lit-element": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.2.tgz", - "integrity": "sha512-xXAeVWKGr4/njq0rGC9dethMnYCq5hpKYrgQZYTzawt9YQhMiXfD+T1RgrdY3NamOxwq2aXlb0vOI6e29CKgVQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.1.0", "@lit/reactive-element": "^1.3.0", - "lit-html": "^2.7.0" + "lit-html": "^2.8.0" } }, "node_modules/@lit-labs/virtualizer/node_modules/lit-html": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.4.tgz", - "integrity": "sha512-/Jw+FBpeEN+z8X6PJva5n7+0MzCVAH2yypN99qHYYkq8bI+j7I39GH+68Z/MZD6rGKDK9RpzBw7CocfmHfq6+g==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", "dependencies": { "@types/trusted-types": "^2.0.2" } diff --git a/pw_web/log-viewer/package.json b/pw_web/log-viewer/package.json index 4236d50d7..180621128 100644 --- a/pw_web/log-viewer/package.json +++ b/pw_web/log-viewer/package.json @@ -10,7 +10,7 @@ "lint": "eslint --max-warnings=0 src" }, "dependencies": { - "@lit-labs/virtualizer": "^2.0.3", + "@lit-labs/virtualizer": "^2.0.7", "@material/web": "^1.0.0-pre.16", "date-fns": "^2.30.0", "lit": "^3.0.0-pre.0", diff --git a/pw_web/log-viewer/src/components/log-list/log-list.styles.ts b/pw_web/log-viewer/src/components/log-list/log-list.styles.ts index 8b9013cec..7f95c1d96 100644 --- a/pw_web/log-viewer/src/components/log-list/log-list.styles.ts +++ b/pw_web/log-viewer/src/components/log-list/log-list.styles.ts @@ -40,10 +40,10 @@ export const styles = css` table { border-collapse: collapse; + display: block; height: 100%; - min-width: 100vw; table-layout: fixed; - width: auto; + width: 100%; } thead, @@ -56,6 +56,8 @@ export const styles = css` thead { background-color: var(--sys-log-viewer-color-table-header-bg); color: var(--sys-log-viewer-color-table-header-text); + display: block; + width: 100%; } tr { @@ -63,6 +65,7 @@ export const styles = css` display: grid; justify-content: flex-start; width: 100%; + will-change: transform; } .log-row--warning { @@ -98,13 +101,29 @@ export const styles = css` color: var(--text-color); } - .log-row--nowrap .cell-content { + .log-row .cell-content { overflow: hidden; - white-space: nowrap; text-overflow: ellipsis; } - tr:hover > td { + .log-row--nowrap .cell-content { + white-space: nowrap; + } + + tbody tr::before { + background-color: transparent; + bottom: 0; + content: ''; + display: block; + left: 0; + position: absolute; + right: 0; + top: 0; + width: 100%; + z-index: -1; + } + + tbody tr:hover::before { background-color: rgba(var(--md-sys-inverse-surface-rgb), 0.05); } @@ -129,6 +148,10 @@ export const styles = css` white-space: nowrap; } + th[title='severity'] { + visibility: hidden; + } + td { display: inline-flex; position: relative; @@ -139,8 +162,8 @@ export const styles = css` .jump-to-bottom-btn { --md-filled-button-container-elevation: 4; --md-filled-button-hover-container-elevation: 4; - bottom: 2rem; - font-family: 'Google Sans', sans-serif; + bottom: 2.25rem; + font-family: 'Roboto Flex', sans-serif; left: 50%; position: absolute; transform: translate(-50%); diff --git a/pw_web/log-viewer/src/components/log-list/log-list.ts b/pw_web/log-viewer/src/components/log-list/log-list.ts index f6a39df7c..c7377d175 100644 --- a/pw_web/log-viewer/src/components/log-list/log-list.ts +++ b/pw_web/log-viewer/src/components/log-list/log-list.ts @@ -22,7 +22,7 @@ import { } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { styles } from './log-list.styles'; -import { FieldData, LogEntry, Severity } from '../../shared/interfaces'; +import { LogEntry, Severity, TableColumn } from '../../shared/interfaces'; import { virtualize } from '@lit-labs/virtualizer/virtualize.js'; import '@lit-labs/virtualizer'; @@ -52,9 +52,8 @@ export class LogList extends LitElement { @property({ type: Boolean }) lineWrap = false; - /** The field keys (column values) for the incoming log entries. */ - @state() - private _fieldKeys = new Set<string>(); + @property({ type: Array }) + columnData: TableColumn[] = []; /** Indicates whether the table content is overflowing to the right. */ @state() @@ -68,18 +67,22 @@ export class LogList extends LitElement { @state() private _scrollDownOpacity = 0; + /** Indicates whether to enable autosizing of incoming log entries. */ + @state() + private _autosizeLocked = false; + /** - * Indicates whether to automatically scroll the table container to the - * bottom when new log entries are added. + * Indicates whether to automatically scroll the table container to the bottom + * when new log entries are added. */ @state() private _autoscrollIsEnabled = true; - @query('.jump-to-bottom-btn') private _jumpBottomBtn!: HTMLButtonElement; @query('.table-container') private _tableContainer!: HTMLDivElement; @query('table') private _table!: HTMLTableElement; @query('tbody') private _tableBody!: HTMLTableSectionElement; @queryAll('tr') private _tableRows!: HTMLTableRowElement[]; + @query('.jump-to-bottom-btn') private _jumpBottomBtn!: HTMLButtonElement; /** * Data used for column resizing including the column index, the starting @@ -91,11 +94,14 @@ export class LogList extends LitElement { startWidth: number; } | null = null; + /** The number of times the `logs` array has been updated. */ + private logUpdateCount: number = 0; /** The maximum number of log entries to render in the list. */ private readonly MAX_ENTRIES = 100_000; - - @property({ type: Array }) - colsHidden: (boolean | undefined)[] = []; + /** The maximum number of log updates until autosize is disabled. */ + private readonly AUTOSIZE_LIMIT: number = 8; + /** The minimum width (in px) for table columns. */ + private readonly MIN_COL_WIDTH: number = 52; firstUpdated() { setInterval(() => this.updateHorizontalOverflowState(), 1000); @@ -103,9 +109,11 @@ export class LogList extends LitElement { window.addEventListener('scroll', this.handleTableScroll); this._tableBody.addEventListener('rangeChanged', this.onRangeChanged); - if (this.logs.length > 0) { - this.performUpdate(); - } + const newRowObserver = new MutationObserver(this.onTableRowAdded); + newRowObserver.observe(this._table, { + childList: true, + subtree: true, + }); } updated(changedProperties: PropertyValues) { @@ -119,13 +127,12 @@ export class LogList extends LitElement { } if (changedProperties.has('logs')) { - this.setFieldNames(this.logs); + this.logUpdateCount++; this.handleTableScroll(); } - if (changedProperties.has('colsHidden')) { - this.clearGridTemplateColumns(); - this.updateGridTemplateColumns(); + if (changedProperties.has('columnData')) { + this.updateColumnWidths(); } } @@ -135,27 +142,40 @@ export class LogList extends LitElement { this._tableBody.removeEventListener('rangeChanged', this.onRangeChanged); } - /** - * Sets the field names based on the provided log entries; used to define - * the table columns. - * - * @param logs An array of LogEntry objects. - */ - private setFieldNames(logs: LogEntry[]) { - logs.forEach((logEntry) => { - logEntry.fields.forEach((fieldData) => { - if (fieldData.key === 'severity') { - this._fieldKeys.add(''); + private onTableRowAdded = (mutations: MutationRecord[]) => { + mutations.forEach((mutation) => { + // Check for added nodes + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + const addedRows = Array.from(mutation.addedNodes).filter( + (node) => node.nodeName === 'TR', + ) as HTMLTableRowElement[]; + + if (addedRows.length <= 0) { return; } - this._fieldKeys.add(fieldData.key); - }); + + // Force repaint to prevent flickering + this._table.offsetTop; + + // Update header row alongside newly-added rows + const rowsToUpdate = [this._tableRows[0], ...addedRows]; + + if (!this._autosizeLocked) { + this.autosizeColumns(); + } else { + this.updateColumnWidths(rowsToUpdate); + } + + // Disable auto-sizing once a certain number of updates to the logs array have been made + if (this.logUpdateCount >= this.AUTOSIZE_LIMIT) { + this._autosizeLocked = true; + } + } }); - } + }; /** Called when the Lit virtualizer updates its range of entries. */ private onRangeChanged = () => { - this.updateGridTemplateColumns(); if (this._autoscrollIsEnabled) { this.scrollTableToBottom(); } @@ -165,48 +185,20 @@ export class LogList extends LitElement { private scrollTableToBottom() { const container = this._tableContainer; - // TODO(b/289101398): Refactor `setTimeout` usage + // TODO(b/298097109): Refactor `setTimeout` usage setTimeout(() => { container.scrollTop = container.scrollHeight; - this._jumpBottomBtn.hidden = true; - this._scrollDownOpacity = 0; }, 0); // Complete any rendering tasks before scrolling - } - /** Clears the `gridTemplateColumns` value for all rows in the table. */ - private clearGridTemplateColumns() { - this._tableRows.forEach((row) => { - row.style.gridTemplateColumns = ''; - }); + this._scrollDownOpacity = 0; + this._jumpBottomBtn.hidden = true; } /** - * Updates column visibility and calculates maximum column widths for the - * table. + * Calculates the maximum column widths for the table and updates the table + * rows. */ - private updateGridTemplateColumns = () => { - const rows = this._tableRows; - - // Set column visibility based on `colsHidden` array - rows.forEach((row) => { - const cells = Array.from(row.querySelectorAll('td, th')); - cells.forEach((cell, index: number) => { - const colHidden = this.colsHidden[index]; - - const cellEl = cell as HTMLElement; - cellEl.hidden = colHidden as boolean; - }); - }); - - // Get the number of visible columns - const columnCount = - Array.from(rows[0]?.children || []).filter( - (child) => !child.hasAttribute('hidden'), - ).length || 0; - - // Initialize an array to store the maximum width of each column - const columnWidths: number[] = new Array(columnCount).fill(0); - + private autosizeColumns = (rows = this._tableRows) => { // Iterate through each row to find the maximum width in each column rows.forEach((row) => { const cells = Array.from(row.children).filter( @@ -214,30 +206,71 @@ export class LogList extends LitElement { ) as HTMLTableCellElement[]; cells.forEach((cell, columnIndex) => { - const cellWidth = cell.getBoundingClientRect().width; - columnWidths[columnIndex] = Math.max( - columnWidths[columnIndex], - cellWidth, - ); + if (columnIndex === 0) return; + + const textLength = cell.textContent?.trim().length || 0; + + if (!this._autosizeLocked) { + // Update the preferred width if it's smaller than the new one + if (this.columnData[columnIndex]) { + this.columnData[columnIndex].characterLength = Math.max( + this.columnData[columnIndex].characterLength, + textLength, + ); + } else { + // Initialize if the column data for this index does not exist + this.columnData[columnIndex] = { + fieldName: '', + characterLength: textLength, + manualWidth: null, + isVisible: true, + }; + } + } }); }); - // Generate the gridTemplateColumns value for each row - rows.forEach((row) => { - const gridTemplateColumns = columnWidths - .map((width, index) => { - if (index === columnWidths.length - 1) { - return '1fr'; - } - if (index === 0) { - return '3.25rem'; + this.updateColumnWidths(rows); + }; + + private generateGridTemplateColumns( + newWidth?: number, + resizingIndex?: number, + ): string { + let gridTemplateColumns = ''; + + this.columnData.forEach((col, i) => { + let columnValue = ''; + + if (col.isVisible) { + if (i === resizingIndex) { + columnValue = `${newWidth}px`; + } else if (col.manualWidth !== null) { + columnValue = `${col.manualWidth}px`; + } else { + if (i === 0) { + columnValue = '3.25rem'; + } else { + const chWidth = col.characterLength; + const padding = 34; + columnValue = `clamp(${this.MIN_COL_WIDTH}px, ${chWidth}ch + ${padding}px, 80ch)`; } - return `${width}px`; - }) - .join(' '); - row.style.gridTemplateColumns = gridTemplateColumns; + } + + gridTemplateColumns += columnValue + ' '; + } }); - }; + + return gridTemplateColumns.trim(); + } + + private updateColumnWidths(rows: HTMLTableRowElement[] = this._tableRows) { + const gridTemplateColumns = this.generateGridTemplateColumns(); + + for (const row of rows) { + row.style.gridTemplateColumns = gridTemplateColumns; + } + } /** * Highlights text content within the table cell based on the current filter @@ -270,8 +303,8 @@ export class LogList extends LitElement { } /** - * Calculates scroll-related properties and updates the component's state - * when the user scrolls the table. + * Calculates scroll-related properties and updates the component's state when + * the user scrolls the table. */ private handleTableScroll = () => { const container = this._tableContainer; @@ -286,16 +319,27 @@ export class LogList extends LitElement { if (Math.abs(scrollY) <= 1) { this._autoscrollIsEnabled = true; - this.requestUpdate(); return; } if (Math.round(scrollY - rowHeight) >= 1) { this._autoscrollIsEnabled = false; - this._jumpBottomBtn.hidden = false; - this._scrollDownOpacity = 1; - this.requestUpdate(); } + + let debounceTimer: NodeJS.Timer | null = null; + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + debounceTimer = setTimeout(() => { + if (Math.round(scrollY - rowHeight) >= 1) { + this._jumpBottomBtn.hidden = false; + this._scrollDownOpacity = 1; + } else { + this._jumpBottomBtn.hidden = true; + this._scrollDownOpacity = 0; + } + }, 100); }; /** @@ -303,20 +347,38 @@ export class LogList extends LitElement { * * @param {MouseEvent} event - The mouse event triggered during column * resizing. - * @param {number} columnIndex - An index specifying the column being - * resized. + * @param {number} columnIndex - An index specifying the column being resized. */ private handleColumnResizeStart(event: MouseEvent, columnIndex: number) { event.preventDefault(); + // Check if the corresponding index in columnData is not visible. If not, + // check the columnIndex - 1th element until one isn't hidden. + while ( + this.columnData[columnIndex] && + !this.columnData[columnIndex].isVisible + ) { + columnIndex--; + if (columnIndex < 0) { + // Exit the loop if we've checked all possible columns + return; + } + } + + // If no visible columns are found, return early + if (columnIndex < 0) return; + const startX = event.clientX; const columnHeader = this._table.querySelector( `th:nth-child(${columnIndex + 1})`, ) as HTMLTableCellElement; + + if (!columnHeader) return; + const startWidth = columnHeader.offsetWidth; this.columnResizeData = { - columnIndex, + columnIndex: columnIndex, startX, startWidth, }; @@ -329,6 +391,13 @@ export class LogList extends LitElement { this.columnResizeData = null; document.removeEventListener('mousemove', handleColumnResize); document.removeEventListener('mouseup', handleColumnResizeEnd); + + // Communicate column data changes back to parent Log View + const updateColumnData = new CustomEvent('update-column-data', { + detail: this.columnData, + }); + + this.dispatchEvent(updateColumnData); }; document.addEventListener('mousemove', handleColumnResize); @@ -342,29 +411,22 @@ export class LogList extends LitElement { */ private handleColumnResize(event: MouseEvent) { if (!this.columnResizeData) return; + const { columnIndex, startX, startWidth } = this.columnResizeData; - const columnHeader = this._table.querySelector( - `th:nth-child(${columnIndex + 1})`, - ) as HTMLTableCellElement; const offsetX = event.clientX - startX; - const newWidth = Math.max(startWidth + offsetX, 48); // Minimum width - const totalColumns = this._table.querySelectorAll('th').length; - let gridTemplateColumns = ''; - - columnHeader.style.width = `${newWidth}px`; + const newWidth = Math.max(startWidth + offsetX, this.MIN_COL_WIDTH); - for (let i = 0; i < totalColumns; i++) { - if (i === columnIndex) { - gridTemplateColumns += `${newWidth}px `; - continue; - } - const otherColumnHeader = this._table.querySelector( - `th:nth-child(${i + 1})`, - ) as HTMLElement; - const otherColumnWidth = otherColumnHeader.offsetWidth; - gridTemplateColumns += `${otherColumnWidth}px `; + // Ensure the column index exists in columnData + if (this.columnData[columnIndex]) { + this.columnData[columnIndex].manualWidth = newWidth; } + const gridTemplateColumns = this.generateGridTemplateColumns( + newWidth, + columnIndex, + ); + + // Update the grid layout for each row this._tableRows.forEach((row) => { row.style.gridTemplateColumns = gridTemplateColumns; }); @@ -397,7 +459,8 @@ export class LogList extends LitElement { class="jump-to-bottom-btn" title="Jump to Bottom" @click="${this.scrollTableToBottom}" - trailing-icon + leading-icon + hidden > <md-icon slot="icon" aria-hidden="true">arrow_downward</md-icon> Jump to Bottom @@ -408,16 +471,24 @@ export class LogList extends LitElement { private tableHeaderRow() { return html` <tr> - ${Array.from(this._fieldKeys).map((fieldKey, columnIndex) => - this.tableHeaderCell(fieldKey, columnIndex), + ${this.columnData.map((columnData, columnIndex) => + this.tableHeaderCell( + columnData.fieldName, + columnIndex, + columnData.isVisible, + ), )} </tr> `; } - private tableHeaderCell(fieldKey: string, columnIndex: number) { + private tableHeaderCell( + fieldKey: string, + columnIndex: number, + isVisible: boolean, + ) { return html` - <th> + <th title="${fieldKey}" ?hidden=${!isVisible}> ${fieldKey} ${columnIndex > 0 ? this.resizeHandle(columnIndex - 1) : html``} </th> @@ -451,14 +522,29 @@ export class LogList extends LitElement { return html` <tr class="${classMap(classes)}"> - ${log.fields.map((field, columnIndex) => - this.tableDataCell(field, columnIndex), + ${this.columnData.map((columnData, columnIndex) => + this.tableDataCell( + log, + columnData.fieldName, + columnIndex, + columnData.isVisible, + ), )} </tr> `; } - private tableDataCell(field: FieldData, columnIndex: number) { + private tableDataCell( + log: LogEntry, + fieldKey: string, + columnIndex: number, + isVisible: boolean, + ) { + const field = log.fields.find((f) => f.key === fieldKey) || { + key: fieldKey, + value: '', + }; + if (field.key == 'severity') { const severityIcons = new Map<Severity, string>([ [Severity.WARNING, 'warning'], @@ -476,7 +562,7 @@ export class LogList extends LitElement { }; return html` - <td> + <td ?hidden=${!isVisible}> <div class="cell-content cell-content--icon"> <md-icon class="cell-icon" @@ -490,14 +576,11 @@ export class LogList extends LitElement { } return html` - <td> + <td ?hidden=${!isVisible}> <div class="cell-content"> ${this.highlightMatchedText(field.value.toString())} </div> - <!-- Don't add resize handles for default columns 'severity' and 'timestamp' --> - ${!['severity', 'timestamp'].includes(field.key) && columnIndex > 0 - ? this.resizeHandle(columnIndex - 1) - : html``} + ${columnIndex > 0 ? this.resizeHandle(columnIndex - 1) : html``} </td> `; } diff --git a/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts b/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts index 951407dd3..0f17bc9f1 100644 --- a/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts +++ b/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts @@ -18,6 +18,8 @@ export const styles = css` :host { align-items: center; background-color: var(--sys-log-viewer-color-controls-bg); + border-bottom: 1px solid var(--md-sys-color-outline-variant); + box-sizing: border-box; color: var(--sys-log-viewer-color-controls-text); display: flex; flex-shrink: 0; @@ -32,7 +34,10 @@ export const styles = css` display: flex; } - p { + .host-name { + font-size: 1.125rem; + font-weight: 300; + margin: 0; white-space: nowrap; } @@ -64,6 +69,7 @@ export const styles = css` } .input-container { + justify-content: flex-end; width: 100%; } @@ -74,14 +80,13 @@ export const styles = css` border-radius: 1.5rem; cursor: text; display: inline-flex; - font-family: 'Google Sans'; - font-size: 14px; - height: 1rem; - line-height: 1; + font-size: 1rem; + height: 0.75rem; + line-height: 0.75; max-width: 100%; - min-width: 20rem; + overflow: hidden; padding: 0.5rem 1rem; - width: fit-content; + width: 25rem; } input[type='text'] { 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 index 2e9ca9ef2..f31f8e129 100644 --- 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 @@ -21,7 +21,7 @@ import { state, } from 'lit/decorators.js'; import { styles } from './log-view-controls.styles'; -import { State } from '../../shared/interfaces'; +import { State, TableColumn } from '../../shared/interfaces'; import { StateStore, LocalStorageState } from '../../shared/state'; /** @@ -38,17 +38,13 @@ export class LogViewControls extends LitElement { @property({ type: String }) viewId = ''; - /** The field keys (column values) for the incoming log entries. */ @property({ type: Array }) - fieldKeys: string[] = []; + columnData: TableColumn[] = []; /** 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(); @@ -60,7 +56,7 @@ export class LogViewControls extends LitElement { _viewTitle = 'Log View'; @state() - _settingsMenuOpen = false; + _moreActionsMenuOpen = false; @query('.field-menu') _fieldMenu!: HTMLMenuElement; @@ -70,15 +66,13 @@ export class LogViewControls extends LitElement { @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; + @query('.more-actions-button') moreActionsButtonEl!: HTMLElement; constructor() { super(); @@ -103,17 +97,6 @@ export class LogViewControls extends LitElement { 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 @@ -164,8 +147,8 @@ export class LogViewControls extends LitElement { /** * 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. + * `timestamp` object indicating the date/time in which the 'clear-logs' event + * was dispatched. */ private handleClearLogsClick() { const timestamp = new Date(); @@ -179,9 +162,7 @@ export class LogViewControls extends LitElement { this.dispatchEvent(clearLogs); } - /** - * Dispatches a custom event for toggling wrapping. - */ + /** Dispatches a custom event for toggling wrapping. */ private handleWrapToggle() { const wrapToggle = new CustomEvent('wrap-toggle', { bubbles: true, @@ -192,8 +173,8 @@ export class LogViewControls extends LitElement { } /** - * Dispatches a custom event for closing the parent view. This event - * includes a `viewId` object indicating the `id` of the parent log view. + * 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', { @@ -208,10 +189,9 @@ export class LogViewControls extends LitElement { } /** - * 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. + * 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. */ @@ -229,6 +209,15 @@ export class LogViewControls extends LitElement { 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 @@ -250,23 +239,19 @@ export class LogViewControls extends LitElement { this.dispatchEvent(downloadLogs); } - /** - * Opens and closes the column visibility dropdown menu. - */ + /** 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; + /** Opens and closes the More Actions menu. */ + private toggleMoreActionsMenu() { + this._moreActionsMenuOpen = !this._moreActionsMenuOpen; } render() { return html` - <p class="host-name"> ${this._viewTitle}</p> + <p class="host-name">${this._viewTitle}</p> <div class="input-container"> <div class="input-facade" contenteditable="plaintext-only" @input="${ @@ -305,18 +290,18 @@ export class LogViewControls extends LitElement { <md-icon>view_column</md-icon> </md-icon-button> <menu class='field-menu' hidden> - ${Array.from(this.fieldKeys).map( - (field) => html` + ${this.columnData.map( + (column) => html` <li class="field-menu-item"> <input class="item-checkboxes" @click=${this.handleColumnToggle} - checked + ?checked=${column.isVisible} type="checkbox" - value=${field} - id=${field} + value=${column.fieldName} + id=${column.fieldName} /> - <label for=${field}>${field}</label> + <label for=${column.fieldName}>${column.fieldName}</label> </li> `, )} @@ -325,20 +310,27 @@ export class LogViewControls extends LitElement { <span class="action-button" title="Toggle fields"> <md-icon-button @click=${ - this.toggleSettingsMenu - } class="settings-menu-button"> + this.toggleMoreActionsMenu + } class="more-actions-button"> <md-icon >more_vert</md-icon> </md-icon-button> <md-menu quick fixed - ?open=${this._settingsMenuOpen} - .anchor=${this.settingsMenuButtonEl} + ?open=${this._moreActionsMenuOpen} + .anchor=${this.moreActionsButtonEl} @closed=${() => { - this._settingsMenuOpen = false; + 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"> + } 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> diff --git a/pw_web/log-viewer/src/components/log-view/log-view.ts b/pw_web/log-viewer/src/components/log-view/log-view.ts index 8bdbc0d17..e975c044f 100644 --- a/pw_web/log-viewer/src/components/log-view/log-view.ts +++ b/pw_web/log-viewer/src/components/log-view/log-view.ts @@ -16,7 +16,7 @@ import { LitElement, PropertyValues, html } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { styles } from './log-view.styles'; import { LogList } from '../log-list/log-list'; -import { LogColumnState, LogEntry, State } from '../../shared/interfaces'; +import { TableColumn, LogEntry, State } from '../../shared/interfaces'; import { LocalStorageState, StateStore } from '../../shared/state'; import { LogFilter } from '../../utils/log-filter/log-filter'; import '../log-list/log-list'; @@ -37,8 +37,8 @@ export class LogView extends LitElement { static styles = styles; /** - * The component's global `id` attribute. This unique value is set whenever - * a view is created in a log viewer instance. + * The component's global `id` attribute. This unique value is set whenever a + * view is created in a log viewer instance. */ @property({ type: String }) id = `${this.localName}-${crypto.randomUUID()}`; @@ -56,15 +56,15 @@ export class LogView extends LitElement { _lineWrap = false; /** - * An array containing the logs that remain after the current filter has - * been applied. + * An array containing the logs that remain after the current filter has been + * applied. */ @state() private _filteredLogs: LogEntry[] = []; /** The field keys (column values) for the incoming log entries. */ @state() - private _fieldKeys: string[] = []; + private _columnData: TableColumn[] = []; /** A function used for filtering rows that contain a certain substring. */ @state() @@ -88,15 +88,14 @@ export class LogView extends LitElement { @state() _state: State; - @state() - _colsHidden: (boolean | undefined)[] = []; - @query('log-list') _logList!: LogList; private _debounceTimeout: NodeJS.Timeout | null = null; /** The amount of time, in ms, before the filter expression is executed. */ private readonly FILTER_DELAY = 100; + /** The number of elements in the `logs` array since last updated. */ + private lastKnownLogLength: number = 0; constructor() { super(); @@ -104,19 +103,13 @@ export class LogView extends LitElement { } protected firstUpdated(): void { - this._colsHidden = []; - - if (this._state) { - const viewConfigArr = this._state.logViewConfig; - const index = viewConfigArr.findIndex((i) => this.id === i.viewID); - - if (index !== -1) { - viewConfigArr[index].search = this.searchText; - viewConfigArr[index].columns.map((i: LogColumnState) => { - this._colsHidden.push(i.hidden); - }); - this._colsHidden.unshift(undefined); - } + const viewConfigArr = this._state.logViewConfig; + const index = viewConfigArr.findIndex((i) => this.id === i.viewID); + + // Get column data from local storage, if it exists + if (index !== -1) { + const storedColumnData = viewConfigArr[index].columnData; + this._columnData = storedColumnData; } } @@ -124,21 +117,31 @@ export class LogView extends LitElement { super.updated(changedProperties); if (changedProperties.has('logs')) { - this._fieldKeys = this.getFieldsFromLogs(this.logs); + const newLogs = this.logs.slice(this.lastKnownLogLength); + this.lastKnownLogLength = this.logs.length; + + this.updateFieldsFromNewLogs(newLogs); this.filterLogs(); } + + if (changedProperties.has('_columnData')) { + this._state = { logViewConfig: this._state.logViewConfig }; + this._stateStore.setState({ + logViewConfig: this._state.logViewConfig, + }); + } } /** * Updates the log filter based on the provided event type. * - * @param {CustomEvent} event - The custom event containing the information - * to update the filter. + * @param {CustomEvent} event - The custom event containing the information to + * update the filter. */ private updateFilter(event: CustomEvent) { this.searchText = event.detail.inputValue; - const viewConfigArr = this._state.logViewConfig; - const index = viewConfigArr.findIndex((i) => this.id === i.viewID); + const logViewConfig = this._state.logViewConfig; + const index = logViewConfig.findIndex((i) => this.id === i.viewID); switch (event.type) { case 'input-change': @@ -147,9 +150,9 @@ export class LogView extends LitElement { } if (index !== -1) { - viewConfigArr[index].search = this.searchText; - this._state = { logViewConfig: viewConfigArr }; - this._stateStore.setState({ logViewConfig: viewConfigArr }); + logViewConfig[index].search = this.searchText; + this._state = { logViewConfig: logViewConfig }; + this._stateStore.setState({ logViewConfig: logViewConfig }); } if (!this.searchText) { @@ -184,25 +187,29 @@ export class LogView extends LitElement { this.requestUpdate(); } - /** - * Retrieves the field keys from the first entry in the log array. - * - * @param {LogEntry[]} logs - The array of log entries from which to - * retrieve the field keys. - * @returns {string[]} An array containing the field keys from the log - * entries. - */ - public getFieldsFromLogs(logs: LogEntry[]): string[] { - const logEntry = logs[0]; - const logFields = [] as string[]; + private updateFieldsFromNewLogs(newLogs: LogEntry[]): void { + if (!this._columnData) { + this._columnData = []; + } - if (logEntry != undefined) { - logEntry.fields.forEach((field) => { - logFields.push(field.key); + newLogs.forEach((log) => { + log.fields.forEach((field) => { + if (!this._columnData.some((col) => col.fieldName === field.key)) { + this._columnData.push({ + fieldName: field.key, + characterLength: 0, + manualWidth: null, + isVisible: true, + }); + } }); - } + }); + } - return logFields.filter((field) => field !== 'severity'); + public getFields(): string[] { + return this._columnData + .filter((column) => column.isVisible) + .map((column) => column.fieldName); } /** @@ -213,32 +220,37 @@ export class LogView extends LitElement { * toggled. */ private toggleColumns(event: CustomEvent) { - const viewConfigArr = this._state.logViewConfig; - let colIndex = -1; - - this._colsHidden = []; - const index = viewConfigArr - .map((i) => { - return i.viewID; - }) - .indexOf(this.id); - viewConfigArr[index].columns.map((i: LogColumnState) => { - this._colsHidden.push(i.hidden); - }); - this._colsHidden.unshift(undefined); + const logViewConfig = this._state.logViewConfig; + const index = logViewConfig.findIndex((i) => this.id === i.viewID); - this._fieldKeys.forEach((field: string, i: number) => { - if (field == event.detail.field) { - colIndex = i; - viewConfigArr[index].columns[colIndex].hidden = !event.detail.isChecked; - } - }); + if (index === -1) { + return; + } - this._colsHidden[colIndex + 1] = !event.detail.isChecked; // Exclude first column (severity) - this._logList.colsHidden = [...this._colsHidden]; + // Find the relevant column in _columnData + const column = this._columnData.find( + (col) => col.fieldName === event.detail.field, + ); - this._state = { logViewConfig: viewConfigArr }; - this._stateStore.setState({ logViewConfig: viewConfigArr }); + if (!column) { + return; + } + + // Toggle the column's visibility + column.isVisible = event.detail.isChecked; + + // Clear the manually-set width of the last visible column + const lastVisibleColumn = this._columnData + .slice() + .reverse() + .find((col) => col.isVisible); + if (lastVisibleColumn) { + lastVisibleColumn.manualWidth = null; + } + + // Trigger the change in column data and request an update + this._columnData = [...this._columnData]; + this._logList.requestUpdate(); } /** @@ -251,8 +263,8 @@ export class LogView extends LitElement { } /** - * Combines constituent filter expressions and filters the logs. The - * filtered logs are stored in the `_filteredLogs` state property. + * Combines constituent filter expressions and filters the logs. The filtered + * logs are stored in the `_filteredLogs` state property. */ private filterLogs() { const combinedFilter = (logEntry: LogEntry) => @@ -263,6 +275,10 @@ export class LogView extends LitElement { ); } + private updateColumnData(event: CustomEvent) { + this._columnData = event.detail; + } + /** * Generates a log file in the specified format and initiates its download. * @@ -306,9 +322,8 @@ export class LogView extends LitElement { render() { return html` <log-view-controls - .colsHidden=${[...this._colsHidden]} + .columnData=${[...this._columnData]} .viewId=${this.id} - .fieldKeys=${this._fieldKeys} .hideCloseButton=${!this.isOneOfMany} .stateStore=${this._stateStore} @input-change="${this.updateFilter}" @@ -321,11 +336,12 @@ export class LogView extends LitElement { </log-view-controls> <log-list - .colsHidden=${[...this._colsHidden]} + .columnData=${[...this._columnData]} .lineWrap=${this._lineWrap} .viewId=${this.id} .logs=${this._filteredLogs} .searchText=${this.searchText} + @update-column-data="${this.updateColumnData}" > </log-list>`; } diff --git a/pw_web/log-viewer/src/components/log-viewer.styles.ts b/pw_web/log-viewer/src/components/log-viewer.styles.ts index 620a2c044..f5b96313d 100644 --- a/pw_web/log-viewer/src/components/log-viewer.styles.ts +++ b/pw_web/log-viewer/src/components/log-viewer.styles.ts @@ -24,10 +24,7 @@ export const styles = css` flex-direction: column; gap: 2rem; height: var(--sys-log-viewer-height); - } - - button { - font-family: 'Google Sans'; + width: 100%; } .grid-container { diff --git a/pw_web/log-viewer/src/components/log-viewer.ts b/pw_web/log-viewer/src/components/log-viewer.ts index c09424b79..a24a6a25d 100644 --- a/pw_web/log-viewer/src/components/log-viewer.ts +++ b/pw_web/log-viewer/src/components/log-viewer.ts @@ -16,7 +16,7 @@ import { LitElement, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { - LogColumnState, + TableColumn, LogEntry, LogViewConfig, State, @@ -98,18 +98,20 @@ export class LogViewer extends LitElement { /** Creates a new log view state to store in the state object. */ private addLogViewState(view: LogView): LogViewConfig { const fieldColumns = []; - const fields = view.getFieldsFromLogs(this.logs); + const fields = view.getFields(); for (const i in fields) { - const col: LogColumnState = { - hidden: false, - name: fields[i], + const col: TableColumn = { + isVisible: true, + fieldName: fields[i], + characterLength: 0, + manualWidth: null, }; fieldColumns.push(col); } const obj = { - columns: fieldColumns, + columnData: fieldColumns, search: '', viewID: view.id, viewTitle: 'Log View', @@ -136,14 +138,6 @@ export class LogViewer extends LitElement { render() { return html` - <md-outlined-button - class="add-button" - @click="${this.addLogView}" - title="Add a view" - > - Add View - </md-outlined-button> - <div class="grid-container"> ${repeat( this._logViews, @@ -154,6 +148,7 @@ export class LogViewer extends LitElement { .logs=${[...this.logs]} .isOneOfMany=${this._logViews.length > 1} .stateStore=${this._stateStore} + @add-view="${this.addLogView}" ></log-view> `, )} diff --git a/pw_web/log-viewer/src/createLogViewer.ts b/pw_web/log-viewer/src/createLogViewer.ts index d43a908a9..10ce61859 100644 --- a/pw_web/log-viewer/src/createLogViewer.ts +++ b/pw_web/log-viewer/src/createLogViewer.ts @@ -45,9 +45,11 @@ export function createLogViewer( if (lastUpdateTimeoutId) { clearTimeout(lastUpdateTimeoutId); } + // Call requestUpdate at most once every 100 milliseconds. lastUpdateTimeoutId = setTimeout(() => { - logViewer.requestUpdate('logs', []); + const updatedLogs = [...logs]; + logViewer.logs = updatedLogs; }, 100); }; diff --git a/pw_web/log-viewer/src/events/add-view.ts b/pw_web/log-viewer/src/events/add-view.ts new file mode 100644 index 000000000..abe48877a --- /dev/null +++ b/pw_web/log-viewer/src/events/add-view.ts @@ -0,0 +1,23 @@ +// 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. + +interface AddViewEvent extends CustomEvent {} + +declare global { + interface GlobalEventHandlersEventMap { + 'add-view': AddViewEvent; + } +} + +export default AddViewEvent; diff --git a/pw_web/log-viewer/src/index.css b/pw_web/log-viewer/src/index.css index ac1a2861a..ba83bbb6a 100644 --- a/pw_web/log-viewer/src/index.css +++ b/pw_web/log-viewer/src/index.css @@ -14,12 +14,12 @@ * the License. */ - @import url('https://fonts.googleapis.com/css2?family=Google+Sans&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block'); + @import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,300;8..144,400;8..144,500;8..144,600&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block'); :root { background-color: var(--sys-log-viewer-color-bg); color-scheme: light dark; - font-family: "Google Sans", Arial, sans-serif; + font-family: "Roboto Flex", Arial, sans-serif; font-synthesis: none; font-weight: 400; line-height: 1.5; @@ -34,13 +34,13 @@ /* Material component properties */ --md-icon-font: 'Material Symbols Rounded'; --md-icon-size: 1.25rem; - --md-filled-button-label-text-type: "Google Sans", Arial, sans-serif; - --md-outlined-button-label-text-type: "Google Sans", Arial, sans-serif; + --md-filled-button-label-text-type: "Roboto Flex", Arial, sans-serif; + --md-outlined-button-label-text-type: "Roboto Flex", Arial, sans-serif; --md-icon-button-unselected-icon-color: var(--md-sys-color-on-surface-variant); --md-icon-button-unselected-hover-icon-color: var(--md-sys-color-on-primary-container); /* Log View */ - --sys-log-viewer-height: 100vh; + --sys-log-viewer-height: 100%; --sys-log-viewer-view-outline-width: 1px; --sys-log-viewer-view-corner-radius: 0.5rem; } @@ -50,12 +50,13 @@ } button { - font-family: "Google Sans"; + font-family: "Roboto Flex"; } main { height: 100vh; padding: 2rem; + width: 100vw; } a { diff --git a/pw_web/log-viewer/src/shared/interfaces.ts b/pw_web/log-viewer/src/shared/interfaces.ts index 435fe42f7..2b1ac5946 100644 --- a/pw_web/log-viewer/src/shared/interfaces.ts +++ b/pw_web/log-viewer/src/shared/interfaces.ts @@ -17,10 +17,11 @@ export interface FieldData { value: string | boolean | number | object; } -export interface LogColumnState { - hidden: boolean; - name: string; - width?: string; +export interface TableColumn { + fieldName: string; + characterLength: number; + manualWidth: number | null; + isVisible: boolean; } export interface LogEntry { @@ -30,7 +31,7 @@ export interface LogEntry { } export interface LogViewConfig { - columns: LogColumnState[]; + columnData: TableColumn[]; search: string; viewID: string; viewTitle: string; |