| // 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 {Group} from './ic-model.mjs'; |
| import CustomIcProcessor from "./ic-processor.mjs"; |
| import {defineCustomElement, V8CustomElement} from './helper.mjs'; |
| |
| defineCustomElement('ic-panel', (templateText) => |
| class ICPanel extends V8CustomElement { |
| //TODO(zcankara) Entries never set |
| #entries; |
| #filteredEntries; |
| constructor() { |
| super(templateText); |
| this.groupKey.addEventListener( |
| 'change', e => this.updateTable(e)); |
| this.$('#filterICTimeBtn').addEventListener( |
| 'click', e => this.handleICTimeFilter(e)); |
| } |
| |
| get entries(){ |
| return this.#entries; |
| } |
| |
| get groupKey() { |
| return this.$('#group-key'); |
| } |
| |
| get table() { |
| return this.$('#table'); |
| } |
| |
| get tableBody() { |
| return this.$('#table-body'); |
| } |
| |
| get count() { |
| return this.$('#count'); |
| } |
| |
| get spanSelectAll(){ |
| return this.querySelectorAll("span"); |
| } |
| |
| set filteredEntries(value){ |
| this.#filteredEntries = value; |
| this.updateTable(); |
| } |
| |
| get filteredEntries(){ |
| return this.#filteredEntries; |
| } |
| |
| updateTable(event) { |
| let select = this.groupKey; |
| let key = select.options[select.selectedIndex].text; |
| let tableBody = this.tableBody; |
| this.removeAllChildren(tableBody); |
| let groups = Group.groupBy(this.filteredEntries, key, true); |
| this.render(groups, tableBody); |
| } |
| |
| escapeHtml(unsafe) { |
| if (!unsafe) return ""; |
| return unsafe.toString() |
| .replace(/&/g, "&") |
| .replace(/</g, "<") |
| .replace(/>/g, ">") |
| .replace(/"/g, """) |
| .replace(/'/g, "'"); |
| } |
| processValue(unsafe) { |
| if (!unsafe) return ""; |
| if (!unsafe.startsWith("http")) return this.escapeHtml(unsafe); |
| let a = document.createElement("a"); |
| a.href = unsafe; |
| a.textContent = unsafe; |
| return a; |
| } |
| |
| td(tr, content, className) { |
| let node = document.createElement("td"); |
| if (typeof content == "object") { |
| node.appendChild(content); |
| } else { |
| node.innerHTML = content; |
| } |
| node.className = className; |
| tr.appendChild(node); |
| return node |
| } |
| |
| handleMapClick(e){ |
| this.dispatchEvent(new CustomEvent( |
| 'mapclick', {bubbles: true, composed: true, |
| detail: e.target.parentNode.entry})); |
| } |
| handleFilePositionClick(e){ |
| this.dispatchEvent(new CustomEvent( |
| 'filepositionclick', {bubbles: true, composed: true, |
| detail: e.target.parentNode.entry})); |
| } |
| |
| render(entries, parent) { |
| let fragment = document.createDocumentFragment(); |
| let max = Math.min(1000, entries.length) |
| for (let i = 0; i < max; i++) { |
| let entry = entries[i]; |
| let tr = document.createElement("tr"); |
| tr.entry = entry; |
| //TODO(zcankara) Create one bound method and use it everywhere |
| if (entry.property === "map") { |
| tr.addEventListener('click', e => this.handleMapClick(e)); |
| } else if (entry.property == "filePosition") { |
| tr.addEventListener('click', |
| e => this.handleFilePositionClick(e)); |
| } |
| let details = this.td(tr,'<span>ℹ</a>', 'details'); |
| //TODO(zcankara) don't keep the whole function context alive |
| details.onclick = _ => this.toggleDetails(details); |
| this.td(tr, entry.percentage + "%", 'percentage'); |
| this.td(tr, entry.count, 'count'); |
| this.td(tr, this.processValue(entry.key), 'key'); |
| fragment.appendChild(tr); |
| } |
| let omitted = entries.length - max; |
| if (omitted > 0) { |
| let tr = document.createElement("tr"); |
| let tdNode = this.td(tr, 'Omitted ' + omitted + " entries."); |
| tdNode.colSpan = 4; |
| fragment.appendChild(tr); |
| } |
| parent.appendChild(fragment); |
| } |
| |
| |
| renderDrilldown(entry, previousSibling) { |
| let tr = document.createElement('tr'); |
| tr.className = "entry-details"; |
| tr.style.display = "none"; |
| // indent by one td. |
| tr.appendChild(document.createElement("td")); |
| let td = document.createElement("td"); |
| td.colSpan = 3; |
| for (let key in entry.groups) { |
| td.appendChild(this.renderDrilldownGroup(entry, key)); |
| } |
| tr.appendChild(td); |
| // Append the new TR after previousSibling. |
| previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling) |
| } |
| |
| renderDrilldownGroup(entry, key) { |
| let max = 20; |
| let group = entry.groups[key]; |
| let div = document.createElement("div") |
| div.className = 'drilldown-group-title' |
| div.textContent = key + ' [top ' + max + ' out of ' + group.length + ']'; |
| let table = document.createElement("table"); |
| this.render(group.slice(0, max), table, false) |
| div.appendChild(table); |
| return div; |
| } |
| |
| toggleDetails(node) { |
| let tr = node.parentNode; |
| let entry = tr.entry; |
| // Create subgroup in-place if the don't exist yet. |
| if (entry.groups === undefined) { |
| entry.createSubGroups(); |
| this.renderDrilldown(entry, tr); |
| } |
| let details = tr.nextSibling; |
| let display = details.style.display; |
| if (display != "none") { |
| display = "none"; |
| } else { |
| display = "table-row" |
| }; |
| details.style.display = display; |
| } |
| |
| initGroupKeySelect() { |
| let select = this.groupKey; |
| select.options.length = 0; |
| for (let i in CustomIcProcessor.kProperties) { |
| let option = document.createElement("option"); |
| option.text = CustomIcProcessor.kProperties[i]; |
| select.add(option); |
| } |
| } |
| |
| handleICTimeFilter(e) { |
| const startTime = parseInt(this.$('#filter-time-start').value); |
| const endTime = parseInt(this.$('#filter-time-end').value); |
| const dataModel = {startTime, endTime}; |
| this.dispatchEvent(new CustomEvent( |
| 'ictimefilter', {bubbles: true, composed: true, detail: dataModel})); |
| } |
| |
| }); |