tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);