| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * @fileoverview |
| * UI classes and helpers for viewing metadata as tree and/or table. |
| */ |
| |
| /** |
| * @typedef {Object} MetadataItem |
| * @property {string} name - Item name. |
| * @property {number|string} value - Metadata value. |
| * @property {number|string|undefined} beforeValue - Optional "before" metadata |
| * value. |
| */ |
| |
| /** |
| * @typedef {Object} MetadataTreeNode |
| * @property {string|undefined} name - The full name of the node, and is shown |
| * in the UI. |
| * @property {!Array<!MetadataTreeNode>|undefined} children - Child nodes. |
| * Non-existent or null indicates this is a leaf node. |
| * @property {!Array<!MetadataItem>|undefined} items - For leaf node only, a |
| * list of named metadata values. |
| * @property {string|undefined} iconKey - Override value for |
| * getMetadataIconTemplate() to retrieve icon. |
| */ |
| |
| /** |
| * States and helpers for the Metadata Tree. |
| */ |
| class MetadataTreeModel { |
| constructor() { |
| /** |
| * Cached metadata used to create |rootNode|. |
| * @public {?Object} |
| */ |
| this.metadata = null; |
| |
| /** |
| * Root node of the metadata tree, can be regenerated on UI change. |
| * @public {?MetadataTreeNode} |
| */ |
| this.rootNode = null; |
| } |
| |
| /** |
| * Renders primitive |value| to string, and an array of primitive values to |
| * primitive values separated by '\n'. |
| * @param {?Object} value |
| * @return {string} |
| * @private |
| */ |
| renderSimpleValue(value) { |
| if (value === null) |
| return 'null'; |
| if (typeof value !== 'object') |
| return value.toString(); |
| if (Array.isArray(value)) { |
| if (value.every((v) => v === null || typeof v !== 'object')) |
| return value.join('\n'); |
| } |
| return '[Object]'; |
| } |
| |
| /** |
| * Creates MetadataTreeNode populated with commonly used fields. |
| * @param {string} name |
| * @param {?Array<!MetadataTreeNode>} children |
| * @return {!MetadataTreeNode} |
| * @private |
| */ |
| makeDataNode(name, children) { |
| const node = /** @type {!MetadataTreeNode} */ ({name}); |
| if (children) |
| node.children = children; |
| return node; |
| } |
| |
| /** |
| * Jointly visits two key-value objects to yield MetadataItems, with values |
| * rendered to strings. |
| * @param {boolean} diffMode |
| * @param {!Object} obj |
| * @param {!Object} beforeObj |
| * @private @generator |
| */ |
| * makeItems(diffMode, obj, beforeObj) { |
| const keys = uniquifyIterToString( |
| joinIter(Object.keys(obj), Object.keys(beforeObj))); |
| for (const key of keys) { |
| const item = /** @type {!MetadataItem} */ ({name: key}); |
| if (diffMode) |
| item.beforeValue = this.renderSimpleValue(beforeObj[key] ?? ''); |
| item.value = this.renderSimpleValue(obj[key] ?? ''); |
| yield item; |
| } |
| } |
| |
| /** |
| * Converts a container array to a Map from a distinct name to each container. |
| * @param {!Array<!Object>} containers |
| * @return {!Map<!Object>} |
| * @private |
| */ |
| containersToMap(containers) { |
| const ret = new Map(); |
| for (const c of containers) { |
| let name = c.name; |
| while (ret.has(name)) // Ensures distinct name. |
| name += '!'; |
| ret.set(name, c); |
| } |
| return ret; |
| } |
| |
| /** |
| * Jointly visits two container arrays to yield names and container pairs with |
| * null placeholders. |
| * @param {!Array<!Object>} containers |
| * @param {!Array<!Object>} beforeContainers |
| * @private @generator |
| */ |
| * visitContainers(containers, beforeContainers) { |
| const containerMap = this.containersToMap(containers); |
| const beforeContainerMap = this.containersToMap(beforeContainers); |
| const names = uniquifyIterToString( |
| joinIter(containerMap.keys(), beforeContainerMap.keys())); |
| for (const name of names) { |
| yield { |
| name, |
| container: containerMap.get(name) ?? null, |
| beforeContainer: beforeContainerMap.get(name) ?? null |
| }; |
| } |
| } |
| |
| /** |
| * Converts arbitrary |metadata| to a consistent tree form, and stores the |
| * result into |rootNode|. |
| * @param {?Object} metadata Source metadata, must be non-null on first call. |
| * Subsequently, null means to use cached copy from previous call. |
| * @public |
| */ |
| extractAndStoreRoot(metadata) { |
| const EMPTY_OBJ = {}; |
| const diffMode = state.getDiffMode(); |
| if (metadata) |
| this.metadata = metadata; |
| |
| const containers = getOrMakeContainers(this.metadata.size_file); |
| const beforeContainers = |
| getOrMakeContainers(this.metadata.before_size_file); |
| |
| const rootNode = this.makeDataNode('Metadata', []); |
| rootNode.iconKey = 'root'; |
| |
| if (this.metadata.size_file?.build_config) { |
| const configNode = this.makeDataNode('', null); |
| const buildConfig = this.metadata.size_file?.build_config ?? EMPTY_OBJ; |
| const beforeBuildConfig = |
| this.metadata.before_size_file?.build_config ?? EMPTY_OBJ; |
| configNode.items = |
| [...this.makeItems(diffMode, buildConfig, beforeBuildConfig)]; |
| rootNode.children.push(configNode); |
| } |
| |
| for (const {name, container, beforeContainer} of this.visitContainers( |
| containers, beforeContainers)) { |
| const subMetadata = container?.metadata ?? EMPTY_OBJ; |
| const beforeSubMetadata = beforeContainer?.metadata ?? EMPTY_OBJ; |
| const tableNode = this.makeDataNode('', null); |
| tableNode.items = |
| [...this.makeItems(diffMode, subMetadata, beforeSubMetadata)]; |
| const outerNode = this.makeDataNode(name, [tableNode]); |
| rootNode.children.push(outerNode); |
| } |
| |
| this.rootNode = rootNode; |
| } |
| |
| /** |
| * Decides whether a MetadataTreeNode is a leaf node. |
| * @param {!MetadataTreeNode} dataNode |
| * @return {Boolean} |
| * @public |
| */ |
| isLeaf(dataNode) { |
| return !dataNode.children; |
| } |
| } |
| |
| /** |
| * Class to manage UI to display metadata as a trees with tables as leaves. |
| * @extends {TreeUi<MetadataTreeNode>} |
| */ |
| class MetadataTreeUi extends TreeUi { |
| /** @param {!MetadataTreeModel} model */ |
| constructor(model) { |
| super(g_el.ulMetadataTree); |
| |
| /** @private @const {!MetadataTreeModel} */ |
| this.model = model; |
| |
| /** @private @const {function(!KeyboardEvent): *} */ |
| this.boundHandleKeyDown = this.handleKeyDown.bind(this); |
| } |
| |
| /** |
| * @param {!Array<!MetadataItem>} items |
| * @param {!DocumentFragment} fragment |
| * @private |
| */ |
| populateTable(items, fragment) { |
| const table = fragment.querySelector('table'); |
| const diffMode = state.getDiffMode(); |
| for (const item of items) { |
| const tr = document.createElement('tr'); |
| tr.appendChild(dom.textElement('td', item.name, '')); |
| if (diffMode) |
| tr.appendChild(dom.textElement('td', item.beforeValue.toString(), '')); |
| tr.appendChild(dom.textElement('td', item.value.toString(), '')); |
| table.appendChild(tr); |
| } |
| } |
| |
| /** @override @protected */ |
| makeGroupOrLeafFragment(nodeData) { |
| const isLeaf = this.model.isLeaf(nodeData); |
| // Use different template depending on whether node is group or leaf. |
| const tmpl = |
| isLeaf ? g_el.tmplMetadataTreeLeaf : g_el.tmplMetadataTreeGroup; |
| const fragment = document.importNode(tmpl.content, true); |
| const listItemElt = fragment.firstElementChild; |
| const nodeElt = |
| /** @type {HTMLAnchorElement} */ (listItemElt.firstElementChild); |
| |
| // Set the symbol name and hover text. |
| if (nodeData.name) { |
| const spanSymbolName = /** @type {HTMLSpanElement} */ ( |
| fragment.querySelector('.symbol-name')); |
| spanSymbolName.textContent = nodeData.name; |
| spanSymbolName.title = nodeData.name; |
| } |
| |
| if (nodeData.items) |
| this.populateTable(nodeData.items, fragment); |
| |
| // Insert type dependent SVG icon at the start of |nodeElt|. |
| if (!isLeaf) { |
| const icon = getMetadataIconTemplate(nodeData.iconKey ?? 'group'); |
| nodeElt.insertBefore(icon, nodeElt.firstElementChild); |
| } |
| return {fragment, isLeaf}; |
| } |
| |
| /** @override @protected */ |
| async getGroupChildrenData(link) { |
| const data = this.uiNodeToData.get(link); |
| return data.children; |
| } |
| |
| /** |
| * @param {!KeyboardEvent} event |
| * @protected |
| */ |
| handleKeyDown(event) { |
| if (event.altKey || event.ctrlKey || event.metaKey) |
| return; |
| |
| /** @type {!TreeNodeElement} */ |
| const nodeElt = /** @type {!TreeNodeElement} */ (event.target); |
| /** @type {number} Index of this element in the node list */ |
| const focusIndex = Array.prototype.indexOf.call(this.liveNodeList, nodeElt); |
| |
| this.handleKeyNavigationCommon(event, nodeElt, focusIndex); |
| } |
| |
| /** @override @public */ |
| init() { |
| super.init(); |
| |
| g_el.ulMetadataTree.addEventListener('keydown', this.boundHandleKeyDown); |
| } |
| } |