[tools][system-analyzer] Add ToolTip API
Enable more complex tooltips with clickable links and references. - Use short filename for Script.name if they are unique - Use shared App.isClickable method - Remove various toStringLong methods - Rename CodeLogEntry.disassemble to .code - Add DOM.button helper Bug: v8:10644 Change-Id: I5d46ffd560b37278dc46b8347cb9ff0a7fdfa2ef Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2916373 Reviewed-by: Victor Gomes <victorgomes@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/master@{#74746}
This commit is contained in:
parent
42c77e9a83
commit
a6c474fecc
@ -45,10 +45,6 @@ export class SourcePosition {
|
||||
toString() {
|
||||
return `${this.script.name}:${this.line}:${this.column}`;
|
||||
}
|
||||
|
||||
toStringLong() {
|
||||
return this.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export class Script {
|
||||
@ -63,8 +59,9 @@ export class Script {
|
||||
this.sourcePositions = [];
|
||||
}
|
||||
|
||||
update(name, source) {
|
||||
this.name = name;
|
||||
update(url, source) {
|
||||
this.url = url;
|
||||
this.name = Script.getShortestUniqueName(url, this);
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@ -103,8 +100,33 @@ export class Script {
|
||||
return `Script(${this.id}): ${this.name}`;
|
||||
}
|
||||
|
||||
toStringLong() {
|
||||
return this.source;
|
||||
get toolTipDict() {
|
||||
return {
|
||||
title: this.toString(),
|
||||
__this__: this,
|
||||
id: this.id,
|
||||
url: this.url,
|
||||
source: this.source,
|
||||
sourcePositions: this.sourcePositions.length
|
||||
}
|
||||
}
|
||||
|
||||
static getShortestUniqueName(url, script) {
|
||||
const parts = url.split('/');
|
||||
const filename = parts[parts.length -1];
|
||||
const dict = this._dict ?? (this._dict = new Map());
|
||||
const matchingScripts = dict.get(filename);
|
||||
if (matchingScripts == undefined) {
|
||||
dict.set(filename, [script]);
|
||||
return filename;
|
||||
}
|
||||
// TODO: find shortest unique substring
|
||||
// Update all matching scripts to have a unique filename again.
|
||||
for (let matchingScript of matchingScripts) {
|
||||
matchingScript.name = script.url
|
||||
}
|
||||
matchingScripts.push(script);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,7 @@ found in the LICENSE file. -->
|
||||
</a>
|
||||
</dt>
|
||||
<dd>
|
||||
Log<a href="https://v8.dev/blog/fast-properties">Maps</a>
|
||||
Log <a href="https://v8.dev/blog/fast-properties">Maps</a>
|
||||
</dd>
|
||||
<dt>
|
||||
<a href="https://source.chromium.org/search?q=FLAG_trace_ic">
|
||||
|
@ -9,6 +9,7 @@ import {ApiLogEntry} from './log/api.mjs';
|
||||
import {DeoptLogEntry} from './log/code.mjs';
|
||||
import {CodeLogEntry} from './log/code.mjs';
|
||||
import {IcLogEntry} from './log/ic.mjs';
|
||||
import {LogEntry} from './log/log.mjs';
|
||||
import {MapLogEntry} from './log/map.mjs';
|
||||
import {Processor} from './processor.mjs';
|
||||
import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
|
||||
@ -119,6 +120,14 @@ class App {
|
||||
this.selectEntries(entries);
|
||||
}
|
||||
|
||||
static isClickable(object) {
|
||||
if (typeof object !== 'object') return false;
|
||||
if (object instanceof LogEntry) return true;
|
||||
if (object instanceof SourcePosition) return true;
|
||||
if (object instanceof Script) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
handleSelectEntries(e) {
|
||||
e.stopImmediatePropagation();
|
||||
this.showEntries(e.entries);
|
||||
@ -232,6 +241,7 @@ class App {
|
||||
this._state.map = entry;
|
||||
this._view.mapTrack.focusedEntry = entry;
|
||||
this._view.mapPanel.map = entry;
|
||||
this._view.mapPanel.show();
|
||||
}
|
||||
|
||||
focusIcLogEntry(entry) {
|
||||
@ -241,10 +251,11 @@ class App {
|
||||
focusCodeLogEntry(entry) {
|
||||
this._state.code = entry;
|
||||
this._view.codePanel.entry = entry;
|
||||
this._view.codePanel.show();
|
||||
}
|
||||
|
||||
focusDeoptLogEntry(entry) {
|
||||
this._view.deoptList.focusedLogEntry = entry;
|
||||
this._state.DeoptLogEntry = entry;
|
||||
}
|
||||
|
||||
focusApiLogEntry(entry) {
|
||||
@ -255,11 +266,33 @@ class App {
|
||||
focusSourcePosition(sourcePosition) {
|
||||
if (!sourcePosition) return;
|
||||
this._view.scriptPanel.focusedSourcePositions = [sourcePosition];
|
||||
this._view.scriptPanel.show();
|
||||
}
|
||||
|
||||
handleToolTip(event) {
|
||||
this._view.toolTip.positionOrTargetNode = event.positionOrTargetNode;
|
||||
this._view.toolTip.content = event.content;
|
||||
let content = event.content;
|
||||
switch (content.constructor) {
|
||||
case String:
|
||||
break;
|
||||
case Script:
|
||||
case SourcePosition:
|
||||
case MapLogEntry:
|
||||
case IcLogEntry:
|
||||
case ApiLogEntry:
|
||||
case CodeLogEntry:
|
||||
case DeoptLogEntry:
|
||||
content = content.toolTipDict;
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown tooltip content type: ${entry.constructor?.name}`);
|
||||
}
|
||||
this.setToolTip(content, event.positionOrTargetNode);
|
||||
}
|
||||
|
||||
setToolTip(content, positionOrTargetNode) {
|
||||
this._view.toolTip.positionOrTargetNode = positionOrTargetNode;
|
||||
this._view.toolTip.content = content;
|
||||
}
|
||||
|
||||
handleFileUploadStart(e) {
|
||||
|
@ -18,14 +18,6 @@ export class ApiLogEntry extends LogEntry {
|
||||
return this._argument;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Api(${this.type})`;
|
||||
}
|
||||
|
||||
toStringLong() {
|
||||
return `Api(${this.type}): ${this._name}`;
|
||||
}
|
||||
|
||||
static get propertyNames() {
|
||||
return ['type', 'name', 'argument'];
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import {formatBytes} from '../helper.mjs';
|
||||
|
||||
import {LogEntry} from './log.mjs';
|
||||
|
||||
export class DeoptLogEntry extends LogEntry {
|
||||
@ -34,14 +36,6 @@ export class DeoptLogEntry extends LogEntry {
|
||||
return this._entry.functionName;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Deopt(${this.type})`;
|
||||
}
|
||||
|
||||
toStringLong() {
|
||||
return `Deopt(${this.type})${this._reason}: ${this._location}`;
|
||||
}
|
||||
|
||||
static get propertyNames() {
|
||||
return [
|
||||
'type', 'reason', 'functionName', 'sourcePosition',
|
||||
@ -51,9 +45,10 @@ export class DeoptLogEntry extends LogEntry {
|
||||
}
|
||||
|
||||
export class CodeLogEntry extends LogEntry {
|
||||
constructor(type, time, kind, entry) {
|
||||
constructor(type, time, kindName, kind, entry) {
|
||||
super(type, time);
|
||||
this._kind = kind;
|
||||
this._kindName = kindName;
|
||||
this._entry = entry;
|
||||
}
|
||||
|
||||
@ -61,6 +56,10 @@ export class CodeLogEntry extends LogEntry {
|
||||
return this._kind;
|
||||
}
|
||||
|
||||
get kindName() {
|
||||
return this._kindName;
|
||||
}
|
||||
|
||||
get entry() {
|
||||
return this._entry;
|
||||
}
|
||||
@ -77,7 +76,7 @@ export class CodeLogEntry extends LogEntry {
|
||||
return this._entry?.getSourceCode() ?? '';
|
||||
}
|
||||
|
||||
get disassemble() {
|
||||
get code() {
|
||||
return this._entry?.source?.disassemble;
|
||||
}
|
||||
|
||||
@ -85,11 +84,16 @@ export class CodeLogEntry extends LogEntry {
|
||||
return `Code(${this.type})`;
|
||||
}
|
||||
|
||||
toStringLong() {
|
||||
return `Code(${this.type}): ${this._entry.toString()}`;
|
||||
get toolTipDict() {
|
||||
const dict = super.toolTipDict;
|
||||
dict.size = formatBytes(dict.size);
|
||||
return dict;
|
||||
}
|
||||
|
||||
static get propertyNames() {
|
||||
return ['type', 'kind', 'functionName', 'sourcePosition', 'script'];
|
||||
return [
|
||||
'type', 'kind', 'kindName', 'size', 'functionName', 'sourcePosition',
|
||||
'script', 'source', 'code'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -28,14 +28,6 @@ export class IcLogEntry extends LogEntry {
|
||||
this.modifier = modifier;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `IC(${this.type})`;
|
||||
}
|
||||
|
||||
toStringLong() {
|
||||
return `IC(${this.type}):\n${this.state}`;
|
||||
}
|
||||
|
||||
parseMapProperties(parts, offset) {
|
||||
let next = parts[++offset];
|
||||
if (!next.startsWith('dict')) return offset;
|
||||
@ -65,7 +57,7 @@ export class IcLogEntry extends LogEntry {
|
||||
static get propertyNames() {
|
||||
return [
|
||||
'type', 'category', 'functionName', 'script', 'sourcePosition', 'state',
|
||||
'key', 'map', 'reason', 'file'
|
||||
'key', 'map', 'reason'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -22,15 +22,34 @@ export class LogEntry {
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.constructor.name}(${this._type})`;
|
||||
let name = this.constructor.name;
|
||||
const index = name.lastIndexOf('LogEntry');
|
||||
if (index > 0) {
|
||||
name = name.substr(0, index);
|
||||
}
|
||||
return `${name}(${this._type})`;
|
||||
}
|
||||
|
||||
toStringLong() {
|
||||
return this.toString();
|
||||
get toolTipDict() {
|
||||
const toolTipDescription = {
|
||||
__proto__: null,
|
||||
__this__: this,
|
||||
title: this.toString()
|
||||
};
|
||||
for (let key of this.constructor.propertyNames) {
|
||||
toolTipDescription[key] = this[key];
|
||||
}
|
||||
|
||||
return toolTipDescription;
|
||||
}
|
||||
|
||||
// Returns an Array of all possible #type values.
|
||||
static get allTypes() {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
// Returns an array of public property names.
|
||||
static get propertyNames() {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
}
|
@ -60,10 +60,6 @@ class MapLogEntry extends LogEntry {
|
||||
return `Map(${this.id})`;
|
||||
}
|
||||
|
||||
toStringLong() {
|
||||
return `Map(${this.id}):\n${this.description}`;
|
||||
}
|
||||
|
||||
finalizeRootMap(id) {
|
||||
let stack = [this];
|
||||
while (stack.length > 0) {
|
||||
@ -194,7 +190,7 @@ class MapLogEntry extends LogEntry {
|
||||
static get propertyNames() {
|
||||
return [
|
||||
'type', 'reason', 'property', 'functionName', 'sourcePosition', 'script',
|
||||
'id'
|
||||
'id', 'parent', 'description'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -213,8 +213,9 @@ export class Processor extends LogReader {
|
||||
} else {
|
||||
entry = this._profile.addCode(type, name, timestamp, start, size);
|
||||
}
|
||||
this._lastCodeLogEntry =
|
||||
new CodeLogEntry(type + stateName, timestamp, kind, entry);
|
||||
this._lastCodeLogEntry = new CodeLogEntry(
|
||||
type + stateName, timestamp,
|
||||
Profile.getKindFromState(Profile.parseState(stateName)), kind, entry);
|
||||
this._codeTimeline.push(this._lastCodeLogEntry);
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
// Copyright 2020 the V8 project authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import {IcLogEntry} from '../log/ic.mjs';
|
||||
import {MapLogEntry} from '../log/map.mjs';
|
||||
|
||||
import {FocusEvent, SelectionEvent, ToolTipEvent} from './events.mjs';
|
||||
import {CollapsableElement, delay, DOM, formatBytes, formatMicroSeconds} from './helper.mjs';
|
||||
import {SelectRelatedEvent} from './events.mjs';
|
||||
import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs';
|
||||
|
||||
DOM.defineCustomElement('view/code-panel',
|
||||
(templateText) =>
|
||||
@ -51,7 +48,7 @@ DOM.defineCustomElement('view/code-panel',
|
||||
|
||||
_update() {
|
||||
this._updateSelect();
|
||||
this._disassemblyNode.innerText = this._entry?.disassemble ?? '';
|
||||
this._disassemblyNode.innerText = this._entry?.code ?? '';
|
||||
this._sourceNode.innerText = this._entry?.source ?? '';
|
||||
}
|
||||
|
||||
|
@ -76,10 +76,10 @@ export class ToolTipEvent extends AppEvent {
|
||||
|
||||
constructor(content, positionOrTargetNode) {
|
||||
super(ToolTipEvent.name);
|
||||
this._content = content;
|
||||
if (!positionOrTargetNode && !node) {
|
||||
if (!positionOrTargetNode) {
|
||||
throw Error('Either provide a valid position or targetNode');
|
||||
}
|
||||
this._content = content;
|
||||
this._positionOrTargetNode = positionOrTargetNode;
|
||||
}
|
||||
|
||||
|
@ -137,6 +137,13 @@ export class DOM {
|
||||
return document.createTextNode(string);
|
||||
}
|
||||
|
||||
static button(label, clickHandler) {
|
||||
const button = DOM.element('button');
|
||||
button.innerText = label;
|
||||
button.onclick = clickHandler;
|
||||
return button;
|
||||
}
|
||||
|
||||
static div(classes) {
|
||||
return this.element('div', classes);
|
||||
}
|
||||
@ -236,11 +243,19 @@ export class CollapsableElement extends V8CustomElement {
|
||||
this._closer.onclick = _ => this.tryUpdateOnVisibilityChange();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this._contentIsVisible) this._closer.click();
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this._contentIsVisible) this._closer.click();
|
||||
}
|
||||
|
||||
get _closer() {
|
||||
return this.$('#closer');
|
||||
}
|
||||
|
||||
_contentIsVisible() {
|
||||
get _contentIsVisible() {
|
||||
return !this._closer.checked;
|
||||
}
|
||||
|
||||
@ -257,7 +272,7 @@ export class CollapsableElement extends V8CustomElement {
|
||||
}
|
||||
|
||||
requestUpdateIfVisible(useAnimation) {
|
||||
if (!this._contentIsVisible()) return;
|
||||
if (!this._contentIsVisible) return;
|
||||
return super.requestUpdate(useAnimation);
|
||||
}
|
||||
|
||||
@ -267,6 +282,43 @@ export class CollapsableElement extends V8CustomElement {
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpandableText {
|
||||
constructor(node, string, limit = 200) {
|
||||
this._node = node;
|
||||
this._string = string;
|
||||
this._delta = limit / 2;
|
||||
this._start = 0;
|
||||
this._end = string.length;
|
||||
this._button = this._createExpandButton();
|
||||
this.expand();
|
||||
}
|
||||
|
||||
_createExpandButton() {
|
||||
const button = DOM.element('button');
|
||||
button.innerText = '...';
|
||||
button.onclick = (e) => {
|
||||
e.stopImmediatePropagation();
|
||||
this.expand()
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
expand() {
|
||||
DOM.removeAllChildren(this._node);
|
||||
this._start = this._start + this._delta;
|
||||
this._end = this._end - this._delta;
|
||||
if (this._start >= this._end) {
|
||||
this._node.innerText = this._string;
|
||||
this._button.onclick = undefined;
|
||||
return;
|
||||
}
|
||||
this._node.appendChild(DOM.text(this._string.substring(0, this._start)));
|
||||
this._node.appendChild(this._button);
|
||||
this._node.appendChild(
|
||||
DOM.text(this._string.substring(this._end, this._string.length)));
|
||||
}
|
||||
}
|
||||
|
||||
export class Chunked {
|
||||
constructor(iterable, limit) {
|
||||
this._iterator = iterable[Symbol.iterator]();
|
||||
|
@ -2,8 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import {Script, SourcePosition} from '../../profile.mjs';
|
||||
import {LogEntry} from '../log/log.mjs';
|
||||
import {App} from '../index.mjs'
|
||||
|
||||
import {FocusEvent, ToolTipEvent} from './events.mjs';
|
||||
import {groupBy, LazyTable} from './helper.mjs';
|
||||
@ -128,8 +127,7 @@ DOM.defineCustomElement('view/list-panel',
|
||||
|
||||
_logEntryMouseOverHandler(e) {
|
||||
const group = e.currentTarget.group;
|
||||
this.dispatchEvent(
|
||||
new ToolTipEvent(group.key.toStringLong(), e.currentTarget));
|
||||
this.dispatchEvent(new ToolTipEvent(group.key, e.currentTarget));
|
||||
}
|
||||
|
||||
_handleDetailsClick(event) {
|
||||
@ -188,8 +186,8 @@ DOM.defineCustomElement('view/list-panel',
|
||||
details.onclick = this._detailsClickHandler;
|
||||
tr.appendChild(DOM.td(`${group.percent.toFixed(2)}%`, 'percentage'));
|
||||
tr.appendChild(DOM.td(group.count, 'count'));
|
||||
const valueTd = tr.appendChild(DOM.td(`${group.key}`, 'key'));
|
||||
if (this._isClickable(group.key)) {
|
||||
const valueTd = tr.appendChild(DOM.td(group.key.toString(), 'key'));
|
||||
if (App.isClickable(group.key)) {
|
||||
tr.onclick = this._logEntryClickHandler;
|
||||
tr.onmouseover = this._logEntryMouseOverHandler;
|
||||
valueTd.classList.add('clickable');
|
||||
@ -197,12 +195,4 @@ DOM.defineCustomElement('view/list-panel',
|
||||
return tr;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
_isClickable(object) {
|
||||
if (typeof object !== 'object') return false;
|
||||
if (object instanceof LogEntry) return true;
|
||||
if (object instanceof SourcePosition) return true;
|
||||
if (object instanceof Script) return true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
@ -148,8 +148,8 @@ DOM.defineCustomElement(
|
||||
}
|
||||
|
||||
_handleMouseoverMap(event) {
|
||||
this.dispatchEvent(new ToolTipEvent(
|
||||
event.currentTarget.map.toStringLong(), event.currentTarget));
|
||||
this.dispatchEvent(
|
||||
new ToolTipEvent(event.currentTarget.map, event.currentTarget));
|
||||
}
|
||||
|
||||
_handleToggleSubtree(event) {
|
||||
|
@ -232,8 +232,7 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
let relativeIndex = Math.round(
|
||||
event.layerY / event.target.offsetHeight * (chunk.size() - 1));
|
||||
let logEntry = chunk.at(relativeIndex);
|
||||
this.dispatchEvent(new FocusEvent(logEntry));
|
||||
this.dispatchEvent(new ToolTipEvent(logEntry.toStringLong(), event.target));
|
||||
this.dispatchEvent(new ToolTipEvent(logEntry, event.target));
|
||||
}
|
||||
|
||||
_handleChunkClick(event) {
|
||||
|
@ -21,6 +21,10 @@ found in the LICENSE file. -->
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#content > h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.textContent {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
@ -73,6 +77,13 @@ found in the LICENSE file. -->
|
||||
.right > .tip {
|
||||
left: var(--tip-offset);
|
||||
}
|
||||
.properties td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.properties > tbody > tr > td:nth-child(2n+1):after {
|
||||
content: ':';
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="body">
|
||||
|
@ -2,13 +2,19 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import {DOM, V8CustomElement} from './helper.mjs';
|
||||
import {App} from '../index.mjs'
|
||||
|
||||
import {FocusEvent} from './events.mjs';
|
||||
import {DOM, ExpandableText, V8CustomElement} from './helper.mjs';
|
||||
|
||||
DOM.defineCustomElement(
|
||||
'view/tool-tip', (templateText) => class Tooltip extends V8CustomElement {
|
||||
_targetNode;
|
||||
_content;
|
||||
_isHidden = true;
|
||||
_logEntryClickHandler = this._handleLogEntryClick.bind(this);
|
||||
_logEntryRelatedHandler = this._handleLogEntryRelated.bind(this);
|
||||
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this._intersectionObserver = new IntersectionObserver((entries) => {
|
||||
@ -82,14 +88,28 @@ DOM.defineCustomElement(
|
||||
if (typeof content === 'string') {
|
||||
this.contentNode.innerHTML = content;
|
||||
this.contentNode.className = 'textContent';
|
||||
} else if (content?.nodeType && nodeType?.nodeName) {
|
||||
this._setContentNode(content);
|
||||
} else {
|
||||
const newContent = DOM.div();
|
||||
newContent.appendChild(content);
|
||||
this.contentNode.replaceWith(newContent);
|
||||
newContent.id = 'content';
|
||||
this._setContentNode(new TableBuilder(this, content).fragment);
|
||||
}
|
||||
}
|
||||
|
||||
_setContentNode(content) {
|
||||
const newContent = DOM.div();
|
||||
newContent.appendChild(content);
|
||||
this.contentNode.replaceWith(newContent);
|
||||
newContent.id = 'content';
|
||||
}
|
||||
|
||||
_handleLogEntryClick(e) {
|
||||
this.dispatchEvent(new FocusEvent(e.currentTarget.data));
|
||||
}
|
||||
|
||||
_handleLogEntryRelated(e) {
|
||||
this.dispatchEvent(new SelectRelatedEvent(e.currentTarget.data));
|
||||
}
|
||||
|
||||
hide() {
|
||||
this._isHidden = true;
|
||||
this.bodyNode.style.display = 'none';
|
||||
@ -109,3 +129,60 @@ DOM.defineCustomElement(
|
||||
return this.$('#content');
|
||||
}
|
||||
});
|
||||
|
||||
class TableBuilder {
|
||||
_instance;
|
||||
|
||||
constructor(tooltip, descriptor) {
|
||||
this._fragment = new DocumentFragment();
|
||||
this._table = DOM.table('properties');
|
||||
this._tooltip = tooltip;
|
||||
for (let key in descriptor) {
|
||||
const value = descriptor[key];
|
||||
this._addKeyValue(key, value);
|
||||
}
|
||||
this._addFooter();
|
||||
this._fragment.appendChild(this._table);
|
||||
}
|
||||
|
||||
_addKeyValue(key, value) {
|
||||
if (key == 'title') return this._addTitle(value);
|
||||
if (key == '__this__') {
|
||||
this._instance = value;
|
||||
return;
|
||||
}
|
||||
const row = this._table.insertRow();
|
||||
row.insertCell().innerText = key;
|
||||
const cell = row.insertCell();
|
||||
if (value == undefined) return;
|
||||
if (App.isClickable(value)) {
|
||||
cell.innerText = value.toString();
|
||||
cell.className = 'clickable';
|
||||
cell.onclick = this._logEntryClickHandler;
|
||||
cell.data = value;
|
||||
} else {
|
||||
new ExpandableText(cell, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
_addTitle(value) {
|
||||
const title = DOM.element('h3');
|
||||
title.innerText = value;
|
||||
this._fragment.appendChild(title);
|
||||
}
|
||||
|
||||
_addFooter() {
|
||||
if (this._instance === undefined) return;
|
||||
const td = this._table.createTFoot().insertRow().insertCell();
|
||||
let button =
|
||||
td.appendChild(DOM.button('Show', this._tooltip._logEntryClickHandler));
|
||||
button.data = this._instance;
|
||||
button = td.appendChild(
|
||||
DOM.button('Show Related', this._tooltip._logEntryRelatedClickHandler));
|
||||
button.data = this._instance;
|
||||
}
|
||||
|
||||
get fragment() {
|
||||
return this._fragment;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user