aboutsummaryrefslogtreecommitdiff
path: root/pw_web
diff options
context:
space:
mode:
authorLuis Flores <lesprit@google.com>2023-09-12 20:22:55 +0000
committerCQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-09-12 20:22:55 +0000
commite0b85213d6493b72d9d93019f8577b8cfb89867c (patch)
tree3815bc2862a338f0f66c14e0683caf0936156b27 /pw_web
parent93a418acb4fa8c09abeda07ec40c3a0e53e8c7e1 (diff)
downloadpigweed-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.json38
-rw-r--r--pw_web/log-viewer/package.json2
-rw-r--r--pw_web/log-viewer/src/components/log-list/log-list.styles.ts37
-rw-r--r--pw_web/log-viewer/src/components/log-list/log-list.ts335
-rw-r--r--pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts19
-rw-r--r--pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts98
-rw-r--r--pw_web/log-viewer/src/components/log-view/log-view.ts164
-rw-r--r--pw_web/log-viewer/src/components/log-viewer.styles.ts5
-rw-r--r--pw_web/log-viewer/src/components/log-viewer.ts23
-rw-r--r--pw_web/log-viewer/src/createLogViewer.ts4
-rw-r--r--pw_web/log-viewer/src/events/add-view.ts23
-rw-r--r--pw_web/log-viewer/src/index.css13
-rw-r--r--pw_web/log-viewer/src/shared/interfaces.ts11
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;