[logging] Migrate LogRow component to lit
LogRow still uses the global styling. Once we migrate LogList, we can
inline the css.
Change-Id: Ib5bae5fb98b23985c6df7cd1687e7eb228678a29
Reviewed-on: https://fuchsia-review.googlesource.com/c/vscode-plugins/+/753863
Reviewed-by: Miguel Flores <miguelfrde@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/media/webview-logging.css b/media/webview-logging.css
index f07c533..6dd643f 100644
--- a/media/webview-logging.css
+++ b/media/webview-logging.css
@@ -12,7 +12,7 @@
flex-direction: column;
}
-th {
+#logs-table-header>div {
background-color: var(--vscode-editor-background);
border-bottom: 2px solid var(--vscode-panel-border);
padding-bottom: 5px;
@@ -20,26 +20,40 @@
top: 0;
}
-td,
-th {
+#logs-table-header,
+log-view-row {
+ display: table-row-group;
+}
+
+log-view-row[hidden] {
+ display: none;
+}
+
+log-view-row>div {
+ display: table-row;
+}
+
+.log-list-cell,
+#logs-table-header>div {
overflow: clip;
text-align: left;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
+ display: table-cell;
}
td#moniker {
direction: rtl;
}
-table[wrap-log-text] .msg-cell,
-table:not([wrap-log-text]) .msg-cell:hover {
+#log-list[wrap-log-text] .msg-cell,
+#log-list:not([wrap-log-text]) .msg-cell:hover {
text-overflow: clip;
white-space: normal;
}
-table:not([wrap-log-text]) .msg-cell:hover {
+#log-list:not([wrap-log-text]) .msg-cell:hover {
background: var(--vscode-list-hoverBackground);
}
@@ -50,26 +64,27 @@
}
#log-list {
+ display: table;
table-layout: fixed;
width: 100%;
word-wrap: normal;
}
-.log-entry {
+log-view-row {
font-family: var(--vscode-editor-font-family);
font-size: var(--vscode-editor-font-size);
font-weight: var(--vscode-editor-font-weight);
}
-.log-entry[data-severity="warn"] {
+log-view-row div[data-severity="warn"] {
color: var(--vscode-list-warningForeground);
}
-.log-entry[data-severity="error"] {
+log-view-row div[data-severity="error"] {
color: var(--vscode-list-errorForeground);
}
-.log-entry[data-moniker="<VSCode>"] {
+log-view-row div[data-moniker="<VSCode>"] {
line-height: 75%;
opacity: 70%;
}
diff --git a/webviews/logging/components/log_header.ts b/webviews/logging/components/log_header.ts
index 438601f..4a028dc 100644
--- a/webviews/logging/components/log_header.ts
+++ b/webviews/logging/components/log_header.ts
@@ -16,7 +16,7 @@
this.tableHeader = this.initTableHeader();
for (let i = 0; i < keys.length; i++) {
const header = keys[i] as LogField;
- const headerCell = document.createElement('th');
+ const headerCell = document.createElement('div');
this.maxWidths[header] = 0;
headerCell.id = header;
headerCell.style.width = fields[header].width;
@@ -44,8 +44,8 @@
*
* @return the header element.
*/
- private initTableHeader(): HTMLTableRowElement {
- const tableHeader = document.createElement('tr');
+ private initTableHeader(): HTMLDivElement {
+ const tableHeader = document.createElement('div');
tableHeader.id = 'logs-table-header';
return tableHeader;
}
diff --git a/webviews/logging/components/log_list.ts b/webviews/logging/components/log_list.ts
index 892208e..cb2f47c 100644
--- a/webviews/logging/components/log_list.ts
+++ b/webviews/logging/components/log_list.ts
@@ -13,7 +13,7 @@
export const WRAP_LOG_TEXT_ATTR = 'wrap-log-text';
export class LogList {
- private logList: HTMLTableElement;
+ private logList: HTMLDivElement;
private logHeader: LogHeader;
private hasPreviousLog: boolean = false;
private maxWidths: Array<number>;
@@ -35,7 +35,7 @@
}
} else {
for (const element of logRows) {
- if (filter.accepts(element)) {
+ if (filter.accepts(element.children[0])) {
element.removeAttribute('hidden');
} else {
element.setAttribute('hidden', '');
@@ -88,8 +88,8 @@
*
* @return the log list element.
*/
- private initLogList(): HTMLTableElement {
- let logList = document.createElement('table');
+ private initLogList(): HTMLDivElement {
+ let logList = document.createElement('div');
logList.appendChild(this.logHeader.element);
logList.id = 'log-list';
return logList;
@@ -107,8 +107,7 @@
}
private appendLogElement(log: FfxLogData) {
- const newRow = new LogRow(log, this.hasPreviousLog);
- const element = newRow.element;
+ const element = new LogRow(log, this.hasPreviousLog);
if (!this.filtersAllow(element)) {
element.setAttribute('hidden', '');
}
diff --git a/webviews/logging/components/log_row.ts b/webviews/logging/components/log_row.ts
index e951969..28f5515 100644
--- a/webviews/logging/components/log_row.ts
+++ b/webviews/logging/components/log_row.ts
@@ -3,6 +3,9 @@
// found in the LICENSE file.
import * as constant from '../src/constants';
+import {ChildPart, html, LitElement} from 'lit';
+import {Directive, directive} from 'lit/directive.js';
+import {customElement, property, state} from 'lit/decorators.js';
import { LogField } from '../src/fields';
import {
formatLogPayload, formatMalformedLog,
@@ -10,12 +13,33 @@
} from '../src/format_log_text';
import { FfxEventData, FfxLogData, LogData } from '../src/log_data';
-export class LogRow {
- private logRow: HTMLElement;
- private symbolized: string | undefined;
+class AttributeSetter extends Directive {
+ update(part: ChildPart, [attributes]: [any]) {
+ const domElement = (part.parentNode as Element);
+ for (const attr in attributes) {
+ const value = attributes[attr];
+ domElement.setAttribute(`data-${attr}`, value);
+ }
+ return this.render(attributes);
+ }
- constructor(private log: FfxLogData, hasPreviousLog: boolean) {
- this.logRow = this.initLogRow();
+ render(_attributes: any) {
+ return '';
+ }
+}
+@customElement('log-view-row')
+export class LogRow extends LitElement {
+ @property({attribute: true, type: Object})
+ public log: FfxLogData | undefined = undefined;
+
+ @state()
+ private symbolized: string | undefined;
+ private fields!: Record<string, any>;
+ private dataAttributes!: Record<string, any>;
+
+ constructor(log: FfxLogData, hasPreviousLog: boolean) {
+ super();
+ this.log = log;
if (this.log.data.TargetLog !== undefined) {
this.formatRow(this.log.data.TargetLog);
} else if (this.log.data.FfxEvent !== undefined) {
@@ -30,48 +54,29 @@
}
}
- /**
- * Returns the core element.
- */
- get element() {
- return this.logRow;
+ createRenderRoot() {
+ return this;
}
- /**
- * Creates log row element containing useful metadata in `data-*` attributes.
- *
- * @param attributes the map of attribute name to value.
- * @returns the log row html element.
- */
- private createLogRowAttributes(attributes: Record<string, any>) {
- for (const attributeName in attributes) {
- this.logRow.setAttribute(`data-${attributeName}`, attributes[attributeName]);
- }
+ render() {
+ const attributeSetter = directive(AttributeSetter);
+ return html`
+ <div class="log-entry">
+ ${attributeSetter(this.dataAttributes)}
+ ${Object.keys(constant.LOGS_HEADERS).map(this.htmlForFieldKey.bind(this))}
+ </div>`;
}
- /**
- * Creates the inner html of the fields.
- *
- * @param fields fields of the log element.
- * @returns the inner html string with present fields filled.
- */
- private createLogInnerHtml(fields: Record<string, any>) {
- let fieldKey: LogField;
- for (fieldKey in constant.LOGS_HEADERS) {
- let cell = document.createElement('td');
- let content = '';
- if (fields[fieldKey]) {
- content = fields[fieldKey];
- }
- if (fieldKey === 'message') {
- cell.classList.add('msg-cell');
- } else {
- cell.title = content;
- }
- cell.innerText = content;
- cell.setAttribute('id', fieldKey);
- this.logRow.appendChild(cell);
- }
+ private htmlForFieldKey(fieldKey: string) {
+ const content = this.fields[fieldKey] ?? '';
+ return html`
+ <div
+ id="${fieldKey}"
+ title="${fieldKey === 'message' ? '' : content}"
+ class="log-list-cell${fieldKey === 'message' ? ' msg-cell' : ''}"
+ >
+ ${content}
+ </div>`;
}
/**
@@ -79,9 +84,9 @@
*/
private formatFfxEventRow(event: FfxEventData, hasPreviousLog: boolean) {
const fields =
- { 'moniker': constant.FFX_MONIKER, 'message': messageForEvent(event, hasPreviousLog) };
- this.createLogRowAttributes(fields);
- this.createLogInnerHtml(fields);
+ {'moniker': constant.FFX_MONIKER, 'message': messageForEvent(event, hasPreviousLog)};
+ this.dataAttributes = fields;
+ this.fields = fields;
}
/**
@@ -89,9 +94,9 @@
* @param malformedLog the malformed log to render as a message.
*/
private formatMalformedRow(malformedLog: string) {
- const fields = { 'message': formatMalformedLog(malformedLog) };
- this.createLogRowAttributes(fields);
- this.createLogInnerHtml(fields);
+ const fields = {'message': formatMalformedLog(malformedLog)};
+ this.dataAttributes = fields;
+ this.fields = fields;
}
/**
@@ -99,9 +104,9 @@
* @param message the message to display.
*/
private formatViewerEventRow(message: string) {
- const fields = { message, moniker: constant.VSCODE_SYNTHETIC_MONIKER };
- this.createLogRowAttributes(fields);
- this.createLogInnerHtml(fields);
+ const fields = {message, moniker: constant.VSCODE_SYNTHETIC_MONIKER};
+ this.dataAttributes = fields;
+ this.fields = fields;
}
/**
@@ -109,15 +114,15 @@
*/
private formatRow(log: LogData) {
let fields = {} as Record<string, any>;
- const attributes = createAttributes(log);
- this.createLogRowAttributes(attributes);
+ this.dataAttributes = createAttributes(log);
let header: LogField;
for (header in constant.LOGS_HEADERS) {
let field = header.toLowerCase();
fields[field] = this.getLogCell(log, field);
}
- this.createLogInnerHtml(fields);
+
+ this.fields = fields;
}
/**
@@ -151,15 +156,6 @@
return '';
}
}
-
- /**
- * Initialize LogRow element.
- */
- private initLogRow(): HTMLElement {
- let logRow = document.createElement('tr');
- logRow.classList.add('log-entry');
- return logRow;
- }
}
const COMPONENT_URL_REGEX =
diff --git a/webviews/logging/test/filter.test.ts b/webviews/logging/test/filter.test.ts
index 6fc3991..233c48b 100644
--- a/webviews/logging/test/filter.test.ts
+++ b/webviews/logging/test/filter.test.ts
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import chai from 'chai'; // not esm
+import {LitElement} from 'lit';
import { LogRow } from '../components/log_row';
import { FilterExpression, OrExpression, parseFilter } from '../src/filter';
@@ -11,9 +12,11 @@
});
describe('filtering', () => {
+ let testLitElements: LitElement[] = [];
let testElements: HTMLElement[] = [];
- before(() => {
- testElements = [
+
+ before(async () => {
+ testLitElements = [
new LogRow(
{
data: {
@@ -47,7 +50,7 @@
},
timestamp: 12345,
version: 1
- }, false).element,
+ }, false),
new LogRow( //2
{
data: {
@@ -83,7 +86,7 @@
},
timestamp: 12345,
version: 1
- }, false).element,
+ }, false),
new LogRow(
{
data: {
@@ -119,8 +122,20 @@
},
timestamp: 12345,
version: 1
- }, false).element,
+ }, false),
];
+
+ for (const index in testLitElements) {
+ document.body.appendChild(testLitElements[index]);
+ await testLitElements[index].updateComplete;
+ testElements.push(testLitElements[index].children[0] as HTMLElement);
+ }
+ });
+
+ after(() => {
+ for (const index in testLitElements) {
+ testLitElements[index].remove();
+ }
});
describe('FilterExpression', () => {
diff --git a/webviews/logging/test/log_list.test.ts b/webviews/logging/test/log_list.test.ts
index d50d5d0..bd38503 100644
--- a/webviews/logging/test/log_list.test.ts
+++ b/webviews/logging/test/log_list.test.ts
@@ -10,6 +10,7 @@
import { Filter } from '../src/filter';
import { LogList, WRAP_LOG_TEXT_ATTR } from '../components/log_list';
import { State } from '../src/state';
+import {LitElement} from 'lit';
before(function () {
chai.should();
@@ -49,28 +50,31 @@
});
describe('#addLog', () => {
- it('appends a log', () => {
+ it('appends a log', async () => {
const view = new LogList(state);
const logsList = view.element;
+ document.body.appendChild(logsList);
logsList.children.length.should.equal(1);
logsList.children[0].id.should.equal('logs-table-header');
view.addLog(logDataForTest('core/foo'));
logsList.children.length.should.equal(2);
- const logLine = logsList.children[1] as HTMLElement;
- logLine.should.have.class('log-entry');
- logLine.should.have.attr('data-moniker', 'core/foo');
- logLine.should.have.attr('data-pid', '123');
- logLine.should.have.attr('data-tid', '456');
- logLine.hidden.should.be.false;
+ const logLine = logsList.children[1] as LitElement;
+ await logLine.updateComplete;
+ logLine.children[0].should.have.class('log-entry');
+ logLine.children[0].should.have.attr('data-moniker', 'core/foo');
+ logLine.children[0].should.have.attr('data-pid', '123');
+ logLine.children[0].should.have.attr('data-tid', '456');
+ (logLine.children[0] as HTMLElement).hidden.should.be.false;
const testFields = ['000000.12', '123', '456', 'core/foo', 'my_tag', 'Info', 'msg'];
let field: keyof typeof testFields;
for (field in testFields) {
- const logField = logLine.children[field] as HTMLElement;
+ const logField = logLine.children[0].children[field] as HTMLElement;
logField.innerText.should.equal(testFields[field]);
}
+ logsList.remove();
});
@@ -92,44 +96,50 @@
logLine.hidden.should.be.true;
});
- it('handles ffx events', () => {
+ it('handles ffx events', async () => {
const view = new LogList(state);
let moniker = constant.FFX_MONIKER;
const msg = 'Logger lost connection to target. Retrying...';
view.addLog(ffxEventForTest('TargetDisconnected'));
const logsList = view.element;
+ document.body.appendChild(logsList);
logsList.children.length.should.equal(2);
logsList.children[0].id.should.equal('logs-table-header');
- const logLine = logsList.children[1] as HTMLElement;
+ const logLine = logsList.children[1] as LitElement;
+ await logLine.updateComplete;
- logLine.children.length.should.equal(7);
- logLine.should.have.class('log-entry');
- logLine.should.have.attr('data-moniker', '<ffx>');
- logLine.hidden.should.be.false;
+ logLine.children[0].children.length.should.equal(7);
+ logLine.children[0].should.have.class('log-entry');
+ logLine.children[0].should.have.attr('data-moniker', '<ffx>');
+ (logLine.children[0] as HTMLElement).hidden.should.be.false;
- let monikerEl = logLine.children[3] as HTMLElement;
- let msgEl = logLine.children[6] as HTMLElement;
+ let monikerEl = logLine.children[0].children[3] as HTMLElement;
+ let msgEl = logLine.children[0].children[6] as HTMLElement;
monikerEl.innerText.should.equal(moniker);
msgEl.innerText.should.equal(msg);
+ logsList.remove();
});
- it('handles malformed logs', () => {
+ it('handles malformed logs', async () => {
const view = new LogList(state);
const msg = 'Malformed target log: oh no something went wrong';
view.addLog(malformedLogForTest('oh no something went wrong'));
const logsList = view.element;
+ document.body.appendChild(logsList);
logsList.children.length.should.equal(2);
logsList.children[0].id.should.equal('logs-table-header');
- const logLine = logsList.children[1] as HTMLElement;
- logLine.children.length.should.equal(7);
- logLine.should.have.class('log-entry');
- logLine.hidden.should.be.false;
- let msgEl = logLine.children[6] as HTMLElement;
+ const logLine = logsList.children[1] as LitElement;
+ await logLine.updateComplete;
+ logLine.children[0].children.length.should.equal(7);
+ logLine.children[0].should.have.class('log-entry');
+ (logLine.children[0] as HTMLElement).hidden.should.be.false;
+ let msgEl = logLine.children[0].children[6] as HTMLElement;
msgEl.innerText.should.equal(msg);
+ logsList.remove();
});
it('detects user hovering logs', () => {
diff --git a/webviews/logging/test/log_row.test.ts b/webviews/logging/test/log_row.test.ts
index efad7f7..8141bac 100644
--- a/webviews/logging/test/log_row.test.ts
+++ b/webviews/logging/test/log_row.test.ts
@@ -47,7 +47,7 @@
describe('LogRow', () => {
describe('#constructor', () => {
- it('adds title to to non-message cells', () => {
+ it('adds title to to non-message cells', async () => {
const logRow = new LogRow(logDataForTest({
root: {
message: {
@@ -56,19 +56,22 @@
keys: null,
printf: null,
}
- }), false).element;
+ }), false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
const testFields = ['000012.35', '123', '456', 'core/foo', 'bar,baz', 'Info', 'hello vscode'];
let field: keyof typeof testFields;
for (field in testFields) {
if (testFields[field] === 'hello vscode') {
return;
}
- const logField = logRow.children[field] as HTMLElement;
+ const logField = logRow.children[0].children[field] as HTMLElement;
logField.title.should.equal(testFields[field]);
}
+ logRow.remove();
});
- it('formats target logs', () => {
+ it('formats target logs', async () => {
const logRow = new LogRow(logDataForTest({
root: {
message: {
@@ -77,16 +80,19 @@
keys: null,
printf: null,
}
- }), false).element;
+ }), false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
const testFields = ['000012.35', '123', '456', 'core/foo', 'bar,baz', 'Info', 'hello vscode'];
let field: keyof typeof testFields;
for (field in testFields) {
- const logField = logRow.children[field] as HTMLElement;
+ const logField = logRow.children[0].children[field] as HTMLElement;
logField.innerText.should.equal(testFields[field]);
}
+ logRow.remove();
});
- it('formats structured logs', () => {
+ it('formats structured logs', async () => {
const logRow = new LogRow(logDataForTest({
root: {
message: {
@@ -98,18 +104,21 @@
},
printf: null,
}
- }), false).element;
+ }), false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
const testFields =
['000012.35', '123', '456', 'core/foo', 'bar,baz',
'Info', 'hello vscodeos=fuchsia number=1'];
let field: keyof typeof testFields;
for (field in testFields) {
- const logField = logRow.children[field] as HTMLElement;
+ const logField = logRow.children[0].children[field] as HTMLElement;
logField.innerText.should.equal(testFields[field]);
}
+ logRow.remove();
});
- it('formats printf logs', () => {
+ it('formats printf logs', async () => {
const logRow = new LogRow(logDataForTest({
root: {
message: null,
@@ -119,17 +128,20 @@
args: ['Fuchsia', 1]
},
}
- }), false).element;
+ }), false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
const testFields =
['000012.35', '123', '456', 'core/foo', 'bar,baz', 'Info', '%s is #%d args=[Fuchsia, 1]'];
let field: keyof typeof testFields;
for (field in testFields) {
- const logField = logRow.children[field] as HTMLElement;
+ const logField = logRow.children[0].children[field] as HTMLElement;
logField.innerText.should.equal(testFields[field]);
}
+ logRow.remove();
});
- it('formats symbolized log', () => {
+ it('formats symbolized log', async () => {
const logRow = new LogRow(symbolizedDataForTest({
root: {
message: {
@@ -138,54 +150,69 @@
keys: null,
printf: null,
}
- }), false).element;
+ }), false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
const testFields =
['000000.12', '123', '456', 'core/foo', 'my_tag', 'Info', 'symbolized'];
let field: keyof typeof testFields;
for (field in testFields) {
- const logField = logRow.children[field] as HTMLElement;
+ const logField = logRow.children[0].children[field] as HTMLElement;
logField.innerText.should.equal(testFields[field]);
}
+ logRow.remove();
});
- it('formats logging started events', () => {
- let logRow = new LogRow(ffxEventForTest('LoggingStarted'), false).element;
+ it('formats logging started events', async () => {
+ let logRow = new LogRow(ffxEventForTest('LoggingStarted'), false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
let moniker = constant.FFX_MONIKER;
let msg = messageForEvent('LoggingStarted', false);
- let monikerEl = logRow.children[3] as HTMLElement;
- let msgEl = logRow.children[6] as HTMLElement;
+ let monikerEl = logRow.children[0].children[3] as HTMLElement;
+ let msgEl = logRow.children[0].children[6] as HTMLElement;
monikerEl.innerText.should.equal(moniker);
msgEl.innerText.should.equal(msg);
+ logRow.remove();
- logRow = new LogRow(ffxEventForTest('LoggingStarted'), true).element;
+ logRow = new LogRow(ffxEventForTest('LoggingStarted'), true);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
msg = messageForEvent('LoggingStarted', true);
- monikerEl = logRow.children[3] as HTMLElement;
- msgEl = logRow.children[6] as HTMLElement;
+ monikerEl = logRow.children[0].children[3] as HTMLElement;
+ msgEl = logRow.children[0].children[6] as HTMLElement;
monikerEl.innerText.should.equal(moniker);
msgEl.innerText.should.equal(msg);
+ logRow.remove();
});
- it('formats target disconnected events', () => {
- const logRow = new LogRow(ffxEventForTest('TargetDisconnected'), false).element;
+ it('formats target disconnected events', async () => {
+ const logRow = new LogRow(ffxEventForTest('TargetDisconnected'), false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
let moniker = constant.FFX_MONIKER;
let msg = messageForEvent('TargetDisconnected', true);
- let monikerEl = logRow.children[3] as HTMLElement;
- let msgEl = logRow.children[6] as HTMLElement;
+ let monikerEl = logRow.children[0].children[3] as HTMLElement;
+ let msgEl = logRow.children[0].children[6] as HTMLElement;
monikerEl.innerText.should.equal(moniker);
msgEl.innerText.should.equal(msg);
+ logRow.remove();
});
- it('formats malformed log', () => {
- const logRow = new LogRow(malformedLogForTest('hello world'), false).element;
+ it('formats malformed log', async () => {
+ const logRow = new LogRow(malformedLogForTest('hello world'), false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
let msg = 'Malformed target log: hello world';
- let msgEl = logRow.children[6] as HTMLElement;
+ let msgEl = logRow.children[0].children[6] as HTMLElement;
msgEl.innerText.should.equal(msg);
+ logRow.remove();
});
- it('formats viewer synthesized messages', () => {
+ it('formats viewer synthesized messages', async () => {
const msg = 'Logs are cool';
const data = {
data: {
@@ -195,11 +222,14 @@
timestamp: 0,
version: 0
};
- const logRow = new LogRow(data, false).element;
- const monikerEl = logRow.children[3] as HTMLElement;
- const msgEl = logRow.children[6] as HTMLElement;
+ const logRow = new LogRow(data, false);
+ document.body.appendChild(logRow);
+ await logRow.updateComplete;
+ const monikerEl = logRow.children[0].children[3] as HTMLElement;
+ const msgEl = logRow.children[0].children[6] as HTMLElement;
monikerEl.innerText.should.equal(constant.VSCODE_SYNTHETIC_MONIKER);
msgEl.innerText.should.equal(msg);
+ logRow.remove();
});
});
});