/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { Component, createFactory, } = require("resource://devtools/client/shared/vendor/react.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const { getArrayTypeNames, } = require("resource://devtools/shared/webconsole/messages.js"); const { l10n, getDescriptorValue, } = require("resource://devtools/client/webconsole/utils/messages.js"); loader.lazyGetter(this, "MODE", function () { return require("resource://devtools/client/shared/components/reps/index.js") .MODE; }); const GripMessageBody = createFactory( require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js") ); loader.lazyRequireGetter( this, "PropTypes", "resource://devtools/client/shared/vendor/react-prop-types.js" ); const TABLE_ROW_MAX_ITEMS = 1000; // Match Chrome max column number. const TABLE_COLUMN_MAX_ITEMS = 21; class ConsoleTable extends Component { static get propTypes() { return { dispatch: PropTypes.func.isRequired, parameters: PropTypes.array.isRequired, serviceContainer: PropTypes.object.isRequired, id: PropTypes.string.isRequired, setExpanded: PropTypes.func, }; } constructor(props) { super(props); this.getHeaders = this.getHeaders.bind(this); this.getRows = this.getRows.bind(this); } getHeaders(columns) { const headerItems = []; columns.forEach((value, key) => headerItems.push( dom.th( { key, title: value, }, value ) ) ); return dom.thead({}, dom.tr({}, headerItems)); } getRows(columns, items) { const { dispatch, serviceContainer, setExpanded } = this.props; const rows = []; items.forEach((item, index) => { const cells = []; columns.forEach((value, key) => { const cellValue = item[key]; const cellContent = typeof cellValue === "undefined" ? "" : GripMessageBody({ grip: cellValue, mode: MODE.SHORT, useQuotes: false, serviceContainer, dispatch, setExpanded, }); cells.push( dom.td( { key, }, cellContent ) ); }); rows.push(dom.tr({}, cells)); }); return dom.tbody({}, rows); } render() { const { parameters } = this.props; const { valueGrip, headersGrip } = getValueAndHeadersGrip(parameters); const headers = headersGrip?.preview ? headersGrip.preview.items : null; const data = valueGrip?.ownProperties; // if we don't have any data, don't show anything. if (!data) { return null; } const dataType = getParametersDataType(parameters); const { columns, items } = getTableItems(data, dataType, headers); // We need to wrap the in a div so we can have the max-height set properly // without changing the table display. return dom.div( { className: "consoletable-wrapper" }, dom.table( { className: "consoletable", }, this.getHeaders(columns), this.getRows(columns, items) ) ); } } function getValueAndHeadersGrip(parameters) { const [valueFront, headersFront] = parameters; const headersGrip = headersFront?.getGrip ? headersFront.getGrip() : headersFront; const valueGrip = valueFront?.getGrip ? valueFront.getGrip() : valueFront; return { valueGrip, headersGrip }; } function getParametersDataType(parameters = null) { if (!Array.isArray(parameters) || parameters.length === 0) { return null; } const [firstParam] = parameters; if (!firstParam || !firstParam.getGrip) { return null; } const grip = firstParam.getGrip(); return grip.class; } const INDEX_NAME = "_index"; const VALUE_NAME = "_value"; function getNamedIndexes(type) { return { [INDEX_NAME]: getArrayTypeNames().concat("Object").includes(type) ? l10n.getStr("table.index") : l10n.getStr("table.iterationIndex"), [VALUE_NAME]: l10n.getStr("table.value"), key: l10n.getStr("table.key"), }; } function hasValidCustomHeaders(headers) { return ( Array.isArray(headers) && headers.every( header => typeof header === "string" || Number.isInteger(Number(header)) ) ); } function getTableItems(data = {}, type, headers = null) { const namedIndexes = getNamedIndexes(type); let columns = new Map(); const items = []; const addItem = function (item) { items.push(item); Object.keys(item).forEach(key => addColumn(key)); }; const validCustomHeaders = hasValidCustomHeaders(headers); const addColumn = function (columnIndex) { const columnExists = columns.has(columnIndex); const hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS; if ( !columnExists && !hasMaxColumns && (!validCustomHeaders || headers.includes(columnIndex) || columnIndex === INDEX_NAME) ) { columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex); } }; for (let [index, property] of Object.entries(data)) { if (type !== "Object" && index == parseInt(index, 10)) { index = parseInt(index, 10); } const item = { [INDEX_NAME]: index, }; const propertyValue = getDescriptorValue(property); const propertyValueGrip = propertyValue?.getGrip ? propertyValue.getGrip() : propertyValue; if (propertyValueGrip?.ownProperties) { const entries = propertyValueGrip.ownProperties; for (const [key, entry] of Object.entries(entries)) { item[key] = getDescriptorValue(entry); } } else if ( propertyValueGrip?.preview && (type === "Map" || type === "WeakMap") ) { item.key = propertyValueGrip.preview.key; item[VALUE_NAME] = propertyValueGrip.preview.value; } else { item[VALUE_NAME] = propertyValue; } addItem(item); if (items.length === TABLE_ROW_MAX_ITEMS) { break; } } // Some headers might not be present in the items, so we make sure to // return all the headers set by the user. if (validCustomHeaders) { headers.forEach(header => addColumn(header)); } // We want to always have the index column first if (columns.has(INDEX_NAME)) { const index = columns.get(INDEX_NAME); columns.delete(INDEX_NAME); columns = new Map([[INDEX_NAME, index], ...columns.entries()]); } // We want to always have the values column last if (columns.has(VALUE_NAME)) { const index = columns.get(VALUE_NAME); columns.delete(VALUE_NAME); columns.set(VALUE_NAME, index); } return { columns, items, }; } module.exports = ConsoleTable;