Bug 862855 - Add the ability to show/hide more columns in the network panel. r=Honza draft
authorTim Nguyen <ntim.bugs@gmail.com>
Tue, 04 Apr 2017 16:38:44 +0200
changeset 555595 eaaf6e068d94a4dc02451880c50380264d65aa3e
parent 555130 3ad95b0ebffc945db6f51f7e491a2d82b2d13536
child 556062 755052c137434b10b5bc5bd6017e6399d49d6921
push id52272
push userbmo:ntim.bugs@gmail.com
push dateTue, 04 Apr 2017 14:39:10 +0000
reviewersHonza
bugs862855
milestone55.0a1
Bug 862855 - Add the ability to show/hide more columns in the network panel. r=Honza MozReview-Commit-ID: EwFR65651Fs
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/actions/ui.js
devtools/client/netmonitor/src/components/request-list-content.js
devtools/client/netmonitor/src/components/request-list-header.js
devtools/client/netmonitor/src/components/request-list-item.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/middleware/prefs.js
devtools/client/netmonitor/src/moz.build
devtools/client/netmonitor/src/reducers/ui.js
devtools/client/netmonitor/src/request-list-header-context-menu.js
devtools/client/netmonitor/src/utils/create-store.js
devtools/client/netmonitor/src/utils/prefs.js
devtools/client/netmonitor/src/utils/sort-predicates.js
devtools/client/netmonitor/test/browser_net_filter-03.js
devtools/client/netmonitor/test/browser_net_icon-preview.js
devtools/client/netmonitor/test/browser_net_sort-02.js
devtools/client/preferences/devtools.js
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -419,20 +419,20 @@ netmonitor.toolbar.cause=Cause
 # in the network table toolbar, above the "type" column.
 netmonitor.toolbar.type=Type
 
 # LOCALIZATION NOTE (netmonitor.toolbar.transferred): This is the label displayed
 # in the network table toolbar, above the "transferred" column, which is the
 # compressed / encoded size.
 netmonitor.toolbar.transferred=Transferred
 
-# LOCALIZATION NOTE (netmonitor.toolbar.size): This is the label displayed
+# LOCALIZATION NOTE (netmonitor.toolbar.contentSize): This is the label displayed
 # in the network table toolbar, above the "size" column, which is the
 # uncompressed / decoded size.
-netmonitor.toolbar.size=Size
+netmonitor.toolbar.contentSize=Size
 
 # LOCALIZATION NOTE (netmonitor.toolbar.waterfall): This is the label displayed
 # in the network table toolbar, above the "waterfall" column.
 netmonitor.toolbar.waterfall=Timeline
 
 # LOCALIZATION NOTE (netmonitor.tab.headers): This is the label displayed
 # in the network details pane identifying the headers tab.
 netmonitor.tab.headers=Headers
@@ -520,16 +520,20 @@ netmonitor.toolbar.filterFreetext.key=Cm
 # LOCALIZATION NOTE (netmonitor.toolbar.clear): This is the label displayed
 # in the network toolbar for the "Clear" button.
 netmonitor.toolbar.clear=Clear
 
 # LOCALIZATION NOTE (netmonitor.toolbar.perf): This is the label displayed
 # in the network toolbar for the performance analysis button.
 netmonitor.toolbar.perf=Toggle performance analysis…
 
+# LOCALIZATION NOTE (netmonitor.toolbar.resetColumns): This is the label
+# displayed in the network table header context menu.
+netmonitor.toolbar.resetColumns=Reset Columns
+
 # LOCALIZATION NOTE (netmonitor.summary.url): This is the label displayed
 # in the network details headers tab identifying the URL.
 netmonitor.summary.url=Request URL:
 
 # LOCALIZATION NOTE (netmonitor.summary.method): This is the label displayed
 # in the network details headers tab identifying the method.
 netmonitor.summary.method=Request method:
 
--- a/devtools/client/netmonitor/src/actions/ui.js
+++ b/devtools/client/netmonitor/src/actions/ui.js
@@ -3,17 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   ACTIVITY_TYPE,
   OPEN_NETWORK_DETAILS,
   OPEN_STATISTICS,
+  RESET_COLUMNS,
   SELECT_DETAILS_PANEL_TAB,
+  TOGGLE_COLUMN,
   WATERFALL_RESIZE,
 } = require("../constants");
 
 /**
  * Change network details panel.
  *
  * @param {boolean} open - expected network details panel open state
  */
@@ -35,16 +37,26 @@ function openStatistics(open) {
   }
   return {
     type: OPEN_STATISTICS,
     open,
   };
 }
 
 /**
+ * Resets all columns to their default state.
+ *
+ */
+function resetColumns() {
+  return {
+    type: RESET_COLUMNS,
+  };
+}
+
+/**
  * Waterfall width has changed (likely on window resize). Update the UI.
  */
 function resizeWaterfall(width) {
   return {
     type: WATERFALL_RESIZE,
     width
   };
 }
@@ -57,16 +69,28 @@ function resizeWaterfall(width) {
 function selectDetailsPanelTab(id) {
   return {
     type: SELECT_DETAILS_PANEL_TAB,
     id,
   };
 }
 
 /**
+ * Toggles a column
+ *
+ * @param {string} column - The column that is going to be toggled
+ */
+function toggleColumn(column) {
+  return {
+    type: TOGGLE_COLUMN,
+    column,
+  };
+}
+
+/**
  * Toggle network details panel.
  */
 function toggleNetworkDetails() {
   return (dispatch, getState) =>
     dispatch(openNetworkDetails(!getState().ui.networkDetailsOpen));
 }
 
 /**
@@ -75,13 +99,15 @@ function toggleNetworkDetails() {
 function toggleStatistics() {
   return (dispatch, getState) =>
     dispatch(openStatistics(!getState().ui.statisticsOpen));
 }
 
 module.exports = {
   openNetworkDetails,
   openStatistics,
+  resetColumns,
   resizeWaterfall,
   selectDetailsPanelTab,
+  toggleColumn,
   toggleNetworkDetails,
   toggleStatistics,
 };
--- a/devtools/client/netmonitor/src/components/request-list-content.js
+++ b/devtools/client/netmonitor/src/components/request-list-content.js
@@ -30,16 +30,17 @@ const REQUESTS_TOOLTIP_TOGGLE_DELAY = 50
 
 /**
  * Renders the actual contents of the request list.
  */
 const RequestListContent = createClass({
   displayName: "RequestListContent",
 
   propTypes: {
+    columns: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     displayedRequests: PropTypes.object.isRequired,
     firstRequestStartedMillis: PropTypes.number.isRequired,
     fromCache: PropTypes.bool.isRequired,
     onCauseBadgeClick: PropTypes.func.isRequired,
     onItemMouseDown: PropTypes.func.isRequired,
     onSecurityIconClick: PropTypes.func.isRequired,
     onSelectDelta: PropTypes.func.isRequired,
@@ -215,16 +216,17 @@ const RequestListContent = createClass({
    * scrolled to bottom, but allow scrolling up with the selection.
    */
   onFocusedNodeChange() {
     this.shouldScrollBottom = false;
   },
 
   render() {
     const {
+      columns,
       displayedRequests,
       firstRequestStartedMillis,
       selectedRequestId,
       onCauseBadgeClick,
       onItemMouseDown,
       onSecurityIconClick,
     } = this.props;
 
@@ -233,16 +235,17 @@ const RequestListContent = createClass({
         ref: "contentEl",
         className: "requests-list-contents",
         tabIndex: 0,
         onKeyDown: this.onKeyDown,
       },
         displayedRequests.map((item, index) => RequestListItem({
           firstRequestStartedMillis,
           fromCache: item.status === "304" || item.fromCache,
+          columns,
           item,
           index,
           isSelected: item.id === selectedRequestId,
           key: item.id,
           onContextMenu: this.onContextMenu,
           onFocusedNodeChange: this.onFocusedNodeChange,
           onMouseDown: () => onItemMouseDown(item.id),
           onCauseBadgeClick: () => onCauseBadgeClick(item.cause),
@@ -250,16 +253,17 @@ const RequestListContent = createClass({
         }))
       )
     );
   },
 });
 
 module.exports = connect(
   (state) => ({
+    columns: state.ui.columns,
     displayedRequests: getDisplayedRequests(state),
     firstRequestStartedMillis: state.requests.firstStartedMillis,
     selectedRequestId: state.requests.selectedId,
     scale: getWaterfallScale(state),
   }),
   (dispatch) => ({
     dispatch,
     onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
--- a/devtools/client/netmonitor/src/components/request-list-header.js
+++ b/devtools/client/netmonitor/src/components/request-list-header.js
@@ -10,50 +10,61 @@ const {
   DOM,
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const Actions = require("../actions/index");
 const { getWaterfallScale } = require("../selectors/index");
 const { getFormattedTime } = require("../utils/format-utils");
 const { L10N } = require("../utils/l10n");
 const WaterfallBackground = require("../waterfall-background");
+const RequestListHeaderContextMenu = require("../request-list-header-context-menu");
 
 const { div, button } = DOM;
 
 const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
 
 const HEADERS = [
   { name: "status", label: "status3" },
   { name: "method" },
   { name: "file", boxName: "icon-and-file" },
   { name: "domain", boxName: "security-and-domain" },
   { name: "cause" },
   { name: "type" },
   { name: "transferred" },
-  { name: "size" },
+  { name: "contentSize", boxName: "size" },
   { name: "waterfall" }
 ];
 
 /**
  * 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.
  */
 const RequestListHeader = createClass({
   displayName: "RequestListHeader",
 
   propTypes: {
+    columns: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
     sort: PropTypes.object,
     scale: PropTypes.number,
     waterfallWidth: PropTypes.number,
     onHeaderClick: PropTypes.func.isRequired,
     resizeWaterfall: PropTypes.func.isRequired,
   },
 
+  componentWillMount() {
+    const { dispatch } = this.props;
+    this.contextMenu = new RequestListHeaderContextMenu({
+      toggleColumn: (column) => dispatch(Actions.toggleColumn(column)),
+      resetColumns: () => dispatch(Actions.resetColumns()),
+    });
+  },
+
   componentDidMount() {
     // Create the object that takes care of drawing the waterfall canvas background
     this.background = new WaterfallBackground(document);
     this.background.draw(this.props);
     this.resizeWaterfall();
     window.addEventListener("resize", this.resizeWaterfall);
   },
 
@@ -62,32 +73,37 @@ const RequestListHeader = createClass({
   },
 
   componentWillUnmount() {
     this.background.destroy();
     this.background = null;
     window.removeEventListener("resize", this.resizeWaterfall);
   },
 
+  onContextMenu(evt) {
+    evt.preventDefault();
+    this.contextMenu.open(evt);
+  },
+
   resizeWaterfall() {
     // Measure its width and update the 'waterfallWidth' property in the store.
     // The 'waterfallWidth' will be further updated on every window resize.
     setTimeout(() => {
       let { width } = this.refs.header.getBoundingClientRect();
       this.props.resizeWaterfall(width);
     }, 50);
   },
 
   render() {
-    const { sort, scale, waterfallWidth, onHeaderClick } = this.props;
+    const { sort, scale, waterfallWidth, onHeaderClick, columns } = this.props;
 
     return div(
       { className: "devtools-toolbar requests-list-toolbar" },
       div({ className: "toolbar-labels" },
-        HEADERS.map(header => {
+        HEADERS.filter(h => columns.get(h.name)).map(header => {
           const name = header.name;
           const boxName = header.boxName || name;
           const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
 
           let sorted, sortedTitle;
           const active = sort.type == name ? true : undefined;
           if (active) {
             sorted = sort.ascending ? "ascending" : "descending";
@@ -99,16 +115,17 @@ const RequestListHeader = createClass({
           return div(
             {
               id: `requests-list-${boxName}-header-box`,
               className: `requests-list-header requests-list-${boxName}`,
               key: name,
               ref: "header",
               // Used to style the next column.
               "data-active": active,
+              onContextMenu: this.onContextMenu,
             },
             button(
               {
                 id: `requests-list-${name}-button`,
                 className: `requests-list-header-button requests-list-${name}`,
                 "data-sorted": sorted,
                 title: sortedTitle,
                 onClick: () => onHeaderClick(name),
@@ -184,19 +201,21 @@ function WaterfallLabel(waterfallWidth, 
     className += " requests-list-waterfall-visible";
   }
 
   return div({ className }, label);
 }
 
 module.exports = connect(
   state => ({
+    columns: state.ui.columns,
     sort: state.sort,
     scale: getWaterfallScale(state),
     waterfallWidth: state.ui.waterfallWidth,
     firstRequestStartedMillis: state.requests.firstStartedMillis,
     timingMarkers: state.timingMarkers,
   }),
   dispatch => ({
+    dispatch,
     onHeaderClick: type => dispatch(Actions.sortBy(type)),
     resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)),
   })
 )(RequestListHeader);
--- a/devtools/client/netmonitor/src/components/request-list-item.js
+++ b/devtools/client/netmonitor/src/components/request-list-item.js
@@ -5,16 +5,18 @@
 "use strict";
 
 const {
   createClass,
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
+const I = require("devtools/client/shared/vendor/immutable");
+
 const { getFormattedSize } = require("../utils/format-utils");
 const { L10N } = require("../utils/l10n");
 const { getAbbreviatedMimeType } = require("../utils/request-utils");
 
 const { div, img, span } = DOM;
 
 /**
  * Compare two objects on a subset of their properties
@@ -56,16 +58,17 @@ const UPDATED_REQ_PROPS = [
 
 /**
  * Render one row in the request list.
  */
 const RequestListItem = createClass({
   displayName: "RequestListItem",
 
   propTypes: {
+    columns: PropTypes.object.isRequired,
     item: PropTypes.object.isRequired,
     index: PropTypes.number.isRequired,
     isSelected: PropTypes.bool.isRequired,
     firstRequestStartedMillis: PropTypes.number.isRequired,
     fromCache: PropTypes.bool.isRequired,
     onCauseBadgeClick: PropTypes.func.isRequired,
     onContextMenu: PropTypes.func.isRequired,
     onFocusedNodeChange: PropTypes.func,
@@ -76,30 +79,32 @@ const RequestListItem = createClass({
   componentDidMount() {
     if (this.props.isSelected) {
       this.refs.el.focus();
     }
   },
 
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
-      !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
+      !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps) ||
+      !I.is(this.props.columns, nextProps.columns);
   },
 
   componentDidUpdate(prevProps) {
     if (!prevProps.isSelected && this.props.isSelected) {
       this.refs.el.focus();
       if (this.props.onFocusedNodeChange) {
         this.props.onFocusedNodeChange();
       }
     }
   },
 
   render() {
     const {
+      columns,
       item,
       index,
       isSelected,
       firstRequestStartedMillis,
       fromCache,
       onContextMenu,
       onMouseDown,
       onCauseBadgeClick,
@@ -121,25 +126,25 @@ const RequestListItem = createClass({
       div({
         ref: "el",
         className: classList.join(" "),
         "data-id": item.id,
         tabIndex: 0,
         onContextMenu,
         onMouseDown,
       },
-        StatusColumn({ item }),
-        MethodColumn({ item }),
-        FileColumn({ item }),
-        DomainColumn({ item, onSecurityIconClick }),
-        CauseColumn({ item, onCauseBadgeClick }),
-        TypeColumn({ item }),
-        TransferredSizeColumn({ item }),
-        ContentSizeColumn({ item }),
-        WaterfallColumn({ item, firstRequestStartedMillis }),
+        columns.get("status") && StatusColumn({ item }),
+        columns.get("method") && MethodColumn({ item }),
+        columns.get("file") && FileColumn({ item }),
+        columns.get("domain") && DomainColumn({ item, onSecurityIconClick }),
+        columns.get("cause") && CauseColumn({ item, onCauseBadgeClick }),
+        columns.get("type") && TypeColumn({ item }),
+        columns.get("transferred") && TransferredSizeColumn({ item }),
+        columns.get("contentSize") && ContentSizeColumn({ item }),
+        columns.get("waterfall") && WaterfallColumn({ item, firstRequestStartedMillis }),
       )
     );
   }
 });
 
 const UPDATED_STATUS_PROPS = [
   "status",
   "statusText",
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -11,21 +11,23 @@ const actionTypes = {
   BATCH_ENABLE: "BATCH_ENABLE",
   CLEAR_REQUESTS: "CLEAR_REQUESTS",
   CLEAR_TIMING_MARKERS: "CLEAR_TIMING_MARKERS",
   CLONE_SELECTED_REQUEST: "CLONE_SELECTED_REQUEST",
   ENABLE_REQUEST_FILTER_TYPE_ONLY: "ENABLE_REQUEST_FILTER_TYPE_ONLY",
   OPEN_NETWORK_DETAILS: "OPEN_NETWORK_DETAILS",
   OPEN_STATISTICS: "OPEN_STATISTICS",
   REMOVE_SELECTED_CUSTOM_REQUEST: "REMOVE_SELECTED_CUSTOM_REQUEST",
+  RESET_COLUMNS: "RESET_COLUMNS",
   SELECT_REQUEST: "SELECT_REQUEST",
   SELECT_DETAILS_PANEL_TAB: "SELECT_DETAILS_PANEL_TAB",
   SEND_CUSTOM_REQUEST: "SEND_CUSTOM_REQUEST",
   SET_REQUEST_FILTER_TEXT: "SET_REQUEST_FILTER_TEXT",
   SORT_BY: "SORT_BY",
+  TOGGLE_COLUMN: "TOGGLE_COLUMN",
   TOGGLE_REQUEST_FILTER_TYPE: "TOGGLE_REQUEST_FILTER_TYPE",
   UPDATE_REQUEST: "UPDATE_REQUEST",
   WATERFALL_RESIZE: "WATERFALL_RESIZE",
 };
 
 // Descriptions for what this frontend is currently doing.
 const ACTIVITY_TYPE = {
   // Standing by and handling requests normally.
--- a/devtools/client/netmonitor/src/middleware/prefs.js
+++ b/devtools/client/netmonitor/src/middleware/prefs.js
@@ -1,31 +1,46 @@
 /* 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 {
   ENABLE_REQUEST_FILTER_TYPE_ONLY,
+  RESET_COLUMNS,
+  TOGGLE_COLUMN,
   TOGGLE_REQUEST_FILTER_TYPE,
 } = require("../constants");
 const { Prefs } = require("../utils/prefs");
 const { getRequestFilterTypes } = require("../selectors/index");
 
 /**
-  * Whenever the User clicks on a filter in the network monitor, save the new
-  * filters for future tabs
+  * Update the relevant prefs when:
+  *   - a column has been toggled
+  *   - a filter type has been set
   */
 function prefsMiddleware(store) {
   return next => action => {
     const res = next(action);
-    if (action.type === ENABLE_REQUEST_FILTER_TYPE_ONLY ||
-        action.type === TOGGLE_REQUEST_FILTER_TYPE) {
-      Prefs.filters = getRequestFilterTypes(store.getState())
-        .filter(([type, check]) => check)
-        .map(([type, check]) => type);
+    switch (action.type) {
+      case ENABLE_REQUEST_FILTER_TYPE_ONLY:
+      case TOGGLE_REQUEST_FILTER_TYPE:
+        Prefs.filters = getRequestFilterTypes(store.getState())
+          .filter(([type, check]) => check)
+          .map(([type, check]) => type);
+        break;
+
+      case TOGGLE_COLUMN:
+        Prefs.hiddenColumns = [...store.getState().ui.columns]
+          .filter(([column, shown]) => !shown)
+          .map(([column, shown]) => column);
+        break;
+
+      case RESET_COLUMNS:
+        Prefs.hiddenColumns = [];
+        break;
     }
     return res;
   };
 }
 
 module.exports = prefsMiddleware;
--- a/devtools/client/netmonitor/src/moz.build
+++ b/devtools/client/netmonitor/src/moz.build
@@ -11,11 +11,12 @@ DIRS += [
     'selectors',
     'utils',
 ]
 
 DevToolsModules(
     'constants.js',
     'netmonitor-controller.js',
     'request-list-context-menu.js',
+    'request-list-header-context-menu.js',
     'request-list-tooltip.js',
     'waterfall-background.js',
 )
--- a/devtools/client/netmonitor/src/reducers/ui.js
+++ b/devtools/client/netmonitor/src/reducers/ui.js
@@ -5,66 +5,103 @@
 "use strict";
 
 const I = require("devtools/client/shared/vendor/immutable");
 const {
   CLEAR_REQUESTS,
   OPEN_NETWORK_DETAILS,
   OPEN_STATISTICS,
   REMOVE_SELECTED_CUSTOM_REQUEST,
+  RESET_COLUMNS,
   SELECT_DETAILS_PANEL_TAB,
   SEND_CUSTOM_REQUEST,
   SELECT_REQUEST,
+  TOGGLE_COLUMN,
   WATERFALL_RESIZE,
 } = require("../constants");
 
+const Columns = I.Record({
+  status: true,
+  method: true,
+  file: true,
+  domain: true,
+  cause: true,
+  type: true,
+  transferred: true,
+  contentSize: true,
+  waterfall: true,
+});
+
 const UI = I.Record({
+  columns: new Columns(),
   detailsPanelSelectedTab: "headers",
   networkDetailsOpen: false,
   statisticsOpen: false,
   waterfallWidth: null,
 });
 
 // Safe bounds for waterfall width (px)
 const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
 
+function resetColumns(state) {
+  return state.set("columns", new Columns());
+}
+
 function resizeWaterfall(state, action) {
   return state.set("waterfallWidth", action.width - REQUESTS_WATERFALL_SAFE_BOUNDS);
 }
 
 function openNetworkDetails(state, action) {
   return state.set("networkDetailsOpen", action.open);
 }
 
 function openStatistics(state, action) {
   return state.set("statisticsOpen", action.open);
 }
 
 function setDetailsPanelTab(state, action) {
   return state.set("detailsPanelSelectedTab", action.id);
 }
 
+function toggleColumn(state, action) {
+  let { column } = action;
+
+  if (!state.has(column)) {
+    return state;
+  }
+
+  let newState = state.withMutations(columns => {
+    columns.set(column, !state.get(column));
+  });
+  return newState;
+}
+
 function ui(state = new UI(), action) {
   switch (action.type) {
     case CLEAR_REQUESTS:
       return openNetworkDetails(state, { open: false });
     case OPEN_NETWORK_DETAILS:
       return openNetworkDetails(state, action);
     case OPEN_STATISTICS:
       return openStatistics(state, action);
+    case RESET_COLUMNS:
+      return resetColumns(state);
     case REMOVE_SELECTED_CUSTOM_REQUEST:
     case SEND_CUSTOM_REQUEST:
       return openNetworkDetails(state, { open: false });
     case SELECT_DETAILS_PANEL_TAB:
       return setDetailsPanelTab(state, action);
     case SELECT_REQUEST:
       return openNetworkDetails(state, { open: true });
+    case TOGGLE_COLUMN:
+      return state.set("columns", toggleColumn(state.columns, action));
     case WATERFALL_RESIZE:
       return resizeWaterfall(state, action);
     default:
       return state;
   }
 }
 
 module.exports = {
+  Columns,
   UI,
   ui
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/request-list-header-context-menu.js
@@ -0,0 +1,59 @@
+/* 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 Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+const { L10N } = require("./utils/l10n");
+
+const stringMap = {
+  status: "status3"
+};
+
+class RequestListHeaderContextMenu {
+  constructor({ toggleColumn, resetColumns }) {
+    this.toggleColumn = toggleColumn;
+    this.resetColumns = resetColumns;
+  }
+
+  get columns() {
+    return window.gStore.getState().ui.columns;
+  }
+
+  get visibleColumns() {
+    return [...this.columns].filter(([_, shown]) => shown);
+  }
+
+  /**
+   * Handle the context menu opening.
+   */
+  open({ screenX = 0, screenY = 0 } = {}) {
+    let menu = new Menu();
+    let onlyOneColumn = this.visibleColumns.length === 1;
+
+    for (let [column, shown] of this.columns) {
+      menu.append(new MenuItem({
+        label: L10N.getStr(`netmonitor.toolbar.${stringMap[column] || column}`),
+        type: "checkbox",
+        checked: shown,
+        click: () => this.toggleColumn(column),
+        // We don't want to allow hiding the last visible column
+        disabled: onlyOneColumn && shown,
+      }));
+    }
+
+    menu.append(new MenuItem({ type: "separator" }));
+
+    menu.append(new MenuItem({
+      label: L10N.getStr("netmonitor.toolbar.resetColumns"),
+      click: () => this.resetColumns(),
+    }));
+
+    menu.popup(screenX, screenY, { doc: window.parent.document });
+    return menu;
+  }
+}
+
+module.exports = RequestListHeaderContextMenu;
--- a/devtools/client/netmonitor/src/utils/create-store.js
+++ b/devtools/client/netmonitor/src/utils/create-store.js
@@ -9,31 +9,39 @@ const { thunk } = require("devtools/clie
 const batching = require("../middleware/batching");
 const prefs = require("../middleware/prefs");
 const { Prefs } = require("./prefs");
 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 } = require("../reducers/ui");
+const { UI, Columns } = require("../reducers/ui");
 
 function configureStore() {
   let activeFilters = {};
   Prefs.filters.forEach((filter) => {
     activeFilters[filter] = true;
   });
+
+  let inactiveColumns = Prefs.hiddenColumns.reduce((acc, col) => {
+    acc[col] = false;
+    return acc;
+  }, {});
+
   const initialState = {
     filters: new Filters({
       requestFilterTypes: new FilterTypes(activeFilters)
     }),
     requests: new Requests(),
     sort: new Sort(),
     timingMarkers: new TimingMarkers(),
-    ui: new UI()
+    ui: new UI({
+      columns: new Columns(inactiveColumns)
+    }),
   };
 
   return createStore(
     rootReducer,
     initialState,
     applyMiddleware(
       thunk,
       prefs,
--- a/devtools/client/netmonitor/src/utils/prefs.js
+++ b/devtools/client/netmonitor/src/utils/prefs.js
@@ -7,10 +7,11 @@
 const { PrefsHelper } = require("devtools/client/shared/prefs");
 
 /**
  * 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"],
+  hiddenColumns: ["Json", "hiddenColumns"],
   filters: ["Json", "filters"]
 });
--- a/devtools/client/netmonitor/src/utils/sort-predicates.js
+++ b/devtools/client/netmonitor/src/utils/sort-predicates.js
@@ -71,24 +71,24 @@ function type(first, second) {
   return result || waterfall(first, second);
 }
 
 function transferred(first, second) {
   const result = compareValues(first.transferredSize, second.transferredSize);
   return result || waterfall(first, second);
 }
 
-function size(first, second) {
+function contentSize(first, second) {
   const result = compareValues(first.contentSize, second.contentSize);
   return result || waterfall(first, second);
 }
 
 exports.Sorters = {
   status,
   method,
   file,
   domain,
   cause,
   type,
   transferred,
-  size,
+  contentSize,
   waterfall,
 };
--- a/devtools/client/netmonitor/test/browser_net_filter-03.js
+++ b/devtools/client/netmonitor/test/browser_net_filter-03.js
@@ -59,17 +59,17 @@ add_task(function* () {
   is(!!document.querySelector(".network-details-panel"), true,
     "The network details panel should be visible after toggle button was pressed.");
 
   testFilterButtons(monitor, "all");
   testContents([0, 1, 2, 3, 4, 5, 6], 7, 0);
 
   info("Sorting by size, ascending.");
   EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-list-size-button"));
+    document.querySelector("#requests-list-contentSize-button"));
   testFilterButtons(monitor, "all");
   testContents([6, 4, 5, 0, 1, 2, 3], 7, 6);
 
   info("Testing html filtering.");
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector(".requests-list-filter-html-button"));
   testFilterButtons(monitor, "html");
   testContents([6, 4, 5, 0, 1, 2, 3], 1, 6);
@@ -95,17 +95,17 @@ add_task(function* () {
     3, 20);
 
   yield teardown(monitor);
 
   function resetSorting() {
     EventUtils.sendMouseEvent({ type: "click" },
       document.querySelector("#requests-list-waterfall-button"));
     EventUtils.sendMouseEvent({ type: "click" },
-      document.querySelector("#requests-list-size-button"));
+      document.querySelector("#requests-list-contentSize-button"));
   }
 
   function getSelectedIndex(state) {
     if (!state.requests.selectedId) {
       return -1;
     }
     return getSortedRequests(state).findIndex(r => r.id === state.requests.selectedId);
   }
--- a/devtools/client/netmonitor/test/browser_net_icon-preview.js
+++ b/devtools/client/netmonitor/test/browser_net_icon-preview.js
@@ -23,17 +23,17 @@ add_task(function* () {
 
   let wait = waitForEvents();
   yield performRequests();
   yield wait;
 
   info("Checking the image thumbnail when all items are shown.");
   checkImageThumbnail();
 
-  gStore.dispatch(Actions.sortBy("size"));
+  gStore.dispatch(Actions.sortBy("contentSize"));
   info("Checking the image thumbnail when all items are sorted.");
   checkImageThumbnail();
 
   gStore.dispatch(Actions.toggleRequestFilterType("images"));
   info("Checking the image thumbnail when only images are shown.");
   checkImageThumbnail();
 
   info("Reloading the debuggee and performing all requests again...");
--- a/devtools/client/netmonitor/test/browser_net_sort-02.js
+++ b/devtools/client/netmonitor/test/browser_net_sort-02.js
@@ -151,30 +151,30 @@ add_task(function* () {
   info("Testing transferred sort, ascending. Checking sort loops correctly.");
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#requests-list-transferred-button"));
   testHeaders("transferred", "ascending");
   testContents([0, 1, 2, 3, 4]);
 
   info("Testing size sort, ascending.");
   EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-list-size-button"));
-  testHeaders("size", "ascending");
+    document.querySelector("#requests-list-contentSize-button"));
+  testHeaders("contentSize", "ascending");
   testContents([0, 1, 2, 3, 4]);
 
   info("Testing size sort, descending.");
   EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-list-size-button"));
-  testHeaders("size", "descending");
+    document.querySelector("#requests-list-contentSize-button"));
+  testHeaders("contentSize", "descending");
   testContents([4, 3, 2, 1, 0]);
 
   info("Testing size sort, ascending. Checking sort loops correctly.");
   EventUtils.sendMouseEvent({ type: "click" },
-    document.querySelector("#requests-list-size-button"));
-  testHeaders("size", "ascending");
+    document.querySelector("#requests-list-contentSize-button"));
+  testHeaders("contentSize", "ascending");
   testContents([0, 1, 2, 3, 4]);
 
   info("Testing waterfall sort, ascending.");
   EventUtils.sendMouseEvent({ type: "click" },
     document.querySelector("#requests-list-waterfall-button"));
   testHeaders("waterfall", "ascending");
   testContents([0, 2, 4, 3, 1]);
 
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -145,16 +145,17 @@ pref("devtools.serviceWorkers.testing.en
 
 // Enable the Network Monitor
 pref("devtools.netmonitor.enabled", true);
 
 // 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.hiddenColumns", "[]");
 
 // The default Network monitor HAR export setting
 pref("devtools.netmonitor.har.defaultLogDir", "");
 pref("devtools.netmonitor.har.defaultFileName", "Archive %date");
 pref("devtools.netmonitor.har.jsonp", false);
 pref("devtools.netmonitor.har.jsonpCallback", "");
 pref("devtools.netmonitor.har.includeResponseBodies", true);
 pref("devtools.netmonitor.har.compress", false);