summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListHeader.js731
1 files changed, 731 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js b/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js
new file mode 100644
index 0000000000..d02e993b02
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js
@@ -0,0 +1,731 @@
+/* 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 {
+ createRef,
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
+const {
+ getTheme,
+ addThemeObserver,
+ removeThemeObserver,
+} = require("resource://devtools/client/shared/theme.js");
+const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
+const {
+ HEADERS,
+ REQUESTS_WATERFALL,
+ MIN_COLUMN_WIDTH,
+ DEFAULT_COLUMN_WIDTH,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+const {
+ getColumns,
+ getWaterfallScale,
+} = require("resource://devtools/client/netmonitor/src/selectors/index.js");
+const {
+ getFormattedTime,
+} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js");
+const {
+ L10N,
+} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const RequestListHeaderContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js");
+const WaterfallBackground = require("resource://devtools/client/netmonitor/src/widgets/WaterfallBackground.js");
+const Draggable = createFactory(
+ require("resource://devtools/client/shared/components/splitter/Draggable.js")
+);
+
+const { div, button } = dom;
+
+/**
+ * Render the request list header with sorting arrows for columns.
+ * Displays tick marks in the waterfall column header.
+ * Also draws the waterfall background canvas and updates it when needed.
+ */
+class RequestListHeader extends Component {
+ static get propTypes() {
+ return {
+ columns: PropTypes.object.isRequired,
+ resetColumns: PropTypes.func.isRequired,
+ resetSorting: PropTypes.func.isRequired,
+ resizeWaterfall: PropTypes.func.isRequired,
+ scale: PropTypes.number,
+ sort: PropTypes.object,
+ sortBy: PropTypes.func.isRequired,
+ toggleColumn: PropTypes.func.isRequired,
+ waterfallWidth: PropTypes.number,
+ columnsData: PropTypes.object.isRequired,
+ setColumnsWidth: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.requestListHeader = createRef();
+
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.drawBackground = this.drawBackground.bind(this);
+ this.resizeWaterfall = this.resizeWaterfall.bind(this);
+ this.waterfallDivisionLabels = this.waterfallDivisionLabels.bind(this);
+ this.waterfallLabel = this.waterfallLabel.bind(this);
+ this.onHeaderClick = this.onHeaderClick.bind(this);
+ this.resizeColumnToFitContent = this.resizeColumnToFitContent.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ const { resetColumns, resetSorting, toggleColumn } = this.props;
+ this.contextMenu = new RequestListHeaderContextMenu({
+ resetColumns,
+ resetSorting,
+ toggleColumn,
+ resizeColumnToFitContent: this.resizeColumnToFitContent,
+ });
+ }
+
+ componentDidMount() {
+ // Create the object that takes care of drawing the waterfall canvas background
+ this.background = new WaterfallBackground(document);
+ this.drawBackground();
+ // When visible columns add up to less or more than 100% => update widths in prefs.
+ if (this.shouldUpdateWidths()) {
+ this.updateColumnsWidth();
+ }
+ this.resizeWaterfall();
+ window.addEventListener("resize", this.resizeWaterfall);
+ addThemeObserver(this.drawBackground);
+ }
+
+ componentDidUpdate() {
+ this.drawBackground();
+ // check if the widths in prefs need to be updated
+ // e.g. after hide/show column
+ if (this.shouldUpdateWidths()) {
+ this.updateColumnsWidth();
+ this.resizeWaterfall();
+ }
+ }
+
+ componentWillUnmount() {
+ this.background.destroy();
+ this.background = null;
+ window.removeEventListener("resize", this.resizeWaterfall);
+ removeThemeObserver(this.drawBackground);
+ }
+
+ /**
+ * Helper method to get the total width of cell's content.
+ * Used for resizing columns to fit their content.
+ */
+ totalCellWidth(cellEl) {
+ return [...cellEl.childNodes]
+ .map(cNode => {
+ if (cNode.nodeType === 3) {
+ // if it's text node
+ return Math.ceil(
+ cNode.getBoxQuads()[0].p2.x - cNode.getBoxQuads()[0].p1.x
+ );
+ }
+ return cNode.getBoundingClientRect().width;
+ })
+ .reduce((a, b) => a + b, 0);
+ }
+
+ /**
+ * Resize column to fit its content.
+ * Additionally, resize other columns (starting from last) to compensate.
+ */
+ resizeColumnToFitContent(name) {
+ const headerRef = this.refs[`${name}Header`];
+ const parentEl = headerRef.closest(".requests-list-table");
+ const width = headerRef.getBoundingClientRect().width;
+ const parentWidth = parentEl.getBoundingClientRect().width;
+ const items = parentEl.querySelectorAll(".request-list-item");
+ const columnIndex = headerRef.cellIndex;
+ const widths = [...items].map(item =>
+ this.totalCellWidth(item.children[columnIndex])
+ );
+
+ const minW = this.getMinWidth(name);
+
+ // Add 11 to account for cell padding (padding-right + padding-left = 9px), not accurate.
+ let maxWidth = 11 + Math.max.apply(null, widths);
+
+ if (maxWidth < minW) {
+ maxWidth = minW;
+ }
+
+ // Pixel value which, if added to this column's width, will fit its content.
+ let change = maxWidth - width;
+
+ // Max change we can do while taking other columns into account.
+ let maxAllowedChange = 0;
+ const visibleColumns = this.getVisibleColumns();
+ const newWidths = [];
+
+ // Calculate new widths for other columns to compensate.
+ // Start from the 2nd last column if last column is waterfall.
+ // This is done to comply with the existing resizing behavior.
+ const delta =
+ visibleColumns[visibleColumns.length - 1].name === "waterfall" ? 2 : 1;
+
+ for (let i = visibleColumns.length - delta; i > 0; i--) {
+ if (i !== columnIndex) {
+ const columnName = visibleColumns[i].name;
+ const columnHeaderRef = this.refs[`${columnName}Header`];
+ const columnWidth = columnHeaderRef.getBoundingClientRect().width;
+ const minWidth = this.getMinWidth(columnName);
+ const newWidth = columnWidth - change;
+
+ // If this column can compensate for all the remaining change.
+ if (newWidth >= minWidth) {
+ maxAllowedChange += change;
+ change = 0;
+ newWidths.push({
+ name: columnName,
+ width: this.px2percent(newWidth, parentWidth),
+ });
+ break;
+ } else {
+ // Max change we can do in this column.
+ let maxColumnChange = columnWidth - minWidth;
+ maxColumnChange = maxColumnChange > change ? change : maxColumnChange;
+ maxAllowedChange += maxColumnChange;
+ change -= maxColumnChange;
+ newWidths.push({
+ name: columnName,
+ width: this.px2percent(columnWidth - maxColumnChange, parentWidth),
+ });
+ }
+ }
+ }
+ newWidths.push({
+ name,
+ width: this.px2percent(width + maxAllowedChange, parentWidth),
+ });
+ this.props.setColumnsWidth(newWidths);
+ }
+
+ onContextMenu(evt) {
+ evt.preventDefault();
+ this.contextMenu.open(evt, this.props.columns);
+ }
+
+ onHeaderClick(evt, headerName) {
+ const { sortBy, resetSorting } = this.props;
+ if (evt.button == 1) {
+ // reset sort state on middle click
+ resetSorting();
+ } else {
+ sortBy(headerName);
+ }
+ }
+
+ drawBackground() {
+ // The background component is theme dependent, so add the current theme to the props.
+ const props = Object.assign({}, this.props, {
+ theme: getTheme(),
+ });
+ this.background.draw(props);
+ }
+
+ resizeWaterfall() {
+ const { waterfallHeader } = this.refs;
+ if (waterfallHeader) {
+ // Measure its width and update the 'waterfallWidth' property in the store.
+ // The 'waterfallWidth' will be further updated on every window resize.
+ window.cancelIdleCallback(this._resizeTimerId);
+ this._resizeTimerId = window.requestIdleCallback(() =>
+ this.props.resizeWaterfall(
+ waterfallHeader.getBoundingClientRect().width
+ )
+ );
+ }
+ }
+
+ /**
+ * Build the waterfall header - timing tick marks with the right spacing
+ */
+ waterfallDivisionLabels(waterfallWidth, scale) {
+ const labels = [];
+
+ // Build new millisecond tick labels...
+ const timingStep = REQUESTS_WATERFALL.HEADER_TICKS_MULTIPLE;
+ let scaledStep = scale * timingStep;
+
+ // Ignore any divisions that would end up being too close to each other.
+ while (scaledStep < REQUESTS_WATERFALL.HEADER_TICKS_SPACING_MIN) {
+ scaledStep *= 2;
+ }
+
+ // Insert one label for each division on the current scale.
+ for (let x = 0; x < waterfallWidth; x += scaledStep) {
+ const millisecondTime = x / scale;
+ let divisionScale = "millisecond";
+
+ // If the division is greater than 1 minute.
+ if (millisecondTime > 60000) {
+ divisionScale = "minute";
+ } else if (millisecondTime > 1000) {
+ // If the division is greater than 1 second.
+ divisionScale = "second";
+ }
+
+ let width = ((x + scaledStep) | 0) - (x | 0);
+ // Adjust the first marker for the borders
+ if (x == 0) {
+ width -= 2;
+ }
+ // Last marker doesn't need a width specified at all
+ if (x + scaledStep >= waterfallWidth) {
+ width = undefined;
+ }
+
+ labels.push(
+ div(
+ {
+ key: labels.length,
+ className: "requests-list-timings-division",
+ "data-division-scale": divisionScale,
+ style: { width },
+ },
+ getFormattedTime(millisecondTime)
+ )
+ );
+ }
+
+ return labels;
+ }
+
+ waterfallLabel(waterfallWidth, scale, label) {
+ let className = "button-text requests-list-waterfall-label-wrapper";
+
+ if (waterfallWidth !== null && scale !== null) {
+ label = this.waterfallDivisionLabels(waterfallWidth, scale);
+ className += " requests-list-waterfall-visible";
+ }
+
+ return div({ className }, label);
+ }
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire container dragging.
+ * This avoids cursor-flickering when the mouse leaves
+ * the column-resizer area (happens frequently).
+ */
+ onStartMove() {
+ // Set cursor to dragging
+ const container = document.querySelector(".request-list-container");
+ container.style.cursor = "ew-resize";
+ // Class .dragging is used to disable pointer events while dragging - see css.
+ this.requestListHeader.classList.add("dragging");
+ }
+
+ /**
+ * A handler that calculates the new width of the columns
+ * based on mouse position and adjusts the width.
+ */
+ onMove(name, x) {
+ const parentEl = document.querySelector(".requests-list-headers");
+ const parentWidth = parentEl.getBoundingClientRect().width;
+
+ // Get the current column handle and save its old width
+ // before changing so we can compute the adjustment in width
+ const headerRef = this.refs[`${name}Header`];
+ const headerRefRect = headerRef.getBoundingClientRect();
+ const oldWidth = headerRefRect.width;
+
+ // Get the column handle that will compensate the width change.
+ const compensateHeaderName = this.getCompensateHeader();
+
+ if (name === compensateHeaderName) {
+ // this is the case where we are resizing waterfall
+ this.moveWaterfall(x, parentWidth);
+ return;
+ }
+
+ const compensateHeaderRef = this.refs[`${compensateHeaderName}Header`];
+ const compensateHeaderRefRect = compensateHeaderRef.getBoundingClientRect();
+ const oldCompensateWidth = compensateHeaderRefRect.width;
+ const sumOfBothColumns = oldWidth + oldCompensateWidth;
+
+ // Get minimal widths for both changed columns (in px).
+ const minWidth = this.getMinWidth(name);
+ const minCompensateWidth = this.getMinWidth(compensateHeaderName);
+
+ // Calculate new width (according to the mouse x-position) and set to style.
+ // Do not allow to set it below minWidth.
+ let newWidth =
+ document.dir == "ltr" ? x - headerRefRect.left : headerRefRect.right - x;
+ newWidth = Math.max(newWidth, minWidth);
+ headerRef.style.width = `${this.px2percent(newWidth, parentWidth)}%`;
+ const adjustment = oldWidth - newWidth;
+
+ // Calculate new compensate width as the original width + adjustment.
+ // Do not allow to set it below minCompensateWidth.
+ const newCompensateWidth = Math.max(
+ adjustment + oldCompensateWidth,
+ minCompensateWidth
+ );
+ compensateHeaderRef.style.width = `${this.px2percent(
+ newCompensateWidth,
+ parentWidth
+ )}%`;
+
+ // Do not allow to reset size of column when compensate column is at minWidth.
+ if (newCompensateWidth === minCompensateWidth) {
+ headerRef.style.width = `${this.px2percent(
+ sumOfBothColumns - newCompensateWidth,
+ parentWidth
+ )}%`;
+ }
+ }
+
+ /**
+ * After resizing - we get the width for each 'column'
+ * and convert it into % and store it in user prefs.
+ * Also resets the 'resizing' cursor back to initial.
+ */
+ onStopMove() {
+ this.updateColumnsWidth();
+ // If waterfall is visible and width has changed, call resizeWaterfall.
+ const waterfallRef = this.refs.waterfallHeader;
+ if (waterfallRef) {
+ const { waterfallWidth } = this.props;
+ const realWaterfallWidth = waterfallRef.getBoundingClientRect().width;
+ if (Math.round(waterfallWidth) !== Math.round(realWaterfallWidth)) {
+ this.resizeWaterfall();
+ }
+ }
+
+ // Restore cursor back to default.
+ const container = document.querySelector(".request-list-container");
+ container.style.cursor = "initial";
+ this.requestListHeader.classList.remove("dragging");
+ }
+
+ /**
+ * Helper method to get the name of the column that will compensate
+ * the width change. It should be the last column before waterfall,
+ * (if waterfall visible) otherwise it is simply the last visible column.
+ */
+ getCompensateHeader() {
+ const visibleColumns = this.getVisibleColumns();
+ const lastColumn = visibleColumns[visibleColumns.length - 1].name;
+ const delta = lastColumn === "waterfall" ? 2 : 1;
+ return visibleColumns[visibleColumns.length - delta].name;
+ }
+
+ /**
+ * Called from onMove() when resizing waterfall column
+ * because waterfall is a special case, where ALL other
+ * columns are made smaller when waterfall is bigger and vice versa.
+ */
+ moveWaterfall(x, parentWidth) {
+ const visibleColumns = this.getVisibleColumns();
+ const minWaterfall = this.getMinWidth("waterfall");
+ const waterfallRef = this.refs.waterfallHeader;
+
+ // Compute and set style.width for waterfall.
+ const waterfallRefRect = waterfallRef.getBoundingClientRect();
+ const oldWidth = waterfallRefRect.width;
+ const adjustment =
+ document.dir == "ltr"
+ ? waterfallRefRect.left - x
+ : x - waterfallRefRect.right;
+ if (this.allColumnsAtMinWidth() && adjustment > 0) {
+ // When we want to make waterfall wider but all
+ // other columns are already at minWidth => return.
+ return;
+ }
+
+ const newWidth = Math.max(oldWidth + adjustment, minWaterfall);
+
+ // Now distribute evenly the change in width to all other columns except waterfall.
+ const changeInWidth = oldWidth - newWidth;
+ const widths = this.autoSizeWidths(changeInWidth, visibleColumns);
+
+ // Set the new computed width for waterfall into array widths.
+ widths[widths.length - 1] = newWidth;
+
+ // Update style for all columns from array widths.
+ let i = 0;
+ visibleColumns.forEach(col => {
+ const { name } = col;
+ const headerRef = this.refs[`${name}Header`];
+ headerRef.style.width = `${this.px2percent(widths[i], parentWidth)}%`;
+ i++;
+ });
+ }
+
+ /**
+ * Helper method that checks if all columns have reached their minWidth.
+ * This can happen when making waterfall column wider.
+ */
+ allColumnsAtMinWidth() {
+ const visibleColumns = this.getVisibleColumns();
+ // Do not check width for waterfall because
+ // when all are getting smaller, waterfall is getting bigger.
+ for (let i = 0; i < visibleColumns.length - 1; i++) {
+ const { name } = visibleColumns[i];
+ const headerRef = this.refs[`${name}Header`];
+ const minColWidth = this.getMinWidth(name);
+ if (headerRef.getBoundingClientRect().width > minColWidth) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Method takes the total change in width for waterfall column
+ * and distributes it among all other columns. Returns an array
+ * where all visible columns have newly computed width in pixels.
+ */
+ autoSizeWidths(changeInWidth, visibleColumns) {
+ const widths = visibleColumns.map(col => {
+ const headerRef = this.refs[`${col.name}Header`];
+ const colWidth = headerRef.getBoundingClientRect().width;
+ return colWidth;
+ });
+
+ // Divide changeInWidth among all columns but waterfall (that's why -1).
+ const changeInWidthPerColumn = changeInWidth / (widths.length - 1);
+
+ while (changeInWidth) {
+ const lastChangeInWidth = changeInWidth;
+ // In the loop adjust all columns except last one - waterfall
+ for (let i = 0; i < widths.length - 1; i++) {
+ const { name } = visibleColumns[i];
+ const minColWidth = this.getMinWidth(name);
+ const newColWidth = Math.max(
+ widths[i] + changeInWidthPerColumn,
+ minColWidth
+ );
+
+ widths[i] = newColWidth;
+ if (changeInWidth > 0) {
+ changeInWidth -= newColWidth - widths[i];
+ } else {
+ changeInWidth += newColWidth - widths[i];
+ }
+ if (!changeInWidth) {
+ break;
+ }
+ }
+ if (lastChangeInWidth == changeInWidth) {
+ break;
+ }
+ }
+ return widths;
+ }
+
+ /**
+ * Method returns 'true' - if the column widths need to be updated
+ * when the total % is less or more than 100%.
+ * It returns 'false' if they add up to 100% => no need to update.
+ */
+ shouldUpdateWidths() {
+ const visibleColumns = this.getVisibleColumns();
+ let totalPercent = 0;
+
+ visibleColumns.forEach(col => {
+ const { name } = col;
+ const headerRef = this.refs[`${name}Header`];
+ // Get column width from style.
+ let widthFromStyle = 0;
+ // In case the column is in visibleColumns but has display:none
+ // we don't want to count its style.width into totalPercent.
+ if (headerRef.getBoundingClientRect().width > 0) {
+ widthFromStyle = headerRef.style.width.slice(0, -1);
+ }
+ totalPercent += +widthFromStyle; // + converts it to a number
+ });
+
+ // Do not update if total percent is from 99-101% or when it is 0
+ // - it means that no columns are displayed (e.g. other panel is currently selected).
+ return Math.round(totalPercent) !== 100 && totalPercent !== 0;
+ }
+
+ /**
+ * Method reads real width of each column header
+ * and updates the style.width for that header.
+ * It returns updated columnsData.
+ */
+ updateColumnsWidth() {
+ const visibleColumns = this.getVisibleColumns();
+ const parentEl = document.querySelector(".requests-list-headers");
+ const parentElRect = parentEl.getBoundingClientRect();
+ const parentWidth = parentElRect.width;
+ const newWidths = [];
+ visibleColumns.forEach(col => {
+ const { name } = col;
+ const headerRef = this.refs[`${name}Header`];
+ const headerWidth = headerRef.getBoundingClientRect().width;
+
+ // Get actual column width, change into %, update style
+ const width = this.px2percent(headerWidth, parentWidth);
+
+ if (width > 0) {
+ // This prevents saving width 0 for waterfall when it is not showing for
+ // @media (max-width: 700px)
+ newWidths.push({ name, width });
+ }
+ });
+ this.props.setColumnsWidth(newWidths);
+ }
+
+ /**
+ * Helper method to convert pixels into percent based on parent container width
+ */
+ px2percent(pxWidth, parentWidth) {
+ const percent = Math.round(((100 * pxWidth) / parentWidth) * 100) / 100;
+ return percent;
+ }
+
+ /**
+ * Helper method to get visibleColumns;
+ */
+ getVisibleColumns() {
+ const { columns } = this.props;
+ return HEADERS.filter(header => columns[header.name]);
+ }
+
+ /**
+ * Helper method to get minWidth from columnsData;
+ */
+ getMinWidth(colName) {
+ const { columnsData } = this.props;
+ if (columnsData.has(colName)) {
+ return columnsData.get(colName).minWidth;
+ }
+ return MIN_COLUMN_WIDTH;
+ }
+
+ /**
+ * Render one column header from the table headers.
+ */
+ renderColumn(header) {
+ const { columnsData } = this.props;
+ const visibleColumns = this.getVisibleColumns();
+ const lastVisibleColumn = visibleColumns[visibleColumns.length - 1].name;
+ const { name } = header;
+ const boxName = header.boxName || name;
+ const label = header.noLocalization
+ ? name
+ : L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
+
+ const { scale, sort, waterfallWidth } = this.props;
+ let sorted, sortedTitle;
+ const active = sort.type == name ? true : undefined;
+
+ if (active) {
+ sorted = sort.ascending ? "ascending" : "descending";
+ sortedTitle = L10N.getStr(
+ sort.ascending ? "networkMenu.sortedAsc" : "networkMenu.sortedDesc"
+ );
+ }
+
+ // If the pref for this column width exists, set the style
+ // otherwise use default.
+ let colWidth = DEFAULT_COLUMN_WIDTH;
+ if (columnsData.has(name)) {
+ const oneColumnEl = columnsData.get(name);
+ colWidth = oneColumnEl.width;
+ }
+ const columnStyle = {
+ width: colWidth + "%",
+ };
+
+ // Support for columns resizing is currently hidden behind a pref.
+ const draggable = Draggable({
+ className: "column-resizer ",
+ title: L10N.getStr("netmonitor.toolbar.resizeColumnToFitContent.title"),
+ onStart: () => this.onStartMove(),
+ onStop: () => this.onStopMove(),
+ onMove: x => this.onMove(name, x),
+ onDoubleClick: () => this.resizeColumnToFitContent(name),
+ });
+
+ return dom.th(
+ {
+ id: `requests-list-${boxName}-header-box`,
+ className: `requests-list-column requests-list-${boxName}`,
+ scope: "col",
+ style: columnStyle,
+ key: name,
+ ref: `${name}Header`,
+ // Used to style the next column.
+ "data-active": active,
+ },
+ button(
+ {
+ id: `requests-list-${name}-button`,
+ className: `requests-list-header-button`,
+ "data-sorted": sorted,
+ "data-name": name,
+ title: sortedTitle ? `${label} (${sortedTitle})` : label,
+ onClick: evt => this.onHeaderClick(evt, name),
+ },
+ name === "waterfall"
+ ? this.waterfallLabel(waterfallWidth, scale, label)
+ : div({ className: "button-text" }, label),
+ div({ className: "button-icon" })
+ ),
+ name !== lastVisibleColumn && draggable
+ );
+ }
+
+ /**
+ * Render all columns in the table header
+ */
+ renderColumns() {
+ const visibleColumns = this.getVisibleColumns();
+ return visibleColumns.map(header => this.renderColumn(header));
+ }
+
+ render() {
+ return dom.thead(
+ { className: "requests-list-headers-group" },
+ dom.tr(
+ {
+ className: "requests-list-headers",
+ onContextMenu: this.onContextMenu,
+ ref: node => {
+ this.requestListHeader = node;
+ },
+ },
+ this.renderColumns()
+ )
+ );
+ }
+}
+
+module.exports = connect(
+ state => ({
+ columns: getColumns(state),
+ columnsData: state.ui.columnsData,
+ firstRequestStartedMs: state.requests.firstStartedMs,
+ scale: getWaterfallScale(state),
+ sort: state.sort,
+ timingMarkers: state.timingMarkers,
+ waterfallWidth: state.ui.waterfallWidth,
+ }),
+ dispatch => ({
+ resetColumns: () => dispatch(Actions.resetColumns()),
+ resetSorting: () => dispatch(Actions.sortBy(null)),
+ resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)),
+ sortBy: type => dispatch(Actions.sortBy(type)),
+ toggleColumn: column => dispatch(Actions.toggleColumn(column)),
+ setColumnsWidth: widths => dispatch(Actions.setColumnsWidth(widths)),
+ })
+)(RequestListHeader);