RequestListHeader.js (25379B)
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 5 "use strict"; 6 7 const { 8 createRef, 9 Component, 10 createFactory, 11 } = require("resource://devtools/client/shared/vendor/react.mjs"); 12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 13 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 14 const { 15 connect, 16 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 17 const { 18 getTheme, 19 addThemeObserver, 20 removeThemeObserver, 21 } = require("resource://devtools/client/shared/theme.js"); 22 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); 23 const { 24 HEADERS, 25 REQUESTS_WATERFALL, 26 MIN_COLUMN_WIDTH, 27 DEFAULT_COLUMN_WIDTH, 28 } = require("resource://devtools/client/netmonitor/src/constants.js"); 29 const { 30 getColumns, 31 getWaterfallScale, 32 hasOverride, 33 } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); 34 const { 35 getFormattedTime, 36 } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); 37 const { 38 L10N, 39 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 40 const RequestListHeaderContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js"); 41 const WaterfallBackground = require("resource://devtools/client/netmonitor/src/widgets/WaterfallBackground.js"); 42 const Draggable = createFactory( 43 require("resource://devtools/client/shared/components/splitter/Draggable.js") 44 ); 45 46 const { div, button } = dom; 47 48 /** 49 * Render the request list header with sorting arrows for columns. 50 * Displays tick marks in the waterfall column header. 51 * Also draws the waterfall background canvas and updates it when needed. 52 */ 53 class RequestListHeaderContent extends Component { 54 static get propTypes() { 55 return { 56 columns: PropTypes.object.isRequired, 57 resetColumns: PropTypes.func.isRequired, 58 resetSorting: PropTypes.func.isRequired, 59 resizeWaterfall: PropTypes.func.isRequired, 60 scale: PropTypes.number, 61 sort: PropTypes.object, 62 sortBy: PropTypes.func.isRequired, 63 toggleColumn: PropTypes.func.isRequired, 64 waterfallWidth: PropTypes.number, 65 columnsData: PropTypes.object.isRequired, 66 setColumnsWidth: PropTypes.func.isRequired, 67 }; 68 } 69 70 constructor(props) { 71 super(props); 72 this.requestListHeader = createRef(); 73 74 this.onContextMenu = this.onContextMenu.bind(this); 75 this.drawBackground = this.drawBackground.bind(this); 76 this.resizeWaterfall = this.resizeWaterfall.bind(this); 77 this.waterfallDivisionLabels = this.waterfallDivisionLabels.bind(this); 78 this.waterfallLabel = this.waterfallLabel.bind(this); 79 this.onHeaderClick = this.onHeaderClick.bind(this); 80 this.resizeColumnToFitContent = this.resizeColumnToFitContent.bind(this); 81 } 82 83 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 84 UNSAFE_componentWillMount() { 85 const { resetColumns, resetSorting, toggleColumn } = this.props; 86 this.contextMenu = new RequestListHeaderContextMenu({ 87 resetColumns, 88 resetSorting, 89 toggleColumn, 90 resizeColumnToFitContent: this.resizeColumnToFitContent, 91 }); 92 } 93 94 componentDidMount() { 95 // Create the object that takes care of drawing the waterfall canvas background 96 this.background = new WaterfallBackground(document); 97 this.drawBackground(); 98 // When visible columns add up to less or more than 100% => update widths in prefs. 99 if (this.shouldUpdateWidths()) { 100 this.updateColumnsWidth(); 101 } 102 this.resizeWaterfall(); 103 window.addEventListener("resize", this.resizeWaterfall); 104 addThemeObserver(this.drawBackground); 105 } 106 107 componentDidUpdate() { 108 this.drawBackground(); 109 // check if the widths in prefs need to be updated 110 // e.g. after hide/show column 111 if (this.shouldUpdateWidths()) { 112 this.updateColumnsWidth(); 113 this.resizeWaterfall(); 114 } 115 } 116 117 componentWillUnmount() { 118 this.background.destroy(); 119 this.background = null; 120 window.removeEventListener("resize", this.resizeWaterfall); 121 removeThemeObserver(this.drawBackground); 122 } 123 124 /** 125 * Helper method to get the total width of cell's content. 126 * Used for resizing columns to fit their content. 127 */ 128 totalCellWidth(cellEl) { 129 return [...cellEl.childNodes] 130 .map(cNode => { 131 if (cNode.nodeType === 3) { 132 // if it's text node 133 return Math.ceil( 134 cNode.getBoxQuads()[0].p2.x - cNode.getBoxQuads()[0].p1.x 135 ); 136 } 137 return cNode.getBoundingClientRect().width; 138 }) 139 .reduce((a, b) => a + b, 0); 140 } 141 142 /** 143 * Resize column to fit its content. 144 * Additionally, resize other columns (starting from last) to compensate. 145 */ 146 resizeColumnToFitContent(name) { 147 const headerRef = this.refs[`${name}Header`]; 148 const parentEl = headerRef.closest(".requests-list-table"); 149 const width = headerRef.getBoundingClientRect().width; 150 const parentWidth = parentEl.getBoundingClientRect().width; 151 const items = parentEl.querySelectorAll(".request-list-item"); 152 const columnIndex = headerRef.cellIndex; 153 const widths = [...items].map(item => 154 this.totalCellWidth(item.children[columnIndex]) 155 ); 156 157 const minW = this.getMinWidth(name); 158 159 // Add 11 to account for cell padding (padding-right + padding-left = 9px), not accurate. 160 let maxWidth = 11 + Math.max.apply(null, widths); 161 162 if (maxWidth < minW) { 163 maxWidth = minW; 164 } 165 166 // Pixel value which, if added to this column's width, will fit its content. 167 let change = maxWidth - width; 168 169 // Max change we can do while taking other columns into account. 170 let maxAllowedChange = 0; 171 const visibleColumns = this.getVisibleColumns(); 172 const newWidths = []; 173 174 // Calculate new widths for other columns to compensate. 175 // Start from the 2nd last column if last column is waterfall. 176 // This is done to comply with the existing resizing behavior. 177 const delta = 178 visibleColumns[visibleColumns.length - 1].name === "waterfall" ? 2 : 1; 179 180 for (let i = visibleColumns.length - delta; i > 0; i--) { 181 if (i !== columnIndex) { 182 const columnName = visibleColumns[i].name; 183 const columnHeaderRef = this.refs[`${columnName}Header`]; 184 const columnWidth = columnHeaderRef.getBoundingClientRect().width; 185 const minWidth = this.getMinWidth(columnName); 186 const newWidth = columnWidth - change; 187 188 // If this column can compensate for all the remaining change. 189 if (newWidth >= minWidth) { 190 maxAllowedChange += change; 191 change = 0; 192 newWidths.push({ 193 name: columnName, 194 width: this.px2percent(newWidth, parentWidth), 195 }); 196 break; 197 } else { 198 // Max change we can do in this column. 199 let maxColumnChange = columnWidth - minWidth; 200 maxColumnChange = maxColumnChange > change ? change : maxColumnChange; 201 maxAllowedChange += maxColumnChange; 202 change -= maxColumnChange; 203 newWidths.push({ 204 name: columnName, 205 width: this.px2percent(columnWidth - maxColumnChange, parentWidth), 206 }); 207 } 208 } 209 } 210 newWidths.push({ 211 name, 212 width: this.px2percent(width + maxAllowedChange, parentWidth), 213 }); 214 this.props.setColumnsWidth(newWidths); 215 } 216 217 onContextMenu(evt) { 218 evt.preventDefault(); 219 this.contextMenu.open(evt, this.props.columns); 220 } 221 222 onHeaderClick(evt, headerName) { 223 const { sortBy, resetSorting } = this.props; 224 if (evt.button == 1) { 225 // reset sort state on middle click 226 resetSorting(); 227 } else { 228 sortBy(headerName); 229 } 230 } 231 232 drawBackground() { 233 // The background component is theme dependent, so add the current theme to the props. 234 const props = Object.assign({}, this.props, { 235 theme: getTheme(), 236 }); 237 this.background.draw(props); 238 } 239 240 resizeWaterfall() { 241 const { waterfallHeader } = this.refs; 242 if (waterfallHeader) { 243 // Measure its width and update the 'waterfallWidth' property in the store. 244 // The 'waterfallWidth' will be further updated on every window resize. 245 window.cancelIdleCallback(this._resizeTimerId); 246 this._resizeTimerId = window.requestIdleCallback(() => { 247 if (document.visibilityState == "visible") { 248 this.props.resizeWaterfall( 249 waterfallHeader.getBoundingClientRect().width 250 ); 251 } 252 }); 253 } 254 } 255 256 /** 257 * Build the waterfall header - timing tick marks with the right spacing 258 */ 259 waterfallDivisionLabels(waterfallWidth, scale) { 260 const labels = []; 261 262 // Build new millisecond tick labels... 263 const timingStep = REQUESTS_WATERFALL.HEADER_TICKS_MULTIPLE; 264 let scaledStep = scale * timingStep; 265 266 // Ignore any divisions that would end up being too close to each other. 267 while (scaledStep < REQUESTS_WATERFALL.HEADER_TICKS_SPACING_MIN) { 268 scaledStep *= 2; 269 } 270 271 // Insert one label for each division on the current scale. 272 for (let x = 0; x < waterfallWidth; x += scaledStep) { 273 const millisecondTime = x / scale; 274 let divisionScale = "millisecond"; 275 276 // If the division is greater than 1 minute. 277 if (millisecondTime > 60000) { 278 divisionScale = "minute"; 279 } else if (millisecondTime > 1000) { 280 // If the division is greater than 1 second. 281 divisionScale = "second"; 282 } 283 284 let width = ((x + scaledStep) | 0) - (x | 0); 285 // Adjust the first marker for the borders 286 if (x == 0) { 287 width -= 2; 288 } 289 // Last marker doesn't need a width specified at all 290 if (x + scaledStep >= waterfallWidth) { 291 width = undefined; 292 } 293 294 labels.push( 295 div( 296 { 297 key: labels.length, 298 className: "requests-list-timings-division", 299 "data-division-scale": divisionScale, 300 style: { width }, 301 }, 302 getFormattedTime(millisecondTime) 303 ) 304 ); 305 } 306 307 return labels; 308 } 309 310 waterfallLabel(waterfallWidth, scale, label) { 311 let className = "button-text requests-list-waterfall-label-wrapper"; 312 313 if (waterfallWidth !== null && scale !== null) { 314 label = this.waterfallDivisionLabels(waterfallWidth, scale); 315 className += " requests-list-waterfall-visible"; 316 } 317 318 return div({ className }, label); 319 } 320 321 // Dragging Events 322 323 /** 324 * Set 'resizing' cursor on entire container dragging. 325 * This avoids cursor-flickering when the mouse leaves 326 * the column-resizer area (happens frequently). 327 */ 328 onStartMove() { 329 // Set cursor to dragging 330 const container = document.querySelector(".request-list-container"); 331 container.style.cursor = "ew-resize"; 332 // Class .dragging is used to disable pointer events while dragging - see css. 333 this.requestListHeader.classList.add("dragging"); 334 } 335 336 /** 337 * A handler that calculates the new width of the columns 338 * based on mouse position and adjusts the width. 339 */ 340 onMove(name, x) { 341 const parentEl = document.querySelector(".requests-list-headers"); 342 const parentWidth = parentEl.getBoundingClientRect().width; 343 344 // Get the current column handle and save its old width 345 // before changing so we can compute the adjustment in width 346 const headerRef = this.refs[`${name}Header`]; 347 const headerRefRect = headerRef.getBoundingClientRect(); 348 const oldWidth = headerRefRect.width; 349 350 // Get the column handle that will compensate the width change. 351 const compensateHeaderName = this.getCompensateHeader(); 352 353 if (name === compensateHeaderName) { 354 // this is the case where we are resizing waterfall 355 this.moveWaterfall(x, parentWidth); 356 return; 357 } 358 359 const compensateHeaderRef = this.refs[`${compensateHeaderName}Header`]; 360 const compensateHeaderRefRect = compensateHeaderRef.getBoundingClientRect(); 361 const oldCompensateWidth = compensateHeaderRefRect.width; 362 const sumOfBothColumns = oldWidth + oldCompensateWidth; 363 364 // Get minimal widths for both changed columns (in px). 365 const minWidth = this.getMinWidth(name); 366 const minCompensateWidth = this.getMinWidth(compensateHeaderName); 367 368 // Calculate new width (according to the mouse x-position) and set to style. 369 // Do not allow to set it below minWidth. 370 let newWidth = 371 document.dir == "ltr" ? x - headerRefRect.left : headerRefRect.right - x; 372 newWidth = Math.max(newWidth, minWidth); 373 headerRef.style.width = `${this.px2percent(newWidth, parentWidth)}%`; 374 const adjustment = oldWidth - newWidth; 375 376 // Calculate new compensate width as the original width + adjustment. 377 // Do not allow to set it below minCompensateWidth. 378 const newCompensateWidth = Math.max( 379 adjustment + oldCompensateWidth, 380 minCompensateWidth 381 ); 382 compensateHeaderRef.style.width = `${this.px2percent( 383 newCompensateWidth, 384 parentWidth 385 )}%`; 386 387 // Do not allow to reset size of column when compensate column is at minWidth. 388 if (newCompensateWidth === minCompensateWidth) { 389 headerRef.style.width = `${this.px2percent( 390 sumOfBothColumns - newCompensateWidth, 391 parentWidth 392 )}%`; 393 } 394 } 395 396 /** 397 * After resizing - we get the width for each 'column' 398 * and convert it into % and store it in user prefs. 399 * Also resets the 'resizing' cursor back to initial. 400 */ 401 onStopMove() { 402 this.updateColumnsWidth(); 403 // If waterfall is visible and width has changed, call resizeWaterfall. 404 const waterfallRef = this.refs.waterfallHeader; 405 if (waterfallRef) { 406 const { waterfallWidth } = this.props; 407 const realWaterfallWidth = waterfallRef.getBoundingClientRect().width; 408 if (Math.round(waterfallWidth) !== Math.round(realWaterfallWidth)) { 409 this.resizeWaterfall(); 410 } 411 } 412 413 // Restore cursor back to default. 414 const container = document.querySelector(".request-list-container"); 415 container.style.cursor = "initial"; 416 this.requestListHeader.classList.remove("dragging"); 417 } 418 419 /** 420 * Helper method to get the name of the column that will compensate 421 * the width change. It should be the last column before waterfall, 422 * (if waterfall visible) otherwise it is simply the last visible column. 423 */ 424 getCompensateHeader() { 425 const visibleColumns = this.getVisibleColumns(); 426 const lastColumn = visibleColumns[visibleColumns.length - 1].name; 427 const delta = lastColumn === "waterfall" ? 2 : 1; 428 return visibleColumns[visibleColumns.length - delta].name; 429 } 430 431 /** 432 * Called from onMove() when resizing waterfall column 433 * because waterfall is a special case, where ALL other 434 * columns are made smaller when waterfall is bigger and vice versa. 435 */ 436 moveWaterfall(x, parentWidth) { 437 const visibleColumns = this.getVisibleColumns(); 438 const minWaterfall = this.getMinWidth("waterfall"); 439 const waterfallRef = this.refs.waterfallHeader; 440 441 // Compute and set style.width for waterfall. 442 const waterfallRefRect = waterfallRef.getBoundingClientRect(); 443 const oldWidth = waterfallRefRect.width; 444 const adjustment = 445 document.dir == "ltr" 446 ? waterfallRefRect.left - x 447 : x - waterfallRefRect.right; 448 if (this.allColumnsAtMinWidth() && adjustment > 0) { 449 // When we want to make waterfall wider but all 450 // other columns are already at minWidth => return. 451 return; 452 } 453 454 const newWidth = Math.max(oldWidth + adjustment, minWaterfall); 455 456 // Now distribute evenly the change in width to all other columns except waterfall. 457 const changeInWidth = oldWidth - newWidth; 458 const widths = this.autoSizeWidths(changeInWidth, visibleColumns); 459 460 // Set the new computed width for waterfall into array widths. 461 widths[widths.length - 1] = newWidth; 462 463 // Update style for all columns from array widths. 464 let i = 0; 465 visibleColumns.forEach(col => { 466 const { name } = col; 467 const headerRef = this.refs[`${name}Header`]; 468 headerRef.style.width = `${this.px2percent(widths[i], parentWidth)}%`; 469 i++; 470 }); 471 } 472 473 /** 474 * Helper method that checks if all columns have reached their minWidth. 475 * This can happen when making waterfall column wider. 476 */ 477 allColumnsAtMinWidth() { 478 const visibleColumns = this.getVisibleColumns(); 479 // Do not check width for waterfall because 480 // when all are getting smaller, waterfall is getting bigger. 481 for (let i = 0; i < visibleColumns.length - 1; i++) { 482 const { name } = visibleColumns[i]; 483 const headerRef = this.refs[`${name}Header`]; 484 const minColWidth = this.getMinWidth(name); 485 if (headerRef.getBoundingClientRect().width > minColWidth) { 486 return false; 487 } 488 } 489 return true; 490 } 491 492 /** 493 * Method takes the total change in width for waterfall column 494 * and distributes it among all other columns. Returns an array 495 * where all visible columns have newly computed width in pixels. 496 */ 497 autoSizeWidths(changeInWidth, visibleColumns) { 498 const widths = visibleColumns.map(col => { 499 const headerRef = this.refs[`${col.name}Header`]; 500 const colWidth = headerRef.getBoundingClientRect().width; 501 return colWidth; 502 }); 503 504 // Divide changeInWidth among all columns but waterfall (that's why -1). 505 const changeInWidthPerColumn = changeInWidth / (widths.length - 1); 506 507 while (changeInWidth) { 508 const lastChangeInWidth = changeInWidth; 509 // In the loop adjust all columns except last one - waterfall 510 for (let i = 0; i < widths.length - 1; i++) { 511 const { name } = visibleColumns[i]; 512 const minColWidth = this.getMinWidth(name); 513 const newColWidth = Math.max( 514 widths[i] + changeInWidthPerColumn, 515 minColWidth 516 ); 517 518 widths[i] = newColWidth; 519 if (changeInWidth > 0) { 520 changeInWidth -= newColWidth - widths[i]; 521 } else { 522 changeInWidth += newColWidth - widths[i]; 523 } 524 if (!changeInWidth) { 525 break; 526 } 527 } 528 if (lastChangeInWidth == changeInWidth) { 529 break; 530 } 531 } 532 return widths; 533 } 534 535 /** 536 * Method returns 'true' - if the column widths need to be updated 537 * when the total % is less or more than 100%. 538 * It returns 'false' if they add up to 100% => no need to update. 539 */ 540 shouldUpdateWidths() { 541 const visibleColumns = this.getVisibleColumns(); 542 let totalPercent = 0; 543 544 visibleColumns.forEach(col => { 545 const { name } = col; 546 const headerRef = this.refs[`${name}Header`]; 547 // Get column width from style. 548 let widthFromStyle = 0; 549 // In case the column is in visibleColumns but has display:none 550 // we don't want to count its style.width into totalPercent. 551 if (headerRef.getBoundingClientRect().width > 0) { 552 widthFromStyle = headerRef.style.width.slice(0, -1); 553 } 554 totalPercent += +widthFromStyle; // + converts it to a number 555 }); 556 557 // Do not update if total percent is from 99-101% or when it is 0 558 // - it means that no columns are displayed (e.g. other panel is currently selected). 559 return Math.round(totalPercent) !== 100 && totalPercent !== 0; 560 } 561 562 /** 563 * Method reads real width of each column header 564 * and updates the style.width for that header. 565 * It returns updated columnsData. 566 */ 567 updateColumnsWidth() { 568 const visibleColumns = this.getVisibleColumns(); 569 const parentEl = document.querySelector(".requests-list-headers"); 570 const parentElRect = parentEl.getBoundingClientRect(); 571 const parentWidth = parentElRect.width; 572 const newWidths = []; 573 visibleColumns.forEach(col => { 574 const { name } = col; 575 const headerRef = this.refs[`${name}Header`]; 576 const headerWidth = headerRef.getBoundingClientRect().width; 577 578 // Get actual column width, change into %, update style 579 const width = this.px2percent(headerWidth, parentWidth); 580 581 if (width > 0) { 582 // This prevents saving width 0 for waterfall when it is not showing for 583 // @media (max-width: 700px) 584 newWidths.push({ name, width }); 585 } 586 }); 587 this.props.setColumnsWidth(newWidths); 588 } 589 590 /** 591 * Helper method to convert pixels into percent based on parent container width 592 */ 593 px2percent(pxWidth, parentWidth) { 594 const percent = Math.round(((100 * pxWidth) / parentWidth) * 100) / 100; 595 return percent; 596 } 597 598 /** 599 * Helper method to get visibleColumns; 600 */ 601 getVisibleColumns() { 602 const { columns } = this.props; 603 return HEADERS.filter(header => columns[header.name]); 604 } 605 606 /** 607 * Helper method to get minWidth from columnsData; 608 */ 609 getMinWidth(colName) { 610 const { columnsData } = this.props; 611 if (columnsData.has(colName)) { 612 return columnsData.get(colName).minWidth; 613 } 614 return MIN_COLUMN_WIDTH; 615 } 616 617 /** 618 * Render one column header from the table headers. 619 */ 620 renderColumn(header) { 621 const { columnsData } = this.props; 622 const visibleColumns = this.getVisibleColumns(); 623 const lastVisibleColumn = visibleColumns[visibleColumns.length - 1].name; 624 const { name } = header; 625 const boxName = header.boxName || name; 626 const label = header.noLocalization 627 ? name 628 : L10N.getStr(`netmonitor.toolbar.${header.label || name}`); 629 630 const { scale, sort, waterfallWidth } = this.props; 631 let sorted, sortedTitle; 632 const active = sort.type == name ? true : undefined; 633 634 if (active) { 635 sorted = sort.ascending ? "ascending" : "descending"; 636 sortedTitle = L10N.getStr( 637 sort.ascending ? "networkMenu.sortedAsc" : "networkMenu.sortedDesc" 638 ); 639 } 640 641 // If the pref for this column width exists, set the style 642 // otherwise use default. 643 let colWidth = DEFAULT_COLUMN_WIDTH; 644 if (columnsData.has(name)) { 645 const oneColumnEl = columnsData.get(name); 646 colWidth = oneColumnEl.width; 647 } 648 const columnStyle = { 649 width: colWidth + "%", 650 }; 651 652 // Support for columns resizing is currently hidden behind a pref. 653 const draggable = Draggable({ 654 className: "column-resizer ", 655 title: L10N.getStr("netmonitor.toolbar.resizeColumnToFitContent.title"), 656 onStart: () => this.onStartMove(), 657 onStop: () => this.onStopMove(), 658 onMove: x => this.onMove(name, x), 659 onDoubleClick: () => this.resizeColumnToFitContent(name), 660 }); 661 662 return dom.th( 663 { 664 id: `requests-list-${boxName}-header-box`, 665 className: `requests-list-column requests-list-${boxName}`, 666 scope: "col", 667 style: columnStyle, 668 key: name, 669 ref: `${name}Header`, 670 // Used to style the next column. 671 "data-active": active, 672 }, 673 name === "override" 674 ? button( 675 { 676 id: `requests-list-${name}-button`, 677 className: `requests-list-header-button`, 678 title: label, 679 }, 680 div({ className: "button-text" }, label) 681 ) 682 : button( 683 { 684 id: `requests-list-${name}-button`, 685 className: `requests-list-header-button`, 686 "data-sorted": sorted, 687 "data-name": name, 688 title: sortedTitle ? `${label} (${sortedTitle})` : label, 689 onClick: evt => this.onHeaderClick(evt, name), 690 }, 691 name === "waterfall" 692 ? this.waterfallLabel(waterfallWidth, scale, label) 693 : div({ className: "button-text" }, label), 694 div({ className: "button-icon" }) 695 ), 696 name !== lastVisibleColumn && draggable 697 ); 698 } 699 700 /** 701 * Render all columns in the table header 702 */ 703 renderColumns() { 704 const visibleColumns = this.getVisibleColumns(); 705 return visibleColumns.map(header => this.renderColumn(header)); 706 } 707 708 render() { 709 return dom.thead( 710 { className: "requests-list-headers-group" }, 711 dom.tr( 712 { 713 className: "requests-list-headers", 714 onContextMenu: this.onContextMenu, 715 ref: node => { 716 this.requestListHeader = node; 717 }, 718 }, 719 this.renderColumns() 720 ) 721 ); 722 } 723 } 724 725 const RequestListHeader = connect( 726 (state, props) => ({ 727 columns: getColumns(state, props.hasOverride), 728 columnsData: state.ui.columnsData, 729 firstRequestStartedMs: state.requests.firstStartedMs, 730 scale: getWaterfallScale(state), 731 sort: state.sort, 732 timingMarkers: state.timingMarkers, 733 waterfallWidth: state.ui.waterfallWidth, 734 }), 735 dispatch => ({ 736 resetColumns: () => dispatch(Actions.resetColumns()), 737 resetSorting: () => dispatch(Actions.sortBy(null)), 738 resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)), 739 sortBy: type => dispatch(Actions.sortBy(type)), 740 toggleColumn: column => dispatch(Actions.toggleColumn(column)), 741 setColumnsWidth: widths => dispatch(Actions.setColumnsWidth(widths)), 742 }) 743 )(RequestListHeaderContent); 744 745 module.exports = connect( 746 state => { 747 return { 748 hasOverride: hasOverride(state), 749 }; 750 }, 751 {}, 752 undefined, 753 { storeKey: "toolbox-store" } 754 )(RequestListHeader);