274 lines
6.9 KiB
JavaScript
274 lines
6.9 KiB
JavaScript
/* 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.mjs");
|
|
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 ChromeUtils.importESModule(
|
|
"resource://devtools/client/shared/components/reps/index.mjs",
|
|
{ global: "current" }
|
|
).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 => {
|
|
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 <table> 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;
|