Bug 1317648 - Implement Headers Panel draft
authorRicky Chien <rchien@mozilla.com>
Sat, 31 Dec 2016 21:03:18 +0800
changeset 458385 9783e516f543a4867a0edde0c4b8cc7b364695f9
parent 457629 701868bfddcba5bdec516be33a86dcd525dc74cf
child 541629 f013289f4338b304e0a396739de677204a7b431a
push id40936
push userbmo:rchien@mozilla.com
push dateTue, 10 Jan 2017 07:16:43 +0000
bugs1317648
milestone53.0a1
Bug 1317648 - Implement Headers Panel MozReview-Commit-ID: 74kBBM4YsJR
devtools/client/framework/test/shared-head.js
devtools/client/netmonitor/actions/filters.js
devtools/client/netmonitor/constants.js
devtools/client/netmonitor/details-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/reducers/filters.js
devtools/client/netmonitor/requests-menu-view.js
devtools/client/netmonitor/shared/components/headers-panel.js
devtools/client/netmonitor/shared/components/moz.build
devtools/client/netmonitor/test/browser_net_post-data-03.js
devtools/client/netmonitor/test/browser_net_raw_headers.js
devtools/client/netmonitor/test/browser_net_security-details.js
devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
devtools/client/netmonitor/test/browser_net_status-codes.js
devtools/client/themes/netmonitor.css
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -246,18 +246,52 @@ function waitForNEvents(target, eventNam
         if (++count == numTimes) {
           target[remove](eventName, onEvent, useCapture);
           deferred.resolve.apply(deferred, aArgs);
         }
       }, useCapture);
       break;
     }
   }
+return deferred.promise;
+}
 
-  return deferred.promise;
+/**
+ * Wait for DOM change on target.
+ *
+ * @param {Object} target
+ *        The Node on which to observe DOM mutations.
+ * @param {String} selector
+ *        Given a selector to watch whether the expected element is changed
+ *        on target.
+ * @param {Number} expectedLength
+ *        Optional, default set to 1
+ *        There may be more than one element match an array match the selector,
+ *        give an expected length to wait for more elements.
+ * @return A promise that resolves when the event has been handled
+ */
+function waitForDOM(target, selector, expectedLength = 1) {
+  return new Promise((resolve) => {
+    let observer = new MutationObserver((mutations) => {
+      mutations.forEach((mutation) => {
+        let elements = mutation.target.querySelectorAll(selector);
+
+        if (elements.length === expectedLength) {
+          observer.disconnect();
+          resolve(elements);
+        }
+      });
+    });
+
+    observer.observe(target, {
+      attributes: true,
+      childList: true,
+      subtree: true,
+    });
+  });
 }
 
 /**
  * Wait for eventName on target.
  *
  * @param {Object} target
  *        An observable object that either supports on/off or
  *        addEventListener/removeEventListener
--- a/devtools/client/netmonitor/actions/filters.js
+++ b/devtools/client/netmonitor/actions/filters.js
@@ -2,16 +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/. */
 
 "use strict";
 
 const {
   TOGGLE_REQUEST_FILTER_TYPE,
   ENABLE_REQUEST_FILTER_TYPE_ONLY,
+  SET_HEADERS_FILTER_TEXT,
   SET_REQUEST_FILTER_TEXT,
 } = require("../constants");
 
 /**
  * Toggle an existing filter type state.
  * If type 'all' is specified, all the other filter types are set to false.
  * Available filter types are defined in filters reducer.
  *
@@ -35,24 +36,37 @@ function toggleRequestFilterType(filter)
 function enableRequestFilterTypeOnly(filter) {
   return {
     type: ENABLE_REQUEST_FILTER_TYPE_ONLY,
     filter,
   };
 }
 
 /**
+ * Set filter text in headers panel.
+ *
+ * @param {string} text - A filter text is going to be set
+ */
+function setHeadersFilterText(text) {
+  return {
+    type: SET_HEADERS_FILTER_TEXT,
+    text,
+  };
+}
+
+/**
  * Set filter text.
  *
  * @param {string} text - A filter text is going to be set
  */
 function setRequestFilterText(text) {
   return {
     type: SET_REQUEST_FILTER_TEXT,
     text,
   };
 }
 
 module.exports = {
   toggleRequestFilterType,
   enableRequestFilterTypeOnly,
+  setHeadersFilterText,
   setRequestFilterText,
 };
--- a/devtools/client/netmonitor/constants.js
+++ b/devtools/client/netmonitor/constants.js
@@ -1,34 +1,36 @@
 /* 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 general = {
   CONTENT_SIZE_DECIMALS: 2,
-  FREETEXT_FILTER_SEARCH_DELAY: 200,
+  HEADERS_SIZE_DECIMALS: 3,
   REQUEST_TIME_DECIMALS: 2,
+  FILTER_SEARCH_DELAY: 200,
 };
 
 const actionTypes = {
   ADD_REQUEST: "ADD_REQUEST",
   ADD_TIMING_MARKER: "ADD_TIMING_MARKER",
   BATCH_ACTIONS: "BATCH_ACTIONS",
   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_SIDEBAR: "OPEN_SIDEBAR",
   OPEN_STATISTICS: "OPEN_STATISTICS",
   PRESELECT_REQUEST: "PRESELECT_REQUEST",
   REMOVE_SELECTED_CUSTOM_REQUEST: "REMOVE_SELECTED_CUSTOM_REQUEST",
   SELECT_REQUEST: "SELECT_REQUEST",
+  SET_HEADERS_FILTER_TEXT: "SET_HEADERS_FILTER_TEXT",
   SET_REQUEST_FILTER_TEXT: "SET_REQUEST_FILTER_TEXT",
   SORT_BY: "SORT_BY",
   TOGGLE_REQUEST_FILTER_TYPE: "TOGGLE_REQUEST_FILTER_TYPE",
   UPDATE_REQUEST: "UPDATE_REQUEST",
   WATERFALL_RESIZE: "WATERFALL_RESIZE",
 };
 
 // Descriptions for what this frontend is currently doing.
--- a/devtools/client/netmonitor/details-view.js
+++ b/devtools/client/netmonitor/details-view.js
@@ -14,33 +14,32 @@ const { Heritage } = require("devtools/c
 const { Task } = require("devtools/shared/task");
 const { ToolSidebar } = require("devtools/client/framework/sidebar");
 const { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
 const { VariablesViewController } = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
 const { EVENTS } = require("./events");
 const { L10N } = require("./l10n");
 const { Filters } = require("./filter-predicates");
 const {
-  decodeUnicodeUrl,
   formDataURI,
   getFormDataSections,
   getUrlBaseName,
   getUrlQuery,
   parseQueryString,
 } = require("./request-utils");
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const HeadersPanel = createFactory(require("./shared/components/headers-panel"));
 const PreviewPanel = createFactory(require("./shared/components/preview-panel"));
 const SecurityPanel = createFactory(require("./shared/components/security-panel"));
 const TimingsPanel = createFactory(require("./shared/components/timings-panel"));
 
 // 100 KB in bytes
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400;
-const HEADERS_SIZE_DECIMALS = 3;
 const CONTENT_MIME_TYPE_MAPPINGS = {
   "/ecmascript": Editor.modes.js,
   "/javascript": Editor.modes.js,
   "/x-javascript": Editor.modes.js,
   "/html": Editor.modes.html,
   "/xhtml": Editor.modes.html,
   "/xml": Editor.modes.html,
   "/atom": Editor.modes.html,
@@ -89,16 +88,23 @@ DetailsView.prototype = {
   },
 
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function (store) {
     dumpn("Initializing the DetailsView");
 
+    this._headersPanelNode = $("#react-headers-tabpanel-hook");
+
+    ReactDOM.render(Provider(
+      { store },
+      HeadersPanel()
+    ), this._headersPanelNode);
+
     this._previewPanelNode = $("#react-preview-tabpanel-hook");
 
     ReactDOM.render(Provider(
       { store },
       PreviewPanel()
     ), this._previewPanelNode);
 
     this._securityPanelNode = $("#react-security-tabpanel-hook");
@@ -116,21 +122,16 @@ DetailsView.prototype = {
     ), this._timingsPanelNode);
 
     this.widget = $("#event-details-pane");
     this.sidebar = new ToolSidebar(this.widget, this, "netmonitor", {
       disableTelemetry: true,
       showAllTabsMenu: true
     });
 
-    this._headers = new VariablesView($("#all-headers"),
-      Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
-        emptyText: L10N.getStr("headersEmptyText"),
-        searchPlaceholder: L10N.getStr("headersFilterText")
-      }));
     this._cookies = new VariablesView($("#all-cookies"),
       Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
         emptyText: L10N.getStr("cookiesEmptyText"),
         searchPlaceholder: L10N.getStr("cookiesFilterText")
       }));
     this._params = new VariablesView($("#request-params"),
       Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
         emptyText: L10N.getStr("paramsEmptyText"),
@@ -141,30 +142,28 @@ DetailsView.prototype = {
         onlyEnumVisible: true,
         searchPlaceholder: L10N.getStr("jsonFilterText")
       }));
     VariablesViewController.attach(this._json);
 
     this._paramsQueryString = L10N.getStr("paramsQueryString");
     this._paramsFormData = L10N.getStr("paramsFormData");
     this._paramsPostPayload = L10N.getStr("paramsPostPayload");
-    this._requestHeaders = L10N.getStr("requestHeaders");
-    this._requestHeadersFromUpload = L10N.getStr("requestHeadersFromUpload");
-    this._responseHeaders = L10N.getStr("responseHeaders");
     this._requestCookies = L10N.getStr("requestCookies");
     this._responseCookies = L10N.getStr("responseCookies");
 
     $("tabpanels", this.widget).addEventListener("select", this._onTabSelect);
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
   destroy: function () {
     dumpn("Destroying the DetailsView");
+    ReactDOM.unmountComponentAtNode(this._headersPanelNode);
     ReactDOM.unmountComponentAtNode(this._previewPanelNode);
     ReactDOM.unmountComponentAtNode(this._securityPanelNode);
     ReactDOM.unmountComponentAtNode(this._timingsPanelNode);
     this.sidebar.destroy();
     $("tabpanels", this.widget).removeEventListener("select",
       this._onTabSelect);
   },
 
@@ -178,17 +177,16 @@ DetailsView.prototype = {
    */
   populate: function (data) {
     $("#request-params-box").setAttribute("flex", "1");
     $("#request-params-box").hidden = false;
     $("#request-post-data-textarea-box").hidden = true;
     $("#response-content-info-header").hidden = true;
     $("#response-content-json-box").hidden = true;
     $("#response-content-textarea-box").hidden = true;
-    $("#raw-headers").hidden = true;
     $("#response-content-image-box").hidden = true;
 
     let isHtml = Filters.html(data);
 
     // Show the "Preview" tabpanel only for plain HTML responses.
     this.sidebar.toggleTab(isHtml, "preview-tab");
 
     // Show the "Security" tab only for requests that
@@ -203,17 +201,16 @@ DetailsView.prototype = {
     // request has no security information.
 
     if (!isHtml && this.widget.selectedPanel === $("#preview-tabpanel") ||
         !hasSecurityInfo && this.widget.selectedPanel ===
           $("#security-tabpanel")) {
       this.widget.selectedIndex = 0;
     }
 
-    this._headers.empty();
     this._cookies.empty();
     this._params.empty();
     this._json.empty();
 
     this._dataSrc = { src: data, populated: [] };
     this._onTabSelect();
     window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
 
@@ -242,24 +239,16 @@ DetailsView.prototype = {
       viewState.dirty[tab] = true;
       viewState.latestData = src;
       return;
     }
 
     Task.spawn(function* () {
       viewState.updating[tab] = true;
       switch (tab) {
-        // "Headers"
-        case 0:
-          yield view._setSummary(src);
-          yield view._setResponseHeaders(src.responseHeaders);
-          yield view._setRequestHeaders(
-            src.requestHeaders,
-            src.requestHeadersFromUploadStream);
-          break;
         // "Cookies"
         case 1:
           yield view._setResponseCookies(src.responseCookies);
           yield view._setRequestCookies(src.requestCookies);
           break;
         // "Params"
         case 2:
           yield view._setRequestGetParams(src.url);
@@ -289,140 +278,16 @@ DetailsView.prototype = {
         // Tab is dirty but no longer selected. Don't refresh it now, it'll be
         // done if the tab is shown again.
         viewState.dirty[tab] = false;
       }
     }, e => console.error(e));
   },
 
   /**
-   * Sets the network request summary shown in this view.
-   *
-   * @param object data
-   *        The data source (this should be the attachment of a request item).
-   */
-  _setSummary: function (data) {
-    if (data.url) {
-      let unicodeUrl = decodeUnicodeUrl(data.url);
-      $("#headers-summary-url-value").setAttribute("value", unicodeUrl);
-      $("#headers-summary-url-value").setAttribute("tooltiptext", unicodeUrl);
-      $("#headers-summary-url").removeAttribute("hidden");
-    } else {
-      $("#headers-summary-url").setAttribute("hidden", "true");
-    }
-
-    if (data.method) {
-      $("#headers-summary-method-value").setAttribute("value", data.method);
-      $("#headers-summary-method").removeAttribute("hidden");
-    } else {
-      $("#headers-summary-method").setAttribute("hidden", "true");
-    }
-
-    if (data.remoteAddress) {
-      let address = data.remoteAddress;
-      if (address.indexOf(":") != -1) {
-        address = `[${address}]`;
-      }
-      if (data.remotePort) {
-        address += `:${data.remotePort}`;
-      }
-      $("#headers-summary-address-value").setAttribute("value", address);
-      $("#headers-summary-address-value").setAttribute("tooltiptext", address);
-      $("#headers-summary-address").removeAttribute("hidden");
-    } else {
-      $("#headers-summary-address").setAttribute("hidden", "true");
-    }
-
-    if (data.status) {
-      // "code" attribute is only used by css to determine the icon color
-      let code;
-      if (data.fromCache) {
-        code = "cached";
-      } else if (data.fromServiceWorker) {
-        code = "service worker";
-      } else {
-        code = data.status;
-      }
-      $("#headers-summary-status-circle").setAttribute("data-code", code);
-      $("#headers-summary-status-value").setAttribute("value",
-        data.status + " " + data.statusText);
-      $("#headers-summary-status").removeAttribute("hidden");
-    } else {
-      $("#headers-summary-status").setAttribute("hidden", "true");
-    }
-
-    if (data.httpVersion) {
-      $("#headers-summary-version-value").setAttribute("value",
-        data.httpVersion);
-      $("#headers-summary-version").removeAttribute("hidden");
-    } else {
-      $("#headers-summary-version").setAttribute("hidden", "true");
-    }
-  },
-
-  /**
-   * Sets the network request headers shown in this view.
-   *
-   * @param object headers
-   *        The "requestHeaders" message received from the server.
-   * @param object uploadHeaders
-   *        The "requestHeadersFromUploadStream" inferred from the POST payload.
-   * @return object
-   *        A promise that resolves when request headers are set.
-   */
-  _setRequestHeaders: Task.async(function* (headers, uploadHeaders) {
-    if (headers && headers.headers.length) {
-      yield this._addHeaders(this._requestHeaders, headers);
-    }
-    if (uploadHeaders && uploadHeaders.headers.length) {
-      yield this._addHeaders(this._requestHeadersFromUpload, uploadHeaders);
-    }
-  }),
-
-  /**
-   * Sets the network response headers shown in this view.
-   *
-   * @param object response
-   *        The message received from the server.
-   * @return object
-   *        A promise that resolves when response headers are set.
-   */
-  _setResponseHeaders: Task.async(function* (response) {
-    if (response && response.headers.length) {
-      response.headers.sort((a, b) => a.name > b.name);
-      yield this._addHeaders(this._responseHeaders, response);
-    }
-  }),
-
-  /**
-   * Populates the headers container in this view with the specified data.
-   *
-   * @param string name
-   *        The type of headers to populate (request or response).
-   * @param object response
-   *        The message received from the server.
-   * @return object
-   *        A promise that resolves when headers are added.
-   */
-  _addHeaders: Task.async(function* (name, response) {
-    let kb = response.headersSize / 1024;
-    let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
-    let text = L10N.getFormatStr("networkMenu.sizeKB", size);
-
-    let headersScope = this._headers.addScope(name + " (" + text + ")");
-    headersScope.expanded = true;
-
-    for (let header of response.headers) {
-      let headerVar = headersScope.addItem(header.name, {}, {relaxed: true});
-      let headerValue = yield gNetwork.getString(header.value);
-      headerVar.setGrip(headerValue);
-    }
-  }),
-
-  /**
    * Sets the network request cookies shown in this view.
    *
    * @param object response
    *        The message received from the server.
    * @return object
    *        A promise that is resolved when the request cookies are set.
    */
   _setRequestCookies: Task.async(function* (response) {
@@ -698,22 +563,19 @@ DetailsView.prototype = {
         }
       }
     }
 
     window.emit(EVENTS.RESPONSE_BODY_DISPLAYED);
   }),
 
   _dataSrc: null,
-  _headers: null,
   _cookies: null,
   _params: null,
   _json: null,
   _paramsQueryString: "",
   _paramsFormData: "",
   _paramsPostPayload: "",
-  _requestHeaders: "",
-  _responseHeaders: "",
   _requestCookies: "",
   _responseCookies: ""
 };
 
 exports.DetailsView = DetailsView;
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -118,96 +118,18 @@
                    data-localization="label=netmonitor.tab.security"/>
               <tab id="preview-tab"
                    crop="end"
                    data-localization="label=netmonitor.tab.preview"/>
             </tabs>
             <tabpanels flex="1">
               <tabpanel id="headers-tabpanel"
                         class="tabpanel-content">
-                <vbox flex="1">
-                  <hbox id="headers-summary-url"
-                        class="tabpanel-summary-container"
-                        align="center">
-                    <label class="plain tabpanel-summary-label"
-                           data-localization="content=netmonitor.summary.url"/>
-                    <textbox id="headers-summary-url-value"
-                             class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
-                             flex="1"
-                             readonly="true"/>
-                  </hbox>
-                  <hbox id="headers-summary-method"
-                        class="tabpanel-summary-container"
-                        align="center">
-                    <label class="plain tabpanel-summary-label"
-                           data-localization="content=netmonitor.summary.method"/>
-                    <label id="headers-summary-method-value"
-                           class="plain tabpanel-summary-value devtools-monospace"
-                           crop="end"
-                           flex="1"/>
-                  </hbox>
-                  <hbox id="headers-summary-address"
-                        class="tabpanel-summary-container"
-                        align="center">
-                    <label class="plain tabpanel-summary-label"
-                           data-localization="content=netmonitor.summary.address"/>
-                    <textbox id="headers-summary-address-value"
-                             class="plain tabpanel-summary-value devtools-monospace cropped-textbox"
-                             flex="1"
-                             readonly="true"/>
-                  </hbox>
-                  <hbox id="headers-summary-status"
-                        class="tabpanel-summary-container"
-                        align="center">
-                    <label class="plain tabpanel-summary-label"
-                           data-localization="content=netmonitor.summary.status"/>
-                    <box id="headers-summary-status-circle"
-                         class="requests-menu-status-icon"/>
-                    <label id="headers-summary-status-value"
-                           class="plain tabpanel-summary-value devtools-monospace"
-                           crop="end"
-                           flex="1"/>
-                    <button id="headers-summary-resend"
-                            class="devtools-toolbarbutton"
-                            data-localization="label=netmonitor.summary.editAndResend"/>
-                    <button id="toggle-raw-headers"
-                            class="devtools-toolbarbutton"
-                            data-localization="label=netmonitor.summary.rawHeaders"/>
-                  </hbox>
-                  <hbox id="headers-summary-version"
-                        class="tabpanel-summary-container"
-                        align="center">
-                    <label class="plain tabpanel-summary-label"
-                           data-localization="content=netmonitor.summary.version"/>
-                    <label id="headers-summary-version-value"
-                           class="plain tabpanel-summary-value devtools-monospace"
-                           crop="end"
-                           flex="1"/>
-                  </hbox>
-                  <hbox id="raw-headers"
-                        class="tabpanel-summary-container"
-                        align="center"
-                        hidden="true">
-                    <vbox id="raw-request-headers-textarea-box" flex="1" hidden="false">
-                      <label class="plain tabpanel-summary-label"
-                        data-localization="content=netmonitor.summary.rawHeaders.requestHeaders"/>
-                      <textbox id="raw-request-headers-textarea"
-                        class="raw-response-textarea"
-                        flex="1" multiline="true" readonly="true"/>
-                    </vbox>
-                    <vbox id="raw-response-headers-textarea-box" flex="1" hidden="false">
-                      <label class="plain tabpanel-summary-label"
-                        data-localization="content=netmonitor.summary.rawHeaders.responseHeaders"/>
-                      <textbox id="raw-response-headers-textarea"
-                        class="raw-response-textarea"
-                        flex="1" multiline="true" readonly="true"/>
-                    </vbox>
-                  </hbox>
-                  <vbox id="all-headers" flex="1"/>
-                </vbox>
+                <html:div xmlns="http://www.w3.org/1999/xhtml"
+                          id="react-headers-tabpanel-hook"/>
               </tabpanel>
               <tabpanel id="cookies-tabpanel"
                         class="tabpanel-content">
                 <vbox flex="1">
                   <vbox id="all-cookies" flex="1"/>
                 </vbox>
               </tabpanel>
               <tabpanel id="params-tabpanel"
--- a/devtools/client/netmonitor/reducers/filters.js
+++ b/devtools/client/netmonitor/reducers/filters.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const I = require("devtools/client/shared/vendor/immutable");
 const {
   TOGGLE_REQUEST_FILTER_TYPE,
   ENABLE_REQUEST_FILTER_TYPE_ONLY,
+  SET_HEADERS_FILTER_TEXT,
   SET_REQUEST_FILTER_TEXT,
 } = require("../constants");
 
 const FilterTypes = I.Record({
   all: false,
   html: false,
   css: false,
   js: false,
@@ -22,16 +23,17 @@ const FilterTypes = I.Record({
   media: false,
   flash: false,
   ws: false,
   other: false,
 });
 
 const Filters = I.Record({
   requestFilterTypes: new FilterTypes({ all: true }),
+  headersFilterText: "",
   requestFilterText: "",
 });
 
 function toggleRequestFilterType(state, action) {
   let { filter } = action;
   let newState;
 
   // Ignore unknown filter type
@@ -68,16 +70,18 @@ function enableRequestFilterTypeOnly(sta
 function filters(state = new Filters(), action) {
   switch (action.type) {
     case TOGGLE_REQUEST_FILTER_TYPE:
       return state.set("requestFilterTypes",
         toggleRequestFilterType(state.requestFilterTypes, action));
     case ENABLE_REQUEST_FILTER_TYPE_ONLY:
       return state.set("requestFilterTypes",
         enableRequestFilterTypeOnly(state.requestFilterTypes, action));
+    case SET_HEADERS_FILTER_TEXT:
+      return state.set("headersFilterText", action.text);
     case SET_REQUEST_FILTER_TEXT:
       return state.set("requestFilterText", action.text);
     default:
       return state;
   }
 }
 
 module.exports = filters;
--- a/devtools/client/netmonitor/requests-menu-view.js
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -17,18 +17,17 @@ const ReactDOM = require("devtools/clien
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 const RequestList = createFactory(require("./components/request-list"));
 const RequestListContextMenu = require("./request-list-context-menu");
 const Actions = require("./actions/index");
 const { Prefs } = require("./prefs");
 
 const {
   formDataURI,
-  writeHeaderText,
-  loadCauseString
+  loadCauseString,
 } = require("./request-utils");
 
 const {
   getActiveFilters,
   getSortedRequests,
   getDisplayedRequests,
   getRequestById,
   getSelectedRequest
@@ -85,21 +84,16 @@ RequestsMenuView.prototype = {
     this.store.subscribe(storeWatcher(
       false,
       () => this.store.getState().ui.sidebarOpen,
       () => this.onResize()
     ));
 
     this.sendCustomRequestEvent = this.sendCustomRequest.bind(this);
     this.closeCustomRequestEvent = this.closeCustomRequest.bind(this);
-    this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this);
-    this.toggleRawHeadersEvent = this.toggleRawHeaders.bind(this);
-
-    $("#toggle-raw-headers")
-      .addEventListener("click", this.toggleRawHeadersEvent, false);
 
     this._summary = $("#requests-menu-network-summary-button");
     this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
 
     this.onResize = this.onResize.bind(this);
     this._splitter = $("#network-inspector-view-splitter");
     this._splitter.addEventListener("mouseup", this.onResize, false);
     window.addEventListener("resize", this.onResize, false);
@@ -116,20 +110,16 @@ RequestsMenuView.prototype = {
   },
 
   _onConnect() {
     if (NetMonitorController.supportsCustomRequest) {
       $("#custom-request-send-button")
         .addEventListener("click", this.sendCustomRequestEvent, false);
       $("#custom-request-close-button")
         .addEventListener("click", this.closeCustomRequestEvent, false);
-      $("#headers-summary-resend")
-        .addEventListener("click", this.cloneSelectedRequestEvent, false);
-    } else {
-      $("#headers-summary-resend").hidden = true;
     }
   },
 
   /**
    * Destruction function, called when the network monitor is closed.
    */
   destroy() {
     dumpn("Destroying the RequestsMenuView");
@@ -137,20 +127,16 @@ RequestsMenuView.prototype = {
     Prefs.filters = getActiveFilters(this.store.getState());
 
     // this.flushRequestsTask.disarm();
 
     $("#custom-request-send-button")
       .removeEventListener("click", this.sendCustomRequestEvent, false);
     $("#custom-request-close-button")
       .removeEventListener("click", this.closeCustomRequestEvent, false);
-    $("#headers-summary-resend")
-      .removeEventListener("click", this.cloneSelectedRequestEvent, false);
-    $("#toggle-raw-headers")
-      .removeEventListener("click", this.toggleRawHeadersEvent, false);
 
     this._splitter.removeEventListener("mouseup", this.onResize, false);
     window.removeEventListener("resize", this.onResize, false);
 
     this.tooltip.destroy();
 
     ReactDOM.unmountComponentAtNode(this.mountPoint);
   },
@@ -199,17 +185,46 @@ RequestsMenuView.prototype = {
 
     this.store.dispatch(action).then(() => window.emit(EVENTS.REQUEST_ADDED, action.id));
   },
 
   updateRequest: Task.async(function* (id, data) {
     const action = Actions.updateRequest(id, data, true);
     yield this.store.dispatch(action);
 
-    let { responseContent, requestPostData } = action.data;
+    let {
+      requestHeaders,
+      requestPostData,
+      responseContent,
+      responseHeaders,
+    } = action.data;
+
+    if (requestHeaders && requestHeaders.headers &&
+        requestHeaders.headers.length > 0) {
+      for (let { value } of requestHeaders.headers) {
+        requestHeaders.headers.value = yield gNetwork.getString(value);
+      }
+      yield this.store.dispatch(Actions.updateRequest(
+        action.id,
+        { requestHeaders },
+        true,
+      ));
+    }
+
+    if (responseHeaders && responseHeaders.headers &&
+        responseHeaders.headers.length > 0) {
+      for (let { value } of responseHeaders.headers) {
+        responseHeaders.headers.value = yield gNetwork.getString(value);
+      }
+      yield this.store.dispatch(Actions.updateRequest(
+        action.id,
+        { responseHeaders },
+        true,
+      ));
+    }
 
     if (responseContent && responseContent.content) {
       let request = getRequestById(this.store.getState(), action.id);
       if (request) {
         let { mimeType } = request;
         let { text, encoding } = responseContent.content;
         let response = yield gNetwork.getString(text);
         let payload = {};
@@ -332,38 +347,16 @@ RequestsMenuView.prototype = {
    * Create a new custom request form populated with the data from
    * the currently selected request.
    */
   cloneSelectedRequest() {
     this.store.dispatch(Actions.cloneSelectedRequest());
   },
 
   /**
-   * Shows raw request/response headers in textboxes.
-   */
-  toggleRawHeaders: function () {
-    let requestTextarea = $("#raw-request-headers-textarea");
-    let responseTextarea = $("#raw-response-headers-textarea");
-    let rawHeadersHidden = $("#raw-headers").getAttribute("hidden");
-
-    if (rawHeadersHidden) {
-      let selected = getSelectedRequest(this.store.getState());
-      let selectedRequestHeaders = selected.requestHeaders.headers;
-      let selectedResponseHeaders = selected.responseHeaders.headers;
-      requestTextarea.value = writeHeaderText(selectedRequestHeaders);
-      responseTextarea.value = writeHeaderText(selectedResponseHeaders);
-      $("#raw-headers").hidden = false;
-    } else {
-      requestTextarea.value = null;
-      responseTextarea.value = null;
-      $("#raw-headers").hidden = true;
-    }
-  },
-
-  /**
    * Send a new HTTP request using the data in the custom request form.
    */
   sendCustomRequest: function () {
     let selected = getSelectedRequest(this.store.getState());
 
     let data = {
       url: selected.url,
       method: selected.method,
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/shared/components/headers-panel.js
@@ -0,0 +1,311 @@
+/* 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/. */
+
+/* globals NetMonitorController */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../../l10n");
+const { HEADERS_SIZE_DECIMALS } = require("../../constants");
+const Actions = require("../../actions/index");
+const { getSelectedRequest } = require("../../selectors/index");
+const { decodeUnicodeUrl, writeHeaderText } = require("../../request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./properties-view"));
+
+const { div, input, textarea } = DOM;
+const EDIT_AND_RESEND = L10N.getStr("netmonitor.summary.editAndResend");
+const RAW_HEADERS = L10N.getStr("netmonitor.summary.rawHeaders");
+const RAW_HEADERS_REQUEST = L10N.getStr("netmonitor.summary.rawHeaders.requestHeaders");
+const RAW_HEADERS_RESPONSE = L10N.getStr("netmonitor.summary.rawHeaders.responseHeaders");
+const HEADERS_EMPTY_TEXT = L10N.getStr("headersEmptyText");
+const HEADERS_FILTER_TEXT = L10N.getStr("headersFilterText");
+const REQUEST_HEADERS = L10N.getStr("requestHeaders");
+const REQUEST_HEADERS_FROM_UPLOAD = L10N.getStr("requestHeadersFromUpload");
+const RESPONSE_HEADERS = L10N.getStr("responseHeaders");
+const SUMMARY_ADDRESS = L10N.getStr("netmonitor.summary.address");
+const SUMMARY_METHOD = L10N.getStr("netmonitor.summary.method");
+const SUMMARY_URL = L10N.getStr("netmonitor.summary.url");
+const SUMMARY_STATUS = L10N.getStr("netmonitor.summary.status");
+const SUMMARY_VERSION = L10N.getStr("netmonitor.summary.version");
+
+/*
+ * Headers panel component
+ * Lists basic information about the request
+ */
+const HeadersPanel = createClass({
+  displayName: "HeadersPanel",
+
+  propTypes: {
+    cloneSelectedRequest: PropTypes.func.isRequired,
+    headersFilterText: PropTypes.string,
+    requestHeaders: PropTypes.object,
+    requestHeadersFromUploadStream: PropTypes.object,
+    responseHeaders: PropTypes.object,
+    selectedRequest: PropTypes.object,
+    setHeadersFilterText: PropTypes.func.isRequired,
+  },
+
+  getInitialState() {
+    return {
+      rawHeadersOpened: false,
+    };
+  },
+
+  getSection(header) {
+    let kb = header.headersSize / 1024;
+    let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
+    return L10N.getFormatStr("networkMenu.sizeKB", size);
+  },
+
+  toggleRawHeaders() {
+    this.setState({
+      rawHeadersOpened: !this.state.rawHeadersOpened,
+    });
+  },
+
+  render() {
+    const {
+      cloneSelectedRequest,
+      requestHeaders,
+      requestHeadersFromUploadStream: uploadHeaders,
+      responseHeaders,
+      selectedRequest,
+    } = this.props;
+
+    if ((!requestHeaders || !requestHeaders.headers.length) &&
+        (!uploadHeaders || !uploadHeaders.headers.length) &&
+        (!responseHeaders || !responseHeaders.headers.length)) {
+      return div({ className: "emptyNotice" },
+        HEADERS_EMPTY_TEXT
+      );
+    }
+
+    let object = {};
+    let sectionNames = [];
+
+    if (responseHeaders && responseHeaders.headers.length > 0) {
+      let section = `${RESPONSE_HEADERS} (${this.getSection(responseHeaders)})`;
+      object[section] =
+        responseHeaders.headers
+          .reduce((acc, { name, value }) =>
+            name ? Object.assign(acc, { [name]: value }) : acc
+          , {});
+      sectionNames.push(section);
+    }
+
+    if (requestHeaders && requestHeaders.headers.length > 0) {
+      let section = `${REQUEST_HEADERS} (${this.getSection(requestHeaders)})`;
+      object[section] =
+        requestHeaders.headers
+          .reduce((acc, { name, value }) =>
+            name ? Object.assign(acc, { [name]: value }) : acc
+          , {});
+      sectionNames.push(section);
+    }
+
+    if (uploadHeaders && uploadHeaders.headers.length > 0) {
+      let section =
+        `${REQUEST_HEADERS_FROM_UPLOAD} (${this.getSection(uploadHeaders)})`;
+      object[section] =
+        uploadHeaders.headers
+          .reduce((acc, { name, value }) =>
+            name ? Object.assign(acc, { [name]: value }) : acc
+          , {});
+      sectionNames.push(section);
+    }
+
+    const {
+      url,
+      method,
+      remoteAddress,
+      remotePort,
+      status,
+      statusText,
+      fromCache,
+      fromServiceWorker,
+      httpVersion,
+    } = selectedRequest;
+
+    let summaryUrl;
+    if (url) {
+      summaryUrl = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({ className: "tabpanel-summary-label headers-summary-label" }, SUMMARY_URL),
+          input({
+            className: "tabpanel-summary-value textbox-input devtools-monospace",
+            readOnly: true,
+            value: decodeUnicodeUrl(url),
+          }),
+        )
+      );
+    }
+
+    let summaryMethod;
+    if (method) {
+      summaryMethod = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({
+            className: "tabpanel-summary-label headers-summary-label",
+          }, SUMMARY_METHOD),
+          input({
+            className: "tabpanel-summary-value textbox-input devtools-monospace",
+            readOnly: true,
+            value: method,
+          }),
+        )
+      );
+    }
+
+    let summaryAddress;
+    if (remoteAddress) {
+      summaryAddress = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({
+            className: "tabpanel-summary-label headers-summary-label",
+          }, SUMMARY_ADDRESS),
+          input({
+            className: "tabpanel-summary-value textbox-input devtools-monospace",
+            readOnly: true,
+            value: remotePort ? `${remoteAddress}:${remotePort}` : remoteAddress,
+          }),
+        )
+      );
+    }
+
+    let summaryStatus;
+    if (status) {
+      let code;
+      if (fromCache) {
+        code = "cached";
+      } else if (fromServiceWorker) {
+        code = "service worker";
+      } else {
+        code = status;
+      }
+
+      summaryStatus = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({
+            className: "tabpanel-summary-label headers-summary-label",
+          }, SUMMARY_STATUS),
+          div({
+            className: "requests-menu-status-icon",
+            "data-code": code,
+          }),
+          input({
+            className: "tabpanel-summary-value textbox-input devtools-monospace",
+            readOnly: true,
+            value: `${status} ${statusText}`,
+          }),
+          NetMonitorController.supportsCustomRequest && input({
+            className: "tool-button",
+            onClick: cloneSelectedRequest,
+            type: "button",
+            value: EDIT_AND_RESEND,
+          }),
+          input({
+            className: "tool-button",
+            onClick: this.toggleRawHeaders,
+            type: "button",
+            value: RAW_HEADERS,
+          }),
+        )
+      );
+    }
+
+    let summaryVersion;
+    if (httpVersion) {
+      summaryVersion = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({
+            className: "tabpanel-summary-label headers-summary-label",
+          }, SUMMARY_VERSION),
+          input({
+            className: "tabpanel-summary-value textbox-input devtools-monospace",
+            readOnly: true,
+            value: httpVersion,
+          }),
+        )
+      );
+    }
+
+    let summaryRawHeaders;
+    if (this.state.rawHeadersOpened) {
+      summaryRawHeaders = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({ className: "raw-headers-container" },
+            div({ className: "tabpanel-summary-label" }, RAW_HEADERS_REQUEST),
+            textarea({
+              value: writeHeaderText(requestHeaders.headers),
+              readOnly: true,
+            }),
+          ),
+          div({ className: "raw-headers-container" },
+            div({ className: "tabpanel-summary-label" }, RAW_HEADERS_RESPONSE),
+            textarea({
+              value: writeHeaderText(responseHeaders.headers),
+              readOnly: true,
+            }),
+          ),
+        )
+      );
+    }
+
+    return (
+      div({},
+        div({ className: "section summary" },
+          summaryUrl,
+          summaryMethod,
+          summaryAddress,
+          summaryStatus,
+          summaryVersion,
+          summaryRawHeaders,
+        ),
+        PropertiesView({
+          object,
+          filterPlaceHolder: HEADERS_FILTER_TEXT,
+          sectionNames,
+        }),
+      )
+    );
+  }
+});
+
+module.exports = connect(
+  (state) => {
+    const selectedRequest = getSelectedRequest(state);
+
+    if (selectedRequest) {
+      const {
+        requestHeaders,
+        requestHeadersFromUploadStream,
+        responseHeaders,
+      } = selectedRequest;
+
+      return {
+        headersFilterText: state.filters.headersFilterText,
+        requestHeaders,
+        requestHeadersFromUploadStream,
+        responseHeaders,
+        selectedRequest,
+      };
+    }
+
+    return {};
+  },
+  (dispatch) => ({
+    setHeadersFilterText: (text) =>
+      dispatch(Actions.setHeadersFilterText(text)),
+    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+  })
+)(HeadersPanel);
--- a/devtools/client/netmonitor/shared/components/moz.build
+++ b/devtools/client/netmonitor/shared/components/moz.build
@@ -1,11 +1,12 @@
 # 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/.
 
 DevToolsModules(
     'editor.js',
+    'headers-panel.js',
     'preview-panel.js',
     'properties-view.js',
     'security-panel.js',
     'timings-panel.js',
 )
--- a/devtools/client/netmonitor/test/browser_net_post-data-03.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-03.js
@@ -20,72 +20,67 @@ add_task(function* () {
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
-  NetMonitorView.toggleDetailsPane({ visible: true });
-  RequestsMenu.selectedIndex = 0;
-  yield onEvent;
+  // Wait for all tree view updated by react
+  wait = waitForDOM(document, ".properties-view .treeTable");
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.getElementById("details-pane-toggle"));
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.querySelectorAll("#details-pane tab")[0]);
+  yield wait;
 
-  let tabEl = document.querySelectorAll("#details-pane tab")[0];
   let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
-  let requestFromUploadScope = tabpanel.querySelectorAll(".variables-view-scope")[2];
 
-  is(tabEl.getAttribute("selected"), "true",
-    "The headers tab in the network details pane should be selected.");
-  is(tabpanel.querySelectorAll(".variables-view-scope").length, 3,
-    "There should be 3 header scopes displayed in this tabpanel.");
+  is(tabpanel.querySelectorAll(".tree-section .treeLabel").length, 3,
+    "There should be 3 header sections displayed in this tabpanel.");
 
-  is(requestFromUploadScope.querySelector(".name").getAttribute("value"),
+  is(tabpanel.querySelectorAll(".tree-section .treeLabel")[2].textContent,
     L10N.getStr("requestHeadersFromUpload") + " (" +
     L10N.getFormatStr("networkMenu.sizeKB", L10N.numberWithDecimals(74 / 1024, 3)) + ")",
-    "The request headers from upload scope doesn't have the correct title.");
+    "The request headers from upload section doesn't have the correct title.");
 
-  is(requestFromUploadScope.querySelectorAll(".variables-view-variable").length, 2,
-    "There should be 2 headers displayed in the request headers from upload scope.");
+  let labels = tabpanel
+    .querySelectorAll(".properties-view tr:not(.tree-section) .treeLabelCell .treeLabel");
+  let values = tabpanel
+    .querySelectorAll(".properties-view tr:not(.tree-section) .treeValueCell .objectBox");
 
-  is(requestFromUploadScope.querySelectorAll(".variables-view-variable .name")[0]
-    .getAttribute("value"),
-    "content-type", "The first request header name was incorrect.");
-  is(requestFromUploadScope.querySelectorAll(".variables-view-variable .value")[0]
-    .getAttribute("value"), "\"application/x-www-form-urlencoded\"",
+  is(labels[labels.length - 2].textContent, "content-type",
+    "The first request header name was incorrect.");
+  is(values[values.length - 2].textContent, "\"application/x-www-form-urlencoded\"",
     "The first request header value was incorrect.");
-  is(requestFromUploadScope.querySelectorAll(".variables-view-variable .name")[1]
-    .getAttribute("value"),
-    "custom-header", "The second request header name was incorrect.");
-  is(requestFromUploadScope.querySelectorAll(".variables-view-variable .value")[1]
-    .getAttribute("value"),
-    "\"hello world!\"", "The second request header value was incorrect.");
+  is(labels[labels.length - 1].textContent, "custom-header",
+    "The second request header name was incorrect.");
+  is(values[values.length - 1].textContent, "\"hello world!\"",
+    "The second request header value was incorrect.");
 
-  onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+  let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll("#details-pane tab")[2]);
   yield onEvent;
 
-  tabEl = document.querySelectorAll("#details-pane tab")[2];
   tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
   let formDataScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
 
   is(tab.getAttribute("selected"), "true",
     "The response tab in the network details pane should be selected.");
   is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
     "There should be 1 header scope displayed in this tabpanel.");
 
   is(formDataScope.querySelector(".name").getAttribute("value"),
     L10N.getStr("paramsFormData"),
     "The form data scope doesn't have the correct title.");
 
   is(formDataScope.querySelectorAll(".variables-view-variable").length, 2,
     "There should be 2 payload values displayed in the form data scope.");
-
   is(formDataScope.querySelectorAll(".variables-view-variable .name")[0]
     .getAttribute("value"),
     "foo", "The first payload param name was incorrect.");
   is(formDataScope.querySelectorAll(".variables-view-variable .value")[0]
     .getAttribute("value"),
     "\"bar\"", "The first payload param value was incorrect.");
   is(formDataScope.querySelectorAll(".variables-view-variable .name")[1]
     .getAttribute("value"),
--- a/devtools/client/netmonitor/test/browser_net_raw_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js
@@ -6,65 +6,65 @@
 /**
  * Tests if showing raw headers works.
  */
 
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
-  let { document, EVENTS, NetMonitorView } = monitor.panelWin;
+  let { document, NetMonitorView } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   let origItem = RequestsMenu.getItemAtIndex(0);
 
-  let onTabEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+  wait = waitForDOM(document, ".summary");
   RequestsMenu.selectedItem = origItem;
-  yield onTabEvent;
+  yield wait;
 
+  wait = waitForDOM(document, ".raw-headers-container textarea", 2);
   EventUtils.sendMouseEvent({ type: "click" },
-    document.getElementById("toggle-raw-headers"));
+    document.querySelectorAll(".tool-button")[1]);
+  yield wait;
 
   testShowRawHeaders(origItem);
 
   EventUtils.sendMouseEvent({ type: "click" },
-    document.getElementById("toggle-raw-headers"));
+    document.querySelectorAll(".tool-button")[1]);
 
   testHideRawHeaders(document);
 
   return teardown(monitor);
 
   /*
    * Tests that raw headers were displayed correctly
    */
   function testShowRawHeaders(data) {
-    let requestHeaders = document.getElementById("raw-request-headers-textarea").value;
+    let requestHeaders = document
+      .querySelectorAll(".raw-headers-container textarea")[0].value;
     for (let header of data.requestHeaders.headers) {
       ok(requestHeaders.includes(header.name + ": " + header.value),
         "textarea contains request headers");
     }
-    let responseHeaders = document.getElementById("raw-response-headers-textarea").value;
+    let responseHeaders = document
+      .querySelectorAll(".raw-headers-container textarea")[1].value;
     for (let header of data.responseHeaders.headers) {
       ok(responseHeaders.includes(header.name + ": " + header.value),
         "textarea contains response headers");
     }
   }
 
   /*
-   * Tests that raw headers textareas are hidden and empty
+   * Tests that raw headers textareas are hidden
    */
   function testHideRawHeaders() {
-    let rawHeadersHidden = document.getElementById("raw-headers").getAttribute("hidden");
-    let requestTextarea = document.getElementById("raw-request-headers-textarea");
-    let responseTextarea = document.getElementById("raw-response-headers-textarea");
-    ok(rawHeadersHidden, "raw headers textareas are hidden");
-    ok(requestTextarea.value == "", "raw request headers textarea is empty");
-    ok(responseTextarea.value == "", "raw response headers textarea is empty");
+    ok(!document.querySelector(".raw-headers-container"),
+      "raw request headers textarea is empty");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_security-details.js
+++ b/devtools/client/netmonitor/test/browser_net_security-details.js
@@ -34,32 +34,32 @@ add_task(function* () {
   yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
 
   let errorbox = $("#security-error");
   let infobox = $("#security-information");
 
   is(errorbox, null, "Error box is hidden.");
   ok(infobox, "Information box visible.");
 
-  let textboxes = $all(".textbox-input");
+  let textboxes = $all("#security-information .textbox-input");
   // Connection
 
   // The protocol will be TLS but the exact version depends on which protocol
   // the test server example.com supports.
   let protocol = textboxes[0].value;
   ok(protocol.startsWith("TLS"), "The protocol " + protocol + " seems valid.");
 
   // The cipher suite used by the test server example.com might change at any
   // moment but all of them should start with "TLS_".
   // http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
   let suite = textboxes[1].value;
   ok(suite.startsWith("TLS_"), "The suite " + suite + " seems valid.");
 
   // Host
-  let hostLabel = $all(".treeLabel.objectLabel")[1];
+  let hostLabel = $all("#security-information .treeLabel.objectLabel")[1];
   is(hostLabel.textContent, "Host example.com:", "Label has the expected value.");
   is(textboxes[2].value, "Disabled", "Label has the expected value.");
   is(textboxes[3].value, "Disabled", "Label has the expected value.");
 
   // Cert
   is(textboxes[4].value, "example.com", "Label has the expected value.");
   is(textboxes[5].value, "<Not Available>", "Label has the expected value.");
   is(textboxes[6].value, "<Not Available>", "Label has the expected value.");
--- a/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js
@@ -28,19 +28,20 @@ add_task(function* () {
   yield wait;
 
   info("Selecting secure request.");
   RequestsMenu.selectedIndex = 0;
 
   info("Selecting security tab.");
   NetworkDetails.widget.selectedIndex = 5;
 
+  wait = monitor.panelWin.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
   info("Selecting insecure request.");
   RequestsMenu.selectedIndex = 1;
 
   info("Waiting for security tab to be updated.");
-  yield monitor.panelWin.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
+  yield wait;
 
   is(NetworkDetails.widget.selectedIndex, 0,
     "Selected tab was reset when selected security tab was hidden.");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_status-codes.js
+++ b/devtools/client/netmonitor/test/browser_net_status-codes.js
@@ -151,24 +151,23 @@ add_task(function* () {
 
   /**
    * A function that tests "Summary" contains correct information.
    */
   function* testSummary(data) {
     let tabpanel = document.querySelectorAll("#details-pane tabpanel")[0];
 
     let { method, uri, details: { status, statusText } } = data;
-    is(tabpanel.querySelector("#headers-summary-url-value").getAttribute("value"),
-      uri, "The url summary value is incorrect.");
-    is(tabpanel.querySelector("#headers-summary-method-value").getAttribute("value"),
-      method, "The method summary value is incorrect.");
-    is(tabpanel.querySelector("#headers-summary-status-circle").getAttribute("data-code"),
-      status, "The status summary code is incorrect.");
-    is(tabpanel.querySelector("#headers-summary-status-value").getAttribute("value"),
-      status + " " + statusText, "The status summary value is incorrect.");
+    let summaryValues = tabpanel.querySelectorAll(".tabpanel-summary-value.textbox-input");
+    is(summaryValues[0].value, uri, "The url summary value is incorrect.");
+    is(summaryValues[1].value, method, "The method summary value is incorrect.");
+    is(tabpanel.querySelector(".requests-menu-status-icon").dataset.code, status,
+      "The status summary code is incorrect.");
+    is(summaryValues[3].value, status + " " + statusText,
+      "The status summary value is incorrect.");
   }
 
   /**
    * A function that tests "Params" tab contains correct information.
    */
   function* testParams(data) {
     let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
     let statusParamValue = data.uri.split("=").pop();
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -655,20 +655,16 @@
 .tabpanel-content {
   background-color: var(--theme-sidebar-background);
 }
 
 .theme-dark .tabpanel-content {
   color: var(--theme-selection-color);
 }
 
-#headers-tabpanel {
-  background-color: var(--theme-toolbar-background);
-}
-
 .theme-firebug .variables-view-scope:focus > .title {
   color: var(--theme-body-color);
 }
 
 /* Summary tabpanel */
 
 .tabpanel-summary-container {
   padding: 1px;
@@ -687,16 +683,20 @@
 }
 
 .theme-dark .tabpanel-summary-value {
   color: var(--theme-selection-color);
 }
 
 /* Headers tabpanel */
 
+#headers-tabpanel .summary {
+  background-color: var(--theme-toolbar-background);
+}
+
 #headers-summary-status,
 #headers-summary-version {
   padding-bottom: 2px;
 }
 
 #headers-summary-size {
   padding-top: 2px;
 }
@@ -1097,42 +1097,43 @@
   content: "";
 }
 
 /* Layout additional warning icon in tree value cell  */
 .security-info-value {
   display: flex;
 }
 
-.treeTable .textbox-input {
+.textbox-input {
   text-overflow: ellipsis;
   border: none;
   background: none;
   color: inherit;
   width: 100%;
-  margin-inline-end: 2px;
 }
 
 .treeTable .textbox-input:focus {
   outline: 0;
   box-shadow: var(--theme-focus-box-shadow-textbox);
 }
 
-.treeTable .treeLabel {
-  font-weight: 600;
-}
-
 .properties-view {
   /* FIXME: Minus 24px * 2 for toolbox height + panel height
    * Give a fixed panel container height in order to force tree view scrollable */
   height: calc(100vh - 48px);
   display: flex;
   flex-direction: column;
 }
 
+#headers-tabpanel .properties-view {
+  /* FIXME: Minus 24px * 2 + 87.5 for toolbox height + panel height + headers summary
+   * Give a fixed panel container height in order to force tree view scrollable */
+  height: calc(100vh - 135.5px);
+}
+
 .properties-view .searchbox-section {
   flex: 0 1 auto;
 }
 
 .properties-view .devtools-searchbox {
   padding: 0;
 }
 
@@ -1172,43 +1173,104 @@
 }
 
 .tree-container .treeTable .treeValueCell {
   /* FIXME: Make value cell can be reduced to shorter width */
   max-width: 0;
   padding-inline-end: 5px;
 }
 
+.headers-summary input:not([type="button"]) {
+  width: 100%;
+  background: none;
+  border: none;
+  color: inherit;
+  margin-inline-end: 2px;
+}
+
+.headers-summary input:not([type="button"]):focus {
+  outline: none;
+  box-shadow: var(--theme-focus-box-shadow-textbox);
+  transition: all 0.2s ease-in-out;
+}
+
+.headers-summary-label,
 .tree-container .objectBox {
   white-space: nowrap;
 }
 
+.headers-summary,
+.response-summary {
+  display: flex;
+  align-items: center;
+}
+
+.headers-summary .tool-button {
+  background-color: rgba(0,0,0,0.2);
+  border: 1px solid transparent;
+  color: var(--theme-body-color);
+  transition: background 0.05s ease-in-out;
+  margin-inline-end: 6px;
+  padding: 0 5px;
+}
+
+.headers-summary .tool-button:hover {
+  background-color: rgba(0,0,0,0.3);
+}
+
+.headers-summary .tool-button:hover:active {
+  background-color: rgba(0,0,0,0.4);
+}
+
+.headers-summary .requests-menu-status-icon {
+  min-width: 10px;
+}
+
+.headers-summary .raw-headers-container {
+  flex: 1;
+  margin-inline-end: 8px;
+}
+
+.headers-summary .raw-headers-container textarea {
+  width: 100%;
+  height: 50vh;
+  margin: 5px 4px;
+  font: message-box;
+}
+
+.empty-notice {
+  color: var(--theme-body-color-alt);
+  padding: 3px 8px;
+}
+
 .editor-container,
 .editor-mount,
 .editor-mount iframe {
   border: none;
   width: 100%;
   height: 100%;
 }
 
 /*
  * FIXME: normal html block element cannot fill outer XUL element
  * This workaround should be removed after netmonitor is migrated to react
  */
+#react-headers-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-security-tabpanel-hook,
 #react-timings-tabpanel-hook,
 #network-statistics-charts,
 #primed-cache-chart,
 #empty-cache-chart {
   display: -moz-box;
   -moz-box-flex: 1;
 }
 
 /* For vbox */
+#react-headers-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-security-tabpanel-hook,
 #react-timings-tabpanel-hook,
 #primed-cache-chart,
 #empty-cache-chart {
   -moz-box-orient: vertical;
 }