summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/components/Output/ConsoleTable.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/components/Output/ConsoleTable.js')
-rw-r--r--devtools/client/webconsole/components/Output/ConsoleTable.js272
1 files changed, 272 insertions, 0 deletions
diff --git a/devtools/client/webconsole/components/Output/ConsoleTable.js b/devtools/client/webconsole/components/Output/ConsoleTable.js
new file mode 100644
index 0000000000..f41afce96d
--- /dev/null
+++ b/devtools/client/webconsole/components/Output/ConsoleTable.js
@@ -0,0 +1,272 @@
+/* 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 <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;