Bug 1317648 - Implement Headers Panel r?honza,jsnajdr draft
authorRicky Chien <rchien@mozilla.com>
Sat, 31 Dec 2016 21:03:18 +0800
changeset 459671 252be0210df2855cf0091c3ec0a28f6e31468320
parent 459611 97d6f73643940256c0eb61e384c49bf6f6c49847
child 541970 60155903780f0244a383c663c432fa99425de248
push id41301
push userbmo:rchien@mozilla.com
push dateThu, 12 Jan 2017 13:44:49 +0000
reviewershonza, jsnajdr
bugs1317648
milestone53.0a1
Bug 1317648 - Implement Headers Panel r?honza,jsnajdr MozReview-Commit-ID: 74kBBM4YsJR
devtools/client/framework/sidebar.js
devtools/client/netmonitor/details-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/request-utils.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-tab-deselect.js
devtools/client/netmonitor/test/browser_net_status-codes.js
devtools/client/netmonitor/utils/format-utils.js
devtools/client/themes/netmonitor.css
--- a/devtools/client/framework/sidebar.js
+++ b/devtools/client/framework/sidebar.js
@@ -426,17 +426,18 @@ ToolSidebar.prototype = {
   },
 
   /**
    * Return the tab based on the provided id, if one was registered with this id.
    * @param {String} id
    * @return {DOMNode}
    */
   getTab: function (id) {
-    return this._tabs.get(id);
+    // FIXME: A workaround for broken browser_net_raw_headers.js failure only in non-e10s mode
+    return this._tabs && this._tabs.get(id);
   },
 
   /**
    * Event handler.
    */
   handleEvent: function (event) {
     if (event.type !== "select" || this._destroyed) {
       return;
--- a/devtools/client/netmonitor/details-view.js
+++ b/devtools/client/netmonitor/details-view.js
@@ -11,29 +11,26 @@ const promise = require("promise");
 const EventEmitter = require("devtools/shared/event-emitter");
 const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
 const { Task } = require("devtools/shared/task");
 const { ToolSidebar } = require("devtools/client/framework/sidebar");
 const { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
 const { EVENTS } = require("./events");
 const { L10N } = require("./l10n");
 const { Filters } = require("./filter-predicates");
-const {
-  decodeUnicodeUrl,
-} = 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 ParamsPanel = createFactory(require("./shared/components/params-panel"));
 const PreviewPanel = createFactory(require("./shared/components/preview-panel"));
 const ResponsePanel = createFactory(require("./shared/components/response-panel"));
 const SecurityPanel = createFactory(require("./shared/components/security-panel"));
 const TimingsPanel = createFactory(require("./shared/components/timings-panel"));
 
-const HEADERS_SIZE_DECIMALS = 3;
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
   // ms
   lazyEmptyDelay: 10,
   searchEnabled: true,
   editableValueTooltip: "",
   editableNameTooltip: "",
   preventDisableOnChange: true,
@@ -68,16 +65,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._paramsPanelNode = $("#react-params-tabpanel-hook");
 
     ReactDOM.render(Provider(
       { store },
       ParamsPanel()
     ), this._paramsPanelNode);
 
     this._previewPanelNode = $("#react-preview-tabpanel-hook");
@@ -109,42 +113,35 @@ 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._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._paramsPanelNode);
+    ReactDOM.unmountComponentAtNode(this._headersPanelNode);
     ReactDOM.unmountComponentAtNode(this._previewPanelNode);
     ReactDOM.unmountComponentAtNode(this._responsePanelNode);
     ReactDOM.unmountComponentAtNode(this._securityPanelNode);
     ReactDOM.unmountComponentAtNode(this._timingsPanelNode);
     this.sidebar.destroy();
     $("tabpanels", this.widget).removeEventListener("select",
       this._onTabSelect);
   },
@@ -153,18 +150,16 @@ DetailsView.prototype = {
    * Populates this view with the specified data.
    *
    * @param object data
    *        The data source (this should be the attachment of a request item).
    * @return object
    *        Returns a promise that resolves upon population the view.
    */
   populate: function (data) {
-    $("#raw-headers").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
     //   1) are https (state != insecure)
     //   2) come from a target that provides security information.
@@ -177,17 +172,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._dataSrc = { src: data, populated: [] };
     this._onTabSelect();
     window.emit(EVENTS.NETWORKDETAILSVIEW_POPULATED);
 
     return promise.resolve();
   },
@@ -214,24 +208,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;
       }
       viewState.updating[tab] = false;
     }).then(() => {
@@ -249,140 +235,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) {
@@ -441,17 +303,14 @@ DetailsView.prototype = {
       }
       cookieVar.populate(rawObject);
       cookieVar.twisty = true;
       cookieVar.expanded = true;
     }
   }),
 
   _dataSrc: null,
-  _headers: null,
   _cookies: null,
-  _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/request-utils.js
+++ b/devtools/client/netmonitor/request-utils.js
@@ -73,16 +73,31 @@ const getFormDataSections = Task.async(f
       }
     }
   }
 
   return formDataSections;
 });
 
 /**
+ * Fetch headers full content from actor server
+ *
+ * @param {object} headers - a object presents headers data
+ * @param {function} getString - callback to retrieve a string from a LongStringGrip
+ * @return {object} a headers object with updated content payload
+ */
+const fetchHeaders = Task.async(function* (headers, getString) {
+  for (let { value } of headers.headers) {
+    headers.headers.value = yield getString(value);
+  }
+
+  return headers;
+});
+
+/**
  * Form a data: URI given a mime type, encoding, and some text.
  *
  * @param {string} mimeType - mime type
  * @param {string} encoding - encoding to use; if not set, the
  *                            text will be base64-encoded.
  * @param {string} text - text of the URI.
  * @return {string} a data URI
  */
@@ -236,16 +251,17 @@ function parseQueryString(query) {
       value: param[1] ? decodeUnicodeUrl(param[1]) : "",
     };
   });
 }
 
 module.exports = {
   getKeyWithEvent,
   getFormDataSections,
+  fetchHeaders,
   formDataURI,
   writeHeaderText,
   decodeUnicodeUrl,
   getAbbreviatedMimeType,
   getUrlBaseName,
   getUrlQuery,
   getUrlBaseNameWithQuery,
   getUrlHostName,
--- a/devtools/client/netmonitor/requests-menu-view.js
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -16,18 +16,18 @@ const { createElement, createFactory } =
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 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 {
+  fetchHeaders,
   formDataURI,
-  writeHeaderText,
   getFormDataSections,
 } = require("./request-utils");
 
 const {
   getActiveFilters,
   getSortedRequests,
   getDisplayedRequests,
   getRequestById,
@@ -136,21 +136,16 @@ RequestsMenuView.prototype = {
             ));
           });
         }
       },
     ));
 
     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);
@@ -167,20 +162,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");
@@ -188,20 +179,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);
   },
@@ -244,17 +231,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) {
+      let headers = yield fetchHeaders(
+        requestHeaders, gNetwork.getString.bind(gNetwork));
+      if (headers) {
+        yield this.store.dispatch(Actions.updateRequest(
+          action.id,
+          { requestHeaders: headers },
+          true,
+        ));
+      }
+    }
+
+    if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
+      let headers = yield fetchHeaders(
+        responseHeaders, gNetwork.getString.bind(gNetwork));
+      if (headers) {
+        yield this.store.dispatch(Actions.updateRequest(
+          action.id,
+          { responseHeaders: headers },
+          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 = {};
@@ -375,38 +391,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,231 @@
+/* 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 Actions = require("../../actions/index");
+const { getSelectedRequest } = require("../../selectors/index");
+const { writeHeaderText } = require("../../request-utils");
+const { getFormattedSize } = require("../../utils/format-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,
+    request: PropTypes.object,
+  },
+
+  getInitialState() {
+    return {
+      rawHeadersOpened: false,
+    };
+  },
+
+  getProperties(headers, title) {
+    if (headers && headers.headers.length) {
+      return {
+        [`${title} (${getFormattedSize(headers.headersSize, 3)})`]:
+          headers.headers.reduce((acc, { name, value }) =>
+            name ? Object.assign(acc, { [name]: value }) : acc
+          , {})
+      };
+    }
+
+    return null;
+  },
+
+  toggleRawHeaders() {
+    this.setState({
+      rawHeadersOpened: !this.state.rawHeadersOpened,
+    });
+  },
+
+  renderSummary(label, value) {
+    return (
+      div({ className: "tabpanel-summary-container headers-summary" },
+        div({
+          className: "tabpanel-summary-label headers-summary-label",
+        }, label),
+        input({
+          className: "tabpanel-summary-value textbox-input devtools-monospace",
+          readOnly: true,
+          value,
+        }),
+      )
+    );
+  },
+
+  render() {
+    const {
+      cloneSelectedRequest,
+      request: {
+        fromCache,
+        fromServiceWorker,
+        httpVersion,
+        method,
+        remoteAddress,
+        remotePort,
+        requestHeaders,
+        requestHeadersFromUploadStream: uploadHeaders,
+        responseHeaders,
+        status,
+        statusText,
+        urlDetails,
+      },
+    } = this.props;
+
+    if ((!requestHeaders || !requestHeaders.headers.length) &&
+        (!uploadHeaders || !uploadHeaders.headers.length) &&
+        (!responseHeaders || !responseHeaders.headers.length)) {
+      return div({ className: "empty-notice" },
+        HEADERS_EMPTY_TEXT
+      );
+    }
+
+    let object = Object.assign({},
+      this.getProperties(responseHeaders, RESPONSE_HEADERS),
+      this.getProperties(requestHeaders, REQUEST_HEADERS),
+      this.getProperties(uploadHeaders, REQUEST_HEADERS_FROM_UPLOAD),
+    );
+
+    let summaryUrl = urlDetails.unicodeUrl ?
+      this.renderSummary(SUMMARY_URL, urlDetails.unicodeUrl) : null;
+
+    let summaryMethod = method ?
+      this.renderSummary(SUMMARY_METHOD, method) : null;
+
+    let summaryAddress = remoteAddress ?
+      this.renderSummary(SUMMARY_ADDRESS,
+        remotePort ? `${remoteAddress}:${remotePort}` : remoteAddress) : null;
+
+    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 = httpVersion ?
+      this.renderSummary(SUMMARY_VERSION, httpVersion) : null;
+
+    let summaryRawHeaders;
+    if (this.state.rawHeadersOpened) {
+      summaryRawHeaders = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({ className: "raw-headers-container" },
+            div({ className: "raw-headers" },
+              div({ className: "tabpanel-summary-label" }, RAW_HEADERS_REQUEST),
+              textarea({
+                value: writeHeaderText(requestHeaders.headers),
+                readOnly: true,
+              }),
+            ),
+            div({ className: "raw-headers" },
+              div({ className: "tabpanel-summary-label" }, RAW_HEADERS_RESPONSE),
+              textarea({
+                value: writeHeaderText(responseHeaders.headers),
+                readOnly: true,
+              }),
+            ),
+          )
+        )
+      );
+    }
+
+    return (
+      div({},
+        div({ className: "summary" },
+          summaryUrl,
+          summaryMethod,
+          summaryAddress,
+          summaryStatus,
+          summaryVersion,
+          summaryRawHeaders,
+        ),
+        PropertiesView({
+          object,
+          filterPlaceHolder: HEADERS_FILTER_TEXT,
+          sectionNames: Object.keys(object),
+        }),
+      )
+    );
+  }
+});
+
+module.exports = connect(
+  (state) => ({
+    request: getSelectedRequest(state) || {},
+  }),
+  (dispatch) => ({
+    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+  }),
+)(HeadersPanel);
--- a/devtools/client/netmonitor/shared/components/moz.build
+++ b/devtools/client/netmonitor/shared/components/moz.build
@@ -1,13 +1,14 @@
 # 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',
     'params-panel.js',
     'preview-panel.js',
     'properties-view.js',
     'response-panel.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
@@ -21,52 +21,46 @@ add_task(function* () {
 
   let wait = waitForNetworkEvents(monitor, 0, 1);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   // Wait for all tree view updated by react
-  wait = waitForDOM(document, "#headers-tabpanel .variables-view-scope", 3);
+  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(tabpanel.querySelectorAll(".tree-section .treeLabel").length, 3,
+    "There should be 3 header sections displayed in this tabpanel.");
 
-  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(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.");
+    L10N.getFormatStr("networkMenu.sizeB", 74) + ")",
+    "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.");
 
   // Wait for all tree sections updated by react
   wait = waitForDOM(document, "#params-tabpanel .tree-section");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll("#details-pane tab")[2]);
   yield wait;
 
   tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
@@ -75,19 +69,19 @@ add_task(function* () {
     "The params tree view should be displayed.");
   ok(tabpanel.querySelector(".editor-mount") === null,
     "The post data shouldn't be displayed.");
 
   is(tabpanel.querySelector(".tree-section .treeLabel").textContent,
     L10N.getStr("paramsFormData"),
     "The form data section doesn't have the correct title.");
 
-  let labels = tabpanel
+  labels = tabpanel
     .querySelectorAll("tr:not(.tree-section) .treeLabelCell .treeLabel");
-  let values = tabpanel
+  values = tabpanel
     .querySelectorAll("tr:not(.tree-section) .treeValueCell .objectBox");
 
   is(labels[0].textContent, "foo", "The first payload param name was incorrect.");
   is(values[0].textContent, "\"bar\"", "The first payload param value was incorrect.");
   is(labels[1].textContent, "baz", "The second payload param name was incorrect.");
   is(values[1].textContent, "\"123\"", "The second payload param value was incorrect.");
 
   return teardown(monitor);
--- 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, "#headers-tabpanel .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-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
@@ -150,24 +150,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/netmonitor/utils/format-utils.js
+++ b/devtools/client/netmonitor/utils/format-utils.js
@@ -9,36 +9,35 @@ const { L10N } = require("../l10n");
 // Constants for formatting bytes.
 const BYTES_IN_KB = 1024;
 const BYTES_IN_MB = Math.pow(BYTES_IN_KB, 2);
 const BYTES_IN_GB = Math.pow(BYTES_IN_KB, 3);
 const MAX_BYTES_SIZE = 1000;
 const MAX_KB_SIZE = 1000 * BYTES_IN_KB;
 const MAX_MB_SIZE = 1000 * BYTES_IN_MB;
 
-const CONTENT_SIZE_DECIMALS = 2;
-
 /**
  * Get a human-readable string from a number of bytes, with the B, KB, MB, or
  * GB value. Note that the transition between abbreviations is by 1000 rather
  * than 1024 in order to keep the displayed digits smaller as "1016 KB" is
  * more awkward than 0.99 MB"
  */
-function getFormattedSize(bytes) {
+function getFormattedSize(bytes, decimals = 2) {
   if (bytes < MAX_BYTES_SIZE) {
     return L10N.getFormatStr("networkMenu.sizeB", bytes);
   } else if (bytes < MAX_KB_SIZE) {
     let kb = bytes / BYTES_IN_KB;
-    let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
+    let size = L10N.numberWithDecimals(kb, decimals);
     return L10N.getFormatStr("networkMenu.sizeKB", size);
   } else if (bytes < MAX_MB_SIZE) {
     let mb = bytes / BYTES_IN_MB;
-    let size = L10N.numberWithDecimals(mb, CONTENT_SIZE_DECIMALS);
+    let size = L10N.numberWithDecimals(mb, decimals);
     return L10N.getFormatStr("networkMenu.sizeMB", size);
   }
   let gb = bytes / BYTES_IN_GB;
-  let size = L10N.numberWithDecimals(gb, CONTENT_SIZE_DECIMALS);
+  let size = L10N.numberWithDecimals(gb, decimals);
   return L10N.getFormatStr("networkMenu.sizeGB", size);
 }
 
 module.exports = {
-  getFormattedSize
+  getFormattedSize,
 };
+
--- 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,37 +683,18 @@
 }
 
 .theme-dark .tabpanel-summary-value {
   color: var(--theme-selection-color);
 }
 
 /* Headers tabpanel */
 
-#headers-summary-status,
-#headers-summary-version {
-  padding-bottom: 2px;
-}
-
-#headers-summary-size {
-  padding-top: 2px;
-}
-
-#headers-summary-resend {
-  margin-top: -10px;
-  margin-inline-end: 6px;
-}
-
-#toggle-raw-headers {
-  margin-top: -10px;
-  margin-inline-end: 6px;
-}
-
-.raw-response-textarea {
-  height: 50vh;
+#headers-tabpanel .summary {
+  background-color: var(--theme-toolbar-background);
 }
 
 /* Response tabpanel */
 
 .response-error-header {
   margin: 0;
   padding: 3px 8px;
   background-color: var(--theme-highlight-red);
@@ -1105,17 +1082,17 @@
   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%;
 }
 
 .treeTable .textbox-input:focus {
@@ -1126,16 +1103,22 @@
 .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;
 }
 
@@ -1180,20 +1163,99 @@
 }
 
 .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 {
+  border: 1px solid transparent;
+  color: var(--theme-body-color);
+  transition: background 0.05s ease-in-out;
+  margin-inline-end: 6px;
+  padding: 0 5px;
+}
+
+.theme-light .headers-summary .tool-button {
+  background-color: var(--toolbar-tab-hover);
+}
+
+.theme-light .headers-summary .tool-button:hover {
+  background-color: rgba(170, 170, 170, 0.3);
+}
+
+.theme-light .headers-summary .tool-button:hover:active {
+  background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-dark .headers-summary .tool-button {
+  background-color: rgba(0, 0, 0, 0.2);
+}
+
+.theme-dark .headers-summary .tool-button:hover {
+  background-color: rgba(0, 0, 0, 0.3);
+}
+
+.theme-dark .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 {
+  display: flex;
+  width: 100%;
+}
+
+.headers-summary .raw-headers {
+  width: 50%;
+  padding: 0px 4px;
+}
+
+.headers-summary .raw-headers textarea {
+  width: 100%;
+  height: 50vh;
+  font: message-box;
+  resize: none;
+  box-sizing: border-box;
+}
+
+.headers-summary .raw-headers .tabpanel-summary-label {
+  padding: 0 0 4px 0;
+}
+
 .empty-notice {
   color: var(--theme-body-color-alt);
   padding: 3px 8px;
 }
 
 .response-summary {
   display: flex;
 }
@@ -1205,29 +1267,31 @@
   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-params-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-response-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-params-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-response-tabpanel-hook,
 #react-security-tabpanel-hook,
 #react-timings-tabpanel-hook,
 #primed-cache-chart,
 #empty-cache-chart {
   -moz-box-orient: vertical;