1// Copyright 2020 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import {App} from '../index.mjs' 6 7import {FocusEvent, ToolTipEvent} from './events.mjs'; 8import {groupBy, LazyTable} from './helper.mjs'; 9import {CollapsableElement, DOM} from './helper.mjs'; 10 11DOM.defineCustomElement('view/list-panel', 12 (templateText) => 13 class ListPanel extends CollapsableElement { 14 _selectedLogEntries = []; 15 _displayedLogEntries = []; 16 _timeline; 17 18 _detailsClickHandler = this._handleDetailsClick.bind(this); 19 _logEntryClickHandler = this._handleLogEntryClick.bind(this); 20 _logEntryMouseOverHandler = this._logEntryMouseOverHandler.bind(this); 21 22 constructor() { 23 super(templateText); 24 this.groupKey.addEventListener('change', e => this.requestUpdate()); 25 this.showAllRadio.onclick = _ => this._showEntries(this._timeline); 26 this.showTimerangeRadio.onclick = _ => 27 this._showEntries(this._timeline.selectionOrSelf); 28 this.showSelectionRadio.onclick = _ => 29 this._showEntries(this._selectedLogEntries); 30 } 31 32 static get observedAttributes() { 33 return ['title']; 34 } 35 36 attributeChangedCallback(name, oldValue, newValue) { 37 if (name == 'title') { 38 this.$('#title').innerHTML = newValue; 39 } 40 } 41 42 set timeline(timeline) { 43 console.assert(timeline !== undefined, 'timeline undefined!'); 44 this._timeline = timeline; 45 this.$('.panel').style.display = timeline.isEmpty() ? 'none' : 'inherit'; 46 this._initGroupKeySelect(); 47 } 48 49 set selectedLogEntries(entries) { 50 if (entries === this._timeline) { 51 this.showAllRadio.click(); 52 } else if (entries === this._timeline.selection) { 53 this.showTimerangeRadio.click(); 54 } else { 55 this._selectedLogEntries = entries; 56 this.showSelectionRadio.click(); 57 } 58 } 59 60 get entryClass() { 61 return this._timeline.at(0)?.constructor; 62 } 63 64 get groupKey() { 65 return this.$('#group-key'); 66 } 67 68 get table() { 69 return this.$('#table'); 70 } 71 72 get showAllRadio() { 73 return this.$('#show-all'); 74 } 75 76 get showTimerangeRadio() { 77 return this.$('#show-timerange'); 78 } 79 80 get showSelectionRadio() { 81 return this.$('#show-selection'); 82 } 83 84 get _propertyNames() { 85 return this.entryClass?.propertyNames ?? []; 86 } 87 88 _initGroupKeySelect() { 89 const select = this.groupKey; 90 select.options.length = 0; 91 for (const propertyName of this._propertyNames) { 92 const option = DOM.element('option'); 93 option.text = propertyName; 94 select.add(option); 95 } 96 } 97 98 _showEntries(entries) { 99 this._displayedLogEntries = entries; 100 this.requestUpdate(); 101 } 102 103 _update() { 104 if (this._timeline.isEmpty()) return; 105 DOM.removeAllChildren(this.table); 106 if (this._displayedLogEntries.length == 0) return; 107 const propertyName = this.groupKey.selectedOptions[0].text; 108 const groups = 109 groupBy(this._displayedLogEntries, each => each[propertyName], true); 110 this._render(groups, this.table); 111 } 112 113 createSubgroups(group) { 114 const map = new Map(); 115 const tempGroups = []; 116 for (let propertyName of this._propertyNames) { 117 map.set( 118 propertyName, 119 groupBy(group.entries, each => each[propertyName], true)); 120 } 121 return map; 122 } 123 124 _handleLogEntryClick(e) { 125 const group = e.currentTarget.group; 126 this.dispatchEvent(new FocusEvent(group.key)); 127 } 128 129 _logEntryMouseOverHandler(e) { 130 const group = e.currentTarget.group; 131 this.dispatchEvent(new ToolTipEvent(group.key, e.currentTarget)); 132 } 133 134 _handleDetailsClick(event) { 135 event.stopPropagation(); 136 const tr = event.target.parentNode; 137 const group = tr.group; 138 // Create subgroup in-place if the don't exist yet. 139 if (tr.groups === undefined) { 140 const groups = tr.groups = this.createSubgroups(group); 141 this.renderDrilldown(groups, tr); 142 } 143 const detailsTr = tr.nextSibling; 144 if (tr.classList.contains('open')) { 145 tr.classList.remove('open'); 146 detailsTr.style.display = 'none'; 147 } else { 148 tr.classList.add('open'); 149 detailsTr.style.display = 'table-row'; 150 } 151 } 152 153 renderDrilldown(groups, previousSibling) { 154 const tr = DOM.tr('entry-details'); 155 tr.style.display = 'none'; 156 // indent by one td. 157 tr.appendChild(DOM.td()); 158 const td = DOM.td(); 159 td.colSpan = 3; 160 groups.forEach((group, key) => { 161 this.renderDrilldownGroup(td, group, key); 162 }); 163 tr.appendChild(td); 164 // Append the new TR after previousSibling. 165 previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling); 166 } 167 168 renderDrilldownGroup(td, groups, key) { 169 const div = DOM.div('drilldown-group-title'); 170 div.textContent = `Grouped by ${key}: ${groups[0]?.parentTotal ?? 0}#`; 171 td.appendChild(div); 172 const table = DOM.table(); 173 this._render(groups, table, false) 174 td.appendChild(table); 175 } 176 177 _render(groups, table) { 178 let last; 179 new LazyTable(table, groups, group => { 180 last = group; 181 const tr = DOM.tr(); 182 tr.group = group; 183 const details = tr.appendChild(DOM.td('', 'toggle')); 184 details.onclick = this._detailsClickHandler; 185 tr.appendChild(DOM.td(`${group.percent.toFixed(2)}%`, 'percentage')); 186 tr.appendChild(DOM.td(group.length, 'count')); 187 const valueTd = tr.appendChild(DOM.td(group.key?.toString(), 'key')); 188 if (App.isClickable(group.key)) { 189 tr.onclick = this._logEntryClickHandler; 190 tr.onmouseover = this._logEntryMouseOverHandler; 191 valueTd.classList.add('clickable'); 192 } 193 return tr; 194 }, 10); 195 } 196}); 197