Bug 1317650 - Implement Params Panel r?jsnajdr,honza draft
authorRicky Chien <rchien@mozilla.com>
Sun, 08 Jan 2017 17:54:53 +0800
changeset 457685 b6d1f9909c90972fcbc0f1aa7c22005cd0c17368
parent 457629 701868bfddcba5bdec516be33a86dcd525dc74cf
child 541563 4295fbdc9070cc006d18e9368e1c666e409a5a04
push id40864
push userbmo:rchien@mozilla.com
push dateMon, 09 Jan 2017 16:23:06 +0000
reviewersjsnajdr, honza
bugs1317650
milestone53.0a1
Bug 1317650 - Implement Params Panel r?jsnajdr,honza MozReview-Commit-ID: A0GjfZeSOJi
devtools/client/framework/test/shared-head.js
devtools/client/netmonitor/constants.js
devtools/client/netmonitor/details-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/reducers/requests.js
devtools/client/netmonitor/requests-menu-view.js
devtools/client/netmonitor/shared/components/moz.build
devtools/client/netmonitor/shared/components/params-panel.js
devtools/client/netmonitor/shared/components/properties-view.js
devtools/client/netmonitor/test/browser_net_cached-status.js
devtools/client/netmonitor/test/browser_net_complex-params.js
devtools/client/netmonitor/test/browser_net_post-data-01.js
devtools/client/netmonitor/test/browser_net_post-data-02.js
devtools/client/netmonitor/test/browser_net_post-data-03.js
devtools/client/netmonitor/test/browser_net_post-data-04.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
@@ -251,16 +251,51 @@ function waitForNEvents(target, eventNam
       break;
     }
   }
 
   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
  * @param {String} eventName
  * @param {Boolean} useCapture
  *        Optional, for addEventListener/removeEventListener
--- a/devtools/client/netmonitor/constants.js
+++ b/devtools/client/netmonitor/constants.js
@@ -1,17 +1,17 @@
 /* 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,
+  FILTER_SEARCH_DELAY: 200,
   REQUEST_TIME_DECIMALS: 2,
 };
 
 const actionTypes = {
   ADD_REQUEST: "ADD_REQUEST",
   ADD_TIMING_MARKER: "ADD_TIMING_MARKER",
   BATCH_ACTIONS: "BATCH_ACTIONS",
   BATCH_ENABLE: "BATCH_ENABLE",
--- a/devtools/client/netmonitor/details-view.js
+++ b/devtools/client/netmonitor/details-view.js
@@ -16,24 +16,22 @@ const { ToolSidebar } = require("devtool
 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 ParamsPanel = createFactory(require("./shared/components/params-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 = {
@@ -89,16 +87,23 @@ DetailsView.prototype = {
   },
 
   /**
    * Initialization function, called when the network monitor is started.
    */
   initialize: function (store) {
     dumpn("Initializing the DetailsView");
 
+    this._paramsPanelNode = $("#react-params-tabpanel-hook");
+
+    ReactDOM.render(Provider(
+      { store },
+      ParamsPanel()
+    ), this._paramsPanelNode);
+
     this._previewPanelNode = $("#react-preview-tabpanel-hook");
 
     ReactDOM.render(Provider(
       { store },
       PreviewPanel()
     ), this._previewPanelNode);
 
     this._securityPanelNode = $("#react-security-tabpanel-hook");
@@ -126,45 +131,38 @@ DetailsView.prototype = {
         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"),
-        searchPlaceholder: L10N.getStr("paramsFilterText")
-      }));
     this._json = new VariablesView($("#response-content-json"),
       Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
         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._paramsPanelNode);
     ReactDOM.unmountComponentAtNode(this._previewPanelNode);
     ReactDOM.unmountComponentAtNode(this._securityPanelNode);
     ReactDOM.unmountComponentAtNode(this._timingsPanelNode);
     this.sidebar.destroy();
     $("tabpanels", this.widget).removeEventListener("select",
       this._onTabSelect);
   },
 
@@ -172,19 +170,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) {
-    $("#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);
 
@@ -205,17 +200,16 @@ DetailsView.prototype = {
     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);
 
     return promise.resolve();
   },
@@ -255,24 +249,16 @@ DetailsView.prototype = {
             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);
-          yield view._setRequestPostParams(
-            src.requestHeaders,
-            src.requestHeadersFromUploadStream,
-            src.requestPostData);
-          break;
         // "Response"
         case 3:
           yield view._setResponseBody(src.url, src.responseContent);
           break;
       }
       viewState.updating[tab] = false;
     }).then(() => {
       if (tab == this.widget.selectedIndex) {
@@ -481,118 +467,16 @@ DetailsView.prototype = {
       }
       cookieVar.populate(rawObject);
       cookieVar.twisty = true;
       cookieVar.expanded = true;
     }
   }),
 
   /**
-   * Sets the network request get params shown in this view.
-   *
-   * @param string url
-   *        The request's url.
-   */
-  _setRequestGetParams: function (url) {
-    let query = getUrlQuery(url);
-    if (query) {
-      this._addParams(this._paramsQueryString, query);
-    }
-  },
-
-  /**
-   * Sets the network request post params shown in this view.
-   *
-   * @param object headers
-   *        The "requestHeaders" message received from the server.
-   * @param object uploadHeaders
-   *        The "requestHeadersFromUploadStream" inferred from the POST payload.
-   * @param object postData
-   *        The "requestPostData" message received from the server.
-   * @return object
-   *        A promise that is resolved when the request post params are set.
-   */
-  _setRequestPostParams: Task.async(function* (headers, uploadHeaders,
-    postData) {
-    if (!headers || !uploadHeaders || !postData) {
-      return;
-    }
-
-    let formDataSections = yield getFormDataSections(
-      headers,
-      uploadHeaders,
-      postData,
-      gNetwork.getString.bind(gNetwork));
-
-    this._params.onlyEnumVisible = false;
-
-    // Handle urlencoded form data sections (e.g. "?foo=bar&baz=42").
-    if (formDataSections.length > 0) {
-      formDataSections.forEach(section => {
-        this._addParams(this._paramsFormData, section);
-      });
-    } else {
-      // Handle JSON and actual forms ("multipart/form-data" content type).
-      let postDataLongString = postData.postData.text;
-      let text = yield gNetwork.getString(postDataLongString);
-      let jsonVal = null;
-      try {
-        jsonVal = JSON.parse(text);
-      } catch (ex) { // eslint-disable-line
-      }
-
-      if (jsonVal) {
-        this._params.onlyEnumVisible = true;
-        let jsonScopeName = L10N.getStr("jsonScopeName");
-        let jsonScope = this._params.addScope(jsonScopeName);
-        jsonScope.expanded = true;
-        let jsonItem = jsonScope.addItem(undefined, { enumerable: true });
-        jsonItem.populate(jsonVal, { sorted: true });
-      } else {
-        // This is really awkward, but hey, it works. Let's show an empty
-        // scope in the params view and place the source editor containing
-        // the raw post data directly underneath.
-        $("#request-params-box").removeAttribute("flex");
-        let paramsScope = this._params.addScope(this._paramsPostPayload);
-        paramsScope.expanded = true;
-        paramsScope.locked = true;
-
-        $("#request-post-data-textarea-box").hidden = false;
-        let editor = yield NetMonitorView.editor("#request-post-data-textarea");
-        editor.setMode(Editor.modes.text);
-        editor.setText(text);
-      }
-    }
-
-    window.emit(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
-  }),
-
-  /**
-   * Populates the params container in this view with the specified data.
-   *
-   * @param string name
-   *        The type of params to populate (get or post).
-   * @param string queryString
-   *        A query string of params (e.g. "?foo=bar&baz=42").
-   */
-  _addParams: function (name, queryString) {
-    let paramsArray = parseQueryString(queryString);
-    if (!paramsArray) {
-      return;
-    }
-    let paramsScope = this._params.addScope(name);
-    paramsScope.expanded = true;
-
-    for (let param of paramsArray) {
-      let paramVar = paramsScope.addItem(param.name, {}, {relaxed: true});
-      paramVar.setGrip(param.value);
-    }
-  },
-
-  /**
    * Sets the network response body shown in this view.
    *
    * @param string url
    *        The request's url.
    * @param object response
    *        The message received from the server.
    * @return object
    *         A promise that is resolved when the response body is set.
@@ -700,20 +584,16 @@ 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
@@ -207,24 +207,18 @@
               <tabpanel id="cookies-tabpanel"
                         class="tabpanel-content">
                 <vbox flex="1">
                   <vbox id="all-cookies" flex="1"/>
                 </vbox>
               </tabpanel>
               <tabpanel id="params-tabpanel"
                         class="tabpanel-content">
-                <vbox flex="1">
-                  <vbox id="request-params-box" flex="1" hidden="true">
-                    <vbox id="request-params" flex="1"/>
-                  </vbox>
-                  <vbox id="request-post-data-textarea-box" flex="1" hidden="true">
-                    <vbox id="request-post-data-textarea" flex="1"/>
-                  </vbox>
-                </vbox>
+                <html:div xmlns="http://www.w3.org/1999/xhtml"
+                          id="react-params-tabpanel-hook"/>
               </tabpanel>
               <tabpanel id="response-tabpanel"
                         class="tabpanel-content">
                 <vbox flex="1">
                   <label id="response-content-info-header"/>
                   <vbox id="response-content-json-box" flex="1" hidden="true">
                     <vbox id="response-content-json" flex="1" context="network-response-popup" />
                   </vbox>
--- a/devtools/client/netmonitor/reducers/requests.js
+++ b/devtools/client/netmonitor/reducers/requests.js
@@ -9,17 +9,17 @@ const { getUrlDetails } = require("../re
 const {
   ADD_REQUEST,
   UPDATE_REQUEST,
   CLEAR_REQUESTS,
   SELECT_REQUEST,
   PRESELECT_REQUEST,
   CLONE_SELECTED_REQUEST,
   REMOVE_SELECTED_CUSTOM_REQUEST,
-  OPEN_SIDEBAR
+  OPEN_SIDEBAR,
 } = require("../constants");
 
 const Request = I.Record({
   id: null,
   // Set to true in case of a request that's being edited as part of "edit and resend"
   isCustom: false,
   // Request properties - at the beginning, they are unknown and are gradually filled in
   startedMillis: undefined,
@@ -32,30 +32,31 @@ const Request = I.Record({
   cause: undefined,
   fromCache: undefined,
   fromServiceWorker: undefined,
   status: undefined,
   statusText: undefined,
   httpVersion: undefined,
   securityState: undefined,
   securityInfo: undefined,
-  mimeType: undefined,
+  mimeType: "text/plain",
   contentSize: undefined,
   transferredSize: undefined,
   totalTime: undefined,
   eventTimings: undefined,
   headersSize: undefined,
   requestHeaders: undefined,
   requestHeadersFromUploadStream: undefined,
   requestCookies: undefined,
   requestPostData: undefined,
   responseHeaders: undefined,
   responseCookies: undefined,
   responseContent: undefined,
   responseContentDataUri: undefined,
+  formDataSections: undefined,
 });
 
 const Requests = I.Record({
   // The request list
   requests: I.List(),
   // Selection state
   selectedId: null,
   preselectedId: null,
@@ -82,17 +83,18 @@ const UPDATE_PROPS = [
   "headersSize",
   "requestHeaders",
   "requestHeadersFromUploadStream",
   "requestCookies",
   "requestPostData",
   "responseHeaders",
   "responseCookies",
   "responseContent",
-  "responseContentDataUri"
+  "responseContentDataUri",
+  "formDataSections",
 ];
 
 function requestsReducer(state = new Requests(), action) {
   switch (action.type) {
     case ADD_REQUEST: {
       return state.withMutations(st => {
         let newRequest = new Request(Object.assign(
           { id: action.id },
@@ -134,23 +136,16 @@ function requestsReducer(state = new Req
 
           request[key] = value;
 
           switch (key) {
             case "url":
               // Compute the additional URL details
               request.urlDetails = getUrlDetails(value);
               break;
-            case "responseContent":
-              // If there's no mime type available when the response content
-              // is received, assume text/plain as a fallback.
-              if (!request.mimeType) {
-                request.mimeType = "text/plain";
-              }
-              break;
             case "totalTime":
               const endedMillis = request.startedMillis + value;
               lastEndedMillis = Math.max(lastEndedMillis, endedMillis);
               break;
             case "requestPostData":
               request.requestHeadersFromUploadStream = {
                 headers: [],
                 headersSize: 0,
--- a/devtools/client/netmonitor/requests-menu-view.js
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -18,25 +18,26 @@ const { Provider } = require("devtools/c
 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,
+  getFormDataSections,
 } = require("./request-utils");
 
 const {
   getActiveFilters,
   getSortedRequests,
   getDisplayedRequests,
   getRequestById,
-  getSelectedRequest
+  getSelectedRequest,
 } = require("./selectors/index");
 
 // ms
 const RESIZE_REFRESH_RATE = 50;
 
 // A smart store watcher to notify store changes as necessary
 function storeWatcher(initialValue, reduceValue, onChange) {
   let currentValue = initialValue;
@@ -83,16 +84,67 @@ RequestsMenuView.prototype = {
 
     // Watch the sidebar status and resize the waterfall column on change
     this.store.subscribe(storeWatcher(
       false,
       () => this.store.getState().ui.sidebarOpen,
       () => this.onResize()
     ));
 
+    // Watch the requestHeaders, requestHeadersFromUploadStream and requestPostData
+    // in order to update formDataSections for composing form data
+    this.store.subscribe(storeWatcher(
+      false,
+      (currentRequest) => {
+        const request = getSelectedRequest(this.store.getState());
+        if (!request) {
+          return {};
+        }
+
+        const isChanged = request.requestHeaders !== currentRequest.requestHeaders ||
+        request.requestHeadersFromUploadStream !==
+        currentRequest.requestHeadersFromUploadStream ||
+        request.requestPostData !== currentRequest.requestPostData;
+
+        if (isChanged) {
+          return {
+            id: request.id,
+            requestHeaders: request.requestHeaders,
+            requestHeadersFromUploadStream: request.requestHeadersFromUploadStream,
+            requestPostData: request.requestPostData,
+          };
+        }
+
+        return currentRequest;
+      },
+      (newRequest) => {
+        const {
+          id,
+          requestHeaders,
+          requestHeadersFromUploadStream,
+          requestPostData,
+        } = newRequest;
+
+        if (requestHeaders && requestHeadersFromUploadStream && requestPostData) {
+          getFormDataSections(
+            requestHeaders,
+            requestHeadersFromUploadStream,
+            requestPostData,
+            gNetwork.getString.bind(gNetwork),
+          ).then((formDataSections) => {
+            this.store.dispatch(Actions.updateRequest(
+              id,
+              { formDataSections },
+              true,
+            ));
+          });
+        }
+      },
+    ));
+
     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);
 
--- 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',
+    'params-panel.js',
     'preview-panel.js',
     'properties-view.js',
     'security-panel.js',
     'timings-panel.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/shared/components/params-panel.js
@@ -0,0 +1,128 @@
+/* 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 {
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../../l10n");
+const { getSelectedRequest } = require("../../selectors/index");
+const { getUrlQuery, parseQueryString } = require("../../request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./properties-view"));
+
+const { div } = DOM;
+
+const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
+const PARAMS_EMPTY_TEXT = L10N.getStr("paramsEmptyText");
+const PARAMS_FILTER_TEXT = L10N.getStr("paramsFilterText");
+const PARAMS_FORM_DATA = L10N.getStr("paramsFormData");
+const PARAMS_POST_PAYLOAD = L10N.getStr("paramsPostPayload");
+const PARAMS_QUERY_STRING = L10N.getStr("paramsQueryString");
+const SECTION_NAMES = [
+  JSON_SCOPE_NAME,
+  PARAMS_FORM_DATA,
+  PARAMS_POST_PAYLOAD,
+  PARAMS_QUERY_STRING,
+];
+
+/*
+ * Params panel component
+ * Displays the GET parameters and POST data of a request
+ */
+function ParamsPanel({
+  formDataSections,
+  mimeType,
+  postData,
+  query,
+}) {
+  if (!formDataSections && !postData && !query) {
+    return div({ className: "empty-notice" },
+      PARAMS_EMPTY_TEXT
+    );
+  }
+
+  let object = {};
+  let json;
+
+  // Query String section
+  if (query) {
+    object[PARAMS_QUERY_STRING] =
+      parseQueryString(query)
+        .reduce((acc, { name, value }) =>
+          name ? Object.assign(acc, { [name]: value }) : acc
+        , {});
+  }
+  // Form Data section
+  if (formDataSections && formDataSections.length > 0) {
+    let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
+    object[PARAMS_FORM_DATA] =
+      parseQueryString(sections)
+        .reduce((acc, { name, value }) =>
+          name ? Object.assign(acc, { [name]: value }) : acc
+        , {});
+  }
+
+  // Request payload section
+  if (formDataSections && formDataSections.length === 0 && postData) {
+    try {
+      json = JSON.parse(postData);
+    } catch (error) {
+      // Continue regardless of parsing error
+    }
+
+    if (json) {
+      object[JSON_SCOPE_NAME] = json;
+    } else {
+      object[PARAMS_POST_PAYLOAD] = {
+        EDITOR_CONFIG: {
+          text: postData,
+          mode: mimeType.replace(/;.+/, ""),
+        },
+      };
+    }
+  } else {
+    postData = "";
+  }
+
+  return (
+    PropertiesView({
+      object,
+      filterPlaceHolder: PARAMS_FILTER_TEXT,
+      sectionNames: SECTION_NAMES,
+    })
+  );
+}
+
+ParamsPanel.displayName = "ParamsPanel";
+
+ParamsPanel.propTypes = {
+  formDataSections: PropTypes.array,
+  postData: PropTypes.string,
+  query: PropTypes.string,
+};
+
+module.exports = connect(
+  (state) => {
+    const selectedRequest = getSelectedRequest(state);
+
+    if (selectedRequest) {
+      const { formDataSections, mimeType, requestPostData, url } = selectedRequest;
+
+      return {
+        formDataSections,
+        mimeType,
+        postData: requestPostData ? requestPostData.postData.text : null,
+        query: getUrlQuery(url),
+      };
+    }
+
+    return {};
+  }
+)(ParamsPanel);
--- a/devtools/client/netmonitor/shared/components/properties-view.js
+++ b/devtools/client/netmonitor/shared/components/properties-view.js
@@ -17,25 +17,27 @@ const { FILTER_SEARCH_DELAY } = require(
 // Components
 const Editor = createFactory(require("devtools/client/netmonitor/shared/components/editor"));
 const SearchBox = createFactory(require("devtools/client/shared/components/search-box"));
 const TreeView = createFactory(require("devtools/client/shared/components/tree/tree-view"));
 const TreeRow = createFactory(require("devtools/client/shared/components/tree/tree-row"));
 const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
 
 const { div, tr, td } = DOM;
+const AUTO_EXPAND_MAX_LEVEL = 7;
+const EDITOR_CONFIG_ID = "EDITOR_CONFIG";
 
 /*
  * Properties View component
  * A scrollable tree view component which provides some useful features for
  * representing object properties.
  *
  * Search filter - Set enableFilter to enable / disable SearchBox feature.
  * Tree view - Default enabled.
- * Source editor - Enable by specifying object level 1 property name to "editorText".
+ * Source editor - Enable by specifying object level 1 property name to EDITOR_CONFIG_ID.
  * Rep - Default enabled.
  */
 const PropertiesView = createClass({
   displayName: "PropertiesView",
 
   propTypes: {
     object: PropTypes.object,
     enableInput: PropTypes.bool,
@@ -71,28 +73,34 @@ const PropertiesView = createClass({
       return true;
     }
 
     let jsonString = JSON.stringify({ [name]: value }).toLowerCase();
     return jsonString.includes(filterText.toLowerCase());
   },
 
   renderRowWithEditor(props) {
-    const { level, name, value } = props.member;
-    // Display source editor when prop name specify to editorText
-    if (level === 1 && name === "editorText") {
+    const { level, name, value, path } = props.member;
+
+    // Display source editor when specifying to EDITOR_CONFIG_ID along with config
+    if (level === 1 && name === EDITOR_CONFIG_ID) {
       return (
         tr({},
           td({ colSpan: 2 },
-            Editor({ text: value })
+            Editor(value)
           )
         )
       );
     }
 
+    // Skip for editor config
+    if (level >= 1 && path.includes(EDITOR_CONFIG_ID)) {
+      return null;
+    }
+
     return TreeRow(props);
   },
 
   renderValueWithRep(props) {
     // Hide rep summary for sections
     if (props.member.level === 0) {
       return null;
     }
@@ -101,58 +109,85 @@ const PropertiesView = createClass({
       // FIXME: A workaround for the issue in StringRep
       // Force StringRep to crop the text everytime
       member: Object.assign({}, props.member, { open: false }),
       mode: MODE.TINY,
       cropLimit: 60,
     }));
   },
 
+  shouldRenderSearchBox(object) {
+    return this.props.enableFilter && object && Object.keys(object)
+      .filter((section) => !object[section][EDITOR_CONFIG_ID]).length > 0;
+  },
+
   updateFilterText(filterText) {
     this.setState({
       filterText,
     });
   },
 
+  getExpandedNodes: function (object, path = "", level = 0) {
+    if (typeof object != "object") {
+      return null;
+    }
+
+    if (level > AUTO_EXPAND_MAX_LEVEL) {
+      return null;
+    }
+
+    let expandedNodes = new Set();
+    for (let prop in object) {
+      let nodePath = path + "/" + prop;
+      expandedNodes.add(nodePath);
+
+      let nodes = this.getExpandedNodes(object[prop], nodePath, level + 1);
+      if (nodes) {
+        expandedNodes = new Set([...expandedNodes, ...nodes]);
+      }
+    }
+    return expandedNodes;
+  },
+
   render() {
     const {
       object,
       decorator,
       enableInput,
-      enableFilter,
       expandableStrings,
       filterPlaceHolder,
       renderRow,
       renderValue,
       sectionNames,
     } = this.props;
 
     return (
       div({ className: "properties-view" },
-        enableFilter && div({ className: "searchbox-section" },
-          SearchBox({
-            delay: FILTER_SEARCH_DELAY,
-            type: "filter",
-            onChange: this.updateFilterText,
-            placeholder: filterPlaceHolder,
-          }),
-        ),
+        this.shouldRenderSearchBox(object) &&
+          div({ className: "searchbox-section" },
+            SearchBox({
+              delay: FILTER_SEARCH_DELAY,
+              type: "filter",
+              onChange: this.updateFilterText,
+              placeholder: filterPlaceHolder,
+            }),
+          ),
         div({ className: "tree-container" },
           TreeView({
             object,
             columns: [{
               id: "value",
               width: "100%",
             }],
             decorator: decorator || {
               getRowClass: (rowObject) => this.getRowClass(rowObject, sectionNames),
             },
             enableInput,
             expandableStrings,
-            expandedNodes: new Set(sectionNames.map((sec) => "/" + sec)),
+            expandedNodes: this.getExpandedNodes(object),
             onFilter: (props) => this.onFilter(props, sectionNames),
             renderRow: renderRow || this.renderRowWithEditor,
             renderValue: renderValue || this.renderValueWithRep,
           }),
         ),
       )
     );
   }
--- a/devtools/client/netmonitor/test/browser_net_cached-status.js
+++ b/devtools/client/netmonitor/test/browser_net_cached-status.js
@@ -10,17 +10,16 @@
 add_task(function* () {
   let { tab, monitor } = yield initNetMonitor(STATUS_CODES_URL, null, true);
   info("Starting test... ");
 
   let { NetMonitorView } = monitor.panelWin;
   let { RequestsMenu, NetworkDetails } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
-  NetworkDetails._params.lazyEmpty = false;
 
   const REQUEST_DATA = [
     {
       method: "GET",
       uri: STATUS_CODES_SJS + "?sts=ok&cached",
       details: {
         status: 200,
         statusText: "OK",
--- a/devtools/client/netmonitor/test/browser_net_complex-params.js
+++ b/devtools/client/netmonitor/test/browser_net_complex-params.js
@@ -9,187 +9,174 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(PARAMS_URL);
   info("Starting test... ");
 
-  let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
-  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
-  NetworkDetails._params.lazyEmpty = false;
 
   let wait = waitForNetworkEvents(monitor, 1, 6);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
-  let onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+  wait = waitForDOM(document, "#params-tabpanel .tree-section", 2);
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.getElementById("details-pane-toggle"));
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll("#details-pane tab")[2]);
-  yield onEvent;
-  yield testParamsTab1("a", '""', '{ "foo": "bar" }', '""');
+  yield wait;
+  testParamsTab1("a", '""', '{ "foo": "bar" }', '""');
 
-  onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+  wait = waitForDOM(document, "#params-tabpanel .tree-section", 2);
   RequestsMenu.selectedIndex = 1;
-  yield onEvent;
-  yield testParamsTab1("a", '"b"', '{ "foo": "bar" }', '""');
+  yield wait;
+  testParamsTab1("a", '"b"', '{ "foo": "bar" }', '""');
 
-  onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+  wait = waitForDOM(document, "#params-tabpanel .tree-section", 2);
   RequestsMenu.selectedIndex = 2;
-  yield onEvent;
-  yield testParamsTab1("a", '"b"', "foo", '"bar"');
+  yield wait;
+  testParamsTab1("a", '"b"', "foo", '"bar"');
 
-  onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+  wait = waitForDOM(document, "#params-tabpanel tr:not(.tree-section).treeRow", 2);
   RequestsMenu.selectedIndex = 3;
-  yield onEvent;
-  yield testParamsTab2("a", '""', '{ "foo": "bar" }', "js");
+  yield wait;
+  testParamsTab2("a", '""', '{ "foo": "bar" }', "js");
 
-  onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+  wait = waitForDOM(document, "#params-tabpanel tr:not(.tree-section).treeRow", 2);
   RequestsMenu.selectedIndex = 4;
-  yield onEvent;
-  yield testParamsTab2("a", '"b"', '{ "foo": "bar" }', "js");
+  yield wait;
+  testParamsTab2("a", '"b"', '{ "foo": "bar" }', "js");
 
-  onEvent = monitor.panelWin.once(EVENTS.REQUEST_POST_PARAMS_DISPLAYED);
+  // Wait for all tree sections and editor updated by react
+  let waitSections = waitForDOM(document, "#params-tabpanel .tree-section", 2);
+  let waitEditor = waitForDOM(document, "#params-tabpanel .editor-mount iframe");
   RequestsMenu.selectedIndex = 5;
-  yield onEvent;
-  yield testParamsTab2("a", '"b"', "?foo=bar", "text");
+  let [, editorFrames] = yield Promise.all([waitSections, waitEditor]);
+  yield once(editorFrames[0], "DOMContentLoaded");
+  yield waitForDOM(editorFrames[0].contentDocument, ".CodeMirror-code");
+  testParamsTab2("a", '"b"', "?foo=bar", "text");
 
-  onEvent = monitor.panelWin.once(EVENTS.SIDEBAR_POPULATED);
+  wait = waitForDOM(document, "#params-tabpanel .empty-notice");
   RequestsMenu.selectedIndex = 6;
-  yield onEvent;
-  yield testParamsTab3("a", '"b"');
+  yield wait;
+  testParamsTab3();
 
   yield teardown(monitor);
 
   function testParamsTab1(queryStringParamName, queryStringParamValue,
                           formDataParamName, formDataParamValue) {
     let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
 
-    is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
-      "The number of param scopes displayed in this tabpanel is incorrect.");
-    is(tabpanel.querySelectorAll(".variable-or-property").length, 2,
-      "The number of param values displayed in this tabpanel is incorrect.");
-    is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+    is(tabpanel.querySelectorAll(".tree-section").length, 2,
+      "The number of param tree sections displayed in this tabpanel is incorrect.");
+    is(tabpanel.querySelectorAll("tr:not(.tree-section).treeRow").length, 2,
+      "The number of param rows displayed in this tabpanel is incorrect.");
+    is(tabpanel.querySelectorAll(".empty-notice").length, 0,
       "The empty notice should not be displayed in this tabpanel.");
 
-    is(tabpanel.querySelector("#request-params-box")
-      .hasAttribute("hidden"), false,
-      "The request params box should not be hidden.");
-    is(tabpanel.querySelector("#request-post-data-textarea-box")
-      .hasAttribute("hidden"), true,
-      "The request post data textarea box should be hidden.");
+    ok(tabpanel.querySelector(".treeTable"),
+      "The request params box should be displayed.");
+    ok(tabpanel.querySelector(".editor-mount") === null,
+      "The request post data editor should not be displayed.");
 
-    let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
-    let formDataScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+    let treeSections = tabpanel.querySelectorAll(".tree-section");
+    let labels = tabpanel
+      .querySelectorAll("tr:not(.tree-section) .treeLabelCell .treeLabel");
+    let values = tabpanel
+      .querySelectorAll("tr:not(.tree-section) .treeValueCell .objectBox");
 
-    is(paramsScope.querySelector(".name").getAttribute("value"),
+    is(treeSections[0].querySelector(".treeLabel").textContent,
       L10N.getStr("paramsQueryString"),
-      "The params scope doesn't have the correct title.");
-    is(formDataScope.querySelector(".name").getAttribute("value"),
+      "The params section doesn't have the correct title.");
+    is(treeSections[1].querySelector(".treeLabel").textContent,
       L10N.getStr("paramsFormData"),
-      "The form data scope doesn't have the correct title.");
+      "The form data section doesn't have the correct title.");
 
-    is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
-      .getAttribute("value"),
-      queryStringParamName,
+    is(labels[0].textContent, queryStringParamName,
       "The first query string param name was incorrect.");
-    is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
-      .getAttribute("value"),
-      queryStringParamValue,
+    is(values[0].textContent, queryStringParamValue,
       "The first query string param value was incorrect.");
 
-    is(formDataScope.querySelectorAll(".variables-view-variable .name")[0]
-      .getAttribute("value"),
-      formDataParamName,
+    is(labels[1].textContent, formDataParamName,
       "The first form data param name was incorrect.");
-    is(formDataScope.querySelectorAll(".variables-view-variable .value")[0]
-      .getAttribute("value"),
-      formDataParamValue,
+    is(values[1].textContent, formDataParamValue,
       "The first form data param value was incorrect.");
   }
 
-  function* testParamsTab2(queryStringParamName, queryStringParamValue,
+  function testParamsTab2(queryStringParamName, queryStringParamValue,
                           requestPayload, editorMode) {
-    let isJSON = editorMode == "js";
+    let isJSON = editorMode === "js";
     let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
 
-    is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
-      "The number of param scopes displayed in this tabpanel is incorrect.");
-    is(tabpanel.querySelectorAll(".variable-or-property").length, isJSON ? 4 : 1,
-      "The number of param values displayed in this tabpanel is incorrect.");
-    is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+    is(tabpanel.querySelectorAll(".tree-section").length, 2,
+      "The number of param tree sections displayed in this tabpanel is incorrect.");
+    is(tabpanel.querySelectorAll("tr:not(.tree-section).treeRow").length, isJSON ? 2 : 1,
+      "The number of param rows displayed in this tabpanel is incorrect.");
+    is(tabpanel.querySelectorAll(".empty-notice").length, 0,
       "The empty notice should not be displayed in this tabpanel.");
 
-    is(tabpanel.querySelector("#request-params-box")
-      .hasAttribute("hidden"), false,
-      "The request params box should not be hidden.");
-    is(tabpanel.querySelector("#request-post-data-textarea-box")
-      .hasAttribute("hidden"), isJSON,
-      "The request post data textarea box should be hidden.");
+    ok(tabpanel.querySelector(".treeTable"),
+      "The request params box should be displayed.");
+    is(tabpanel.querySelector(".editor-mount") === null,
+      isJSON,
+      "The request post data editor should be not displayed.");
 
-    let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
-    let payloadScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+    let treeSections = tabpanel.querySelectorAll(".tree-section");
 
-    is(paramsScope.querySelector(".name").getAttribute("value"),
+    is(treeSections[0].querySelector(".treeLabel").textContent,
       L10N.getStr("paramsQueryString"),
-      "The params scope doesn't have the correct title.");
-    is(payloadScope.querySelector(".name").getAttribute("value"),
+      "The query section doesn't have the correct title.");
+    is(treeSections[1].querySelector(".treeLabel").textContent,
       isJSON ? L10N.getStr("jsonScopeName") : L10N.getStr("paramsPostPayload"),
-      "The request payload scope doesn't have the correct title.");
+      "The post section doesn't have the correct title.");
 
-    is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
-      .getAttribute("value"),
-      queryStringParamName,
+    let labels = tabpanel
+      .querySelectorAll("tr:not(.tree-section) .treeLabelCell .treeLabel");
+    let values = tabpanel
+      .querySelectorAll("tr:not(.treeS-section) .treeValueCell .objectBox");
+
+    is(labels[0].textContent, queryStringParamName,
       "The first query string param name was incorrect.");
-    is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
-      .getAttribute("value"),
-      queryStringParamValue,
+    is(values[0].textContent, queryStringParamValue,
       "The first query string param value was incorrect.");
 
     if (isJSON) {
       let requestPayloadObject = JSON.parse(requestPayload);
       let requestPairs = Object.keys(requestPayloadObject)
         .map(k => [k, requestPayloadObject[k]]);
-      let displayedNames = payloadScope.querySelectorAll(
-        ".variables-view-property.variable-or-property .name");
-      let displayedValues = payloadScope.querySelectorAll(
-        ".variables-view-property.variable-or-property .value");
-      for (let i = 0; i < requestPairs.length; i++) {
+      for (let i = 1; i < requestPairs.length; i++) {
         let [requestPayloadName, requestPayloadValue] = requestPairs[i];
-        is(requestPayloadName, displayedNames[i].getAttribute("value"),
+        is(requestPayloadName, labels[i].textContent,
           "JSON property name " + i + " should be displayed correctly");
-        is('"' + requestPayloadValue + '"', displayedValues[i].getAttribute("value"),
+        is('"' + requestPayloadValue + '"', values[i].textContent,
           "JSON property value " + i + " should be displayed correctly");
       }
     } else {
-      let editor = yield NetMonitorView.editor("#request-post-data-textarea");
-      is(editor.getText(), requestPayload,
+      let editor = editorFrames[0].contentDocument.querySelector(".CodeMirror-code");
+      ok(editor.textContent.includes(requestPayload),
         "The text shown in the source editor is incorrect.");
-      is(editor.getMode(), Editor.modes[editorMode],
-        "The mode active in the source editor is incorrect.");
     }
   }
 
-  function testParamsTab3(queryStringParamName, queryStringParamValue) {
+  function testParamsTab3() {
     let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
 
-    is(tabpanel.querySelectorAll(".variables-view-scope").length, 0,
-      "The number of param scopes displayed in this tabpanel is incorrect.");
-    is(tabpanel.querySelectorAll(".variable-or-property").length, 0,
-      "The number of param values displayed in this tabpanel is incorrect.");
-    is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 1,
+    is(tabpanel.querySelectorAll(".tree-section").length, 0,
+      "The number of param tree sections displayed in this tabpanel is incorrect.");
+    is(tabpanel.querySelectorAll("tr:not(.tree-section).treeRow").length, 0,
+      "The number of param rows displayed in this tabpanel is incorrect.");
+    is(tabpanel.querySelectorAll(".empty-notice").length, 1,
       "The empty notice should be displayed in this tabpanel.");
 
-    is(tabpanel.querySelector("#request-params-box")
-      .hasAttribute("hidden"), false,
-      "The request params box should not be hidden.");
-    is(tabpanel.querySelector("#request-post-data-textarea-box")
-      .hasAttribute("hidden"), true,
-      "The request post data textarea box should be hidden.");
+    ok(!tabpanel.querySelector(".treeTable"),
+      "The request params box should be hidden.");
+    ok(!tabpanel.querySelector(".editor-mount iframe"),
+      "The request post data editor should be hidden.");
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_post-data-01.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-01.js
@@ -8,21 +8,20 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
-  let { document, EVENTS, Editor, NetMonitorView } = monitor.panelWin;
-  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
-  NetworkDetails._params.lazyEmpty = false;
 
   let wait = waitForNetworkEvents(monitor, 0, 2);
   yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
     content.wrappedJSObject.performRequests();
   });
   yield wait;
 
   verifyRequestItemTarget(RequestsMenu, RequestsMenu.getItemAtIndex(0),
@@ -39,128 +38,99 @@ add_task(function* () {
       status: 200,
       statusText: "Och Aye",
       type: "plain",
       fullMimeType: "text/plain; charset=utf-8",
       size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
       time: true
     });
 
-  let onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+  // Wait for all tree sections updated by react
+  wait = waitForDOM(document, "#params-tabpanel .tree-section", 2);
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.getElementById("details-pane-toggle"));
   EventUtils.sendMouseEvent({ type: "mousedown" },
     document.querySelectorAll("#details-pane tab")[2]);
-  yield onEvent;
+  yield wait;
   yield testParamsTab("urlencoded");
 
-  onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+  // Wait for all tree sections and editor updated by react
+  let waitForSections = waitForDOM(document, "#params-tabpanel .tree-section", 2);
+  let waitForEditor = waitForDOM(document, "#params-tabpanel .editor-mount iframe");
   RequestsMenu.selectedIndex = 1;
-  yield onEvent;
+  let [, editorFrames] = yield Promise.all([waitForSections, waitForEditor]);
+  yield once(editorFrames[0], "DOMContentLoaded");
+  yield waitForDOM(editorFrames[0].contentDocument, ".CodeMirror-code");
   yield testParamsTab("multipart");
 
   return teardown(monitor);
 
   function* testParamsTab(type) {
     let tabEl = document.querySelectorAll("#details-pane tab")[2];
     let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
 
     is(tabEl.getAttribute("selected"), "true",
       "The params tab in the network details pane should be selected.");
 
     function checkVisibility(box) {
-      is(tabpanel.querySelector("#request-params-box")
-        .hasAttribute("hidden"), !box.includes("params"),
-        "The request params box doesn't have the indended visibility.");
-      is(tabpanel.querySelector("#request-post-data-textarea-box")
-        .hasAttribute("hidden"), !box.includes("textarea"),
-        "The request post data textarea box doesn't have the indended visibility.");
+      is(!tabpanel.querySelector(".treeTable"), !box.includes("params"),
+        "The request params doesn't have the indended visibility.");
+      is(tabpanel.querySelector(".editor-mount") === null,
+        !box.includes("editor"),
+        "The request post data doesn't have the indended visibility.");
     }
 
-    is(tabpanel.querySelectorAll(".variables-view-scope").length, 2,
-      "There should be 2 param scopes displayed in this tabpanel.");
-    is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+    is(tabpanel.querySelectorAll(".tree-section").length, 2,
+      "There should be 2 tree sections displayed in this tabpanel.");
+    is(tabpanel.querySelectorAll(".empty-notice").length, 0,
       "The empty notice should not be displayed in this tabpanel.");
 
-    let queryScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
-    let postScope = tabpanel.querySelectorAll(".variables-view-scope")[1];
+    let treeSections = tabpanel.querySelectorAll(".tree-section");
 
-    is(queryScope.querySelector(".name").getAttribute("value"),
+    is(treeSections[0].querySelector(".treeLabel").textContent,
       L10N.getStr("paramsQueryString"),
-      "The query scope doesn't have the correct title.");
+      "The query section doesn't have the correct title.");
 
-    is(postScope.querySelector(".name").getAttribute("value"),
+    is(treeSections[1].querySelector(".treeLabel").textContent,
       L10N.getStr(type == "urlencoded" ? "paramsFormData" : "paramsPostPayload"),
-      "The post scope doesn't have the correct title.");
+      "The post section doesn't have the correct title.");
 
-    is(queryScope.querySelectorAll(".variables-view-variable .name")[0]
-      .getAttribute("value"),
-      "foo", "The first query param name was incorrect.");
-    is(queryScope.querySelectorAll(".variables-view-variable .value")[0]
-      .getAttribute("value"),
-      "\"bar\"", "The first query param value was incorrect.");
-    is(queryScope.querySelectorAll(".variables-view-variable .name")[1]
-      .getAttribute("value"),
-      "baz", "The second query param name was incorrect.");
-    is(queryScope.querySelectorAll(".variables-view-variable .value")[1]
-      .getAttribute("value"),
-      "\"42\"", "The second query param value was incorrect.");
-    is(queryScope.querySelectorAll(".variables-view-variable .name")[2]
-      .getAttribute("value"),
-      "type", "The third query param name was incorrect.");
-    is(queryScope.querySelectorAll(".variables-view-variable .value")[2]
-      .getAttribute("value"),
-      "\"" + type + "\"", "The third query param value was incorrect.");
+    let labels = tabpanel.querySelectorAll("tr:not(.tree-section) .treeLabelCell .treeLabel");
+    let values = tabpanel.querySelectorAll("tr:not(.tree-section) .treeValueCell .objectBox");
+
+    is(labels[0].textContent, "foo", "The first query param name was incorrect.");
+    is(values[0].textContent, "\"bar\"", "The first query param value was incorrect.");
+    is(labels[1].textContent, "baz", "The second query param name was incorrect.");
+    is(values[1].textContent, "\"42\"", "The second query param value was incorrect.");
+    is(labels[2].textContent, "type", "The third query param name was incorrect.");
+    is(values[2].textContent, "\"" + type + "\"", "The third query param value was incorrect.");
 
     if (type == "urlencoded") {
       checkVisibility("params");
-
-      is(tabpanel.querySelectorAll(".variables-view-variable").length, 5,
-        "There should be 5 param values displayed in this tabpanel.");
-      is(queryScope.querySelectorAll(".variables-view-variable").length, 3,
-        "There should be 3 param values displayed in the query scope.");
-      is(postScope.querySelectorAll(".variables-view-variable").length, 2,
-        "There should be 2 param values displayed in the post scope.");
+      is(labels.length, 5, "There should be 5 param values displayed in this tabpanel.");
+      is(labels[3].textContent, "foo", "The first post param name was incorrect.");
+      is(values[3].textContent, "\"bar\"", "The first post param value was incorrect.");
+      is(labels[4].textContent, "baz", "The second post param name was incorrect.");
+      is(values[4].textContent, "\"123\"", "The second post param value was incorrect.");
+    } else {
+      checkVisibility("params editor");
 
-      is(postScope.querySelectorAll(".variables-view-variable .name")[0]
-        .getAttribute("value"),
-        "foo", "The first post param name was incorrect.");
-      is(postScope.querySelectorAll(".variables-view-variable .value")[0]
-        .getAttribute("value"),
-        "\"bar\"", "The first post param value was incorrect.");
-      is(postScope.querySelectorAll(".variables-view-variable .name")[1]
-        .getAttribute("value"),
-        "baz", "The second post param name was incorrect.");
-      is(postScope.querySelectorAll(".variables-view-variable .value")[1]
-        .getAttribute("value"),
-        "\"123\"", "The second post param value was incorrect.");
-    } else {
-      checkVisibility("params textarea");
+      is(labels.length, 3, "There should be 3 param values displayed in this tabpanel.");
 
-      is(tabpanel.querySelectorAll(".variables-view-variable").length, 3,
-        "There should be 3 param values displayed in this tabpanel.");
-      is(queryScope.querySelectorAll(".variables-view-variable").length, 3,
-        "There should be 3 param values displayed in the query scope.");
-      is(postScope.querySelectorAll(".variables-view-variable").length, 0,
-        "There should be 0 param values displayed in the post scope.");
-
-      let editor = yield NetMonitorView.editor("#request-post-data-textarea");
-      let text = editor.getText();
+      let text = editorFrames[0].contentDocument.querySelector(".CodeMirror-code").textContent;
 
       ok(text.includes("Content-Disposition: form-data; name=\"text\""),
         "The text shown in the source editor is incorrect (1.1).");
       ok(text.includes("Content-Disposition: form-data; name=\"email\""),
         "The text shown in the source editor is incorrect (2.1).");
       ok(text.includes("Content-Disposition: form-data; name=\"range\""),
         "The text shown in the source editor is incorrect (3.1).");
       ok(text.includes("Content-Disposition: form-data; name=\"Custom field\""),
         "The text shown in the source editor is incorrect (4.1).");
       ok(text.includes("Some text..."),
         "The text shown in the source editor is incorrect (2.2).");
       ok(text.includes("42"),
         "The text shown in the source editor is incorrect (3.2).");
       ok(text.includes("Extra data"),
         "The text shown in the source editor is incorrect (4.2).");
-      is(editor.getMode(), Editor.modes.text,
-        "The mode active in the source editor is incorrect.");
     }
   }
 });
--- a/devtools/client/netmonitor/test/browser_net_post-data-02.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-02.js
@@ -9,65 +9,55 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(POST_RAW_URL);
   info("Starting test... ");
 
-  let { document, EVENTS, NetMonitorView } = monitor.panelWin;
-  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
-  NetworkDetails._params.lazyEmpty = 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 }, 2);
-  RequestsMenu.selectedIndex = 0;
-  yield onEvent;
-
-  let tabEl = document.querySelectorAll("#event-details-pane tab")[2];
-  let tabpanel = document.querySelectorAll("#event-details-pane tabpanel")[2];
-
-  is(tabEl.getAttribute("selected"), "true",
-    "The params tab in the network details pane should be selected.");
+  // Wait for all tree view updated by react
+  wait = waitForDOM(document, "#params-tabpanel .tree-section");
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.getElementById("details-pane-toggle"));
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.querySelectorAll("#details-pane tab")[2]);
+  yield wait;
 
-  is(tabpanel.querySelector("#request-params-box")
-    .hasAttribute("hidden"), false,
-    "The request params box doesn't have the indended visibility.");
-  is(tabpanel.querySelector("#request-post-data-textarea-box")
-    .hasAttribute("hidden"), true,
-    "The request post data textarea box doesn't have the indended visibility.");
+  let tabpanel = document.querySelectorAll("#details-pane tabpanel")[2];
 
-  is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
-    "There should be 1 param scopes displayed in this tabpanel.");
-  is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+  ok(tabpanel.querySelector(".treeTable"),
+    "The request params doesn't have the indended visibility.");
+  ok(tabpanel.querySelector(".editor-mount") === null,
+    "The request post data doesn't have the indended visibility.");
+
+  is(tabpanel.querySelectorAll(".tree-section").length, 1,
+    "There should be 1 tree sections displayed in this tabpanel.");
+  is(tabpanel.querySelectorAll(".empty-notice").length, 0,
     "The empty notice should not be displayed in this tabpanel.");
 
-  let postScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
-  is(postScope.querySelector(".name").getAttribute("value"),
+  is(tabpanel.querySelector(".tree-section .treeLabel").textContent,
     L10N.getStr("paramsFormData"),
-    "The post scope doesn't have the correct title.");
+    "The post section doesn't have the correct title.");
 
-  is(postScope.querySelectorAll(".variables-view-variable").length, 2,
-    "There should be 2 param values displayed in the post scope.");
-  is(postScope.querySelectorAll(".variables-view-variable .name")[0]
-    .getAttribute("value"),
-    "foo", "The first query param name was incorrect.");
-  is(postScope.querySelectorAll(".variables-view-variable .value")[0]
-    .getAttribute("value"),
-    "\"bar\"", "The first query param value was incorrect.");
-  is(postScope.querySelectorAll(".variables-view-variable .name")[1]
-    .getAttribute("value"),
-    "baz", "The second query param name was incorrect.");
-  is(postScope.querySelectorAll(".variables-view-variable .value")[1]
-    .getAttribute("value"),
-    "\"123\"", "The second query param value was incorrect.");
+  let labels = tabpanel
+    .querySelectorAll("tr:not(.tree-section) .treeLabelCell .treeLabel");
+  let values = tabpanel
+    .querySelectorAll("tr:not(.tree-section) .treeValueCell .objectBox");
+
+  is(labels[0].textContent, "foo", "The first query param name was incorrect.");
+  is(values[0].textContent, "\"bar\"", "The first query param value was incorrect.");
+  is(labels[1].textContent, "baz", "The second query param name was incorrect.");
+  is(values[1].textContent, "\"123\"", "The second query param value was incorrect.");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_post-data-03.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-03.js
@@ -9,31 +9,34 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(POST_RAW_WITH_HEADERS_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, 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, "#headers-tabpanel .variables-view-scope", 3);
+  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,
@@ -55,44 +58,37 @@ add_task(function* () {
     "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.");
 
-  onEvent = monitor.panelWin.once(EVENTS.TAB_UPDATED);
+  // 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 onEvent;
+  yield wait;
 
-  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.");
+  ok(tabpanel.querySelector(".treeTable"),
+    "The params tree view should be displayed.");
+  ok(tabpanel.querySelector(".editor-mount") === null,
+    "The post data shouldn't be displayed.");
 
-  is(formDataScope.querySelectorAll(".variables-view-variable").length, 2,
-    "There should be 2 payload values displayed in the form data scope.");
+  is(tabpanel.querySelector(".tree-section .treeLabel").textContent,
+    L10N.getStr("paramsFormData"),
+    "The form data section doesn't have the correct title.");
 
-  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"),
-    "baz", "The second payload param name was incorrect.");
-  is(formDataScope.querySelectorAll(".variables-view-variable .value")[1]
-    .getAttribute("value"),
-    "\"123\"", "The second payload param value was incorrect.");
+  let labels = tabpanel
+    .querySelectorAll("tr:not(.tree-section) .treeLabelCell .treeLabel");
+  let 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_post-data-04.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-04.js
@@ -9,66 +9,53 @@
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/l10n");
 
   let { tab, monitor } = yield initNetMonitor(POST_JSON_URL);
   info("Starting test... ");
 
-  let { document, EVENTS, NetMonitorView } = monitor.panelWin;
-  let { RequestsMenu, NetworkDetails } = NetMonitorView;
+  let { document, NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
-  NetworkDetails._params.lazyEmpty = 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 }, 2);
-  RequestsMenu.selectedIndex = 0;
-  yield onEvent;
+  // Wait for all tree view updated by react
+  wait = waitForDOM(document, "#params-tabpanel .tree-section");
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.getElementById("details-pane-toggle"));
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.querySelectorAll("#details-pane tab")[2]);
+  yield wait;
 
-  let tabEl = document.querySelectorAll("#event-details-pane tab")[2];
   let tabpanel = document.querySelectorAll("#event-details-pane tabpanel")[2];
 
-  is(tabEl.getAttribute("selected"), "true",
-    "The params tab in the network details pane should be selected.");
+  ok(tabpanel.querySelector(".treeTable"),
+    "The request params doesn't have the indended visibility.");
+  ok(tabpanel.querySelector(".editor-mount") === null,
+    "The request post data doesn't have the indended visibility.");
 
-  is(tabpanel.querySelector("#request-params-box")
-    .hasAttribute("hidden"), false,
-    "The request params box doesn't have the intended visibility.");
-  is(tabpanel.querySelector("#request-post-data-textarea-box")
-    .hasAttribute("hidden"), true,
-    "The request post data textarea box doesn't have the intended visibility.");
-
-  is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
-    "There should be 1 param scopes displayed in this tabpanel.");
-  is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+  is(tabpanel.querySelectorAll(".tree-section").length, 1,
+    "There should be 1 tree sections displayed in this tabpanel.");
+  is(tabpanel.querySelectorAll(".empty-notice").length, 0,
     "The empty notice should not be displayed in this tabpanel.");
 
-  let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
-  is(jsonScope.querySelector(".name").getAttribute("value"),
+  is(tabpanel.querySelector(".tree-section .treeLabel").textContent,
     L10N.getStr("jsonScopeName"),
-    "The JSON scope doesn't have the correct title.");
-
-  let valueScope = tabpanel.querySelector(
-    ".variables-view-scope > .variables-view-element-details");
+    "The JSON section doesn't have the correct title.");
 
-  is(valueScope.querySelectorAll(".variables-view-variable").length, 1,
-    "There should be 1 value displayed in the JSON scope.");
-  is(valueScope.querySelector(".variables-view-property .name")
-    .getAttribute("value"),
-    "a", "The JSON var name was incorrect.");
-  is(valueScope.querySelector(".variables-view-property .value")
-    .getAttribute("value"),
-    "1", "The JSON var value was incorrect.");
+  let labels = tabpanel
+    .querySelectorAll("tr:not(.tree-section) .treeLabelCell .treeLabel");
+  let values = tabpanel
+    .querySelectorAll("tr:not(.tree-section) .treeValueCell .objectBox");
 
-  let detailsParent = valueScope.querySelector(".variables-view-property .name")
-    .closest(".variables-view-element-details");
-  is(detailsParent.hasAttribute("open"), true, "The JSON value must be visible");
+  is(labels[0].textContent, "a", "The JSON var name was incorrect.");
+  is(values[0].textContent, "1", "The JSON var value was incorrect.");
 
   return teardown(monitor);
 });
--- a/devtools/client/netmonitor/test/browser_net_status-codes.js
+++ b/devtools/client/netmonitor/test/browser_net_status-codes.js
@@ -14,17 +14,16 @@ add_task(function* () {
 
   info("Starting test... ");
 
   let { document, EVENTS, NetMonitorView } = monitor.panelWin;
   let { RequestsMenu, NetworkDetails } = NetMonitorView;
   let requestItems = [];
 
   RequestsMenu.lazyUpdate = false;
-  NetworkDetails._params.lazyEmpty = false;
 
   const REQUEST_DATA = [
     {
       // request #0
       method: "GET",
       uri: STATUS_CODES_SJS + "?sts=100",
       details: {
         status: 101,
@@ -168,43 +167,42 @@ add_task(function* () {
 
   /**
    * 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();
     let statusParamShownValue = "\"" + statusParamValue + "\"";
+    let treeSections = tabpanel.querySelectorAll(".tree-section");
 
-    is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
-      "There should be 1 param scope displayed in this tabpanel.");
-    is(tabpanel.querySelectorAll(".variable-or-property").length, 1,
-      "There should be 1 param value displayed in this tabpanel.");
-    is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
+    is(treeSections.length, 1,
+      "There should be 1 param section displayed in this tabpanel.");
+    is(tabpanel.querySelectorAll("tr:not(.tree-section).treeRow").length, 1,
+      "There should be 1 param row displayed in this tabpanel.");
+    is(tabpanel.querySelectorAll(".empty-notice").length, 0,
       "The empty notice should not be displayed in this tabpanel.");
 
-    let paramsScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
+    let labels = tabpanel
+      .querySelectorAll("tr:not(.tree-section) .treeLabelCell .treeLabel");
+    let values = tabpanel
+      .querySelectorAll("tr:not(.tree-section) .treeValueCell .objectBox");
 
-    is(paramsScope.querySelector(".name").getAttribute("value"),
+    is(treeSections[0].querySelector(".treeLabel").textContent,
       L10N.getStr("paramsQueryString"),
       "The params scope doesn't have the correct title.");
 
-    is(paramsScope.querySelectorAll(".variables-view-variable .name")[0]
-      .getAttribute("value"),
-      "sts", "The param name was incorrect.");
-    is(paramsScope.querySelectorAll(".variables-view-variable .value")[0]
-      .getAttribute("value"),
-      statusParamShownValue, "The param value was incorrect.");
+    is(labels[0].textContent, "sts", "The param name was incorrect.");
+    is(values[0].textContent, statusParamShownValue, "The param value was incorrect.");
 
-    is(tabpanel.querySelector("#request-params-box")
-      .hasAttribute("hidden"), false,
-      "The request params box should not be hidden.");
-    is(tabpanel.querySelector("#request-post-data-textarea-box")
-      .hasAttribute("hidden"), true,
-      "The request post data textarea box should be hidden.");
+    ok(tabpanel.querySelector(".treeTable"),
+      "The request params tree view should be displayed.");
+    is(tabpanel.querySelector(".editor-mount") === null,
+      true,
+      "The request post data editor should be hidden.");
   }
 
   /**
    * A helper that clicks on a specified request and returns a promise resolved
    * when NetworkDetails has been populated with the data of the given request.
    */
   function chooseRequest(index) {
     let onTabUpdated = monitor.panelWin.once(EVENTS.TAB_UPDATED);
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -1103,28 +1103,23 @@
 }
 
 .treeTable .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;
 }
 
@@ -1157,16 +1152,21 @@
 }
 
 .properties-view .devtools-searchbox,
 .tree-container .treeTable .tree-section {
   width: 100%;
   background-color: var(--theme-toolbar-background);
 }
 
+.properties-view .devtools-searchbox,
+.tree-container .treeTable tr:not(:last-child) td:not([class=""]) {
+  border-bottom: 1px solid var(--theme-splitter-color);
+}
+
 .tree-container .treeTable .tree-section > * {
   vertical-align: middle;
 }
 
 .tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel,
 .tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel:hover {
   color: var(--theme-body-color-alt);
 }
@@ -1176,39 +1176,46 @@
   max-width: 0;
   padding-inline-end: 5px;
 }
 
 .tree-container .objectBox {
   white-space: nowrap;
 }
 
+.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-params-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-params-tabpanel-hook,
 #react-preview-tabpanel-hook,
 #react-security-tabpanel-hook,
 #react-timings-tabpanel-hook,
 #primed-cache-chart,
 #empty-cache-chart {
   -moz-box-orient: vertical;
 }