[logging] Allow for custom formatter for content in log-row
This will allow downstream users to insert custom color/text formatting
for every column.
This also adds a showControls option to show/hide controls and filter
input.
Change-Id: I019dd302aa358dadf37d957a2ba2e48fef75fd4e
Reviewed-on: https://fuchsia-review.googlesource.com/c/vscode-plugins/+/769723
Reviewed-by: Miguel Flores <miguelfrde@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/webviews/logging/components/log_list.ts b/webviews/logging/components/log_list.ts
index ca526ff..19904b6 100644
--- a/webviews/logging/components/log_list.ts
+++ b/webviews/logging/components/log_list.ts
@@ -11,6 +11,7 @@
import {
LogField,
LogHeadersData,
+ LogViewOptions,
HeaderWidthChangeHandler
} from '../src/fields';
import {FilterExpression} from '../src/filter';
@@ -123,7 +124,8 @@
private headerFields: LogHeadersData,
shouldWrapLogs: boolean,
onHeaderWidthChange: HeaderWidthChangeHandler,
- private currentFilter: FilterExpression) {
+ private currentFilter: FilterExpression,
+ private options: LogViewOptions) {
super();
this.maxWidths = new Array(Object.keys(headerFields).length).fill(0);
this.logHeader = new LogHeader(headerFields, onHeaderWidthChange);
@@ -209,7 +211,8 @@
}
private appendLogElement(log: LogRowData) {
- const element = new LogRow(log, this.headerFields);
+ const element = new LogRow(
+ log, this.headerFields, this.options.columnFormatter);
this.logRows = [...this.logRows, element];
element.updateComplete
.then(() => {
diff --git a/webviews/logging/components/log_row.ts b/webviews/logging/components/log_row.ts
index fdb7c1f..1e3af09 100644
--- a/webviews/logging/components/log_row.ts
+++ b/webviews/logging/components/log_row.ts
@@ -5,7 +5,7 @@
import {ChildPart, html, LitElement} from 'lit';
import {Directive, directive} from 'lit/directive.js';
import {customElement, property, state} from 'lit/decorators.js';
-import {LogHeadersData} from '../src/fields';
+import {LogColumnFormatter, LogHeadersData} from '../src/fields';
import {LogRowData} from '../src/log_data';
class AttributeSetter extends Directive {
@@ -31,7 +31,10 @@
private fields!: Record<string, any>;
private dataAttributes!: Record<string, any>;
- constructor(log: LogRowData, private headerFields: LogHeadersData) {
+ constructor(
+ log: LogRowData,
+ private headerFields: LogHeadersData,
+ private columnFormatter: LogColumnFormatter) {
super();
this.log = log;
this.formatRow(this.log);
@@ -58,7 +61,7 @@
title="${fieldKey === 'message' ? '' : content}"
class="log-list-cell${fieldKey === 'message' ? ' msg-cell' : ''}"
>
- ${content}
+ ${this.columnFormatter(fieldKey, content)}
</div>`;
}
diff --git a/webviews/logging/components/view.ts b/webviews/logging/components/view.ts
index c8cb235..bb7c29b 100644
--- a/webviews/logging/components/view.ts
+++ b/webviews/logging/components/view.ts
@@ -8,6 +8,7 @@
import { State } from '../src/state';
import { LogList } from './log_list';
import {ffxLogToLogRowData} from '../src/ffxLogToLogRow';
+import {LogViewOptions} from '../src/fields';
export const RESUMING_LOG_STREAMING_LOG = 'Resuming log streaming, press [pause icon] to pause.';
export const PAUSING_LOG_STREAMING_LOG = 'Playback has been paused, press [play icon] to resume.';
@@ -19,26 +20,33 @@
export class LoggingView extends EventTarget {
private logActionContainer: HTMLDivElement;
- private logControl: LogControl;
+ private logControl?: LogControl;
private logList: LogList;
private logListContainer: HTMLDivElement;
private logViewActions: LogViewActions;
public static readonly externalActionRequestEvent: string = 'external-action-request-event';
- constructor(private state: State, private root: HTMLElement) {
+ constructor(
+ private state: State,
+ private root: HTMLElement,
+ private options: LogViewOptions = {
+ columnFormatter: (_fieldName, text) => text,
+ showControls: true
+ }) {
super();
this.logList = new LogList(state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
-
- this.logControl = new LogControl(this.state.currentFilterText);
- this.logControl.addEventListener(LogControl.filterChangeEvent, (e) => {
- const event = e as CustomEvent;
- const { filter, text } = event.detail;
- this.state.registerFilter(filter, text);
- this.logControl.resultsMessage = filter.isEmpty() ? '' : `${this.logList.filterResultsCount} results`;
-
- });
+ state.setHeaderWidth.bind(state), state.currentFilter, options);
+ if (this.options.showControls) {
+ this.logControl = new LogControl(this.state.currentFilterText);
+ this.logControl.addEventListener(LogControl.filterChangeEvent, (e) => {
+ const event = e as CustomEvent;
+ const { filter, text } = event.detail;
+ this.state.registerFilter(filter, text);
+ this.logControl!.resultsMessage =
+ filter.isEmpty() ? '' : `${this.logList.filterResultsCount} results`;
+ });
+ }
this.state.addEventListener('filterChange', (e) => {
const event = e as CustomEvent;
@@ -77,7 +85,7 @@
this.logListContainer.id = 'log-list-container';
this.root.appendChild(this.logActionContainer);
- this.logActionContainer.appendChild(this.logControl);
+ if (this.logControl) {this.logActionContainer.appendChild(this.logControl);}
this.logActionContainer.appendChild(this.logViewActions);
this.root.appendChild(this.logListContainer);
@@ -98,7 +106,7 @@
public reset() {
this.state.reset();
this.logList.reset();
- this.logControl.reset();
+ this.logControl?.reset();
}
/**
diff --git a/webviews/logging/src/fields.ts b/webviews/logging/src/fields.ts
index 08a69e7..a708c12 100644
--- a/webviews/logging/src/fields.ts
+++ b/webviews/logging/src/fields.ts
@@ -10,7 +10,15 @@
export type LogFieldData = {
displayName: string,
- width: string,
+ width: string
+};
+
+export type LogColumnFormatter =
+ (fieldName: string, text: string) => HTMLElement | string;
+
+export type LogViewOptions = {
+ columnFormatter: LogColumnFormatter,
+ showControls: boolean
};
export type LogHeadersData = Record<LogField, LogFieldData>;
diff --git a/webviews/logging/test/filter.test.ts b/webviews/logging/test/filter.test.ts
index 3315e35..36735b2 100644
--- a/webviews/logging/test/filter.test.ts
+++ b/webviews/logging/test/filter.test.ts
@@ -8,6 +8,9 @@
import { FilterExpression, OrExpression, parseFilter } from '../src/filter';
import {ffxLogToLogRowData} from '../src/ffxLogToLogRow';
import {State} from '../src/state';
+import {LogColumnFormatter} from '../src/fields';
+
+const columnFormatter: LogColumnFormatter = (_fieldName, text) => text;
before(() => {
chai.should();
@@ -55,7 +58,7 @@
},
timestamp: 12345,
version: 1
- }, false)!, state.currentFields),
+ }, false)!, state.currentFields, columnFormatter),
new LogRow( //2
ffxLogToLogRowData(
{
@@ -92,7 +95,7 @@
},
timestamp: 12345,
version: 1
- }, false)!, state.currentFields),
+ }, false)!, state.currentFields, columnFormatter),
new LogRow(
ffxLogToLogRowData(
{
@@ -129,7 +132,7 @@
},
timestamp: 12345,
version: 1
- }, false)!, state.currentFields),
+ }, false)!, state.currentFields, columnFormatter),
];
for (const index in testLitElements) {
diff --git a/webviews/logging/test/log_header.test.ts b/webviews/logging/test/log_header.test.ts
index bc1f570..055d694 100644
--- a/webviews/logging/test/log_header.test.ts
+++ b/webviews/logging/test/log_header.test.ts
@@ -8,6 +8,12 @@
import { LogField } from '../src/fields';
import {LogHeader} from '../components/log_header';
import {LogList} from '../components/log_list';
+import {LogViewOptions} from '../src/fields';
+
+const logOptions: LogViewOptions = {
+ columnFormatter: (_fieldName, text) => text,
+ showControls: true
+};
before(() => {
chai.should();
@@ -24,7 +30,7 @@
document.body.appendChild(root);
state = new State();
logList = new LogList(state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
root.appendChild(logList);
await logList.updateComplete;
logHeader = logList.shadowRoot!.children[0] as LogHeader;
diff --git a/webviews/logging/test/log_list.test.ts b/webviews/logging/test/log_list.test.ts
index 0136a2a..b8aed93 100644
--- a/webviews/logging/test/log_list.test.ts
+++ b/webviews/logging/test/log_list.test.ts
@@ -12,6 +12,12 @@
import {MemoryStore, State} from '../src/state';
import {LitElement} from 'lit';
import {ffxLogToLogRowData} from '../src/ffxLogToLogRow';
+import {LogViewOptions} from '../src/fields';
+
+const logOptions: LogViewOptions = {
+ columnFormatter: (_fieldName, text) => text,
+ showControls: true
+};
before(function () {
chai.should();
@@ -36,7 +42,7 @@
it('crates a new empty view', async () => {
const logsList = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
root.appendChild(logsList);
await logsList.updateComplete;
logsList.shadowRoot!.children.length.should.equal(1);
@@ -51,7 +57,7 @@
state.shouldWrapLogs = true;
let logList = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
root.appendChild(logList);
await logList.updateComplete;
logList.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.true;
@@ -59,7 +65,7 @@
state.shouldWrapLogs = false;
logList = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
logList.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.false;
});
});
@@ -68,7 +74,7 @@
it('appends a log', async () => {
const logsList = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
document.body.appendChild(logsList);
await logsList.updateComplete;
@@ -106,7 +112,7 @@
}), 'moniker:core/bar');
const logsList = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
root.appendChild(logsList);
logsList.addLog(ffxLogToLogRowData(logDataForTest('core/foo'))!);
@@ -123,7 +129,7 @@
it('handles ffx events', async () => {
const logsList = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
let moniker = constant.FFX_MONIKER;
const msg = 'Logger lost connection to target. Retrying...';
logsList.addLog(ffxLogToLogRowData(ffxEventForTest('TargetDisconnected'))!);
@@ -149,7 +155,7 @@
it('handles malformed logs', async () => {
const logsList = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
const msg = 'Malformed target log: oh no something went wrong';
logsList.addLog(ffxLogToLogRowData(malformedLogForTest('oh no something went wrong'))!);
@@ -171,7 +177,7 @@
it('detects user hovering logs', () => {
const view = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
view.addLog(ffxLogToLogRowData(logDataForTest('core/foo'))!);
const scrollArea = view.parentElement;
scrollArea?.matches(':hover').should.equal(false);
@@ -184,7 +190,7 @@
it('resets the log list', async () => {
const logsList = new LogList(
state.currentFields, state.shouldWrapLogs,
- state.setHeaderWidth.bind(state), state.currentFilter);
+ state.setHeaderWidth.bind(state), state.currentFilter, logOptions);
root.appendChild(logsList);
logsList.addLog(ffxLogToLogRowData(logDataForTest('core/foo'))!);
await logsList.updateComplete;
diff --git a/webviews/logging/test/log_row.test.ts b/webviews/logging/test/log_row.test.ts
index cb613a4..04099cd 100644
--- a/webviews/logging/test/log_row.test.ts
+++ b/webviews/logging/test/log_row.test.ts
@@ -10,6 +10,9 @@
import { messageForEvent } from '../src/format_log_text';
import {ffxLogToLogRowData} from '../src/ffxLogToLogRow';
import {State} from '../src/state';
+import {LogColumnFormatter} from '../src/fields';
+
+const columnFormatter: LogColumnFormatter = (_fieldName, text) => text;
before(() => {
chai.should();
@@ -64,7 +67,7 @@
keys: null,
printf: null,
}
- }), false)!, state.currentFields);
+ }), false)!, state.currentFields, columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
const testFields = [
@@ -96,7 +99,7 @@
keys: null,
printf: null,
}
- }), false)!, state.currentFields);
+ }), false)!, state.currentFields, columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
const testFields = [
@@ -115,6 +118,31 @@
logRow.remove();
});
+ it('formats html logs', async () => {
+ const logRow = new LogRow(
+ {fields: [{key: 'message', text: 'hello vscode'}]}
+ , {
+ 'timestamp': {displayName: 'timestamp', width: ''},
+ 'pid': {displayName: 'pid', width: ''},
+ 'tid': {displayName: 'tid', width: ''},
+ 'tags': {displayName: 'tags', width: ''},
+ 'moniker': {displayName: 'moniker', width: ''},
+ 'severity': {displayName: 'severity', width: ''},
+ 'message': {displayName: 'messagepw', width: ''},
+ }, (fieldName, text) => {
+ const el = document.createElement('span');
+ if (fieldName === 'message') {
+ el.innerHTML = text.replace('vscode', '<b>vscode</b>');
+ }
+ return el;
+ });
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
+ const msgField = logRow.children[0].children[6] as HTMLElement;
+ msgField.querySelector('b')!.innerText.should.equal('vscode');
+ logRow.remove();
+ });
+
it('formats structured logs', async () => {
const logRow = new LogRow(ffxLogToLogRowData(
logDataForTest({
@@ -128,7 +156,7 @@
},
printf: null,
}
- }), false)!, state.currentFields);
+ }), false)!, state.currentFields, columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
const testFields =
@@ -153,7 +181,7 @@
args: ['Fuchsia', 1]
},
}
- }), false)!, state.currentFields);
+ }), false)!, state.currentFields, columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
const testFields =
@@ -182,7 +210,7 @@
keys: null,
printf: null,
}
- }), false)!, state.currentFields);
+ }), false)!, state.currentFields, columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
const testFields =
@@ -198,7 +226,9 @@
it('formats logging started events', async () => {
let logRow = new LogRow(
ffxLogToLogRowData(
- ffxEventForTest('LoggingStarted'), false)!, state.currentFields);
+ ffxEventForTest('LoggingStarted'), false)!,
+ state.currentFields,
+ columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
let moniker = constant.FFX_MONIKER;
@@ -212,7 +242,9 @@
logRow = new LogRow(
ffxLogToLogRowData(
- ffxEventForTest('LoggingStarted'), true)!, state.currentFields);
+ ffxEventForTest('LoggingStarted'), true)!,
+ state.currentFields,
+ columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
msg = messageForEvent('LoggingStarted', true);
@@ -227,7 +259,9 @@
it('formats target disconnected events', async () => {
const logRow = new LogRow(
ffxLogToLogRowData(
- ffxEventForTest('TargetDisconnected'), false)!, state.currentFields);
+ ffxEventForTest('TargetDisconnected'), false)!,
+ state.currentFields,
+ columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
let moniker = constant.FFX_MONIKER;
@@ -243,7 +277,9 @@
it('formats malformed log', async () => {
const logRow = new LogRow(
ffxLogToLogRowData(
- malformedLogForTest('hello world'), false)!, state.currentFields);
+ malformedLogForTest('hello world'), false)!,
+ state.currentFields,
+ columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
let msg = 'Malformed target log: hello world';
@@ -263,7 +299,7 @@
version: 0
};
const logRow = new LogRow(
- ffxLogToLogRowData(data, false)!, state.currentFields);
+ ffxLogToLogRowData(data, false)!, state.currentFields, columnFormatter);
document.body.appendChild(logRow);
await logRow.updateComplete;
const monikerEl = logRow.children[0].children[3] as HTMLElement;
diff --git a/webviews/logging/test/view.test.ts b/webviews/logging/test/view.test.ts
index 14141ce..0e5b7cc 100644
--- a/webviews/logging/test/view.test.ts
+++ b/webviews/logging/test/view.test.ts
@@ -12,9 +12,16 @@
import { LogControl } from '../components/log_control';
import { Filter } from '../src/filter';
import { LogViewActions } from '../components/log_view_actions';
-import { WRAP_LOG_TEXT_ATTR } from '../components/log_list';
-import { LitElement } from 'lit';
import { ffxLogToLogRowData } from '../src/ffxLogToLogRow';
+import {LogList, WRAP_LOG_TEXT_ATTR} from '../components/log_list';
+import {LitElement} from 'lit';
+import {LogViewOptions} from '../src/fields';
+
+const logOptions: LogViewOptions = {
+ columnFormatter: (_fieldName, text) => text,
+ showControls: false
+};
+
before(function () {
chai.should();
chai.use(chaiDom);
@@ -105,6 +112,13 @@
state.currentFilterText.should.deep.equal(filterText);
});
+ it('log control and filter field can be disabled', async () => {
+ new LoggingView(state, root, logOptions);
+ let logView = root.getElementsByTagName('log-view')[0] as LogList;
+ await logView.updateComplete;
+ root.getElementsByTagName('log-control').length.should.equal(0);
+ });
+
it('announces the number of filtered results', async () => {
const view = new LoggingView(state, root);
const logList = root.querySelector('log-view') as LitElement;