Bug 1358414 - Introduce column resizer in request list; r=Honza
authorlenka <lpelechova@mozilla.com>
Fri, 08 Mar 2019 15:42:54 +0000
changeset 521054 13115d00f301e54ba7fb2a38a92a81d5b22f6da3
parent 521053 7520d595404d3be1a0649f745d738f0a4d0c0521
child 521055 63625c423683a941a2d6afeb3c67b0d578847bb7
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1358414
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1358414 - Introduce column resizer in request list; r=Honza Adding feature to netmonitor for resizing of columns. In this patch the functionality is hidden behind the pref devtools.netmonitor.features.resizeColumns. This feature is currently turned off - false. Differential Revision: https://phabricator.services.mozilla.com/D22719
devtools/client/netmonitor/src/actions/ui.js
devtools/client/netmonitor/src/assets/styles/RequestList.css
devtools/client/netmonitor/src/components/RequestListContent.js
devtools/client/netmonitor/src/components/RequestListEmptyNotice.js
devtools/client/netmonitor/src/components/RequestListHeader.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/create-store.js
devtools/client/netmonitor/src/middleware/prefs.js
devtools/client/netmonitor/src/reducers/ui.js
devtools/client/netmonitor/src/utils/prefs.js
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_columns_last_column.js
devtools/client/netmonitor/test/browser_net_columns_pref.js
devtools/client/netmonitor/test/browser_net_columns_reset.js
devtools/client/netmonitor/test/browser_net_columns_time.js
devtools/client/netmonitor/test/browser_net_headers-resize.js
devtools/client/netmonitor/test/head.js
devtools/client/preferences/devtools-client.js
--- a/devtools/client/netmonitor/src/actions/ui.js
+++ b/devtools/client/netmonitor/src/actions/ui.js
@@ -10,16 +10,17 @@ const {
   RESIZE_NETWORK_DETAILS,
   ENABLE_PERSISTENT_LOGS,
   DISABLE_BROWSER_CACHE,
   OPEN_STATISTICS,
   RESET_COLUMNS,
   SELECT_DETAILS_PANEL_TAB,
   TOGGLE_COLUMN,
   WATERFALL_RESIZE,
+  SET_COLUMNS_WIDTH,
 } = require("../constants");
 
 const { getDisplayedRequests } = require("../selectors/index");
 
 /**
  * Change network details panel.
  *
  * @param {boolean} open - expected network details panel open state
@@ -133,16 +134,28 @@ function selectDetailsPanelTab(id) {
 function toggleColumn(column) {
   return {
     type: TOGGLE_COLUMN,
     column,
   };
 }
 
 /**
+ * Set width of multiple columns
+ *
+ * @param {array} widths - array of pairs {name, width}
+ */
+function setColumnsWidth(widths) {
+  return {
+    type: SET_COLUMNS_WIDTH,
+    widths,
+  };
+}
+
+/**
  * Toggle network details panel.
  */
 function toggleNetworkDetails() {
   return (dispatch, getState) =>
     dispatch(openNetworkDetails(!getState().ui.networkDetailsOpen));
 }
 
 /**
@@ -174,13 +187,14 @@ module.exports = {
   resizeNetworkDetails,
   enablePersistentLogs,
   disableBrowserCache,
   openStatistics,
   resetColumns,
   resizeWaterfall,
   selectDetailsPanelTab,
   toggleColumn,
+  setColumnsWidth,
   toggleNetworkDetails,
   togglePersistentLogs,
   toggleBrowserCache,
   toggleStatistics,
 };
--- a/devtools/client/netmonitor/src/assets/styles/RequestList.css
+++ b/devtools/client/netmonitor/src/assets/styles/RequestList.css
@@ -2,17 +2,17 @@
  * 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/. */
 
 /* Request list empty panel */
 
 .request-list-empty-notice {
   margin: 0;
   flex: 1;
-  overflow: auto;
+  overflow-x: hidden;
 }
 
 .empty-notice-element {
   padding-top: 12px;
   padding-left: 12px;
   padding-right: 12px;
   font-size: 1.2rem;
 }
@@ -54,26 +54,28 @@
 .requests-list-scroll {
   overflow-x: hidden;
   overflow-y: auto;
 }
 
 .requests-list-table {
   /* Reset default browser style of <table> */
   border-spacing: 0;
+  width: 100%;
+  /* The layout must be fixed for resizing of columns to work.
+  The layout is based on the first row.
+  Set the width of those cells, and the rest of the table follows. */
+  table-layout: fixed;
 }
 
 .requests-list-column {
-  cursor: default;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
   vertical-align: middle;
-  max-width: 50px;
-  min-width: 50px;
 
   /* Reset default browser style of <td> */
   padding: 0;
 }
 
 .requests-list-column > * {
   display: inline-block;
 }
@@ -103,38 +105,38 @@
 .requests-list-header-button {
   background-color: transparent;
   border-image: linear-gradient(transparent 15%,
                                 var(--theme-splitter-color) 15%,
                                 var(--theme-splitter-color) 85%,
                                 transparent 85%) 1 1;
   border-width: 0;
   border-inline-start-width: 1px;
-  padding-inline-start: 16px;
   width: 100%;
   min-height: 23px;
   text-align: center;
   color: inherit;
+  padding: 1px 4px;
 }
 
 .requests-list-header-button::-moz-focus-inner {
   border: 0;
   padding: 0;
 }
 
 .requests-list-header-button:hover {
   background-color: rgba(0, 0, 0, 0.1);
 }
 
 .requests-list-header-button > .button-text {
   display: inline-block;
   text-align: center;
   vertical-align: middle;
   /* Align button text to center */
-  width: calc(100% - 8px);
+  width: 100%;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
 .requests-list-header-button > .button-icon {
   display: inline-block;
   width: 7px;
   height: 4px;
@@ -157,23 +159,42 @@
   color: var(--theme-selection-color);
 }
 
 .requests-list-header-button[data-sorted],
 .requests-list-column[data-active] + .requests-list-column .requests-list-header-button {
   border-image: linear-gradient(var(--theme-splitter-color), var(--theme-splitter-color)) 1 1;
 }
 
+/* Requests list headers column-resizer */
+
+.requests-list-headers .column-resizer {
+  z-index: 1000;
+  cursor: ew-resize;
+  margin-left: -3px;
+  width: 7px;
+  min-height: 23px;
+  position: absolute;
+  background-color: transparent;
+}
+
+/**
+ * Make sure headers are not processing any mouse
+ * events. This is good for performance during dragging.
+ */
+.requests-list-headers.dragging {
+  pointer-events: none;
+}
+
 /* Requests list column */
 
 /* Status column */
 
 .requests-list-status {
-  min-width: 70px;
-   /* Don't ellipsize status codes */
+  /* Don't ellipsize status codes */
   text-overflow: initial;
 }
 
 .requests-list-status-icon {
   background: #fff;
   height: 10px;
   width: 10px;
   margin-inline-start: 5px;
@@ -221,103 +242,24 @@
 }
 
 .requests-list-status-icon[data-code^="5"] {
   background-color: var(--status-code-color-5xx);
   border-radius: 0;
   transform: rotate(45deg);
 }
 
-/* Method column */
-
-.requests-list-method {
-  min-width: 85px;
-}
-
-/* File column */
-
-.requests-list-file {
-  width: 40%;
-}
-
 .requests-list-file.requests-list-column {
   text-align: start;
 }
 
 .request-list-item.selected {
   filter: brightness(1.3);
 }
 
-/* Protocol column */
-
-.requests-list-protocol {
-  width: 8%;
-}
-
-/* Cookies column */
-
-.requests-list-cookies {
-  width: 6%;
-}
-
-/* Set Cookies column */
-
-.requests-list-set-cookies {
-  width: 8%;
-}
-
-/* Scheme column */
-
-.requests-list-scheme {
-  width: 8%;
-}
-
-/* Start Time column */
-
-.requests-list-start-time {
-  width: 8%;
-}
-
-/* End Time column */
-
-.requests-list-end-time {
-  width: 8%;
-}
-
-/* Response Time column */
-
-.requests-list-response-time {
-  width: 10%;
-}
-
-/* Duration column */
-
-.requests-list-duration-time {
-  width: 8%;
-}
-
-/* Latency column */
-
-.requests-list-latency-time {
-  width: 8%;
-}
-
-/* Response header columns */
-
-.requests-list-response-header {
-  width: 10%;
-}
-
-/* Domain column */
-
-.requests-list-domain {
-  min-width: 100px;
-  width: 30%;
-}
-
 .requests-list-domain.requests-list-column {
   text-align: start;
 }
 
 .requests-security-state-icon {
   display: inline-block;
   width: 16px;
   height: 16px;
@@ -362,28 +304,16 @@
   background-image: url(chrome://devtools/content/netmonitor/src/assets/icons/shield.svg);
   background-repeat: no-repeat;
 }
 
 .selected .tracking-resource {
   filter: brightness(500%);
 }
 
-/* RemoteIP column */
-
-.requests-list-remoteip {
-  width: 9%;
-}
-
-/* Cause column */
-
-.requests-list-cause {
-  min-width: 75px;
-}
-
 .request-list-item .requests-list-cause.requests-list-column {
   padding-left: 5px;
 }
 
 .requests-list-cause-stack {
   display: inline-block;
   background-color: var(--theme-body-color-alt);
   color: var(--theme-body-background);
@@ -391,40 +321,19 @@
   font-weight: bold;
   line-height: 10px;
   border-radius: 3px;
   padding: 0 2px;
   margin: 0;
   margin-inline-end: 3px;
 }
 
-/* Type column */
-
-.requests-list-type {
-  min-width: 65px;
-}
-
-/* Transferred column */
-
-.requests-list-transferred {
-  min-width: 110px;
-}
-
-/* Size column */
-
-.requests-list-size {
-  min-width: 80px;
-}
-
 /* Waterfall column */
 
 .requests-list-waterfall {
-  width: 25vw;
-  max-width: 25vw;
-  min-width: 25vw;
   background-repeat: repeat-y;
   background-position: left center;
   /* Background created on a <canvas> in js. */
   /* @see devtools/client/netmonitor/src/waterfall-background.js */
   background-image: -moz-element(#waterfall-background);
 }
 
 .requests-list-waterfall:dir(rtl) {
@@ -567,20 +476,16 @@
 
 .request-list-item:not(.selected).fromCache > .requests-list-column:not(.requests-list-waterfall) {
   opacity: 0.7;
 }
 
 /* Responsive web design support */
 
 @media (max-width: 700px) {
-  .requests-list-header-button {
-    padding-inline-start: 8px;
-  }
-
   .requests-list-status-code {
     width: auto;
   }
 
   .requests-list-size {
     /* Given a fix max-width to display all columns in RWD mode */
     max-width: 7%;
   }
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -118,16 +118,20 @@ class RequestListContent extends Compone
   componentWillUnmount() {
     this.refs.scrollEl.removeEventListener("scroll", this.onScroll, true);
 
     // Uninstall the tooltip event handler
     this.tooltip.stopTogglingOnHover();
     window.removeEventListener("resize", this.onResize);
   }
 
+  /*
+   * Removing onResize() method causes perf regression - too many repaints of the panel.
+   * So it is needed in ComponentDidMount and ComponentDidUpdate. See Bug 1532914.
+   */
   onResize() {
     const parent = this.refs.scrollEl.parentNode;
     this.refs.scrollEl.style.width = parent.offsetWidth + "px";
     this.refs.scrollEl.style.height = parent.offsetHeight + "px";
   }
 
   isScrolledToBottom() {
     const { scrollEl, rowGroupEl } = this.refs;
--- a/devtools/client/netmonitor/src/components/RequestListEmptyNotice.js
+++ b/devtools/client/netmonitor/src/components/RequestListEmptyNotice.js
@@ -10,17 +10,16 @@ const PropTypes = require("devtools/clie
 const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
 const Actions = require("../actions/index");
 const { ACTIVITY_TYPE } = require("../constants");
 const { L10N } = require("../utils/l10n");
 const { getPerformanceAnalysisURL } = require("../utils/mdn-utils");
 
 // Components
 const MDNLink = createFactory(require("devtools/client/shared/components/MdnLink"));
-const RequestListHeader = createFactory(require("./RequestListHeader"));
 
 const { button, div, span } = dom;
 
 const RELOAD_NOTICE_1 = L10N.getStr("netmonitor.reloadNotice1");
 const RELOAD_NOTICE_2 = L10N.getStr("netmonitor.reloadNotice2");
 const RELOAD_NOTICE_3 = L10N.getStr("netmonitor.reloadNotice3");
 const PERFORMANCE_NOTICE_1 = L10N.getStr("netmonitor.perfNotice1");
 const PERFORMANCE_NOTICE_2 = L10N.getStr("netmonitor.perfNotice2");
@@ -40,17 +39,16 @@ class RequestListEmptyNotice extends Com
     };
   }
 
   render() {
     return div(
       {
         className: "request-list-empty-notice",
       },
-      RequestListHeader(),
       div({ className: "notice-reload-message empty-notice-element" },
         span(null, RELOAD_NOTICE_1),
         button(
           {
             className: "devtools-button requests-list-reload-notice-button",
             "data-standalone": true,
             onClick: this.props.onReloadClick,
           },
--- a/devtools/client/netmonitor/src/components/RequestListHeader.js
+++ b/devtools/client/netmonitor/src/components/RequestListHeader.js
@@ -1,51 +1,69 @@
 /* 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 } = require("devtools/client/shared/vendor/react");
+const Services = require("Services");
+const { createRef, Component, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
-const { getTheme, addThemeObserver, removeThemeObserver } =
-  require("devtools/client/shared/theme");
+const {
+  getTheme,
+  addThemeObserver,
+  removeThemeObserver,
+} = require("devtools/client/shared/theme");
 const Actions = require("../actions/index");
-const { HEADERS, REQUESTS_WATERFALL } = require("../constants");
+const {
+  HEADERS,
+  REQUESTS_WATERFALL,
+  MIN_COLUMN_WIDTH,
+  DEFAULT_COLUMN_WIDTH,
+} = require("../constants");
 const { getWaterfallScale } = require("../selectors/index");
 const { getFormattedTime } = require("../utils/format-utils");
 const { L10N } = require("../utils/l10n");
 const RequestListHeaderContextMenu = require("../widgets/RequestListHeaderContextMenu");
 const WaterfallBackground = require("../widgets/WaterfallBackground");
+const Draggable = createFactory(require("devtools/client/shared/components/splitter/Draggable"));
 
 const { div, button } = dom;
 
+// Support for columns resizing is currently hidden behind this pref.
+const RESIZE_COLUMNS =
+  Services.prefs.getBoolPref("devtools.netmonitor.features.resizeColumns");
+
 /**
  * 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,
       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);
   }
 
   componentWillMount() {
@@ -55,23 +73,33 @@ class RequestListHeader extends Componen
       toggleColumn,
     });
   }
 
   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);
   }
@@ -158,78 +186,402 @@ class RequestListHeader extends Componen
     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.
+    const newWidth = Math.max(x - headerRefRect.left, 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 = waterfallRefRect.left - x;
+    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.name;
+      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].name;
+      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].name;
+        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.name;
+      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.name;
+      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.columnsData;
+    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.columnsData;
+    const visibleColumns = this.getVisibleColumns();
+    const lastVisibleColumn = visibleColumns[visibleColumns.length - 1].name;
+    const name = header.name;
+    const boxName = header.boxName || name;
+    const label = header.noLocalization
+      ? name : L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
+
+    const { scale, sort, sortBy, 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 = RESIZE_COLUMNS ? Draggable({
+      className: "column-resizer ",
+      onStart: () => this.onStartMove(),
+      onStop: () => this.onStopMove(),
+      onMove: (x) => this.onMove(name, x),
+    }) : undefined;
+
+    return (
+      dom.td({
+        id: `requests-list-${boxName}-header-box`,
+        className: `requests-list-column requests-list-${boxName}`,
+        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,
+          title: sortedTitle ? `${label} (${sortedTitle})` : label,
+          onClick: () => sortBy(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() {
-    const { columns, scale, sort, sortBy, waterfallWidth } = this.props;
-
     return (
       dom.thead({ className: "devtools-toolbar requests-list-headers-group" },
         dom.tr({
           className: "requests-list-headers",
           onContextMenu: this.onContextMenu,
+          ref: node => {
+            this.requestListHeader = node;
+          },
         },
-          HEADERS.filter((header) => columns[header.name]).map((header) => {
-            const name = header.name;
-            const boxName = header.boxName || name;
-            const label = header.noLocalization
-              ? name : L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
-            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");
-            }
-
-            return (
-              dom.td({
-                id: `requests-list-${boxName}-header-box`,
-                className: `requests-list-column requests-list-${boxName}`,
-                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,
-                  title: sortedTitle ? `${label} (${sortedTitle})` : label,
-                  onClick: () => sortBy(name),
-                },
-                  name === "waterfall"
-                    ? this.waterfallLabel(waterfallWidth, scale, label)
-                    : div({ className: "button-text" }, label),
-                  div({ className: "button-icon" })
-                )
-              )
-            );
-          })
+          this.renderColumns(),
         )
       )
     );
   }
 }
 
 module.exports = connect(
   (state) => ({
     columns: state.ui.columns,
+    columnsData: state.ui.columnsData,
     firstRequestStartedMillis: state.requests.firstStartedMillis,
     scale: getWaterfallScale(state),
     sort: state.sort,
     timingMarkers: state.timingMarkers,
     waterfallWidth: state.ui.waterfallWidth,
   }),
   (dispatch) => ({
     resetColumns: () => dispatch(Actions.resetColumns()),
     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);
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -25,16 +25,17 @@ const actionTypes = {
   SEND_CUSTOM_REQUEST: "SEND_CUSTOM_REQUEST",
   SET_REQUEST_FILTER_TEXT: "SET_REQUEST_FILTER_TEXT",
   SORT_BY: "SORT_BY",
   TOGGLE_COLUMN: "TOGGLE_COLUMN",
   TOGGLE_RECORDING: "TOGGLE_RECORDING",
   TOGGLE_REQUEST_FILTER_TYPE: "TOGGLE_REQUEST_FILTER_TYPE",
   UPDATE_REQUEST: "UPDATE_REQUEST",
   WATERFALL_RESIZE: "WATERFALL_RESIZE",
+  SET_COLUMNS_WIDTH: "SET_COLUMNS_WIDTH",
 };
 
 // Descriptions for what this frontend is currently doing.
 const ACTIVITY_TYPE = {
   // Standing by and handling requests normally.
   NONE: 0,
 
   // Forcing the target to reload with cache enabled or disabled.
@@ -327,24 +328,31 @@ const TIMING_KEYS = [
   "dns",
   "connect",
   "ssl",
   "send",
   "wait",
   "receive",
 ];
 
+// Minimal width of Network Monitor column is 30px, for Waterfall 150px
+// Default width of columns (which are not defined in DEFAULT_COLUMNS_DATA) is 8%
+const MIN_COLUMN_WIDTH = 30; // in px
+const DEFAULT_COLUMN_WIDTH = 8; // in %
+
 const general = {
   ACTIVITY_TYPE,
   EVENTS,
   FILTER_SEARCH_DELAY: 200,
   UPDATE_PROPS,
   HEADERS,
   RESPONSE_HEADERS,
   FILTER_FLAGS,
   FILTER_TAGS,
   REQUESTS_WATERFALL,
   PANELS,
   TIMING_KEYS,
+  MIN_COLUMN_WIDTH,
+  DEFAULT_COLUMN_WIDTH,
 };
 
 // flatten constants
 module.exports = Object.assign({}, general, actionTypes);
--- a/devtools/client/netmonitor/src/create-store.js
+++ b/devtools/client/netmonitor/src/create-store.js
@@ -2,46 +2,52 @@
  * 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 Services = require("Services");
 const { applyMiddleware, createStore } = require("devtools/client/shared/vendor/redux");
 
+const {
+  MIN_COLUMN_WIDTH,
+  DEFAULT_COLUMN_WIDTH,
+} = require("./constants");
+
 // Middleware
 const batching = require("./middleware/batching");
 const prefs = require("./middleware/prefs");
 const thunk = require("./middleware/thunk");
 const recording = require("./middleware/recording");
 const throttling = require("./middleware/throttling");
 const eventTelemetry = require("./middleware/event-telemetry");
 
 // Reducers
 const rootReducer = require("./reducers/index");
 const { FilterTypes, Filters } = require("./reducers/filters");
 const { Requests } = require("./reducers/requests");
 const { Sort } = require("./reducers/sort");
 const { TimingMarkers } = require("./reducers/timing-markers");
-const { UI, Columns } = require("./reducers/ui");
+const { UI, Columns, ColumnsData } = require("./reducers/ui");
 
 /**
  * Configure state and middleware for the Network monitor tool.
  */
 function configureStore(connector, telemetry) {
   // Prepare initial state.
   const initialState = {
     filters: new Filters({
       requestFilterTypes: getFilterState(),
     }),
     requests: new Requests(),
     sort: new Sort(),
     timingMarkers: new TimingMarkers(),
     ui: UI({
       columns: getColumnState(),
+      columnsData: getColumnsData(),
     }),
   };
 
   // Prepare middleware.
   const middleware = applyMiddleware(
     thunk,
     prefs,
     batching,
@@ -66,16 +72,37 @@ function getColumnState() {
   for (const col in columns) {
     state[col] = visibleColumns.includes(col);
   }
 
   return state;
 }
 
 /**
+ * Get columns data (width, min-width)
+ */
+function getColumnsData() {
+  const columnsData = getPref("devtools.netmonitor.columnsData");
+  if (!columnsData.length) {
+    return ColumnsData();
+  }
+
+  const newMap = new Map();
+  columnsData.forEach(col => {
+    if (col.name) {
+      col.minWidth = col.minWidth ? col.minWidth : MIN_COLUMN_WIDTH;
+      col.width = col.width ? col.width : DEFAULT_COLUMN_WIDTH;
+      newMap.set(col.name, col);
+    }
+  });
+
+  return newMap;
+}
+
+/**
  * Get filter state from preferences.
  */
 function getFilterState() {
   const activeFilters = {};
   const filters = getPref("devtools.netmonitor.filters");
   filters.forEach((filter) => {
     activeFilters[filter] = true;
   });
--- a/devtools/client/netmonitor/src/middleware/prefs.js
+++ b/devtools/client/netmonitor/src/middleware/prefs.js
@@ -7,16 +7,17 @@
 const Services = require("Services");
 const {
   ENABLE_REQUEST_FILTER_TYPE_ONLY,
   RESET_COLUMNS,
   TOGGLE_COLUMN,
   TOGGLE_REQUEST_FILTER_TYPE,
   ENABLE_PERSISTENT_LOGS,
   DISABLE_BROWSER_CACHE,
+  SET_COLUMNS_WIDTH,
 } = require("../constants");
 
 /**
   * Update the relevant prefs when:
   *   - a column has been toggled
   *   - a filter type has been set
   */
 function prefsMiddleware(store) {
@@ -35,25 +36,50 @@ function prefsMiddleware(store) {
         Services.prefs.setBoolPref(
           "devtools.netmonitor.persistlog", store.getState().ui.persistentLogsEnabled);
         break;
       case DISABLE_BROWSER_CACHE:
         Services.prefs.setBoolPref(
           "devtools.cache.disabled", store.getState().ui.browserCacheDisabled);
         break;
       case TOGGLE_COLUMN:
+        persistVisibleColumns(store.getState());
+        break;
       case RESET_COLUMNS:
-        const visibleColumns = [];
-        const columns = store.getState().ui.columns;
-        for (const column in columns) {
-          if (columns[column]) {
-            visibleColumns.push(column);
-          }
-        }
-        Services.prefs.setCharPref(
-          "devtools.netmonitor.visibleColumns", JSON.stringify(visibleColumns));
+        persistVisibleColumns(store.getState());
+        persistColumnsData(store.getState());
+        break;
+      case SET_COLUMNS_WIDTH:
+        persistColumnsData(store.getState());
         break;
     }
     return res;
   };
 }
 
+/**
+ * Store list of visible columns into preferences.
+ */
+function persistVisibleColumns(state) {
+  const visibleColumns = [];
+  const columns = state.ui.columns;
+  for (const column in columns) {
+    if (columns[column]) {
+      visibleColumns.push(column);
+    }
+  }
+
+  Services.prefs.setCharPref(
+    "devtools.netmonitor.visibleColumns",
+    JSON.stringify(visibleColumns));
+}
+
+/**
+ * Store columns data (width, min-width, etc.) into preferences.
+ */
+function persistColumnsData(state) {
+  const columnsData = [...state.ui.columnsData.values()];
+  Services.prefs.setCharPref(
+    "devtools.netmonitor.columnsData",
+    JSON.stringify(columnsData));
+}
+
 module.exports = prefsMiddleware;
--- a/devtools/client/netmonitor/src/reducers/ui.js
+++ b/devtools/client/netmonitor/src/reducers/ui.js
@@ -16,16 +16,18 @@ const {
   RESET_COLUMNS,
   RESPONSE_HEADERS,
   SELECT_DETAILS_PANEL_TAB,
   SEND_CUSTOM_REQUEST,
   SELECT_REQUEST,
   TOGGLE_COLUMN,
   WATERFALL_RESIZE,
   PANELS,
+  MIN_COLUMN_WIDTH,
+  SET_COLUMNS_WIDTH,
 } = require("../constants");
 
 const cols = {
   status: true,
   method: true,
   domain: true,
   file: true,
   protocol: false,
@@ -39,42 +41,52 @@ const cols = {
   contentSize: true,
   startTime: false,
   endTime: false,
   responseTime: false,
   duration: false,
   latency: false,
   waterfall: true,
 };
+
 function Columns() {
   return Object.assign(
     cols,
     RESPONSE_HEADERS.reduce((acc, header) => Object.assign(acc, { [header]: false }), {})
   );
 }
 
+function ColumnsData() {
+  const defaultColumnsData = JSON.parse(
+    Services.prefs.getDefaultBranch(null).getCharPref("devtools.netmonitor.columnsData")
+  );
+  return new Map(defaultColumnsData.map(i => [i.name, i]));
+}
+
 function UI(initialState = {}) {
   return {
     columns: Columns(),
+    columnsData: ColumnsData(),
     detailsPanelSelectedTab: PANELS.HEADERS,
     networkDetailsOpen: false,
     networkDetailsWidth: null,
     networkDetailsHeight: null,
     persistentLogsEnabled: Services.prefs.getBoolPref("devtools.netmonitor.persistlog"),
     browserCacheDisabled: Services.prefs.getBoolPref("devtools.cache.disabled"),
     statisticsOpen: false,
     waterfallWidth: null,
     ...initialState,
   };
 }
 
 function resetColumns(state) {
   return {
     ...state,
     columns: Columns(),
+    columnsData: ColumnsData(),
   };
 }
 
 function resizeWaterfall(state, action) {
   return {
     ...state,
     waterfallWidth: action.width,
   };
@@ -134,16 +146,40 @@ function toggleColumn(state, action) {
     ...state,
     columns: {
       ...state.columns,
       [column]: !state.columns[column],
     },
   };
 }
 
+function setColumnsWidth(state, action) {
+  const { widths } = action;
+  const columnsData = new Map(state.columnsData);
+
+  widths.forEach(col => {
+    let data = columnsData.get(col.name);
+    if (!data) {
+      data = {
+        name: col.name,
+        minWidth: MIN_COLUMN_WIDTH,
+      };
+    }
+    columnsData.set(col.name, {
+      ...data,
+      width: col.width,
+    });
+  });
+
+  return {
+    ...state,
+    columnsData: columnsData,
+  };
+}
+
 function ui(state = UI(), action) {
   switch (action.type) {
     case CLEAR_REQUESTS:
       return openNetworkDetails(state, { open: false });
     case OPEN_NETWORK_DETAILS:
       return openNetworkDetails(state, action);
     case RESIZE_NETWORK_DETAILS:
       return resizeNetworkDetails(state, action);
@@ -162,18 +198,21 @@ function ui(state = UI(), action) {
     case SELECT_DETAILS_PANEL_TAB:
       return setDetailsPanelTab(state, action);
     case SELECT_REQUEST:
       return openNetworkDetails(state, { open: true });
     case TOGGLE_COLUMN:
       return toggleColumn(state, action);
     case WATERFALL_RESIZE:
       return resizeWaterfall(state, action);
+    case SET_COLUMNS_WIDTH:
+      return setColumnsWidth(state, action);
     default:
       return state;
   }
 }
 
 module.exports = {
   Columns,
+  ColumnsData,
   UI,
   ui,
 };
--- a/devtools/client/netmonitor/src/utils/prefs.js
+++ b/devtools/client/netmonitor/src/utils/prefs.js
@@ -8,10 +8,11 @@ const { PrefsHelper } = require("devtool
 
 /**
  * Shortcuts for accessing various network monitor preferences.
  */
 exports.Prefs = new PrefsHelper("devtools.netmonitor", {
   networkDetailsWidth: ["Int", "panes-network-details-width"],
   networkDetailsHeight: ["Int", "panes-network-details-height"],
   visibleColumns: ["Json", "visibleColumns"],
+  columnsData: ["Json", "columnsData"],
   filters: ["Json", "filters"],
 });
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -130,16 +130,17 @@ skip-if = (os == 'mac') || (os == 'win' 
 [browser_net_filter-04.js]
 [browser_net_filter-autocomplete.js]
 [browser_net_filter-flags.js]
 [browser_net_footer-summary.js]
 [browser_net_header-ref-policy.js]
 [browser_net_headers-alignment.js]
 [browser_net_headers_filter.js]
 [browser_net_headers_sorted.js]
+[browser_net_headers-resize.js]
 [browser_net_image-tooltip.js]
 [browser_net_json-b64.js]
 [browser_net_json-empty.js]
 [browser_net_json-null.js]
 [browser_net_json-long.js]
 [browser_net_json-malformed.js]
 [browser_net_json-nogrip.js]
 [browser_net_json_custom_mime.js]
--- a/devtools/client/netmonitor/test/browser_net_columns_last_column.js
+++ b/devtools/client/netmonitor/test/browser_net_columns_last_column.js
@@ -1,22 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
- * Tests that last visible column can't be hidden
+ * Tests that last visible column can't be hidden. Note that the column
+ * header is visible only if there are requests in the list.
  */
 
 add_task(async function() {
-  const { monitor } = await initNetMonitor(SIMPLE_URL);
+  const { monitor, tab } = await initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  const { document, store, parent } = monitor.panelWin;
+  const { document, store, parent, windowRequire } = monitor.panelWin;
+  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  store.dispatch(Actions.batchEnable(false));
+
+  const wait = waitForNetworkEvents(monitor, 1);
+  tab.linkedBrowser.reload();
+  await wait;
 
   const initialColumns = store.getState().ui.columns;
   for (const column in initialColumns) {
     const shown = initialColumns[column];
 
     const columns = store.getState().ui.columns;
     const visibleColumns = [];
     for (const c in columns) {
--- a/devtools/client/netmonitor/test/browser_net_columns_pref.js
+++ b/devtools/client/netmonitor/test/browser_net_columns_pref.js
@@ -1,25 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
- * Tests if visible columns are properly saved
+ * Tests if visible columns are properly saved. Note that the column
+ * header is visible only if there are requests in the list.
  */
 
 add_task(async function() {
   Services.prefs.setCharPref("devtools.netmonitor.visibleColumns",
     '["status", "contentSize", "waterfall"]');
 
-  const { monitor } = await initNetMonitor(SIMPLE_URL);
+  const { monitor, tab } = await initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  const { document } = monitor.panelWin;
+  const { document, store, windowRequire } = monitor.panelWin;
+  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  store.dispatch(Actions.batchEnable(false));
+
+  const wait = waitForNetworkEvents(monitor, 1);
+  tab.linkedBrowser.reload();
+  await wait;
 
   ok(document.querySelector("#requests-list-status-button"),
      "Status column should be shown");
   ok(document.querySelector("#requests-list-contentSize-button"),
      "Content size column should be shown");
 
   await hideColumn(monitor, "status");
   await hideColumn(monitor, "contentSize");
--- a/devtools/client/netmonitor/test/browser_net_columns_reset.js
+++ b/devtools/client/netmonitor/test/browser_net_columns_reset.js
@@ -1,24 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
- * Tests reset column menu item
+ * Tests reset column menu item. Note that the column
+ * header is visible only if there are requests in the list.
  */
 add_task(async function() {
-  const { monitor } = await initNetMonitor(SIMPLE_URL);
+  const { monitor, tab } = await initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
-  const { document, parent, windowRequire } = monitor.panelWin;
+  const { document, store, parent, windowRequire } = monitor.panelWin;
   const { Prefs } = windowRequire("devtools/client/netmonitor/src/utils/prefs");
 
   const prefBefore = Prefs.visibleColumns;
+  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  store.dispatch(Actions.batchEnable(false));
+
+  const wait = waitForNetworkEvents(monitor, 1);
+  tab.linkedBrowser.reload();
+  await wait;
 
   await hideColumn(monitor, "status");
   await hideColumn(monitor, "waterfall");
 
   const onRequestsFinished = waitForRequestsFinished(monitor);
   EventUtils.sendMouseEvent({ type: "contextmenu" },
     document.querySelector("#requests-list-contentSize-button"));
 
--- a/devtools/client/netmonitor/test/browser_net_columns_time.js
+++ b/devtools/client/netmonitor/test/browser_net_columns_time.js
@@ -1,26 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
- * Tests for timings columns.
+ * Tests for timings columns. Note that the column
+ * header is visible only if there are requests in the list.
  */
 add_task(async function() {
   const { tab, monitor } = await initNetMonitor(SIMPLE_URL);
   info("Starting test... ");
 
   const { document, store, windowRequire } = monitor.panelWin;
   const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   store.dispatch(Actions.batchEnable(false));
 
   const visibleColumns = store.getState().ui.columns;
 
+  const wait = waitForNetworkEvents(monitor, 1);
+  tab.linkedBrowser.reload();
+  await wait;
+
   // Hide the waterfall column to make sure timing data are fetched
   // by the other timing columns ("endTime", "responseTime", "duration",
   // "latency").
   // Note that all these timing columns are based on the same
   // `RequestListColumnTime` component.
   if (visibleColumns.waterfall) {
     await hideColumn(monitor, "waterfall");
   }
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_headers-resize.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests resizing of columns in NetMonitor.
+ */
+add_task(async function() {
+  // Reset visibleColumns so we only get the default ones
+  // and not all that are set in head.js
+  Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns");
+  let visibleColumns = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.visibleColumns")
+  );
+
+  // Init network monitor
+  const { tab, monitor } = await initNetMonitor(SIMPLE_URL);
+  info("Starting test... ");
+
+  const { document, windowRequire, store } = monitor.panelWin;
+  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+  store.dispatch(Actions.batchEnable(false));
+
+  // Wait for network events (to have some requests in the table)
+  const wait = waitForNetworkEvents(monitor, 1);
+  tab.linkedBrowser.reload();
+  await wait;
+
+  const headers = document.querySelector(".requests-list-headers");
+  const parentWidth = headers.getBoundingClientRect().width;
+
+  // 1. Change File column from 25% (default) to 20%
+  // Size column should then change from 5% (default) to 10%
+  // When File width changes, contentSize should compensate the change.
+  info("Resize file & check changed prefs...");
+  const fileHeader = document.querySelector(`#requests-list-file-header-box`);
+
+  resizeColumn(fileHeader, 20, parentWidth);
+
+  // after resize - get fresh prefs for tests
+  let columnsData = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.columnsData")
+  );
+  checkColumnsData(columnsData, "file", 20);
+  checkColumnsData(columnsData, "contentSize", 10);
+  checkSumOfVisibleColumns(columnsData, visibleColumns);
+
+  // 2. Change Waterfall column width and check that the size
+  // of waterfall changed correctly and all the other columns changed size.
+  info("Resize waterfall & check changed prefs...");
+  const waterfallHeader = document.querySelector(`#requests-list-waterfall-header-box`);
+  // before resizing waterfall -> save old columnsData for later testing
+  const oldColumnsData = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.columnsData")
+  );
+  resizeWaterfallColumn(waterfallHeader, 30, parentWidth); // 30 fails currently!
+
+  // after resize - get fresh prefs for tests
+  columnsData = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.columnsData")
+  );
+
+  checkColumnsData(columnsData, "waterfall", 30);
+  checkSumOfVisibleColumns(columnsData, visibleColumns);
+  checkAllColumnsChanged(columnsData, oldColumnsData, visibleColumns);
+
+  // 3. Check that all rows have the right column sizes.
+  info("Checking alignment of columns and headers...");
+  const requestsContainer = document.querySelector(".requests-list-row-group");
+  testColumnsAlignment(headers, requestsContainer);
+
+  // 4. Hide all columns but size and waterfall
+  // and check that they resize correctly. Then resize
+  // waterfall to 50% => size should take up 50%
+  info("Hide all but 2 columns - size & waterfall and check resizing...");
+  await hideMoreColumns(monitor,
+    ["status", "method", "domain", "file", "cause", "type", "transferred"]);
+
+  resizeWaterfallColumn(waterfallHeader, 50, parentWidth);
+  // after resize - get fresh prefs for tests
+  columnsData = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.columnsData")
+  );
+  visibleColumns = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.visibleColumns")
+  );
+
+  checkColumnsData(columnsData, "contentSize", 50);
+  checkColumnsData(columnsData, "waterfall", 50);
+  checkSumOfVisibleColumns(columnsData, visibleColumns);
+
+  // 5. Hide all columns but domain and file
+  // and resize domain to 50% => file should be 50%
+  info("Hide all but 2 columns - domain & file and check resizing...");
+  await showMoreColumns(monitor, ["domain", "file"]);
+  await hideMoreColumns(monitor, ["contentSize", "waterfall"]);
+
+  const domainHeader = document.querySelector(`#requests-list-domain-header-box`);
+  resizeColumn(domainHeader, 50, parentWidth);
+
+  // after resize - get fresh prefs for tests
+  columnsData = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.columnsData")
+  );
+
+  visibleColumns = JSON.parse(
+    Services.prefs.getCharPref("devtools.netmonitor.visibleColumns")
+  );
+
+  checkColumnsData(columnsData, "domain", 50);
+  checkColumnsData(columnsData, "file", 50);
+  checkSumOfVisibleColumns(columnsData, visibleColumns);
+
+  // Done: clean up.
+  return teardown(monitor);
+});
+
+async function hideMoreColumns(monitor, arr) {
+  for (let i = 0; i < arr.length; i++) {
+    await hideColumn(monitor, arr[i]);
+  }
+}
+
+async function showMoreColumns(monitor, arr) {
+  for (let i = 0; i < arr.length; i++) {
+    await showColumn(monitor, arr[i]);
+  }
+}
+
+function resizeColumn(columnHeader, newPercent, parentWidth) {
+  const newWidthInPixels = newPercent * parentWidth / 100;
+  const win = columnHeader.ownerDocument.defaultView;
+  const mouseDown = columnHeader.getBoundingClientRect().width;
+  const mouseMove = newWidthInPixels;
+
+  EventUtils.synthesizeMouse(columnHeader, mouseDown, 1, { type: "mousedown" }, win);
+  EventUtils.synthesizeMouse(columnHeader, mouseMove, 1, { type: "mousemove" }, win);
+  EventUtils.synthesizeMouse(columnHeader, mouseMove, 1, { type: "mouseup" }, win);
+}
+
+function resizeWaterfallColumn(columnHeader, newPercent, parentWidth) {
+  const newWidthInPixels = newPercent * parentWidth / 100;
+  const win = columnHeader.ownerDocument.defaultView;
+  const mouseDown = columnHeader.getBoundingClientRect().left;
+  const mouseMove =
+    mouseDown + (columnHeader.getBoundingClientRect().width - newWidthInPixels);
+
+  EventUtils.synthesizeMouse(
+    columnHeader.parentElement, mouseDown, 1, { type: "mousedown" }, win);
+  EventUtils.synthesizeMouse(
+    columnHeader.parentElement, mouseMove, 1, { type: "mousemove" }, win);
+  EventUtils.synthesizeMouse(
+    columnHeader.parentElement, mouseMove, 1, { type: "mouseup" }, win);
+}
+
+function checkColumnsData(columnsData, column, expectedWidth) {
+  const widthInPref = Math.round(getWidthFromPref(columnsData, column));
+  is(widthInPref, expectedWidth, "Column " + column + " has expected size.");
+}
+
+function checkSumOfVisibleColumns(columnsData, visibleColumns) {
+  let sum = 0;
+  visibleColumns.forEach(column => {
+    sum += getWidthFromPref(columnsData, column);
+  });
+  sum = Math.round(sum);
+  is(sum, 100, "All visible columns cover 100%.");
+}
+
+function getWidthFromPref(columnsData, column) {
+  const widthInPref = columnsData.find(function(element) {
+    return element.name === column;
+  }).width;
+  return widthInPref;
+}
+
+function checkAllColumnsChanged(columnsData, oldColumnsData, visibleColumns) {
+  const oldWaterfallWidth = getWidthFromPref(oldColumnsData, "waterfall");
+  const newWaterfallWidth = getWidthFromPref(columnsData, "waterfall");
+  visibleColumns.forEach(column => {
+    // do not test waterfall against waterfall
+    if (column !== "waterfall") {
+      const oldWidth = getWidthFromPref(oldColumnsData, column);
+      const newWidth = getWidthFromPref(columnsData, column);
+
+      // Test that if waterfall is smaller all other columns are bigger
+      if (oldWaterfallWidth > newWaterfallWidth) {
+        is(oldWidth < newWidth, true,
+          "Column " + column + " has changed width correctly.");
+      }
+      // Test that if waterfall is bigger all other columns are smaller
+      if (oldWaterfallWidth < newWaterfallWidth) {
+        is(oldWidth > newWidth, true,
+          "Column " + column + " has changed width correctly.");
+      }
+    }
+  });
+}
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -113,26 +113,44 @@ const gDefaultFilters = Services.prefs.g
 Services.prefs.setCharPref(
   "devtools.netmonitor.visibleColumns",
   "[\"cause\",\"contentSize\",\"cookies\",\"domain\",\"duration\"," +
   "\"endTime\",\"file\",\"latency\",\"method\",\"protocol\"," +
   "\"remoteip\",\"responseTime\",\"scheme\",\"setCookies\"," +
   "\"startTime\",\"status\",\"transferred\",\"type\",\"waterfall\"]"
 );
 
+Services.prefs.setCharPref("devtools.netmonitor.columnsData",
+'[{"name":"status","minWidth":30,"width":5},' +
+  '{"name":"method","minWidth":30,"width":5},' +
+  '{"name":"domain","minWidth":30,"width":10},' +
+  '{"name":"file","minWidth":30,"width":25},' +
+  '{"name":"cause","minWidth":30,"width":10},' +
+  '{"name":"type","minWidth":30,"width":5},' +
+  '{"name":"transferred","minWidth":30,"width":10},' +
+  '{"name":"contentSize","minWidth":30,"width":5},' +
+  '{"name":"waterfall","minWidth":150,"width":25}]');
+
 // Increase UI limit for responses rendered using CodeMirror in tests.
 Services.prefs.setIntPref("devtools.netmonitor.response.ui.limit", 1024 * 105);
 
+// Support for columns resizing is currently hidden behind this pref,
+// but testing is on
+Services.prefs.setBoolPref("devtools.netmonitor.features.resizeColumns", true);
+
 registerCleanupFunction(() => {
   info("finish() was called, cleaning up...");
 
   Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
   Services.prefs.setCharPref("devtools.netmonitor.filters", gDefaultFilters);
   Services.prefs.clearUserPref("devtools.cache.disabled");
+  Services.prefs.clearUserPref("devtools.netmonitor.columnsData");
   Services.prefs.clearUserPref("devtools.netmonitor.response.ui.limit");
+  Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns");
+  Services.prefs.clearUserPref("devtools.netmonitor.features.resizeColumns");
   Services.cookies.removeAll();
 });
 
 function waitForNavigation(target) {
   return new Promise((resolve) => {
     target.once("will-navigate", () => {
       target.once("navigate", () => {
         resolve();
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -170,16 +170,22 @@ pref("devtools.application.enabled", fal
 
 // The default Network Monitor UI settings
 pref("devtools.netmonitor.panes-network-details-width", 550);
 pref("devtools.netmonitor.panes-network-details-height", 450);
 pref("devtools.netmonitor.filters", "[\"all\"]");
 pref("devtools.netmonitor.visibleColumns",
   "[\"status\",\"method\",\"domain\",\"file\",\"cause\",\"type\",\"transferred\",\"contentSize\",\"waterfall\"]"
 );
+pref("devtools.netmonitor.columnsData",
+  '[{"name":"status","minWidth":30,"width":5}, {"name":"method","minWidth":30,"width":5}, {"name":"domain","minWidth":30,"width":10}, {"name":"file","minWidth":30,"width":25}, {"name":"cause","minWidth":30,"width":10},{"name":"type","minWidth":30,"width":5},{"name":"transferred","minWidth":30,"width":10},{"name":"contentSize","minWidth":30,"width":5},{"name":"waterfall","minWidth":150,"width":25}]');
+
+// Support for columns resizing is currently hidden behind this pref.
+pref("devtools.netmonitor.features.resizeColumns", false);
+
 pref("devtools.netmonitor.response.ui.limit", 10240);
 
 // Save request/response bodies yes/no.
 pref("devtools.netmonitor.saveRequestAndResponseBodies", true);
 
 // The default Network monitor HAR export setting
 pref("devtools.netmonitor.har.defaultLogDir", "");
 pref("devtools.netmonitor.har.defaultFileName", "Archive %date");