Skip to content

Commit

Permalink
pw_web: Create log store and enable download logs from it
Browse files Browse the repository at this point in the history
Fixed: 316966729
Change-Id: I89e406777dbc2aef9baf40e731e9073aab4f966d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/186874
Presubmit-Verified: CQ Bot Account <[email protected]>
Reviewed-by: Luis Flores <[email protected]>
Commit-Queue: Amy Hu <[email protected]>
  • Loading branch information
Amy Hu authored and CQ Bot Account committed Feb 23, 2024
1 parent ff074e7 commit 9c1540d
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 11 deletions.
12 changes: 6 additions & 6 deletions pw_web/log-viewer/src/createLogViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

import { LogViewer as RootComponent } from './components/log-viewer';
import { StateStore, LocalStorageState } from './shared/state';
import { LogEntry, LogSourceEvent } from '../src/shared/interfaces';
import { LogSourceEvent } from '../src/shared/interfaces';
import { LogSource } from '../src/log-source';
import { LogStore } from './log-store';

import '@material/web/button/filled-button.js';
import '@material/web/button/outlined-button.js';
Expand All @@ -31,26 +32,25 @@ import '@material/web/menu/menu-item.js';
export function createLogViewer(
root: HTMLElement,
state: StateStore = new LocalStorageState(),
logStore: LogStore,
...logSources: LogSource[]
) {
const logViewer = new RootComponent(state);
const logs: LogEntry[] = [];
root.appendChild(logViewer);
let lastUpdateTimeoutId: NodeJS.Timeout;

// Define an event listener for the 'logEntry' event
const logEntryListener = (event: LogSourceEvent) => {
if (event.type === 'log-entry') {
const logEntry = event.data;
logs.push(logEntry);
logViewer.logs = logs;
logStore.addLogEntry(logEntry);
logViewer.logs = logStore.getLogs();
if (lastUpdateTimeoutId) {
clearTimeout(lastUpdateTimeoutId);
}

// Call requestUpdate at most once every 100 milliseconds.
lastUpdateTimeoutId = setTimeout(() => {
const updatedLogs = [...logs];
const updatedLogs = [...logStore.getLogs()];
logViewer.logs = updatedLogs;
}, 100);
}
Expand Down
4 changes: 3 additions & 1 deletion pw_web/log-viewer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { createLogViewer } from './createLogViewer';
import { MockLogSource } from './custom/mock-log-source';
import { LocalStorageState } from './shared/state';
import { LogSource } from './log-source';
import { LogStore } from './log-store';

const logStore = new LogStore();
const logSources = [new MockLogSource(), new JsonLogSource()] as LogSource[];
const state = new LocalStorageState();

Expand All @@ -26,7 +28,7 @@ const containerEl = document.querySelector(
) as HTMLElement;

if (containerEl) {
createLogViewer(containerEl, state, ...logSources);
createLogViewer(containerEl, state, logStore, ...logSources);
}

// Start reading log data
Expand Down
69 changes: 69 additions & 0 deletions pw_web/log-viewer/src/log-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

import { LogEntry } from './shared/interfaces';
import { titleCaseToKebabCase } from './utils/strings';

export class LogStore {
private logs: LogEntry[];

constructor() {
this.logs = [];
}

addLogEntry(logEntry: LogEntry) {
this.logs.push(logEntry);
}

downloadLogs(event: CustomEvent) {
const logs = this.getLogs();
const headers = logs[0]?.fields.map((field) => field.key) || [];
const maxWidths = headers.map((header) => header.length);
const viewTitle = event.detail.viewTitle;
const fileName = viewTitle ? titleCaseToKebabCase(viewTitle) : 'logs';

logs.forEach((log) => {
log.fields.forEach((field, columnIndex) => {
maxWidths[columnIndex] = Math.max(
maxWidths[columnIndex],
field.value.toString().length,
);
});
});

const headerRow = headers
.map((header, columnIndex) => header.padEnd(maxWidths[columnIndex]))
.join('\t');
const separator = '';
const logRows = logs.map((log) => {
const values = log.fields.map((field, columnIndex) =>
field.value.toString().padEnd(maxWidths[columnIndex]),
);
return values.join('\t');
});

const formattedLogs = [headerRow, separator, ...logRows].join('\n');
const blob = new Blob([formattedLogs], { type: 'text/plain' });
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = `${fileName}.txt`;
downloadLink.click();

URL.revokeObjectURL(downloadLink.href);
}

getLogs(): LogEntry[] {
return this.logs;
}
}
78 changes: 78 additions & 0 deletions pw_web/log-viewer/test/log-store.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

import { expect } from '@open-wc/testing';
import { createLogViewer } from '../src/createLogViewer';
import { MockLogSource } from '../src/custom/mock-log-source';
import { LogStore } from '../src/log-store';

// Initialize the log viewer component with a mock log source
function setUpLogViewer() {
const mockLogSource = new MockLogSource();
const logStore = new LogStore();
const destroyLogViewer = createLogViewer(
document.body,
undefined,
logStore,
mockLogSource,
);
const logViewer = document.querySelector('log-viewer');
return { mockLogSource, destroyLogViewer, logViewer, logStore };
}

// Handle benign ResizeObserver error caused by custom log viewer initialization
// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
function handleResizeObserverError() {
const e = window.onerror;
window.onerror = function (err) {
if (
err === 'ResizeObserver loop completed with undelivered notifications.'
) {
console.warn(
'Ignored: ResizeObserver loop completed with undelivered notifications.',
);
return false;
} else {
return e(...arguments);
}
};
}

describe('log-store', () => {
let mockLogSource;
let destroyLogViewer;
let logViewer;
let logStore;

beforeEach(() => {
window.localStorage.clear();
({ mockLogSource, destroyLogViewer, logViewer, logStore } =
setUpLogViewer());
handleResizeObserverError();
});

afterEach(() => {
mockLogSource.stop();
destroyLogViewer();
});

it('should maintain log entries emitted', async () => {
const logEntry = mockLogSource.readLogEntryFromHost();
mockLogSource.publishLogEntry(logEntry);
const logs = logStore.getLogs();

expect(logs.length).equal(1);
expect(logs[0]).equal(logEntry);
});
});
12 changes: 8 additions & 4 deletions pw_web/log-viewer/test/log-view.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,20 @@ import { assert } from '@open-wc/testing';
import { MockLogSource } from '../src/custom/mock-log-source';
import { createLogViewer } from '../src/createLogViewer';
import { LocalStorageState } from '../src/shared/state';
import { LogStore } from '../src/log-store';

function setUpLogViewer(logSources) {
const logStore = new LogStore();
const destroyLogViewer = createLogViewer(
document.body,
undefined,
logStore,
...logSources,
);
const logViewer = document.querySelector('log-viewer');

handleResizeObserverError();
return { logSources, destroyLogViewer, logViewer };
return { logSources, destroyLogViewer, logViewer, logStore };
}

// Handle benign ResizeObserver error caused by custom log viewer initialization
Expand All @@ -49,6 +52,7 @@ function handleResizeObserverError() {

describe('log-view', () => {
let logSources;
let logStore;
let destroyLogViewer;
let logViewer;
let stateStore;
Expand All @@ -66,7 +70,7 @@ describe('log-view', () => {
describe('state', () => {
beforeEach(() => {
window.localStorage.clear();
({ logSources, destroyLogViewer, logViewer } = setUpLogViewer([
({ logSources, destroyLogViewer, logViewer, logStore } = setUpLogViewer([
new MockLogSource(),
]));
stateStore = new LocalStorageState();
Expand Down Expand Up @@ -107,7 +111,7 @@ describe('log-view', () => {
stateStore.setState(state);

// Create a new log viewer with an existing state
({ logSources, destroyLogViewer, logViewer } = setUpLogViewer([
({ logSources, destroyLogViewer, logViewer, logStore } = setUpLogViewer([
new MockLogSource(),
]));
const logViews = await getLogViews();
Expand All @@ -119,7 +123,7 @@ describe('log-view', () => {
describe('sources', () => {
before(() => {
window.localStorage.clear();
({ logSources, destroyLogViewer, logViewer } = setUpLogViewer([
({ logSources, destroyLogViewer, logViewer, logStore } = setUpLogViewer([
new MockLogSource('Source 1'),
new MockLogSource('Source 2'),
]));
Expand Down
3 changes: 3 additions & 0 deletions pw_web/log-viewer/test/log-viewer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import { expect } from '@open-wc/testing';
import '../src/components/log-viewer';
import { MockLogSource } from '../src/custom/mock-log-source';
import { createLogViewer } from '../src/createLogViewer';
import { LogStore } from '../src/log-store';

// Initialize the log viewer component with a mock log source
function setUpLogViewer() {
const mockLogSource = new MockLogSource();
const logStore = new LogStore();
const destroyLogViewer = createLogViewer(
document.body,
undefined,
logStore,
mockLogSource,
);
const logViewer = document.querySelector('log-viewer');
Expand Down

0 comments on commit 9c1540d

Please sign in to comment.