ConsoleTable.js (7099B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 "use strict"; 5 6 const { 7 Component, 8 createFactory, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 11 const { 12 getArrayTypeNames, 13 } = require("resource://devtools/shared/webconsole/messages.js"); 14 const { 15 l10n, 16 getDescriptorValue, 17 } = require("resource://devtools/client/webconsole/utils/messages.js"); 18 loader.lazyGetter(this, "MODE", function () { 19 return ChromeUtils.importESModule( 20 "resource://devtools/client/shared/components/reps/index.mjs", 21 { global: "current" } 22 ).MODE; 23 }); 24 25 const GripMessageBody = createFactory( 26 require("resource://devtools/client/webconsole/components/Output/GripMessageBody.js") 27 ); 28 29 loader.lazyRequireGetter( 30 this, 31 "PropTypes", 32 "resource://devtools/client/shared/vendor/react-prop-types.js" 33 ); 34 35 const TABLE_ROW_MAX_ITEMS = 1000; 36 // Match Chrome max column number. 37 const TABLE_COLUMN_MAX_ITEMS = 21; 38 39 class ConsoleTable extends Component { 40 static get propTypes() { 41 return { 42 dispatch: PropTypes.func.isRequired, 43 parameters: PropTypes.array.isRequired, 44 serviceContainer: PropTypes.object.isRequired, 45 id: PropTypes.string.isRequired, 46 setExpanded: PropTypes.func, 47 }; 48 } 49 50 constructor(props) { 51 super(props); 52 this.getHeaders = this.getHeaders.bind(this); 53 this.getRows = this.getRows.bind(this); 54 } 55 56 getHeaders(columns) { 57 const headerItems = []; 58 columns.forEach((value, key) => 59 headerItems.push( 60 dom.th( 61 { 62 key, 63 title: value, 64 }, 65 value 66 ) 67 ) 68 ); 69 return dom.thead({}, dom.tr({}, headerItems)); 70 } 71 72 getRows(columns, items) { 73 const { dispatch, serviceContainer, setExpanded } = this.props; 74 75 const rows = []; 76 items.forEach(item => { 77 const cells = []; 78 79 columns.forEach((value, key) => { 80 const cellValue = item[key]; 81 const cellContent = 82 typeof cellValue === "undefined" 83 ? "" 84 : GripMessageBody({ 85 grip: cellValue, 86 mode: MODE.SHORT, 87 useQuotes: false, 88 serviceContainer, 89 dispatch, 90 setExpanded, 91 }); 92 93 cells.push( 94 dom.td( 95 { 96 key, 97 }, 98 cellContent 99 ) 100 ); 101 }); 102 rows.push(dom.tr({}, cells)); 103 }); 104 return dom.tbody({}, rows); 105 } 106 107 render() { 108 const { parameters } = this.props; 109 const { valueGrip, headersGrip } = getValueAndHeadersGrip(parameters); 110 111 const headers = headersGrip?.preview ? headersGrip.preview.items : null; 112 113 const data = valueGrip?.ownProperties; 114 115 // if we don't have any data, don't show anything. 116 if (!data) { 117 return null; 118 } 119 120 const dataType = getParametersDataType(parameters); 121 const { columns, items } = getTableItems(data, dataType, headers); 122 123 // We need to wrap the <table> in a div so we can have the max-height set properly 124 // without changing the table display. 125 return dom.div( 126 { className: "consoletable-wrapper" }, 127 dom.table( 128 { 129 className: "consoletable", 130 }, 131 this.getHeaders(columns), 132 this.getRows(columns, items) 133 ) 134 ); 135 } 136 } 137 138 function getValueAndHeadersGrip(parameters) { 139 const [valueFront, headersFront] = parameters; 140 141 const headersGrip = headersFront?.getGrip 142 ? headersFront.getGrip() 143 : headersFront; 144 145 const valueGrip = valueFront?.getGrip ? valueFront.getGrip() : valueFront; 146 147 return { valueGrip, headersGrip }; 148 } 149 150 function getParametersDataType(parameters = null) { 151 if (!Array.isArray(parameters) || parameters.length === 0) { 152 return null; 153 } 154 const [firstParam] = parameters; 155 if (!firstParam || !firstParam.getGrip) { 156 return null; 157 } 158 const grip = firstParam.getGrip(); 159 return grip.class; 160 } 161 162 const INDEX_NAME = "_index"; 163 const VALUE_NAME = "_value"; 164 165 function getNamedIndexes(type) { 166 return { 167 [INDEX_NAME]: getArrayTypeNames().concat("Object").includes(type) 168 ? l10n.getStr("table.index") 169 : l10n.getStr("table.iterationIndex"), 170 [VALUE_NAME]: l10n.getStr("table.value"), 171 key: l10n.getStr("table.key"), 172 }; 173 } 174 175 function hasValidCustomHeaders(headers) { 176 return ( 177 Array.isArray(headers) && 178 headers.every( 179 header => typeof header === "string" || Number.isInteger(Number(header)) 180 ) 181 ); 182 } 183 184 function getTableItems(data = {}, type, headers = null) { 185 const namedIndexes = getNamedIndexes(type); 186 187 let columns = new Map(); 188 const items = []; 189 190 const addItem = function (item) { 191 items.push(item); 192 Object.keys(item).forEach(key => addColumn(key)); 193 }; 194 195 const validCustomHeaders = hasValidCustomHeaders(headers); 196 197 const addColumn = function (columnIndex) { 198 const columnExists = columns.has(columnIndex); 199 const hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS; 200 201 if ( 202 !columnExists && 203 !hasMaxColumns && 204 (!validCustomHeaders || 205 headers.includes(columnIndex) || 206 columnIndex === INDEX_NAME) 207 ) { 208 columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex); 209 } 210 }; 211 212 for (let [index, property] of Object.entries(data)) { 213 if (type !== "Object" && index == parseInt(index, 10)) { 214 index = parseInt(index, 10); 215 } 216 217 const item = { 218 [INDEX_NAME]: index, 219 }; 220 221 const propertyValue = getDescriptorValue(property); 222 const propertyValueGrip = propertyValue?.getGrip 223 ? propertyValue.getGrip() 224 : propertyValue; 225 226 if (propertyValueGrip?.ownProperties) { 227 const entries = propertyValueGrip.ownProperties; 228 for (const [key, entry] of Object.entries(entries)) { 229 item[key] = getDescriptorValue(entry); 230 } 231 } else if ( 232 propertyValueGrip?.preview && 233 (type === "Map" || type === "WeakMap") 234 ) { 235 item.key = propertyValueGrip.preview.key; 236 item[VALUE_NAME] = propertyValueGrip.preview.value; 237 } else { 238 item[VALUE_NAME] = propertyValue; 239 } 240 241 addItem(item); 242 243 if (items.length === TABLE_ROW_MAX_ITEMS) { 244 break; 245 } 246 } 247 248 // Some headers might not be present in the items, so we make sure to 249 // return all the headers set by the user. 250 if (validCustomHeaders) { 251 headers.forEach(header => addColumn(header)); 252 } 253 254 // We want to always have the index column first 255 if (columns.has(INDEX_NAME)) { 256 const index = columns.get(INDEX_NAME); 257 columns.delete(INDEX_NAME); 258 columns = new Map([[INDEX_NAME, index], ...columns.entries()]); 259 } 260 261 // We want to always have the values column last 262 if (columns.has(VALUE_NAME)) { 263 const index = columns.get(VALUE_NAME); 264 columns.delete(VALUE_NAME); 265 columns.set(VALUE_NAME, index); 266 } 267 268 return { 269 columns, 270 items, 271 }; 272 } 273 274 module.exports = ConsoleTable;