Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 29 Mar 2017 09:44:13 -0400
changeset 553291 8efb75a46b6204eb5c4f1f3e1f3b20226b0981ab
parent 553290 235e1d29916fc30848b796ef9b416ec3932a743b (current diff)
parent 553172 6ea713ccc9abea93126423fefb855d0e051c95e2 (diff)
child 553292 5a8025dae7ee71a96a67ada51610837461c7b308
push id51582
push userasasaki@mozilla.com
push dateWed, 29 Mar 2017 18:30:11 +0000
reviewersmerge
milestone55.0a1
Merge m-c to inbound. a=merge
browser/components/preferences/in-content/content.js
browser/components/preferences/in-content/content.xul
browser/components/preferences/in-content/search.js
browser/components/preferences/in-content/search.xul
browser/components/preferences/in-content/security.js
browser/components/preferences/in-content/security.xul
devtools/client/netmonitor/actions/batching.js
devtools/client/netmonitor/actions/filters.js
devtools/client/netmonitor/actions/index.js
devtools/client/netmonitor/actions/moz.build
devtools/client/netmonitor/actions/requests.js
devtools/client/netmonitor/actions/selection.js
devtools/client/netmonitor/actions/sort.js
devtools/client/netmonitor/actions/timing-markers.js
devtools/client/netmonitor/actions/ui.js
devtools/client/netmonitor/components/monitor-panel.js
devtools/client/netmonitor/components/moz.build
devtools/client/netmonitor/components/network-monitor.js
devtools/client/netmonitor/components/request-list-content.js
devtools/client/netmonitor/components/request-list-empty.js
devtools/client/netmonitor/components/request-list-header.js
devtools/client/netmonitor/components/request-list-item.js
devtools/client/netmonitor/components/request-list.js
devtools/client/netmonitor/components/statistics-panel.js
devtools/client/netmonitor/components/toolbar.js
devtools/client/netmonitor/constants.js
devtools/client/netmonitor/har/har-automation.js
devtools/client/netmonitor/har/har-builder.js
devtools/client/netmonitor/har/har-collector.js
devtools/client/netmonitor/har/har-exporter.js
devtools/client/netmonitor/har/har-utils.js
devtools/client/netmonitor/har/moz.build
devtools/client/netmonitor/har/test/.eslintrc.js
devtools/client/netmonitor/har/test/browser.ini
devtools/client/netmonitor/har/test/browser_net_har_copy_all_as_har.js
devtools/client/netmonitor/har/test/browser_net_har_post_data.js
devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js
devtools/client/netmonitor/har/test/head.js
devtools/client/netmonitor/har/test/html_har_post-data-test-page.html
devtools/client/netmonitor/har/toolbox-overlay.js
devtools/client/netmonitor/middleware/batching.js
devtools/client/netmonitor/middleware/moz.build
devtools/client/netmonitor/middleware/prefs.js
devtools/client/netmonitor/netmonitor-controller.js
devtools/client/netmonitor/netmonitor.js
devtools/client/netmonitor/netmonitor.xhtml
devtools/client/netmonitor/reducers/batching.js
devtools/client/netmonitor/reducers/filters.js
devtools/client/netmonitor/reducers/index.js
devtools/client/netmonitor/reducers/moz.build
devtools/client/netmonitor/reducers/requests.js
devtools/client/netmonitor/reducers/sort.js
devtools/client/netmonitor/reducers/timing-markers.js
devtools/client/netmonitor/reducers/ui.js
devtools/client/netmonitor/request-list-context-menu.js
devtools/client/netmonitor/request-list-tooltip.js
devtools/client/netmonitor/selectors/filters.js
devtools/client/netmonitor/selectors/index.js
devtools/client/netmonitor/selectors/moz.build
devtools/client/netmonitor/selectors/requests.js
devtools/client/netmonitor/selectors/ui.js
devtools/client/netmonitor/shared/components/cookies-panel.js
devtools/client/netmonitor/shared/components/custom-request-panel.js
devtools/client/netmonitor/shared/components/editor.js
devtools/client/netmonitor/shared/components/headers-panel.js
devtools/client/netmonitor/shared/components/mdn-link.js
devtools/client/netmonitor/shared/components/moz.build
devtools/client/netmonitor/shared/components/network-details-panel.js
devtools/client/netmonitor/shared/components/params-panel.js
devtools/client/netmonitor/shared/components/preview-panel.js
devtools/client/netmonitor/shared/components/properties-view.js
devtools/client/netmonitor/shared/components/response-panel.js
devtools/client/netmonitor/shared/components/security-panel.js
devtools/client/netmonitor/shared/components/tabbox-panel.js
devtools/client/netmonitor/shared/components/timings-panel.js
devtools/client/netmonitor/shared/moz.build
devtools/client/netmonitor/store.js
devtools/client/netmonitor/utils/client.js
devtools/client/netmonitor/utils/filter-predicates.js
devtools/client/netmonitor/utils/format-utils.js
devtools/client/netmonitor/utils/l10n.js
devtools/client/netmonitor/utils/mdn-utils.js
devtools/client/netmonitor/utils/moz.build
devtools/client/netmonitor/utils/prefs.js
devtools/client/netmonitor/utils/request-utils.js
devtools/client/netmonitor/utils/sort-predicates.js
devtools/client/netmonitor/waterfall-background.js
devtools/client/themes/netmonitor.css
editor/nsIHTMLObjectResizeListener.idl
media/webrtc/signaling/test/FakeIPC.cpp
media/webrtc/signaling/test/FakeIPC.h
media/webrtc/trunk/testing/generate_gmock_mutant.py
media/webrtc/trunk/testing/gmock.gyp
media/webrtc/trunk/testing/gmock.target.mk
media/webrtc/trunk/testing/gmock_main.target.mk
media/webrtc/trunk/testing/gmock_mutant.h
media/webrtc/trunk/testing/gtest.gyp
media/webrtc/trunk/testing/gtest.target.mk
media/webrtc/trunk/testing/gtest/CHANGES
media/webrtc/trunk/testing/gtest/CMakeLists.txt
media/webrtc/trunk/testing/gtest/CONTRIBUTORS
media/webrtc/trunk/testing/gtest/LICENSE
media/webrtc/trunk/testing/gtest/Makefile.am
media/webrtc/trunk/testing/gtest/README
media/webrtc/trunk/testing/gtest/build-aux/.keep
media/webrtc/trunk/testing/gtest/cmake/internal_utils.cmake
media/webrtc/trunk/testing/gtest/codegear/gtest.cbproj
media/webrtc/trunk/testing/gtest/codegear/gtest.groupproj
media/webrtc/trunk/testing/gtest/codegear/gtest_all.cc
media/webrtc/trunk/testing/gtest/codegear/gtest_link.cc
media/webrtc/trunk/testing/gtest/codegear/gtest_main.cbproj
media/webrtc/trunk/testing/gtest/codegear/gtest_unittest.cbproj
media/webrtc/trunk/testing/gtest/configure.ac
media/webrtc/trunk/testing/gtest/include/gtest/gtest-death-test.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest-message.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest-param-test.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest-param-test.h.pump
media/webrtc/trunk/testing/gtest/include/gtest/gtest-printers.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest-spi.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest-test-part.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest-typed-test.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest_pred_impl.h
media/webrtc/trunk/testing/gtest/include/gtest/gtest_prod.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-death-test-internal.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-filepath.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-internal.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-linked_ptr.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-param-util-generated.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-param-util-generated.h.pump
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-param-util.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-port.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-string.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-tuple.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-tuple.h.pump
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-type-util.h
media/webrtc/trunk/testing/gtest/include/gtest/internal/gtest-type-util.h.pump
media/webrtc/trunk/testing/gtest/m4/acx_pthread.m4
media/webrtc/trunk/testing/gtest/m4/gtest.m4
media/webrtc/trunk/testing/gtest/make/Makefile
media/webrtc/trunk/testing/gtest/msvc/gtest-md.sln
media/webrtc/trunk/testing/gtest/msvc/gtest-md.vcproj
media/webrtc/trunk/testing/gtest/msvc/gtest.sln
media/webrtc/trunk/testing/gtest/msvc/gtest.vcproj
media/webrtc/trunk/testing/gtest/msvc/gtest_main-md.vcproj
media/webrtc/trunk/testing/gtest/msvc/gtest_main.vcproj
media/webrtc/trunk/testing/gtest/msvc/gtest_prod_test-md.vcproj
media/webrtc/trunk/testing/gtest/msvc/gtest_prod_test.vcproj
media/webrtc/trunk/testing/gtest/msvc/gtest_unittest-md.vcproj
media/webrtc/trunk/testing/gtest/msvc/gtest_unittest.vcproj
media/webrtc/trunk/testing/gtest/samples/prime_tables.h
media/webrtc/trunk/testing/gtest/samples/sample1.cc
media/webrtc/trunk/testing/gtest/samples/sample1.h
media/webrtc/trunk/testing/gtest/samples/sample10_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample1_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample2.cc
media/webrtc/trunk/testing/gtest/samples/sample2.h
media/webrtc/trunk/testing/gtest/samples/sample2_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample3-inl.h
media/webrtc/trunk/testing/gtest/samples/sample3_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample4.cc
media/webrtc/trunk/testing/gtest/samples/sample4.h
media/webrtc/trunk/testing/gtest/samples/sample4_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample5_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample6_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample7_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample8_unittest.cc
media/webrtc/trunk/testing/gtest/samples/sample9_unittest.cc
media/webrtc/trunk/testing/gtest/scripts/fuse_gtest_files.py
media/webrtc/trunk/testing/gtest/scripts/gen_gtest_pred_impl.py
media/webrtc/trunk/testing/gtest/scripts/gtest-config.in
media/webrtc/trunk/testing/gtest/scripts/pump.py
media/webrtc/trunk/testing/gtest/scripts/test/Makefile
media/webrtc/trunk/testing/gtest/scripts/upload.py
media/webrtc/trunk/testing/gtest/scripts/upload_gtest.py
media/webrtc/trunk/testing/gtest/src/gtest-all.cc
media/webrtc/trunk/testing/gtest/src/gtest-death-test.cc
media/webrtc/trunk/testing/gtest/src/gtest-filepath.cc
media/webrtc/trunk/testing/gtest/src/gtest-internal-inl.h
media/webrtc/trunk/testing/gtest/src/gtest-port.cc
media/webrtc/trunk/testing/gtest/src/gtest-printers.cc
media/webrtc/trunk/testing/gtest/src/gtest-test-part.cc
media/webrtc/trunk/testing/gtest/src/gtest-typed-test.cc
media/webrtc/trunk/testing/gtest/src/gtest.cc
media/webrtc/trunk/testing/gtest/src/gtest_main.cc
media/webrtc/trunk/testing/gtest/test/gtest-death-test_ex_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-death-test_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-filepath_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-linked_ptr_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-listener_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-message_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-options_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-param-test2_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-param-test_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-param-test_test.h
media/webrtc/trunk/testing/gtest/test/gtest-port_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-printers_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-test-part_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-tuple_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-typed-test2_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-typed-test_test.cc
media/webrtc/trunk/testing/gtest/test/gtest-typed-test_test.h
media/webrtc/trunk/testing/gtest/test/gtest-unittest-api_test.cc
media/webrtc/trunk/testing/gtest/test/gtest_all_test.cc
media/webrtc/trunk/testing/gtest/test/gtest_break_on_failure_unittest.py
media/webrtc/trunk/testing/gtest/test/gtest_break_on_failure_unittest_.cc
media/webrtc/trunk/testing/gtest/test/gtest_catch_exceptions_test.py
media/webrtc/trunk/testing/gtest/test/gtest_catch_exceptions_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_color_test.py
media/webrtc/trunk/testing/gtest/test/gtest_color_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_env_var_test.py
media/webrtc/trunk/testing/gtest/test/gtest_env_var_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_environment_test.cc
media/webrtc/trunk/testing/gtest/test/gtest_filter_unittest.py
media/webrtc/trunk/testing/gtest/test/gtest_filter_unittest_.cc
media/webrtc/trunk/testing/gtest/test/gtest_help_test.py
media/webrtc/trunk/testing/gtest/test/gtest_help_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_list_tests_unittest.py
media/webrtc/trunk/testing/gtest/test/gtest_list_tests_unittest_.cc
media/webrtc/trunk/testing/gtest/test/gtest_main_unittest.cc
media/webrtc/trunk/testing/gtest/test/gtest_no_test_unittest.cc
media/webrtc/trunk/testing/gtest/test/gtest_output_test.py
media/webrtc/trunk/testing/gtest/test/gtest_output_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_output_test_golden_lin.txt
media/webrtc/trunk/testing/gtest/test/gtest_pred_impl_unittest.cc
media/webrtc/trunk/testing/gtest/test/gtest_prod_test.cc
media/webrtc/trunk/testing/gtest/test/gtest_repeat_test.cc
media/webrtc/trunk/testing/gtest/test/gtest_shuffle_test.py
media/webrtc/trunk/testing/gtest/test/gtest_shuffle_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_sole_header_test.cc
media/webrtc/trunk/testing/gtest/test/gtest_stress_test.cc
media/webrtc/trunk/testing/gtest/test/gtest_test_utils.py
media/webrtc/trunk/testing/gtest/test/gtest_throw_on_failure_ex_test.cc
media/webrtc/trunk/testing/gtest/test/gtest_throw_on_failure_test.py
media/webrtc/trunk/testing/gtest/test/gtest_throw_on_failure_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_uninitialized_test.py
media/webrtc/trunk/testing/gtest/test/gtest_uninitialized_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_unittest.cc
media/webrtc/trunk/testing/gtest/test/gtest_xml_outfile1_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_xml_outfile2_test_.cc
media/webrtc/trunk/testing/gtest/test/gtest_xml_outfiles_test.py
media/webrtc/trunk/testing/gtest/test/gtest_xml_output_unittest.py
media/webrtc/trunk/testing/gtest/test/gtest_xml_output_unittest_.cc
media/webrtc/trunk/testing/gtest/test/gtest_xml_test_utils.py
media/webrtc/trunk/testing/gtest/test/production.cc
media/webrtc/trunk/testing/gtest/test/production.h
media/webrtc/trunk/testing/gtest/xcode/Config/DebugProject.xcconfig
media/webrtc/trunk/testing/gtest/xcode/Config/FrameworkTarget.xcconfig
media/webrtc/trunk/testing/gtest/xcode/Config/General.xcconfig
media/webrtc/trunk/testing/gtest/xcode/Config/ReleaseProject.xcconfig
media/webrtc/trunk/testing/gtest/xcode/Config/StaticLibraryTarget.xcconfig
media/webrtc/trunk/testing/gtest/xcode/Config/TestTarget.xcconfig
media/webrtc/trunk/testing/gtest/xcode/Resources/Info.plist
media/webrtc/trunk/testing/gtest/xcode/Samples/FrameworkSample/Info.plist
media/webrtc/trunk/testing/gtest/xcode/Samples/FrameworkSample/WidgetFramework.xcodeproj/project.pbxproj
media/webrtc/trunk/testing/gtest/xcode/Samples/FrameworkSample/runtests.sh
media/webrtc/trunk/testing/gtest/xcode/Samples/FrameworkSample/widget.cc
media/webrtc/trunk/testing/gtest/xcode/Samples/FrameworkSample/widget.h
media/webrtc/trunk/testing/gtest/xcode/Samples/FrameworkSample/widget_test.cc
media/webrtc/trunk/testing/gtest/xcode/Scripts/runtests.sh
media/webrtc/trunk/testing/gtest/xcode/Scripts/versiongenerate.py
media/webrtc/trunk/testing/gtest/xcode/gtest.xcodeproj/project.pbxproj
media/webrtc/trunk/testing/gtest_mac.h
media/webrtc/trunk/testing/gtest_mac.mm
media/webrtc/trunk/testing/gtest_mac_unittest.mm
media/webrtc/trunk/testing/gtest_main.target.mk
media/webrtc/trunk/testing/gtest_prod.host.mk
media/webrtc/trunk/testing/gtest_prod.target.mk
media/webrtc/trunk/testing/multiprocess_func_list.cc
media/webrtc/trunk/testing/multiprocess_func_list.h
media/webrtc/trunk/testing/platform_test.h
media/webrtc/trunk/testing/platform_test_mac.mm
testing/marionette/harness/marionette_harness/www/modal_dialogs.html
toolkit/themes/faststripe/global/button.css
toolkit/themes/faststripe/global/checkbox.css
toolkit/themes/faststripe/global/checkbox/cbox-check-dis.gif
toolkit/themes/faststripe/global/checkbox/cbox-check.gif
toolkit/themes/faststripe/global/dropmarker.css
toolkit/themes/faststripe/global/groupbox.css
toolkit/themes/faststripe/global/jar.mn
toolkit/themes/faststripe/global/menu.css
toolkit/themes/faststripe/global/menulist.css
toolkit/themes/faststripe/global/moz.build
toolkit/themes/faststripe/global/popup.css
toolkit/themes/faststripe/global/radio.css
toolkit/themes/faststripe/global/tabbox.css
toolkit/themes/faststripe/global/textbox.css
toolkit/themes/faststripe/global/xulscrollbars.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -665,16 +665,21 @@ pref("plugin.defaultXpi.state", 2);
 pref("plugin.state.flash", 2);
 pref("plugin.state.java", 1);
 
 #ifdef XP_WIN
 pref("browser.preferences.instantApply", false);
 #else
 pref("browser.preferences.instantApply", true);
 #endif
+// Once the Storage Management is completed.
+// (The Storage Management-related prefs are browser.storageManager.* )
+// The Offline(Appcache) Group section in about:preferences will be hidden.
+// And the task to clear appcache will be done by Storage Management.
+pref("browser.preferences.offlineGroup.enabled", true);
 
 pref("browser.download.show_plugins_in_list", true);
 pref("browser.download.hide_plugins_without_extensions", true);
 
 // Backspace and Shift+Backspace behavior
 // 0 goes Back/Forward
 // 1 act like PgUp/PgDown
 // 2 and other values, nothing
--- a/browser/base/content/test/forms/browser_selectpopup_colors.js
+++ b/browser/base/content/test/forms/browser_selectpopup_colors.js
@@ -95,16 +95,36 @@ const SELECT_CHANGES_COLOR_ON_FOCUS =
   "<html><head><style>" +
   "  select:focus { background-color: orange; color: black; }" +
   "</style></head>" +
   "<body><select id='one'>" +
   '  <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
   '  <option selected="true">{"end": "true"}</option>' +
   "</select></body></html>";
 
+const SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS =
+  "<html><head><style>" +
+  "  select { background-color: black; }" +
+  "  option { color: white; }" +
+  "</style></head>" +
+  "<body><select id='one'>" +
+  '  <option>{"color": "rgb(255, 255, 255)", "backgroundColor": "rgba(0, 0, 0, 0)"}</option>' +
+  '  <option selected="true">{"end": "true"}</option>' +
+  "</select></body></html>";
+
+const SELECT_STYLE_OF_OPTION_IS_BASED_ON_FOCUS_OF_SELECT =
+  "<html><head><style>" +
+  "  select:focus { background-color: #3a96dd; }" +
+  "  select:focus option { background-color: #fff; }" +
+  "</style></head>" +
+  "<body><select id='one'>" +
+  '  <option>{"color": "rgb(0, 0, 0)", "backgroundColor": "rgb(255, 255, 255)"}</option>' +
+  '  <option selected="true">{"end": "true"}</option>' +
+  "</select></body></html>";
+
 function getSystemColor(color) {
   // Need to convert system color to RGB color.
   let textarea = document.createElementNS("http://www.w3.org/1999/xhtml", "textarea");
   textarea.style.color = color;
   return getComputedStyle(textarea).color;
 }
 
 function testOptionColors(index, item, menulist) {
@@ -292,8 +312,26 @@ add_task(function* test_disabled_optgrou
 add_task(function* test_disabled_optgroup_and_options() {
   let options = {
     selectColor: "rgb(0, 0, 0)",
     selectBgColor: "rgb(255, 165, 0)"
   };
 
   yield testSelectColors(SELECT_CHANGES_COLOR_ON_FOCUS, 2, options);
 });
+
+add_task(function* test_bgcolor_on_select_color_on_options() {
+  let options = {
+    selectColor: "rgb(0, 0, 0)",
+    selectBgColor: "rgb(0, 0, 0)"
+  };
+
+  yield testSelectColors(SELECT_BGCOLOR_ON_SELECT_COLOR_ON_OPTIONS, 2, options);
+});
+
+add_task(function* test_style_of_options_is_dependent_on_focus_of_select() {
+  let options = {
+    selectColor: "rgb(0, 0, 0)",
+    selectBgColor: "rgb(58, 150, 221)"
+  };
+
+  yield testSelectColors(SELECT_STYLE_OF_OPTION_IS_BASED_ON_FOCUS_OF_SELECT, 2, options);
+});
--- a/browser/base/content/test/general/browser_ssl_error_reports.js
+++ b/browser/base/content/test/general/browser_ssl_error_reports.js
@@ -61,17 +61,17 @@ function* testSendReportAutomatically(te
 
   // Add a tab and wait until it's loaded.
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
   let browser = tab.linkedBrowser;
 
   // Load the page and wait for the error report submission.
   let promiseStatus = createReportResponseStatusPromise(URL_REPORTS + suffix);
   browser.loadURI(testURL);
-  yield promiseErrorPageLoaded(browser);
+  yield BrowserTestUtils.waitForErrorPage(browser);
 
   ok(!isErrorStatus(yield promiseStatus),
      "SSL error report submitted successfully");
 
   // Check that we loaded the right error page.
   yield checkErrorPage(browser, errorURISuffix);
 
   // Cleanup.
@@ -85,17 +85,17 @@ function* testSetAutomatic(testURL, suff
   Services.prefs.setCharPref(PREF_REPORT_URL, URL_REPORTS + suffix);
 
   // Add a tab and wait until it's loaded.
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
   let browser = tab.linkedBrowser;
 
   // Load the page.
   browser.loadURI(testURL);
-  yield promiseErrorPageLoaded(browser);
+  yield BrowserTestUtils.waitForErrorPage(browser);
 
   // Check that we loaded the right error page.
   yield checkErrorPage(browser, errorURISuffix);
 
   let statusPromise = createReportResponseStatusPromise(URL_REPORTS + suffix);
 
   // Click the checkbox, enable automatic error reports.
   yield ContentTask.spawn(browser, null, function* () {
@@ -126,17 +126,17 @@ function* testSetAutomatic(testURL, suff
 
 function* testSendReportDisabled(testURL, errorURISuffix) {
   // Add a tab and wait until it's loaded.
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
   let browser = tab.linkedBrowser;
 
   // Load the page.
   browser.loadURI(testURL);
-  yield promiseErrorPageLoaded(browser);
+  yield BrowserTestUtils.waitForErrorPage(browser);
 
   // Check that we loaded the right error page.
   yield checkErrorPage(browser, errorURISuffix);
 
   // Check that the error reporting section is hidden.
   yield ContentTask.spawn(browser, null, function* () {
     let section = content.document.getElementById("certificateErrorReporting");
     Assert.equal(content.getComputedStyle(section).display, "none",
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -770,25 +770,16 @@ function promiseOnBookmarkItemAdded(aExp
         Ci.nsINavBookmarkObserver,
       ])
     };
     info("Waiting for a bookmark to be added");
     PlacesUtils.bookmarks.addObserver(bookmarksObserver, false);
   });
 }
 
-function promiseErrorPageLoaded(browser) {
-  return new Promise(resolve => {
-    browser.addEventListener("DOMContentLoaded", function onLoad() {
-      browser.removeEventListener("DOMContentLoaded", onLoad, false, true);
-      resolve();
-    }, false, true);
-  });
-}
-
 function* loadBadCertPage(url) {
   const EXCEPTION_DIALOG_URI = "chrome://pippki/content/exceptionDialog.xul";
   let exceptionDialogResolved = new Promise(function(resolve) {
     // When the certificate exception dialog has opened, click the button to add
     // an exception.
     let certExceptionDialogObserver = {
       observe(aSubject, aTopic, aData) {
         if (aTopic == "cert-exception-ui-ready") {
--- a/browser/components/preferences/in-content-old/advanced.js
+++ b/browser/components/preferences/in-content-old/advanced.js
@@ -37,28 +37,26 @@ var gAdvancedPane = {
       let onUnload = function() {
         window.removeEventListener("unload", onUnload);
         Services.prefs.removeObserver("app.update.", this);
       }.bind(this);
       window.addEventListener("unload", onUnload);
       Services.prefs.addObserver("app.update.", this, false);
       this.updateReadPrefs();
     }
-    this.updateOfflineApps();
     if (AppConstants.MOZ_CRASHREPORTER) {
       this.initSubmitCrashes();
     }
     this.initTelemetry();
     if (AppConstants.MOZ_TELEMETRY_REPORTING) {
       this.initSubmitHealthReport();
     }
     this.updateOnScreenKeyboardVisibility();
     this.updateCacheSizeInputField();
     this.updateActualCacheSize();
-    this.updateActualAppCacheSize();
 
     if (Services.prefs.getBoolPref("browser.storageManager.enabled")) {
       Services.obs.addObserver(this, "sitedatamanager:sites-updated", false);
       let unload = () => {
         window.removeEventListener("unload", unload);
         Services.obs.removeObserver(this, "sitedatamanager:sites-updated");
       };
       window.addEventListener("unload", unload);
@@ -80,40 +78,47 @@ var gAdvancedPane = {
       setEventListener("submitHealthReportBox", "command",
                        gAdvancedPane.updateSubmitHealthReport);
     }
 
     setEventListener("connectionSettings", "command",
                      gAdvancedPane.showConnections);
     setEventListener("clearCacheButton", "command",
                      gAdvancedPane.clearCache);
-    setEventListener("clearOfflineAppCacheButton", "command",
-                     gAdvancedPane.clearOfflineAppCache);
-    setEventListener("offlineNotifyExceptions", "command",
-                     gAdvancedPane.showOfflineExceptions);
-    setEventListener("offlineAppsList", "select",
-                     gAdvancedPane.offlineAppSelected);
-    let bundlePrefs = document.getElementById("bundlePreferences");
-    document.getElementById("offlineAppsList")
-            .style.height = bundlePrefs.getString("offlineAppsList.height");
-    setEventListener("offlineAppsListRemove", "command",
-                     gAdvancedPane.removeOfflineApp);
     if (AppConstants.MOZ_UPDATER) {
       setEventListener("updateRadioGroup", "command",
                        gAdvancedPane.updateWritePrefs);
       setEventListener("showUpdateHistory", "command",
                        gAdvancedPane.showUpdates);
     }
     setEventListener("viewCertificatesButton", "command",
                      gAdvancedPane.showCertificates);
     setEventListener("viewSecurityDevicesButton", "command",
                      gAdvancedPane.showSecurityDevices);
     setEventListener("cacheSize", "change",
                      gAdvancedPane.updateCacheSizePref);
 
+    if (Services.prefs.getBoolPref("browser.preferences.offlineGroup.enabled")) {
+      this.updateOfflineApps();
+      this.updateActualAppCacheSize();
+      setEventListener("offlineNotifyExceptions", "command",
+                      gAdvancedPane.showOfflineExceptions);
+      setEventListener("offlineAppsList", "select",
+                      gAdvancedPane.offlineAppSelected);
+      setEventListener("offlineAppsListRemove", "command",
+                      gAdvancedPane.removeOfflineApp);
+      setEventListener("clearOfflineAppCacheButton", "command",
+                      gAdvancedPane.clearOfflineAppCache);
+      let bundlePrefs = document.getElementById("bundlePreferences");
+      document.getElementById("offlineAppsList")
+              .style.height = bundlePrefs.getString("offlineAppsList.height");
+      let offlineGroup = document.getElementById("offlineGroup");
+      offlineGroup.hidden = false;
+    }
+
     if (AppConstants.MOZ_WIDGET_GTK) {
       // GTK tabbox' allow the scroll wheel to change the selected tab,
       // but we don't want this behavior for the in-content preferences.
       let tabsElement = document.getElementById("tabsElement");
       tabsElement.addEventListener("DOMMouseScroll", event => {
         event.stopPropagation();
       }, true);
     }
@@ -393,42 +398,16 @@ var gAdvancedPane = {
     try {
       var cacheService =
         Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
                   .getService(Components.interfaces.nsICacheStorageService);
       cacheService.asyncGetDiskConsumption(this.observer);
     } catch (e) {}
   },
 
-  // Retrieves the amount of space currently used by offline cache
-  updateActualAppCacheSize() {
-    var visitor = {
-      onCacheStorageInfo(aEntryCount, aConsumption, aCapacity, aDiskDirectory) {
-        var actualSizeLabel = document.getElementById("actualAppCacheSize");
-        var sizeStrings = DownloadUtils.convertByteUnits(aConsumption);
-        var prefStrBundle = document.getElementById("bundlePreferences");
-        // The XBL binding for the string bundle may have been destroyed if
-        // the page was closed before this callback was executed.
-        if (!prefStrBundle.getFormattedString) {
-          return;
-        }
-        var sizeStr = prefStrBundle.getFormattedString("actualAppCacheSize", sizeStrings);
-        actualSizeLabel.value = sizeStr;
-      }
-    };
-
-    try {
-      var cacheService =
-        Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
-                  .getService(Components.interfaces.nsICacheStorageService);
-      var storage = cacheService.appCacheStorage(LoadContextInfo.default, null);
-      storage.asyncVisitStorage(visitor, false);
-    } catch (e) {}
-  },
-
   updateCacheSizeUI(smartSizeEnabled) {
     document.getElementById("useCacheBefore").disabled = smartSizeEnabled;
     document.getElementById("cacheSize").disabled = smartSizeEnabled;
     document.getElementById("useCacheAfter").disabled = smartSizeEnabled;
   },
 
   readSmartSizeEnabled() {
     // The smart_size.enabled preference element is inverted="true", so its
@@ -470,27 +449,16 @@ var gAdvancedPane = {
     try {
       var cache = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
                             .getService(Components.interfaces.nsICacheStorageService);
       cache.clear();
     } catch (ex) {}
     this.updateActualCacheSize();
   },
 
-  /**
-   * Clears the application cache.
-   */
-  clearOfflineAppCache() {
-    Components.utils.import("resource:///modules/offlineAppCache.jsm");
-    OfflineAppCacheHelper.clear();
-
-    this.updateActualAppCacheSize();
-    this.updateOfflineApps();
-  },
-
   clearSiteData() {
     let flags =
       Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
       Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
       Services.prompt.BUTTON_POS_0_DEFAULT;
     let prefStrBundle = document.getElementById("bundlePreferences");
     let title = prefStrBundle.getString("clearSiteDataPromptTitle");
     let text = prefStrBundle.getString("clearSiteDataPromptText");
@@ -498,16 +466,55 @@ var gAdvancedPane = {
 
     let result = Services.prompt.confirmEx(
       window, title, text, flags, btn0Label, null, null, null, {});
     if (result == 0) {
       SiteDataManager.removeAll();
     }
   },
 
+  // Methods for Offline Apps(Appcache)
+
+  /**
+   * Clears the application cache.
+   */
+  clearOfflineAppCache() {
+    Components.utils.import("resource:///modules/offlineAppCache.jsm");
+    OfflineAppCacheHelper.clear();
+
+    this.updateActualAppCacheSize();
+    this.updateOfflineApps();
+  },
+
+  // Retrieves the amount of space currently used by offline cache
+  updateActualAppCacheSize() {
+    var visitor = {
+      onCacheStorageInfo(aEntryCount, aConsumption, aCapacity, aDiskDirectory) {
+        var actualSizeLabel = document.getElementById("actualAppCacheSize");
+        var sizeStrings = DownloadUtils.convertByteUnits(aConsumption);
+        var prefStrBundle = document.getElementById("bundlePreferences");
+        // The XBL binding for the string bundle may have been destroyed if
+        // the page was closed before this callback was executed.
+        if (!prefStrBundle.getFormattedString) {
+          return;
+        }
+        var sizeStr = prefStrBundle.getFormattedString("actualAppCacheSize", sizeStrings);
+        actualSizeLabel.value = sizeStr;
+      }
+    };
+
+    try {
+      var cacheService =
+        Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
+                  .getService(Components.interfaces.nsICacheStorageService);
+      var storage = cacheService.appCacheStorage(LoadContextInfo.default, null);
+      storage.asyncVisitStorage(visitor, false);
+    } catch (e) {}
+  },
+
   readOfflineNotify() {
     var pref = document.getElementById("browser.offline-apps.notify");
     var button = document.getElementById("offlineNotifyExceptions");
     button.disabled = !pref.value;
     return pref.value;
   },
 
   showOfflineExceptions() {
@@ -641,16 +648,17 @@ var gAdvancedPane = {
       } catch (e) {}
 
       pm.removePermission(perm);
     }
     list.removeChild(item);
     gAdvancedPane.offlineAppSelected();
     this.updateActualAppCacheSize();
   },
+  // Methods for Offline Apps(Appcache) end
 
   // UPDATE TAB
 
   /*
    * Preferences:
    *
    * app.update.enabled
    * - true if updates to the application are enabled, false otherwise
--- a/browser/components/preferences/in-content-old/advanced.xul
+++ b/browser/components/preferences/in-content-old/advanced.xul
@@ -286,17 +286,17 @@
           </label>
           <textbox id="cacheSize" type="number" size="4" max="1024"
                    aria-labelledby="useCacheBefore cacheSize useCacheAfter"/>
           <label id="useCacheAfter" flex="1">&limitCacheSizeAfter.label;</label>
         </hbox>
       </groupbox>
 
       <!-- Offline apps -->
-      <groupbox id="offlineGroup">
+      <groupbox id="offlineGroup" hidden="true">
         <caption><label>&offlineStorage2.label;</label></caption>
 
         <hbox align="center">
           <label id="actualAppCacheSize" flex="1"/>
           <button id="clearOfflineAppCacheButton" icon="clear"
                   label="&clearOfflineAppCacheNow.label;" accesskey="&clearOfflineAppCacheNow.accesskey;"/>
         </hbox>
         <hbox align="center">
--- a/browser/components/preferences/in-content/advanced.js
+++ b/browser/components/preferences/in-content/advanced.js
@@ -37,28 +37,26 @@ var gAdvancedPane = {
       let onUnload = function() {
         window.removeEventListener("unload", onUnload);
         Services.prefs.removeObserver("app.update.", this);
       }.bind(this);
       window.addEventListener("unload", onUnload);
       Services.prefs.addObserver("app.update.", this, false);
       this.updateReadPrefs();
     }
-    this.updateOfflineApps();
     if (AppConstants.MOZ_CRASHREPORTER) {
       this.initSubmitCrashes();
     }
     this.initTelemetry();
     if (AppConstants.MOZ_TELEMETRY_REPORTING) {
       this.initSubmitHealthReport();
     }
     this.updateOnScreenKeyboardVisibility();
     this.updateCacheSizeInputField();
     this.updateActualCacheSize();
-    this.updateActualAppCacheSize();
 
     if (Services.prefs.getBoolPref("browser.storageManager.enabled")) {
       Services.obs.addObserver(this, "sitedatamanager:sites-updated", false);
       let unload = () => {
         window.removeEventListener("unload", unload);
         Services.obs.removeObserver(this, "sitedatamanager:sites-updated");
       };
       window.addEventListener("unload", unload);
@@ -80,40 +78,47 @@ var gAdvancedPane = {
       setEventListener("submitHealthReportBox", "command",
                        gAdvancedPane.updateSubmitHealthReport);
     }
 
     setEventListener("connectionSettings", "command",
                      gAdvancedPane.showConnections);
     setEventListener("clearCacheButton", "command",
                      gAdvancedPane.clearCache);
-    setEventListener("clearOfflineAppCacheButton", "command",
-                     gAdvancedPane.clearOfflineAppCache);
-    setEventListener("offlineNotifyExceptions", "command",
-                     gAdvancedPane.showOfflineExceptions);
-    setEventListener("offlineAppsList", "select",
-                     gAdvancedPane.offlineAppSelected);
-    let bundlePrefs = document.getElementById("bundlePreferences");
-    document.getElementById("offlineAppsList")
-            .style.height = bundlePrefs.getString("offlineAppsList.height");
-    setEventListener("offlineAppsListRemove", "command",
-                     gAdvancedPane.removeOfflineApp);
     if (AppConstants.MOZ_UPDATER) {
       setEventListener("updateRadioGroup", "command",
                        gAdvancedPane.updateWritePrefs);
       setEventListener("showUpdateHistory", "command",
                        gAdvancedPane.showUpdates);
     }
     setEventListener("viewCertificatesButton", "command",
                      gAdvancedPane.showCertificates);
     setEventListener("viewSecurityDevicesButton", "command",
                      gAdvancedPane.showSecurityDevices);
     setEventListener("cacheSize", "change",
                      gAdvancedPane.updateCacheSizePref);
 
+    if (Services.prefs.getBoolPref("browser.preferences.offlineGroup.enabled")) {
+      this.updateOfflineApps();
+      this.updateActualAppCacheSize();
+      setEventListener("offlineNotifyExceptions", "command",
+                      gAdvancedPane.showOfflineExceptions);
+      setEventListener("offlineAppsList", "select",
+                      gAdvancedPane.offlineAppSelected);
+      setEventListener("offlineAppsListRemove", "command",
+                      gAdvancedPane.removeOfflineApp);
+      setEventListener("clearOfflineAppCacheButton", "command",
+                      gAdvancedPane.clearOfflineAppCache);
+      let bundlePrefs = document.getElementById("bundlePreferences");
+      document.getElementById("offlineAppsList")
+              .style.height = bundlePrefs.getString("offlineAppsList.height");
+      let offlineGroup = document.getElementById("offlineGroup");
+      offlineGroup.hidden = false;
+    }
+
     if (AppConstants.MOZ_WIDGET_GTK) {
       // GTK tabbox' allow the scroll wheel to change the selected tab,
       // but we don't want this behavior for the in-content preferences.
       let tabsElement = document.getElementById("tabsElement");
       tabsElement.addEventListener("DOMMouseScroll", event => {
         event.stopPropagation();
       }, true);
     }
@@ -393,42 +398,16 @@ var gAdvancedPane = {
     try {
       var cacheService =
         Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
                   .getService(Components.interfaces.nsICacheStorageService);
       cacheService.asyncGetDiskConsumption(this.observer);
     } catch (e) {}
   },
 
-  // Retrieves the amount of space currently used by offline cache
-  updateActualAppCacheSize() {
-    var visitor = {
-      onCacheStorageInfo(aEntryCount, aConsumption, aCapacity, aDiskDirectory) {
-        var actualSizeLabel = document.getElementById("actualAppCacheSize");
-        var sizeStrings = DownloadUtils.convertByteUnits(aConsumption);
-        var prefStrBundle = document.getElementById("bundlePreferences");
-        // The XBL binding for the string bundle may have been destroyed if
-        // the page was closed before this callback was executed.
-        if (!prefStrBundle.getFormattedString) {
-          return;
-        }
-        var sizeStr = prefStrBundle.getFormattedString("actualAppCacheSize", sizeStrings);
-        actualSizeLabel.value = sizeStr;
-      }
-    };
-
-    try {
-      var cacheService =
-        Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
-                  .getService(Components.interfaces.nsICacheStorageService);
-      var storage = cacheService.appCacheStorage(LoadContextInfo.default, null);
-      storage.asyncVisitStorage(visitor, false);
-    } catch (e) {}
-  },
-
   updateCacheSizeUI(smartSizeEnabled) {
     document.getElementById("useCacheBefore").disabled = smartSizeEnabled;
     document.getElementById("cacheSize").disabled = smartSizeEnabled;
     document.getElementById("useCacheAfter").disabled = smartSizeEnabled;
   },
 
   readSmartSizeEnabled() {
     // The smart_size.enabled preference element is inverted="true", so its
@@ -470,27 +449,16 @@ var gAdvancedPane = {
     try {
       var cache = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
                             .getService(Components.interfaces.nsICacheStorageService);
       cache.clear();
     } catch (ex) {}
     this.updateActualCacheSize();
   },
 
-  /**
-   * Clears the application cache.
-   */
-  clearOfflineAppCache() {
-    Components.utils.import("resource:///modules/offlineAppCache.jsm");
-    OfflineAppCacheHelper.clear();
-
-    this.updateActualAppCacheSize();
-    this.updateOfflineApps();
-  },
-
   clearSiteData() {
     let flags =
       Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
       Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
       Services.prompt.BUTTON_POS_0_DEFAULT;
     let prefStrBundle = document.getElementById("bundlePreferences");
     let title = prefStrBundle.getString("clearSiteDataPromptTitle");
     let text = prefStrBundle.getString("clearSiteDataPromptText");
@@ -498,16 +466,55 @@ var gAdvancedPane = {
 
     let result = Services.prompt.confirmEx(
       window, title, text, flags, btn0Label, null, null, null, {});
     if (result == 0) {
       SiteDataManager.removeAll();
     }
   },
 
+  // Methods for Offline Apps(Appcache)
+
+  /**
+   * Clears the application cache.
+   */
+  clearOfflineAppCache() {
+    Components.utils.import("resource:///modules/offlineAppCache.jsm");
+    OfflineAppCacheHelper.clear();
+
+    this.updateActualAppCacheSize();
+    this.updateOfflineApps();
+  },
+
+  // Retrieves the amount of space currently used by offline cache
+  updateActualAppCacheSize() {
+    var visitor = {
+      onCacheStorageInfo(aEntryCount, aConsumption, aCapacity, aDiskDirectory) {
+        var actualSizeLabel = document.getElementById("actualAppCacheSize");
+        var sizeStrings = DownloadUtils.convertByteUnits(aConsumption);
+        var prefStrBundle = document.getElementById("bundlePreferences");
+        // The XBL binding for the string bundle may have been destroyed if
+        // the page was closed before this callback was executed.
+        if (!prefStrBundle.getFormattedString) {
+          return;
+        }
+        var sizeStr = prefStrBundle.getFormattedString("actualAppCacheSize", sizeStrings);
+        actualSizeLabel.value = sizeStr;
+      }
+    };
+
+    try {
+      var cacheService =
+        Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
+                  .getService(Components.interfaces.nsICacheStorageService);
+      var storage = cacheService.appCacheStorage(LoadContextInfo.default, null);
+      storage.asyncVisitStorage(visitor, false);
+    } catch (e) {}
+  },
+
   readOfflineNotify() {
     var pref = document.getElementById("browser.offline-apps.notify");
     var button = document.getElementById("offlineNotifyExceptions");
     button.disabled = !pref.value;
     return pref.value;
   },
 
   showOfflineExceptions() {
@@ -641,16 +648,17 @@ var gAdvancedPane = {
       } catch (e) {}
 
       pm.removePermission(perm);
     }
     list.removeChild(item);
     gAdvancedPane.offlineAppSelected();
     this.updateActualAppCacheSize();
   },
+  // Methods for Offline Apps(Appcache) end
 
   // UPDATE TAB
 
   /*
    * Preferences:
    *
    * app.update.enabled
    * - true if updates to the application are enabled, false otherwise
--- a/browser/components/preferences/in-content/advanced.xul
+++ b/browser/components/preferences/in-content/advanced.xul
@@ -286,17 +286,17 @@
           </label>
           <textbox id="cacheSize" type="number" size="4" max="1024"
                    aria-labelledby="useCacheBefore cacheSize useCacheAfter"/>
           <label id="useCacheAfter" flex="1">&limitCacheSizeAfter.label;</label>
         </hbox>
       </groupbox>
 
       <!-- Offline apps -->
-      <groupbox id="offlineGroup">
+      <groupbox id="offlineGroup" hidden="true">
         <caption><label>&offlineStorage2.label;</label></caption>
 
         <hbox align="center">
           <label id="actualAppCacheSize" flex="1"/>
           <button id="clearOfflineAppCacheButton" icon="clear"
                   label="&clearOfflineAppCacheNow.label;" accesskey="&clearOfflineAppCacheNow.accesskey;"/>
         </hbox>
         <hbox align="center">
--- a/browser/extensions/formautofill/skin/shared/autocomplete-item.css
+++ b/browser/extensions/formautofill/skin/shared/autocomplete-item.css
@@ -16,16 +16,17 @@ xul|richlistitem[originaltype="autofill-
   padding-top: 6px;
   padding-bottom: 6px;
   padding-inline-start: 16px;
   padding-inline-end: 10px;
   display: flex;
   flex-direction: row;
   flex-wrap: wrap;
   align-items: center;
+  color: -moz-FieldText
 }
 
 .profile-item-box:last-child {
   border-bottom: 0;
 }
 
 .profile-item-box > .profile-item-col {
   box-sizing: border-box;
--- a/build/clang-plugin/Utils.h
+++ b/build/clang-plugin/Utils.h
@@ -232,16 +232,17 @@ inline bool isIgnoredPathForSprintfLiter
   llvm::sys::fs::make_absolute(FileName);
   llvm::sys::path::reverse_iterator Begin = llvm::sys::path::rbegin(FileName),
                                     End = llvm::sys::path::rend(FileName);
   for (; Begin != End; ++Begin) {
     if (Begin->compare_lower(StringRef("angle")) == 0 ||
         Begin->compare_lower(StringRef("chromium")) == 0 ||
         Begin->compare_lower(StringRef("crashreporter")) == 0 ||
         Begin->compare_lower(StringRef("google-breakpad")) == 0 ||
+        Begin->compare_lower(StringRef("gflags")) == 0 ||
         Begin->compare_lower(StringRef("harfbuzz")) == 0 ||
         Begin->compare_lower(StringRef("libstagefright")) == 0 ||
         Begin->compare_lower(StringRef("mtransport")) == 0 ||
         Begin->compare_lower(StringRef("protobuf")) == 0 ||
         Begin->compare_lower(StringRef("skia")) == 0 ||
         Begin->compare_lower(StringRef("sfntly")) == 0 ||
         // Gtest uses snprintf as GTEST_SNPRINTF_ with sizeof
         Begin->compare_lower(StringRef("testing")) == 0) {
--- a/config/config.mk
+++ b/config/config.mk
@@ -408,20 +408,25 @@ endif
 else
 # For setting subsystem version
 WIN32_EXE_LDFLAGS	+= $(WIN32_CONSOLE_EXE_LDFLAGS)
 endif
 endif # WINNT
 
 ifdef _MSC_VER
 ifeq ($(CPU_ARCH),x86_64)
+ifdef MOZ_ASAN
+# ASan could have 3x stack memory usage of normal builds.
+WIN32_EXE_LDFLAGS	+= -STACK:6291456
+else
 # set stack to 2MB on x64 build.  See bug 582910
 WIN32_EXE_LDFLAGS	+= -STACK:2097152
 endif
 endif
+endif
 
 #
 # Include any personal overrides the user might think are needed.
 #
 -include $(topsrcdir)/$(MOZ_BUILD_APP)/app-config.mk
 -include $(MY_CONFIG)
 
 ######################################################################
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -303,17 +303,17 @@ Tools.netMonitor = {
   id: "netmonitor",
   accesskey: l10n("netmonitor.accesskey"),
   key: l10n("netmonitor.commandkey"),
   ordinal: 9,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   visibilityswitch: "devtools.netmonitor.enabled",
   icon: "chrome://devtools/skin/images/tool-network.svg",
   invertIconForDarkTheme: true,
-  url: "chrome://devtools/content/netmonitor/netmonitor.xhtml",
+  url: "chrome://devtools/content/netmonitor/index.html",
   label: l10n("netmonitor.label"),
   panelLabel: l10n("netmonitor.panelLabel"),
   get tooltip() {
     return l10n("netmonitor.tooltip2",
     (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
   },
   inMenu: true,
 
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -61,17 +61,17 @@ loader.lazyRequireGetter(this, "ToolboxB
 loader.lazyRequireGetter(this, "SourceMapService",
   "devtools/client/framework/source-map-service", true);
 loader.lazyRequireGetter(this, "HUDService",
   "devtools/client/webconsole/hudservice");
 loader.lazyRequireGetter(this, "viewSource",
   "devtools/client/shared/view-source");
 
 loader.lazyGetter(this, "registerHarOverlay", () => {
-  return require("devtools/client/netmonitor/har/toolbox-overlay").register;
+  return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
 });
 
 /**
  * A "Toolbox" is the component that holds all the tools for one specific
  * target. Visually, it's a document that includes the tools tabs and all
  * the iframes where the tool panels will be living in.
  *
  * @param {object} target
--- a/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
@@ -7,18 +7,18 @@
 // Test that the highlighter stays correctly positioned and has the right aspect
 // ratio even when the page is zoomed in or out.
 
 const TEST_URL = "data:text/html;charset=utf-8,<div>zoom me</div>";
 
 // TEST_LEVELS entries should contain the zoom level to test.
 const TEST_LEVELS = [2, 1, .5];
 
-// Returns the expected style attribute value to check for on the root highlighter
-// element, for the values given.
+// Returns the expected style attribute value to check for on the highlighter's elements
+// node, for the values given.
 const expectedStyle = (w, h, z) =>
         (z !== 1 ? `transform-origin:top left; transform:scale(${1 / z}); ` : "") +
         `position:absolute; width:${w * z}px;height:${h * z}px; ` +
         "overflow:hidden";
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
 
@@ -35,17 +35,17 @@ add_task(function* () {
     yield testActor.zoomPageTo(level);
     isVisible = yield testActor.isHighlighting();
     ok(isVisible, "The highlighter is still visible at zoom level " + level);
 
     yield testActor.isNodeCorrectlyHighlighted("div", is);
 
     info("Check that the highlighter root wrapper node was scaled down");
 
-    let style = yield getRootNodeStyle(testActor);
+    let style = yield getElementsNodeStyle(testActor);
     let { width, height } = yield testActor.getWindowDimensions();
     is(style, expectedStyle(width, height, level),
       "The style attribute of the root element is correct");
   }
 });
 
 function* hoverElement(selector, inspector) {
   info("Hovering node " + selector + " in the markup view");
@@ -55,13 +55,13 @@ function* hoverElement(selector, inspect
 
 function* hoverContainer(container, inspector) {
   let onHighlight = inspector.toolbox.once("node-highlight");
   EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
       inspector.markup.doc.defaultView);
   yield onHighlight;
 }
 
-function* getRootNodeStyle(testActor) {
+function* getElementsNodeStyle(testActor) {
   let value = yield testActor.getHighlighterNodeAttribute(
-    "box-model-root", "style");
+    "box-model-elements", "style");
   return value;
 }
--- a/devtools/client/inspector/test/browser_inspector_infobar_01.js
+++ b/devtools/client/inspector/test/browser_inspector_infobar_01.js
@@ -13,47 +13,52 @@ add_task(function* () {
 
   let testData = [
     {
       selector: "#top",
       position: "bottom",
       tag: "div",
       id: "top",
       classes: ".class1.class2",
-      dims: "500" + " \u00D7 " + "100"
+      dims: "500" + " \u00D7 " + "100",
+      arrowed: true
     },
     {
       selector: "#vertical",
-      position: "overlap",
+      position: "top",
       tag: "div",
       id: "vertical",
-      classes: ""
+      classes: "",
+      arrowed: false
       // No dims as they will vary between computers
     },
     {
       selector: "#bottom",
       position: "top",
       tag: "div",
       id: "bottom",
       classes: "",
-      dims: "500" + " \u00D7 " + "100"
+      dims: "500" + " \u00D7 " + "100",
+      arrowed: true
     },
     {
       selector: "body",
       position: "bottom",
       tag: "body",
-      classes: ""
+      classes: "",
+      arrowed: true
       // No dims as they will vary between computers
     },
     {
       selector: "clipPath",
       position: "bottom",
       tag: "clipPath",
       id: "clip",
-      classes: ""
+      classes: "",
+      arrowed: false
       // No dims as element is not displayed and we just want to test tag name
     },
   ];
 
   for (let currTest of testData) {
     yield testPosition(currTest, inspector, testActor);
   }
 });
@@ -76,14 +81,19 @@ function* testPosition(test, inspector, 
       "box-model-infobar-id");
     is(id, "#" + test.id, "node " + test.selector + ": id matches.");
   }
 
   let classes = yield testActor.getHighlighterNodeTextContent(
     "box-model-infobar-classes");
   is(classes, test.classes, "node " + test.selector + ": classes match.");
 
+  let arrowed = !(yield testActor.getHighlighterNodeAttribute(
+    "box-model-infobar-container", "hide-arrow"));
+
+  is(arrowed, test.arrowed, "node " + test.selector + ": arrow visibility match.");
+
   if (test.dims) {
     let dims = yield testActor.getHighlighterNodeTextContent(
       "box-model-infobar-dimensions");
     is(dims, test.dims, "node " + test.selector + ": dims match.");
   }
 }
--- a/devtools/client/inspector/test/browser_inspector_infobar_03.js
+++ b/devtools/client/inspector/test/browser_inspector_infobar_03.js
@@ -9,33 +9,33 @@
 const TEST_URI = URL_ROOT + "doc_inspector_infobar_03.html";
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
 
   let testData = {
     selector: "body",
     position: "overlap",
-    style: "top:0px",
+    style: "position:fixed",
   };
 
   yield testPositionAndStyle(testData, inspector, testActor);
 });
 
 function* testPositionAndStyle(test, inspector, testActor) {
   info("Testing " + test.selector);
 
   yield selectAndHighlightNode(test.selector, inspector);
 
   let style = yield testActor.getHighlighterNodeAttribute(
     "box-model-infobar-container", "style");
 
-  is(style.split(";")[0], test.style,
+  is(style.split(";")[0].trim(), test.style,
     "Infobar shows on top of the page when page isn't scrolled");
 
   yield testActor.scrollWindow(0, 500);
 
   style = yield testActor.getHighlighterNodeAttribute(
     "box-model-infobar-container", "style");
 
-  is(style.split(";")[0], test.style,
+  is(style.split(";")[0].trim(), test.style,
     "Infobar shows on top of the page even if the page is scrolled");
 }
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -5,18 +5,17 @@
 devtools.jar:
 %   content devtools %content/
     content/shared/vendor/d3.js (shared/vendor/d3.js)
     content/shared/vendor/dagre-d3.js (shared/vendor/dagre-d3.js)
     content/shared/widgets/widgets.css (shared/widgets/widgets.css)
     content/shared/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
     content/projecteditor/chrome/content/projecteditor.xul (projecteditor/chrome/content/projecteditor.xul)
     content/projecteditor/lib/helpers/readdir.js (projecteditor/lib/helpers/readdir.js)
-    content/netmonitor/netmonitor.xhtml (netmonitor/netmonitor.xhtml)
-    content/netmonitor/netmonitor.js (netmonitor/netmonitor.js)
+    content/netmonitor/index.html (netmonitor/index.html)
     content/webconsole/webconsole.xul (webconsole/webconsole.xul)
 *   content/scratchpad/scratchpad.xul (scratchpad/scratchpad.xul)
     content/scratchpad/scratchpad.js (scratchpad/scratchpad.js)
     content/shared/splitview.css (shared/splitview.css)
     content/shared/theme-switching.js (shared/theme-switching.js)
     content/shared/frame-script-utils.js (shared/frame-script-utils.js)
     content/styleeditor/styleeditor.xul (styleeditor/styleeditor.xul)
     content/storage/storage.xul (storage/storage.xul)
@@ -150,17 +149,17 @@ devtools.jar:
     skin/images/breakpoint.svg (themes/images/breakpoint.svg)
     skin/webconsole.css (themes/webconsole.css)
     skin/images/webconsole.svg (themes/images/webconsole.svg)
     skin/images/breadcrumbs-scrollbutton.png (themes/images/breadcrumbs-scrollbutton.png)
     skin/images/breadcrumbs-scrollbutton@2x.png (themes/images/breadcrumbs-scrollbutton@2x.png)
     skin/animationinspector.css (themes/animationinspector.css)
     skin/canvasdebugger.css (themes/canvasdebugger.css)
     skin/debugger.css (themes/debugger.css)
-    skin/netmonitor.css (themes/netmonitor.css)
+    skin/netmonitor.css (netmonitor/src/assets/styles/netmonitor.css)
     skin/performance.css (themes/performance.css)
     skin/memory.css (themes/memory.css)
     skin/scratchpad.css (themes/scratchpad.css)
     skin/shadereditor.css (themes/shadereditor.css)
     skin/storage.css (themes/storage.css)
     skin/splitview.css (themes/splitview.css)
     skin/styleeditor.css (themes/styleeditor.css)
     skin/webaudioeditor.css (themes/webaudioeditor.css)
deleted file mode 100644
--- a/devtools/client/netmonitor/components/monitor-panel.js
+++ /dev/null
@@ -1,135 +0,0 @@
-/* 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 {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
-const Actions = require("../actions/index");
-const { getLongString } = require("../utils/client");
-const { Prefs } = require("../utils/prefs");
-const { getFormDataSections } = require("../utils/request-utils");
-const { getSelectedRequest } = require("../selectors/index");
-
-// Components
-const SplitBox = createFactory(require("devtools/client/shared/components/splitter/split-box"));
-const NetworkDetailsPanel = createFactory(require("../shared/components/network-details-panel"));
-const RequestList = createFactory(require("./request-list"));
-const Toolbar = createFactory(require("./toolbar"));
-
-const { div } = DOM;
-const MediaQueryList = window.matchMedia("(min-width: 700px)");
-
-/*
- * Monitor panel component
- * The main panel for displaying various network request information
- */
-const MonitorPanel = createClass({
-  displayName: "MonitorPanel",
-
-  propTypes: {
-    isEmpty: PropTypes.bool.isRequired,
-    networkDetailsOpen: PropTypes.bool.isRequired,
-    openNetworkDetails: PropTypes.func.isRequired,
-    request: PropTypes.object,
-    updateRequest: PropTypes.func.isRequired,
-  },
-
-  getInitialState() {
-    return {
-      isVerticalSpliter: MediaQueryList.matches,
-    };
-  },
-
-  componentDidMount() {
-    MediaQueryList.addListener(this.onLayoutChange);
-  },
-
-  componentWillReceiveProps(nextProps) {
-    let {
-      request = {},
-      updateRequest,
-    } = nextProps;
-    let {
-      formDataSections,
-      requestHeaders,
-      requestHeadersFromUploadStream,
-      requestPostData,
-    } = request;
-
-    if (!formDataSections && requestHeaders &&
-        requestHeadersFromUploadStream && requestPostData) {
-      getFormDataSections(
-        requestHeaders,
-        requestHeadersFromUploadStream,
-        requestPostData,
-        getLongString,
-      ).then((newFormDataSections) => {
-        updateRequest(
-          request.id,
-          { formDataSections: newFormDataSections },
-          true,
-        );
-      });
-    }
-  },
-
-  componentWillUnmount() {
-    MediaQueryList.removeListener(this.onLayoutChange);
-
-    let { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
-
-    if (this.state.isVerticalSpliter && clientWidth) {
-      Prefs.networkDetailsWidth = clientWidth;
-    }
-    if (!this.state.isVerticalSpliter && clientHeight) {
-      Prefs.networkDetailsHeight = clientHeight;
-    }
-  },
-
-  onLayoutChange() {
-    this.setState({
-      isVerticalSpliter: MediaQueryList.matches,
-    });
-  },
-
-  render() {
-    let { isEmpty, networkDetailsOpen } = this.props;
-    return (
-      div({ className: "monitor-panel" },
-        Toolbar(),
-        SplitBox({
-          className: "devtools-responsive-container",
-          initialWidth: `${Prefs.networkDetailsWidth}px`,
-          initialHeight: `${Prefs.networkDetailsHeight}px`,
-          minSize: "50px",
-          maxSize: "80%",
-          splitterSize: "1px",
-          startPanel: RequestList({ isEmpty }),
-          endPanel: networkDetailsOpen && NetworkDetailsPanel({ ref: "endPanel" }),
-          endPanelControl: true,
-          vert: this.state.isVerticalSpliter,
-        }),
-      )
-    );
-  }
-});
-
-module.exports = connect(
-  (state) => ({
-    isEmpty: state.requests.requests.isEmpty(),
-    networkDetailsOpen: state.ui.networkDetailsOpen,
-    request: getSelectedRequest(state),
-  }),
-  (dispatch) => ({
-    openNetworkDetails: (open) => dispatch(Actions.openNetworkDetails(open)),
-    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
-  }),
-)(MonitorPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/network-monitor.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/* 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");
-
-// Components
-const MonitorPanel = createFactory(require("./monitor-panel"));
-const StatisticsPanel = createFactory(require("./statistics-panel"));
-
-const { div } = DOM;
-
-/*
- * Network monitor component
- */
-function NetworkMonitor({ statisticsOpen }) {
-  return (
-    div({ className: "network-monitor" },
-      !statisticsOpen ? MonitorPanel() : StatisticsPanel()
-    )
-  );
-}
-
-NetworkMonitor.displayName = "NetworkMonitor";
-
-NetworkMonitor.propTypes = {
-  statisticsOpen: PropTypes.bool.isRequired,
-};
-
-module.exports = connect(
-  (state) => ({ statisticsOpen: state.ui.statisticsOpen }),
-)(NetworkMonitor);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-content.js
+++ /dev/null
@@ -1,280 +0,0 @@
-/* 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 { KeyCodes } = require("devtools/client/shared/keycodes");
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
-const Actions = require("../actions/index");
-const {
-  setTooltipImageContent,
-  setTooltipStackTraceContent,
-} = require("../request-list-tooltip");
-const {
-  getDisplayedRequests,
-  getWaterfallScale,
-} = require("../selectors/index");
-
-// Components
-const RequestListItem = createFactory(require("./request-list-item"));
-const RequestListContextMenu = require("../request-list-context-menu");
-
-const { div } = DOM;
-
-// tooltip show/hide delay in ms
-const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
-
-/**
- * Renders the actual contents of the request list.
- */
-const RequestListContent = createClass({
-  displayName: "RequestListContent",
-
-  propTypes: {
-    dispatch: PropTypes.func.isRequired,
-    displayedRequests: PropTypes.object.isRequired,
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    fromCache: PropTypes.bool.isRequired,
-    onItemMouseDown: PropTypes.func.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-    onSelectDelta: PropTypes.func.isRequired,
-    scale: PropTypes.number,
-    selectedRequestId: PropTypes.string,
-  },
-
-  componentWillMount() {
-    const { dispatch } = this.props;
-    this.contextMenu = new RequestListContextMenu({
-      cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-      openStatistics: (open) => dispatch(Actions.openStatistics(open)),
-    });
-    this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
-  },
-
-  componentDidMount() {
-    // Set the CSS variables for waterfall scaling
-    this.setScalingStyles();
-
-    // Install event handler for displaying a tooltip
-    this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
-      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
-      interactive: true
-    });
-
-    // Install event handler to hide the tooltip on scroll
-    this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
-  },
-
-  componentWillUpdate(nextProps) {
-    // Check if the list is scrolled to bottom before the UI update.
-    // The scroll is ever needed only if new rows are added to the list.
-    const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
-    this.shouldScrollBottom = delta > 0 && this.isScrolledToBottom();
-  },
-
-  componentDidUpdate(prevProps) {
-    // Update the CSS variables for waterfall scaling after props change
-    this.setScalingStyles(prevProps);
-
-    // Keep the list scrolled to bottom if a new row was added
-    if (this.shouldScrollBottom) {
-      let node = this.refs.contentEl;
-      node.scrollTop = node.scrollHeight;
-    }
-  },
-
-  componentWillUnmount() {
-    this.refs.contentEl.removeEventListener("scroll", this.onScroll, true);
-
-    // Uninstall the tooltip event handler
-    this.tooltip.stopTogglingOnHover();
-  },
-
-  /**
-   * Set the CSS variables for waterfall scaling. If React supported setting CSS
-   * variables as part of the "style" property of a DOM element, we would use that.
-   *
-   * However, React doesn't support this, so we need to use a hack and update the
-   * DOM element directly: https://github.com/facebook/react/issues/6411
-   */
-  setScalingStyles(prevProps) {
-    const { scale } = this.props;
-    if (prevProps && prevProps.scale === scale) {
-      return;
-    }
-
-    const { style } = this.refs.contentEl;
-    style.removeProperty("--timings-scale");
-    style.removeProperty("--timings-rev-scale");
-    style.setProperty("--timings-scale", scale);
-    style.setProperty("--timings-rev-scale", 1 / scale);
-  },
-
-  isScrolledToBottom() {
-    const { contentEl } = this.refs;
-    const lastChildEl = contentEl.lastElementChild;
-
-    if (!lastChildEl) {
-      return false;
-    }
-
-    let lastChildRect = lastChildEl.getBoundingClientRect();
-    let contentRect = contentEl.getBoundingClientRect();
-
-    return (lastChildRect.height + lastChildRect.top) <= contentRect.bottom;
-  },
-
-  /**
-   * The predicate used when deciding whether a popup should be shown
-   * over a request item or not.
-   *
-   * @param nsIDOMNode target
-   *        The element node currently being hovered.
-   * @param object tooltip
-   *        The current tooltip instance.
-   * @return {Promise}
-   */
-  onHover(target, tooltip) {
-    let itemEl = target.closest(".request-list-item");
-    if (!itemEl) {
-      return false;
-    }
-    let itemId = itemEl.dataset.id;
-    if (!itemId) {
-      return false;
-    }
-    let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
-    if (!requestItem) {
-      return false;
-    }
-
-    if (requestItem.responseContent && target.closest(".requests-list-icon-and-file")) {
-      return setTooltipImageContent(tooltip, itemEl, requestItem);
-    } else if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
-      return setTooltipStackTraceContent(tooltip, requestItem);
-    }
-
-    return false;
-  },
-
-  /**
-   * Scroll listener for the requests menu view.
-   */
-  onScroll() {
-    this.tooltip.hide();
-  },
-
-  /**
-   * Handler for keyboard events. For arrow up/down, page up/down, home/end,
-   * move the selection up or down.
-   */
-  onKeyDown(e) {
-    let delta;
-
-    switch (e.keyCode) {
-      case KeyCodes.DOM_VK_UP:
-      case KeyCodes.DOM_VK_LEFT:
-        delta = -1;
-        break;
-      case KeyCodes.DOM_VK_DOWN:
-      case KeyCodes.DOM_VK_RIGHT:
-        delta = +1;
-        break;
-      case KeyCodes.DOM_VK_PAGE_UP:
-        delta = "PAGE_UP";
-        break;
-      case KeyCodes.DOM_VK_PAGE_DOWN:
-        delta = "PAGE_DOWN";
-        break;
-      case KeyCodes.DOM_VK_HOME:
-        delta = -Infinity;
-        break;
-      case KeyCodes.DOM_VK_END:
-        delta = +Infinity;
-        break;
-    }
-
-    if (delta) {
-      // Prevent scrolling when pressing navigation keys.
-      e.preventDefault();
-      e.stopPropagation();
-      this.props.onSelectDelta(delta);
-    }
-  },
-
-  onContextMenu(evt) {
-    evt.preventDefault();
-    this.contextMenu.open(evt);
-  },
-
-  /**
-   * If selection has just changed (by keyboard navigation), don't keep the list
-   * scrolled to bottom, but allow scrolling up with the selection.
-   */
-  onFocusedNodeChange() {
-    this.shouldScrollBottom = false;
-  },
-
-  render() {
-    const {
-      displayedRequests,
-      firstRequestStartedMillis,
-      selectedRequestId,
-      onItemMouseDown,
-      onSecurityIconClick,
-    } = this.props;
-
-    return (
-      div({
-        ref: "contentEl",
-        className: "requests-list-contents",
-        tabIndex: 0,
-        onKeyDown: this.onKeyDown,
-      },
-        displayedRequests.map((item, index) => RequestListItem({
-          firstRequestStartedMillis,
-          fromCache: item.status === "304" || item.fromCache,
-          item,
-          index,
-          isSelected: item.id === selectedRequestId,
-          key: item.id,
-          onContextMenu: this.onContextMenu,
-          onFocusedNodeChange: this.onFocusedNodeChange,
-          onMouseDown: () => onItemMouseDown(item.id),
-          onSecurityIconClick: () => onSecurityIconClick(item.securityState),
-        }))
-      )
-    );
-  },
-});
-
-module.exports = connect(
-  (state) => ({
-    displayedRequests: getDisplayedRequests(state),
-    firstRequestStartedMillis: state.requests.firstStartedMillis,
-    selectedRequestId: state.requests.selectedId,
-    scale: getWaterfallScale(state),
-  }),
-  (dispatch) => ({
-    dispatch,
-    onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
-    /**
-     * A handler that opens the security tab in the details view if secure or
-     * broken security indicator is clicked.
-     */
-    onSecurityIconClick: (securityState) => {
-      if (securityState && securityState !== "insecure") {
-        dispatch(Actions.selectDetailsPanelTab("security"));
-      }
-    },
-    onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
-  }),
-)(RequestListContent);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-header.js
+++ /dev/null
@@ -1,198 +0,0 @@
-/* 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 { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
-const { div, button } = DOM;
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
-const { L10N } = require("../utils/l10n");
-const { getWaterfallScale } = require("../selectors/index");
-const Actions = require("../actions/index");
-const WaterfallBackground = require("../waterfall-background");
-const { getFormattedTime } = require("../utils/format-utils");
-
-const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
-const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
-
-const HEADERS = [
-  { name: "status", label: "status3" },
-  { name: "method" },
-  { name: "file", boxName: "icon-and-file" },
-  { name: "domain", boxName: "security-and-domain" },
-  { name: "cause" },
-  { name: "type" },
-  { name: "transferred" },
-  { name: "size" },
-  { name: "waterfall" }
-];
-
-/**
- * Render the request list header with sorting arrows for columns.
- * Displays tick marks in the waterfall column header.
- * Also draws the waterfall background canvas and updates it when needed.
- */
-const RequestListHeader = createClass({
-  displayName: "RequestListHeader",
-
-  propTypes: {
-    sort: PropTypes.object,
-    scale: PropTypes.number,
-    waterfallWidth: PropTypes.number,
-    onHeaderClick: PropTypes.func.isRequired,
-    resizeWaterfall: PropTypes.func.isRequired,
-  },
-
-  componentDidMount() {
-    // Create the object that takes care of drawing the waterfall canvas background
-    this.background = new WaterfallBackground(document);
-    this.background.draw(this.props);
-    this.resizeWaterfall();
-    window.addEventListener("resize", this.resizeWaterfall);
-  },
-
-  componentDidUpdate() {
-    this.background.draw(this.props);
-  },
-
-  componentWillUnmount() {
-    this.background.destroy();
-    this.background = null;
-    window.removeEventListener("resize", this.resizeWaterfall);
-  },
-
-  resizeWaterfall() {
-    // Measure its width and update the 'waterfallWidth' property in the store.
-    // The 'waterfallWidth' will be further updated on every window resize.
-    setNamedTimeout("resize-events", 50, () => {
-      const { width } = this.refs.header.getBoundingClientRect();
-      this.props.resizeWaterfall(width);
-    });
-  },
-
-  render() {
-    const { sort, scale, waterfallWidth, onHeaderClick } = this.props;
-
-    return div(
-      { className: "devtools-toolbar requests-list-toolbar" },
-      div({ className: "toolbar-labels" },
-        HEADERS.map(header => {
-          const name = header.name;
-          const boxName = header.boxName || name;
-          const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
-
-          let sorted, sortedTitle;
-          const active = sort.type == name ? true : undefined;
-          if (active) {
-            sorted = sort.ascending ? "ascending" : "descending";
-            sortedTitle = L10N.getStr(sort.ascending
-              ? "networkMenu.sortedAsc"
-              : "networkMenu.sortedDesc");
-          }
-
-          return div(
-            {
-              id: `requests-list-${boxName}-header-box`,
-              className: `requests-list-header requests-list-${boxName}`,
-              key: name,
-              ref: "header",
-              // Used to style the next column.
-              "data-active": active,
-            },
-            button(
-              {
-                id: `requests-list-${name}-button`,
-                className: `requests-list-header-button requests-list-${name}`,
-                "data-sorted": sorted,
-                title: sortedTitle,
-                onClick: () => onHeaderClick(name),
-              },
-              name == "waterfall" ? WaterfallLabel(waterfallWidth, scale, label)
-                                  : div({ className: "button-text" }, label),
-              div({ className: "button-icon" })
-            )
-          );
-        })
-      )
-    );
-  }
-});
-
-/**
- * Build the waterfall header - timing tick marks with the right spacing
- */
-function waterfallDivisionLabels(waterfallWidth, scale) {
-  let labels = [];
-
-  // Build new millisecond tick labels...
-  let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
-  let scaledStep = scale * timingStep;
-
-  // Ignore any divisions that would end up being too close to each other.
-  while (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
-    scaledStep *= 2;
-  }
-
-  // Insert one label for each division on the current scale.
-  for (let x = 0; x < waterfallWidth; x += scaledStep) {
-    let millisecondTime = x / scale;
-    let divisionScale = "millisecond";
-
-    // If the division is greater than 1 minute.
-    if (millisecondTime > 60000) {
-      divisionScale = "minute";
-    } else if (millisecondTime > 1000) {
-      // If the division is greater than 1 second.
-      divisionScale = "second";
-    }
-
-    let width = (x + scaledStep | 0) - (x | 0);
-    // Adjust the first marker for the borders
-    if (x == 0) {
-      width -= 2;
-    }
-    // Last marker doesn't need a width specified at all
-    if (x + scaledStep >= waterfallWidth) {
-      width = undefined;
-    }
-
-    labels.push(div(
-      {
-        key: labels.length,
-        className: "requests-list-timings-division",
-        "data-division-scale": divisionScale,
-        style: { width }
-      },
-      getFormattedTime(millisecondTime)
-    ));
-  }
-
-  return labels;
-}
-
-function WaterfallLabel(waterfallWidth, scale, label) {
-  let className = "button-text requests-list-waterfall-label-wrapper";
-
-  if (waterfallWidth != null && scale != null) {
-    label = waterfallDivisionLabels(waterfallWidth, scale);
-    className += " requests-list-waterfall-visible";
-  }
-
-  return div({ className }, label);
-}
-
-module.exports = connect(
-  state => ({
-    sort: state.sort,
-    scale: getWaterfallScale(state),
-    waterfallWidth: state.ui.waterfallWidth,
-    firstRequestStartedMillis: state.requests.firstStartedMillis,
-    timingMarkers: state.timingMarkers,
-  }),
-  dispatch => ({
-    onHeaderClick: type => dispatch(Actions.sortBy(type)),
-    resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)),
-  })
-)(RequestListHeader);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-item.js
+++ /dev/null
@@ -1,528 +0,0 @@
-/* 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 {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../utils/l10n");
-const { getAbbreviatedMimeType } = require("../utils/request-utils");
-const { getFormattedSize } = require("../utils/format-utils");
-
-const { div, img, span } = DOM;
-
-/**
- * Compare two objects on a subset of their properties
- */
-function propertiesEqual(props, item1, item2) {
-  return item1 === item2 || props.every(p => item1[p] === item2[p]);
-}
-
-/**
- * Used by shouldComponentUpdate: compare two items, and compare only properties
- * relevant for rendering the RequestListItem. Other properties (like request and
- * response headers, cookies, bodies) are ignored. These are very useful for the
- * network details, but not here.
- */
-const UPDATED_REQ_ITEM_PROPS = [
-  "mimeType",
-  "eventTimings",
-  "securityState",
-  "responseContentDataUri",
-  "status",
-  "statusText",
-  "fromCache",
-  "fromServiceWorker",
-  "method",
-  "url",
-  "remoteAddress",
-  "cause",
-  "contentSize",
-  "transferredSize",
-  "startedMillis",
-  "totalTime",
-];
-
-const UPDATED_REQ_PROPS = [
-  "index",
-  "isSelected",
-  "firstRequestStartedMillis",
-];
-
-/**
- * Render one row in the request list.
- */
-const RequestListItem = createClass({
-  displayName: "RequestListItem",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-    index: PropTypes.number.isRequired,
-    isSelected: PropTypes.bool.isRequired,
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    fromCache: PropTypes.bool.isRequired,
-    onContextMenu: PropTypes.func.isRequired,
-    onFocusedNodeChange: PropTypes.func,
-    onMouseDown: PropTypes.func.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-  },
-
-  componentDidMount() {
-    if (this.props.isSelected) {
-      this.refs.el.focus();
-    }
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
-      !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
-  },
-
-  componentDidUpdate(prevProps) {
-    if (!prevProps.isSelected && this.props.isSelected) {
-      this.refs.el.focus();
-      if (this.props.onFocusedNodeChange) {
-        this.props.onFocusedNodeChange();
-      }
-    }
-  },
-
-  render() {
-    const {
-      item,
-      index,
-      isSelected,
-      firstRequestStartedMillis,
-      fromCache,
-      onContextMenu,
-      onMouseDown,
-      onSecurityIconClick
-    } = this.props;
-
-    let classList = ["request-list-item"];
-    if (isSelected) {
-      classList.push("selected");
-    }
-
-    if (fromCache) {
-      classList.push("fromCache");
-    }
-
-    classList.push(index % 2 ? "odd" : "even");
-
-    return (
-      div({
-        ref: "el",
-        className: classList.join(" "),
-        "data-id": item.id,
-        tabIndex: 0,
-        onContextMenu,
-        onMouseDown,
-      },
-        StatusColumn({ item }),
-        MethodColumn({ item }),
-        FileColumn({ item }),
-        DomainColumn({ item, onSecurityIconClick }),
-        CauseColumn({ item }),
-        TypeColumn({ item }),
-        TransferredSizeColumn({ item }),
-        ContentSizeColumn({ item }),
-        WaterfallColumn({ item, firstRequestStartedMillis }),
-      )
-    );
-  }
-});
-
-const UPDATED_STATUS_PROPS = [
-  "status",
-  "statusText",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const StatusColumn = createFactory(createClass({
-  displayName: "StatusColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { status, statusText, fromCache, fromServiceWorker } = this.props.item;
-
-    let code, title;
-
-    if (status) {
-      if (fromCache) {
-        code = "cached";
-      } else if (fromServiceWorker) {
-        code = "service worker";
-      } else {
-        code = status;
-      }
-
-      if (statusText) {
-        title = `${status} ${statusText}`;
-        if (fromCache) {
-          title += " (cached)";
-        }
-        if (fromServiceWorker) {
-          title += " (service worker)";
-        }
-      }
-    }
-
-    return (
-        div({ className: "requests-list-subitem requests-list-status", title },
-        div({ className: "requests-list-status-icon", "data-code": code }),
-        span({ className: "subitem-label requests-list-status-code" }, status)
-      )
-    );
-  }
-}));
-
-const MethodColumn = createFactory(createClass({
-  displayName: "MethodColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.method !== nextProps.item.method;
-  },
-
-  render() {
-    const { method } = this.props.item;
-    return (
-      div({ className: "requests-list-subitem requests-list-method-box" },
-        span({ className: "subitem-label requests-list-method" }, method)
-      )
-    );
-  }
-}));
-
-const UPDATED_FILE_PROPS = [
-  "urlDetails",
-  "responseContentDataUri",
-];
-
-const FileColumn = createFactory(createClass({
-  displayName: "FileColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { urlDetails, responseContentDataUri } = this.props.item;
-
-    return (
-      div({ className: "requests-list-subitem requests-list-icon-and-file" },
-        img({
-          className: "requests-list-icon",
-          src: responseContentDataUri,
-          hidden: !responseContentDataUri,
-          "data-type": responseContentDataUri ? "thumbnail" : undefined,
-        }),
-        div({
-          className: "subitem-label requests-list-file",
-          title: urlDetails.unicodeUrl,
-        },
-          urlDetails.baseNameWithQuery,
-        ),
-      )
-    );
-  }
-}));
-
-const UPDATED_DOMAIN_PROPS = [
-  "urlDetails",
-  "remoteAddress",
-  "securityState",
-];
-
-const DomainColumn = createFactory(createClass({
-  displayName: "DomainColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { item, onSecurityIconClick } = this.props;
-    const { urlDetails, remoteAddress, securityState } = item;
-
-    let iconClassList = ["requests-security-state-icon"];
-    let iconTitle;
-    if (urlDetails.isLocal) {
-      iconClassList.push("security-state-local");
-      iconTitle = L10N.getStr("netmonitor.security.state.secure");
-    } else if (securityState) {
-      iconClassList.push(`security-state-${securityState}`);
-      iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
-    }
-
-    let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : "");
-
-    return (
-      div({ className: "requests-list-subitem requests-list-security-and-domain" },
-        div({
-          className: iconClassList.join(" "),
-          title: iconTitle,
-          onClick: onSecurityIconClick,
-        }),
-        span({ className: "subitem-label requests-list-domain", title }, urlDetails.host),
-      )
-    );
-  }
-}));
-
-const CauseColumn = createFactory(createClass({
-  displayName: "CauseColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.cause !== nextProps.item.cause;
-  },
-
-  render() {
-    const { cause } = this.props.item;
-
-    let causeType = "";
-    let causeUri = undefined;
-    let causeHasStack = false;
-
-    if (cause) {
-      // Legacy server might send a numeric value. Display it as "unknown"
-      causeType = typeof cause.type === "string" ? cause.type : "unknown";
-      causeUri = cause.loadingDocumentUri;
-      causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-cause",
-        title: causeUri,
-      },
-        span({
-          className: "requests-list-cause-stack",
-          hidden: !causeHasStack,
-        }, "JS"),
-        span({ className: "subitem-label" }, causeType),
-      )
-    );
-  }
-}));
-
-const CONTENT_MIME_TYPE_ABBREVIATIONS = {
-  "ecmascript": "js",
-  "javascript": "js",
-  "x-javascript": "js"
-};
-
-const TypeColumn = createFactory(createClass({
-  displayName: "TypeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.mimeType !== nextProps.item.mimeType;
-  },
-
-  render() {
-    const { mimeType } = this.props.item;
-    let abbrevType;
-    if (mimeType) {
-      abbrevType = getAbbreviatedMimeType(mimeType);
-      abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-type",
-        title: mimeType,
-      },
-        span({ className: "subitem-label" }, abbrevType),
-      )
-    );
-  }
-}));
-
-const UPDATED_TRANSFERRED_PROPS = [
-  "transferredSize",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const TransferredSizeColumn = createFactory(createClass({
-  displayName: "TransferredSizeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { transferredSize, fromCache, fromServiceWorker, status } = this.props.item;
-
-    let text;
-    let className = "subitem-label";
-    if (fromCache || status === "304") {
-      text = L10N.getStr("networkMenu.sizeCached");
-      className += " theme-comment";
-    } else if (fromServiceWorker) {
-      text = L10N.getStr("networkMenu.sizeServiceWorker");
-      className += " theme-comment";
-    } else if (typeof transferredSize == "number") {
-      text = getFormattedSize(transferredSize);
-    } else if (transferredSize === null) {
-      text = L10N.getStr("networkMenu.sizeUnavailable");
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-transferred",
-        title: text,
-      },
-        span({ className }, text),
-      )
-    );
-  }
-}));
-
-const ContentSizeColumn = createFactory(createClass({
-  displayName: "ContentSizeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.contentSize !== nextProps.item.contentSize;
-  },
-
-  render() {
-    const { contentSize } = this.props.item;
-
-    let text;
-    if (typeof contentSize == "number") {
-      text = getFormattedSize(contentSize);
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem subitem-label requests-list-size",
-        title: text,
-      },
-        span({ className: "subitem-label" }, text),
-      )
-    );
-  }
-}));
-
-const UPDATED_WATERFALL_PROPS = [
-  "eventTimings",
-  "totalTime",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const WaterfallColumn = createFactory(createClass({
-  displayName: "WaterfallColumn",
-
-  propTypes: {
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis ||
-      !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { item, firstRequestStartedMillis } = this.props;
-
-    return (
-      div({ className: "requests-list-subitem requests-list-waterfall" },
-        div({
-          className: "requests-list-timings",
-          style: {
-            paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`,
-          },
-        },
-          timingBoxes(item),
-        )
-      )
-    );
-  }
-}));
-
-// List of properties of the timing info we want to create boxes for
-const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"];
-
-function timingBoxes(item) {
-  const { eventTimings, totalTime, fromCache, fromServiceWorker } = item;
-  let boxes = [];
-
-  if (fromCache || fromServiceWorker) {
-    return boxes;
-  }
-
-  if (eventTimings) {
-    // Add a set of boxes representing timing information.
-    for (let key of TIMING_KEYS) {
-      let width = eventTimings.timings[key];
-
-      // Don't render anything if it surely won't be visible.
-      // One millisecond == one unscaled pixel.
-      if (width > 0) {
-        boxes.push(div({
-          key,
-          className: "requests-list-timings-box " + key,
-          style: { width }
-        }));
-      }
-    }
-  }
-
-  if (typeof totalTime === "number") {
-    let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
-    boxes.push(div({
-      key: "total",
-      className: "requests-list-timings-total",
-      title: text
-    }, text));
-  }
-
-  return boxes;
-}
-
-module.exports = RequestListItem;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* 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");
-
-// Components
-const RequestListContent = createFactory(require("./request-list-content"));
-const RequestListEmptyNotice = createFactory(require("./request-list-empty"));
-const RequestListHeader = createFactory(require("./request-list-header"));
-
-const { div } = DOM;
-
-/**
- * Request panel component
- */
-function RequestList({ isEmpty }) {
-  return (
-    div({ className: "request-list-container" },
-      RequestListHeader(),
-      isEmpty ? RequestListEmptyNotice() : RequestListContent(),
-    )
-  );
-}
-
-RequestList.displayName = "RequestList";
-
-RequestList.propTypes = {
-  isEmpty: PropTypes.bool.isRequired,
-};
-
-module.exports = RequestList;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/statistics-panel.js
+++ /dev/null
@@ -1,276 +0,0 @@
-/* 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 {
-  createClass,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { Chart } = require("devtools/client/shared/widgets/Chart");
-const { PluralForm } = require("devtools/shared/plural-form");
-const Actions = require("../actions/index");
-const { Filters } = require("../utils/filter-predicates");
-const { L10N } = require("../utils/l10n");
-const {
-  getSizeWithDecimals,
-  getTimeWithDecimals
-} = require("../utils/format-utils");
-
-const { button, div } = DOM;
-const MediaQueryList = window.matchMedia("(min-width: 700px)");
-
-const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
-const BACK_BUTTON = L10N.getStr("netmonitor.backButton");
-const CHARTS_CACHE_ENABLED = L10N.getStr("charts.cacheEnabled");
-const CHARTS_CACHE_DISABLED = L10N.getStr("charts.cacheDisabled");
-
-/*
- * Statistics panel component
- * Performance analysis tool which shows you how long the browser takes to
- * download the different parts of your site.
- */
-const StatisticsPanel = createClass({
-  displayName: "StatisticsPanel",
-
-  propTypes: {
-    closeStatistics: PropTypes.func.isRequired,
-    enableRequestFilterTypeOnly: PropTypes.func.isRequired,
-    requests: PropTypes.object,
-  },
-
-  getInitialState() {
-    return {
-      isVerticalSpliter: MediaQueryList.matches,
-    };
-  },
-
-  componentDidUpdate(prevProps) {
-    MediaQueryList.addListener(this.onLayoutChange);
-
-    const { requests } = this.props;
-    let ready = requests && !requests.isEmpty() && requests.every((req) =>
-      req.contentSize !== undefined && req.mimeType && req.responseHeaders &&
-      req.status !== undefined && req.totalTime !== undefined
-    );
-
-    this.createChart({
-      id: "primedCacheChart",
-      title: CHARTS_CACHE_ENABLED,
-      data: ready ? this.sanitizeChartDataSource(requests, false) : null,
-    });
-
-    this.createChart({
-      id: "emptyCacheChart",
-      title: CHARTS_CACHE_DISABLED,
-      data: ready ? this.sanitizeChartDataSource(requests, true) : null,
-    });
-  },
-
-  componentWillUnmount() {
-    MediaQueryList.removeListener(this.onLayoutChange);
-  },
-
-  createChart({ id, title, data }) {
-    // Create a new chart.
-    let chart = Chart.PieTable(document, {
-      diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
-      title,
-      header: {
-        cached: "",
-        count: "",
-        label: L10N.getStr("charts.type"),
-        size: L10N.getStr("charts.size"),
-        transferredSize: L10N.getStr("charts.transferred"),
-        time: L10N.getStr("charts.time"),
-      },
-      data,
-      strings: {
-        size: (value) =>
-          L10N.getFormatStr("charts.sizeKB", getSizeWithDecimals(value / 1024)),
-        transferredSize: (value) =>
-          L10N.getFormatStr("charts.transferredSizeKB",
-            getSizeWithDecimals(value / 1024)),
-        time: (value) =>
-          L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
-      },
-      totals: {
-        cached: (total) => L10N.getFormatStr("charts.totalCached", total),
-        count: (total) => L10N.getFormatStr("charts.totalCount", total),
-        size: (total) =>
-          L10N.getFormatStr("charts.totalSize", getSizeWithDecimals(total / 1024)),
-        transferredSize: total =>
-          L10N.getFormatStr("charts.totalTransferredSize",
-            getSizeWithDecimals(total / 1024)),
-        time: (total) => {
-          let seconds = total / 1000;
-          let string = getTimeWithDecimals(seconds);
-          return PluralForm.get(seconds,
-            L10N.getStr("charts.totalSeconds")).replace("#1", string);
-        },
-      },
-      sorted: true,
-    });
-
-    chart.on("click", (_, { label }) => {
-      // Reset FilterButtons and enable one filter exclusively
-      this.props.closeStatistics();
-      this.props.enableRequestFilterTypeOnly(label);
-    });
-
-    let container = this.refs[id];
-
-    // Nuke all existing charts of the specified type.
-    while (container.hasChildNodes()) {
-      container.firstChild.remove();
-    }
-
-    container.appendChild(chart.node);
-  },
-
-  sanitizeChartDataSource(requests, emptyCache) {
-    const data = [
-      "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
-    ].map((type) => ({
-      cached: 0,
-      count: 0,
-      label: type,
-      size: 0,
-      transferredSize: 0,
-      time: 0,
-    }));
-
-    for (let request of requests) {
-      let type;
-
-      if (Filters.html(request)) {
-        // "html"
-        type = 0;
-      } else if (Filters.css(request)) {
-        // "css"
-        type = 1;
-      } else if (Filters.js(request)) {
-        // "js"
-        type = 2;
-      } else if (Filters.fonts(request)) {
-        // "fonts"
-        type = 4;
-      } else if (Filters.images(request)) {
-        // "images"
-        type = 5;
-      } else if (Filters.media(request)) {
-        // "media"
-        type = 6;
-      } else if (Filters.flash(request)) {
-        // "flash"
-        type = 7;
-      } else if (Filters.ws(request)) {
-        // "ws"
-        type = 8;
-      } else if (Filters.xhr(request)) {
-        // Verify XHR last, to categorize other mime types in their own blobs.
-        // "xhr"
-        type = 3;
-      } else {
-        // "other"
-        type = 9;
-      }
-
-      if (emptyCache || !this.responseIsFresh(request)) {
-        data[type].time += request.totalTime || 0;
-        data[type].size += request.contentSize || 0;
-        data[type].transferredSize += request.transferredSize || 0;
-      } else {
-        data[type].cached++;
-      }
-      data[type].count++;
-    }
-
-    return data.filter(e => e.count > 0);
-  },
-
-  /**
-   * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
-   * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
-   *
-   * @param object
-   *        An object containing the { responseHeaders, status } properties.
-   * @return boolean
-   *         True if the response is fresh and loaded from cache.
-   */
-  responseIsFresh({ responseHeaders, status }) {
-    // Check for a "304 Not Modified" status and response headers availability.
-    if (status != 304 || !responseHeaders) {
-      return false;
-    }
-
-    let list = responseHeaders.headers;
-    let cacheControl = list.find(e => e.name.toLowerCase() === "cache-control");
-    let expires = list.find(e => e.name.toLowerCase() === "expires");
-
-    // Check the "Cache-Control" header for a maximum age value.
-    if (cacheControl) {
-      let maxAgeMatch =
-        cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
-        cacheControl.value.match(/max-age\s*=\s*(\d+)/);
-
-      if (maxAgeMatch && maxAgeMatch.pop() > 0) {
-        return true;
-      }
-    }
-
-    // Check the "Expires" header for a valid date.
-    if (expires && Date.parse(expires.value)) {
-      return true;
-    }
-
-    return false;
-  },
-
-  onLayoutChange() {
-    this.setState({
-      isVerticalSpliter: MediaQueryList.matches,
-    });
-  },
-
-  render() {
-    const { closeStatistics } = this.props;
-    let splitterClassName = ["splitter"];
-
-    if (this.state.isVerticalSpliter) {
-      splitterClassName.push("devtools-side-splitter");
-    } else {
-      splitterClassName.push("devtools-horizontal-splitter");
-    }
-
-    return (
-      div({ className: "statistics-panel" },
-        button({
-          className: "back-button devtools-button",
-          "data-text-only": "true",
-          title: BACK_BUTTON,
-          onClick: closeStatistics,
-        }, BACK_BUTTON),
-        div({ className: "charts-container" },
-          div({ ref: "primedCacheChart", className: "charts primed-cache-chart" }),
-          div({ className: splitterClassName.join(" ") }),
-          div({ ref: "emptyCacheChart", className: "charts empty-cache-chart" }),
-        ),
-      )
-    );
-  }
-});
-
-module.exports = connect(
-  (state) => ({
-    requests: state.requests.requests.valueSeq(),
-  }),
-  (dispatch) => ({
-    closeStatistics: () => dispatch(Actions.openStatistics(false)),
-    enableRequestFilterTypeOnly: (label) =>
-      dispatch(Actions.enableRequestFilterTypeOnly(label)),
-  })
-)(StatisticsPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/har/har-builder.js
+++ /dev/null
@@ -1,482 +0,0 @@
-/* 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 Services = require("Services");
-const appInfo = Services.appinfo;
-const { LocalizationHelper } = require("devtools/shared/l10n");
-const { CurlUtils } = require("devtools/client/shared/curl");
-const {
-  getFormDataSections,
-  getUrlQuery,
-  parseQueryString,
-} = require("devtools/client/netmonitor/utils/request-utils");
-
-const L10N = new LocalizationHelper("devtools/client/locales/har.properties");
-const HAR_VERSION = "1.1";
-
-/**
- * This object is responsible for building HAR file. See HAR spec:
- * https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html
- * http://www.softwareishard.com/blog/har-12-spec/
- *
- * @param {Object} options configuration object
- *
- * The following options are supported:
- *
- * - items {Array}: List of Network requests to be exported.
- *
- * - id {String}: ID of the exported page.
- *
- * - title {String}: Title of the exported page.
- *
- * - includeResponseBodies {Boolean}: Set to true to include HTTP response
- *   bodies in the result data structure.
- */
-var HarBuilder = function (options) {
-  this._options = options;
-  this._pageMap = [];
-};
-
-HarBuilder.prototype = {
-  // Public API
-
-  /**
-   * This is the main method used to build the entire result HAR data.
-   * The process is asynchronous since it can involve additional RDP
-   * communication (e.g. resolving long strings).
-   *
-   * @returns {Promise} A promise that resolves to the HAR object when
-   * the entire build process is done.
-   */
-  build: function () {
-    this.promises = [];
-
-    // Build basic structure for data.
-    let log = this.buildLog();
-
-    // Build entries.
-    for (let file of this._options.items) {
-      log.entries.push(this.buildEntry(log, file));
-    }
-
-    // Some data needs to be fetched from the backend during the
-    // build process, so wait till all is done.
-    return Promise.all(this.promises).then(() => ({ log }));
-  },
-
-  // Helpers
-
-  buildLog: function () {
-    return {
-      version: HAR_VERSION,
-      creator: {
-        name: appInfo.name,
-        version: appInfo.version
-      },
-      browser: {
-        name: appInfo.name,
-        version: appInfo.version
-      },
-      pages: [],
-      entries: [],
-    };
-  },
-
-  buildPage: function (file) {
-    let page = {};
-
-    // Page start time is set when the first request is processed
-    // (see buildEntry)
-    page.startedDateTime = 0;
-    page.id = "page_" + this._options.id;
-    page.title = this._options.title;
-
-    return page;
-  },
-
-  getPage: function (log, file) {
-    let id = this._options.id;
-    let page = this._pageMap[id];
-    if (page) {
-      return page;
-    }
-
-    this._pageMap[id] = page = this.buildPage(file);
-    log.pages.push(page);
-
-    return page;
-  },
-
-  buildEntry: function (log, file) {
-    let page = this.getPage(log, file);
-
-    let entry = {};
-    entry.pageref = page.id;
-    entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
-    entry.time = file.endedMillis - file.startedMillis;
-
-    entry.request = this.buildRequest(file);
-    entry.response = this.buildResponse(file);
-    entry.cache = this.buildCache(file);
-    entry.timings = file.eventTimings ? file.eventTimings.timings : {};
-
-    if (file.remoteAddress) {
-      entry.serverIPAddress = file.remoteAddress;
-    }
-
-    if (file.remotePort) {
-      entry.connection = file.remotePort + "";
-    }
-
-    // Compute page load start time according to the first request start time.
-    if (!page.startedDateTime) {
-      page.startedDateTime = entry.startedDateTime;
-      page.pageTimings = this.buildPageTimings(page, file);
-    }
-
-    return entry;
-  },
-
-  buildPageTimings: function (page, file) {
-    // Event timing info isn't available
-    let timings = {
-      onContentLoad: -1,
-      onLoad: -1
-    };
-
-    return timings;
-  },
-
-  buildRequest: function (file) {
-    let request = {
-      bodySize: 0
-    };
-
-    request.method = file.method;
-    request.url = file.url;
-    request.httpVersion = file.httpVersion || "";
-
-    request.headers = this.buildHeaders(file.requestHeaders);
-    request.headers = this.appendHeadersPostData(request.headers, file);
-    request.cookies = this.buildCookies(file.requestCookies);
-
-    request.queryString = parseQueryString(getUrlQuery(file.url)) || [];
-
-    request.postData = this.buildPostData(file);
-
-    request.headersSize = file.requestHeaders.headersSize;
-
-    // Set request body size, but make sure the body is fetched
-    // from the backend.
-    if (file.requestPostData) {
-      this.fetchData(file.requestPostData.postData.text).then(value => {
-        request.bodySize = value.length;
-      });
-    }
-
-    return request;
-  },
-
-  /**
-   * Fetch all header values from the backend (if necessary) and
-   * build the result HAR structure.
-   *
-   * @param {Object} input Request or response header object.
-   */
-  buildHeaders: function (input) {
-    if (!input) {
-      return [];
-    }
-
-    return this.buildNameValuePairs(input.headers);
-  },
-
-  appendHeadersPostData: function (input = [], file) {
-    if (!file.requestPostData) {
-      return input;
-    }
-
-    this.fetchData(file.requestPostData.postData.text).then(value => {
-      let multipartHeaders = CurlUtils.getHeadersFromMultipartText(value);
-      for (let header of multipartHeaders) {
-        input.push(header);
-      }
-    });
-
-    return input;
-  },
-
-  buildCookies: function (input) {
-    if (!input) {
-      return [];
-    }
-
-    return this.buildNameValuePairs(input.cookies);
-  },
-
-  buildNameValuePairs: function (entries) {
-    let result = [];
-
-    // HAR requires headers array to be presented, so always
-    // return at least an empty array.
-    if (!entries) {
-      return result;
-    }
-
-    // Make sure header values are fully fetched from the server.
-    entries.forEach(entry => {
-      this.fetchData(entry.value).then(value => {
-        result.push({
-          name: entry.name,
-          value: value
-        });
-      });
-    });
-
-    return result;
-  },
-
-  buildPostData: function (file) {
-    let postData = {
-      mimeType: findValue(file.requestHeaders.headers, "content-type"),
-      params: [],
-      text: ""
-    };
-
-    if (!file.requestPostData) {
-      return postData;
-    }
-
-    if (file.requestPostData.postDataDiscarded) {
-      postData.comment = L10N.getStr("har.requestBodyNotIncluded");
-      return postData;
-    }
-
-    // Load request body from the backend.
-    this.fetchData(file.requestPostData.postData.text).then(postDataText => {
-      postData.text = postDataText;
-
-      // If we are dealing with URL encoded body, parse parameters.
-      let { headers } = file.requestHeaders;
-      if (CurlUtils.isUrlEncodedRequest({ headers, postDataText })) {
-        postData.mimeType = "application/x-www-form-urlencoded";
-
-        // Extract form parameters and produce nice HAR array.
-        getFormDataSections(
-          file.requestHeaders,
-          file.requestHeadersFromUploadStream,
-          file.requestPostData,
-        ).then(formDataSections => {
-          formDataSections.forEach(section => {
-            let paramsArray = parseQueryString(section);
-            if (paramsArray) {
-              postData.params = [...postData.params, ...paramsArray];
-            }
-          });
-        });
-      }
-    });
-
-    return postData;
-  },
-
-  buildResponse: function (file) {
-    let response = {
-      status: 0
-    };
-
-    // Arbitrary value if it's aborted to make sure status has a number
-    if (file.status) {
-      response.status = parseInt(file.status, 10);
-    }
-
-    let responseHeaders = file.responseHeaders;
-
-    response.statusText = file.statusText || "";
-    response.httpVersion = file.httpVersion || "";
-
-    response.headers = this.buildHeaders(responseHeaders);
-    response.cookies = this.buildCookies(file.responseCookies);
-    response.content = this.buildContent(file);
-
-    let headers = responseHeaders ? responseHeaders.headers : null;
-    let headersSize = responseHeaders ? responseHeaders.headersSize : -1;
-
-    response.redirectURL = findValue(headers, "Location");
-    response.headersSize = headersSize;
-
-    // 'bodySize' is size of the received response body in bytes.
-    // Set to zero in case of responses coming from the cache (304).
-    // Set to -1 if the info is not available.
-    if (typeof file.transferredSize != "number") {
-      response.bodySize = (response.status == 304) ? 0 : -1;
-    } else {
-      response.bodySize = file.transferredSize;
-    }
-
-    return response;
-  },
-
-  buildContent: function (file) {
-    let content = {
-      mimeType: file.mimeType,
-      size: -1
-    };
-
-    let responseContent = file.responseContent;
-    if (responseContent && responseContent.content) {
-      content.size = responseContent.content.size;
-      content.encoding = responseContent.content.encoding;
-    }
-
-    let includeBodies = this._options.includeResponseBodies;
-    let contentDiscarded = responseContent ?
-      responseContent.contentDiscarded : false;
-
-    // The comment is appended only if the response content
-    // is explicitly discarded.
-    if (!includeBodies || contentDiscarded) {
-      content.comment = L10N.getStr("har.responseBodyNotIncluded");
-      return content;
-    }
-
-    if (responseContent) {
-      let text = responseContent.content.text;
-      this.fetchData(text).then(value => {
-        content.text = value;
-      });
-    }
-
-    return content;
-  },
-
-  buildCache: function (file) {
-    let cache = {};
-
-    if (!file.fromCache) {
-      return cache;
-    }
-
-    // There is no such info yet in the Net panel.
-    // cache.beforeRequest = {};
-
-    if (file.cacheEntry) {
-      cache.afterRequest = this.buildCacheEntry(file.cacheEntry);
-    } else {
-      cache.afterRequest = null;
-    }
-
-    return cache;
-  },
-
-  buildCacheEntry: function (cacheEntry) {
-    let cache = {};
-
-    cache.expires = findValue(cacheEntry, "Expires");
-    cache.lastAccess = findValue(cacheEntry, "Last Fetched");
-    cache.eTag = "";
-    cache.hitCount = findValue(cacheEntry, "Fetch Count");
-
-    return cache;
-  },
-
-  getBlockingEndTime: function (file) {
-    if (file.resolveStarted && file.connectStarted) {
-      return file.resolvingTime;
-    }
-
-    if (file.connectStarted) {
-      return file.connectingTime;
-    }
-
-    if (file.sendStarted) {
-      return file.sendingTime;
-    }
-
-    return (file.sendingTime > file.startTime) ?
-      file.sendingTime : file.waitingForTime;
-  },
-
-  // RDP Helpers
-
-  fetchData: function (string) {
-    let promise = this._options.getString(string).then(value => {
-      return value;
-    });
-
-    // Building HAR is asynchronous and not done till all
-    // collected promises are resolved.
-    this.promises.push(promise);
-
-    return promise;
-  }
-};
-
-// Helpers
-
-/**
- * Find specified value within an array of name-value pairs
- * (used for headers, cookies and cache entries)
- */
-function findValue(arr, name) {
-  if (!arr) {
-    return "";
-  }
-
-  name = name.toLowerCase();
-  let result = arr.find(entry => entry.name.toLowerCase() == name);
-  return result ? result.value : "";
-}
-
-/**
- * Generate HAR representation of a date.
- * (YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00)
- * See also HAR Schema: http://janodvarko.cz/har/viewer/
- *
- * Note: it would be great if we could utilize Date.toJSON(), but
- * it doesn't return proper time zone offset.
- *
- * An example:
- * This helper returns:    2015-05-29T16:10:30.424+02:00
- * Date.toJSON() returns:  2015-05-29T14:10:30.424Z
- *
- * @param date {Date} The date object we want to convert.
- */
-function dateToJSON(date) {
-  function f(n, c) {
-    if (!c) {
-      c = 2;
-    }
-    let s = new String(n);
-    while (s.length < c) {
-      s = "0" + s;
-    }
-    return s;
-  }
-
-  let result = date.getFullYear() + "-" +
-    f(date.getMonth() + 1) + "-" +
-    f(date.getDate()) + "T" +
-    f(date.getHours()) + ":" +
-    f(date.getMinutes()) + ":" +
-    f(date.getSeconds()) + "." +
-    f(date.getMilliseconds(), 3);
-
-  let offset = date.getTimezoneOffset();
-  let positive = offset > 0;
-
-  // Convert to positive number before using Math.floor (see issue 5512)
-  offset = Math.abs(offset);
-  let offsetHours = Math.floor(offset / 60);
-  let offsetMinutes = Math.floor(offset % 60);
-  let prettyOffset = (positive > 0 ? "-" : "+") + f(offsetHours) +
-    ":" + f(offsetMinutes);
-
-  return result + prettyOffset;
-}
-
-// Exports from this module
-exports.HarBuilder = HarBuilder;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/index.html
@@ -0,0 +1,60 @@
+<!-- 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/. -->
+<!DOCTYPE html>
+<html dir="">
+  <head>
+    <link rel="stylesheet" href="chrome://devtools/content/shared/widgets/widgets.css"/>
+    <link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
+    <link rel="stylesheet" href="chrome://devtools/skin/netmonitor.css"/>
+    <script type="application/javascript;version=1.8"
+            src="chrome://devtools/content/shared/theme-switching.js">
+    </script>
+  </head>
+  <body class="theme-sidebar" role="application">
+    <div id="mount"></div>
+    <script>
+      "use strict";
+
+      const { BrowserLoader } = Components.utils.import("resource://devtools/client/shared/browser-loader.js", {});
+      const require = window.windowRequire = BrowserLoader({
+        baseURI: "resource://devtools/client/netmonitor/",
+        window,
+      }).require;
+
+      const EventEmitter = require("devtools/shared/event-emitter");
+      const { createFactory } = require("devtools/client/shared/vendor/react");
+      const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+      const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+      const { configureStore } = require("./src/utils/create-store");
+      const store = window.gStore = configureStore();
+      const { NetMonitorController } = require("./src/netmonitor-controller");
+
+      // Inject EventEmitter into global window.
+      EventEmitter.decorate(window);
+
+      // Export NetMonitorController to global window
+      // FIXME: Use module export mechanism instead of this tricky global variables
+      window.NetMonitorController = NetMonitorController;
+
+      window.Netmonitor = {
+        bootstrap({ toolbox }) {
+          this.mount = document.querySelector("#mount");
+          const App = createFactory(require("./src/components/app"));
+          render(Provider({ store }, App()), this.mount);
+          return NetMonitorController.startupNetMonitor({
+            client: {
+              getTabTarget: () => toolbox.target,
+            },
+            toolbox,
+          });
+        },
+
+        destroy() {
+          unmountComponentAtNode(this.mount);
+          return NetMonitorController.shutdownNetMonitor();
+        }
+      };
+    </script>
+  </body>
+</html>
--- a/devtools/client/netmonitor/moz.build
+++ b/devtools/client/netmonitor/moz.build
@@ -1,29 +1,16 @@
 # 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/.
 
 DIRS += [
-    'actions',
-    'components',
-    'har',
-    'middleware',
-    'reducers',
-    'selectors',
-    'shared',
-    'utils',
+    'src'
 ]
 
 DevToolsModules(
-    'constants.js',
-    'netmonitor-controller.js',
-    'panel.js',
-    'request-list-context-menu.js',
-    'request-list-tooltip.js',
-    'store.js',
-    'waterfall-background.js',
+    'panel.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools: Netmonitor')
deleted file mode 100644
--- a/devtools/client/netmonitor/netmonitor.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/* 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/. */
-
-/* exported Netmonitor */
-
-"use strict";
-
-const { BrowserLoader } = Components.utils.import("resource://devtools/client/shared/browser-loader.js", {});
-
-var Netmonitor = {
-  bootstrap: ({ tabTarget, toolbox }) => {
-    const require = window.windowRequire = BrowserLoader({
-      baseURI: "resource://devtools/client/netmonitor/",
-      window,
-      commonLibRequire: toolbox.browserRequire,
-    }).require;
-
-    const EventEmitter = require("devtools/shared/event-emitter");
-    const { createFactory } = require("devtools/client/shared/vendor/react");
-    const { render } = require("devtools/client/shared/vendor/react-dom");
-    const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
-    const { configureStore } = require("./store");
-    const store = window.gStore = configureStore();
-    const { NetMonitorController } = require("./netmonitor-controller");
-    NetMonitorController.toolbox = toolbox;
-    NetMonitorController._target = toolbox.target;
-    this.NetMonitorController = NetMonitorController;
-
-    // Components
-    const NetworkMonitor = createFactory(require("./components/network-monitor"));
-
-    // Inject EventEmitter into netmonitor window.
-    EventEmitter.decorate(window);
-
-    this.root = document.querySelector(".root");
-
-    render(Provider({ store }, NetworkMonitor()), this.root);
-
-    return NetMonitorController.startupNetMonitor();
-  },
-
-  destroy: () => {
-    const require = window.windowRequire;
-    const { unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
-
-    unmountComponentAtNode(this.root);
-
-    return this.NetMonitorController.shutdownNetMonitor();
-  }
-};
deleted file mode 100644
--- a/devtools/client/netmonitor/netmonitor.xhtml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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/. -->
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml" dir="">
-  <head>
-    <link rel="stylesheet" href="chrome://devtools/content/shared/widgets/widgets.css"/>
-    <link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
-    <link rel="stylesheet" href="chrome://devtools/skin/netmonitor.css"/>
-    <script src="chrome://devtools/content/shared/theme-switching.js"/>
-  </head>
-  <body class="theme-sidebar" role="application">
-    <div class="root"></div>
-    <script src="netmonitor.js" defer="true"/>
-  </body>
-</html>
--- a/devtools/client/netmonitor/panel.js
+++ b/devtools/client/netmonitor/panel.js
@@ -10,17 +10,16 @@ function NetMonitorPanel(iframeWindow, t
 }
 
 NetMonitorPanel.prototype = {
   async open() {
     if (!this.toolbox.target.isRemote) {
       await this.toolbox.target.makeRemote();
     }
     await this.panelWin.Netmonitor.bootstrap({
-      tabTarget: this.toolbox.target,
       toolbox: this.toolbox,
     });
     this.emit("ready");
     this.isReady = true;
     return this;
   },
 
   async destroy() {
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/cookies-panel.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/* 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 { L10N } = require("../../utils/l10n");
-
-// Component
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div } = DOM;
-
-const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText");
-const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText");
-const REQUEST_COOKIES = L10N.getStr("requestCookies");
-const RESPONSE_COOKIES = L10N.getStr("responseCookies");
-const SECTION_NAMES = [
-  RESPONSE_COOKIES,
-  REQUEST_COOKIES,
-];
-
-/*
- * Cookies panel component
- * This tab lists full details of any cookies sent with the request or response
- */
-function CookiesPanel({
-  request,
-}) {
-  let {
-    requestCookies = { cookies: [] },
-    responseCookies = { cookies: [] },
-  } = request;
-
-  requestCookies = requestCookies.cookies || requestCookies;
-  responseCookies = responseCookies.cookies || responseCookies;
-
-  if (!requestCookies.length && !responseCookies.length) {
-    return div({ className: "empty-notice" },
-      COOKIES_EMPTY_TEXT
-    );
-  }
-
-  let object = {};
-
-  if (responseCookies.length) {
-    object[RESPONSE_COOKIES] = getProperties(responseCookies);
-  }
-
-  if (requestCookies.length) {
-    object[REQUEST_COOKIES] = getProperties(requestCookies);
-  }
-
-  return (
-    div({ className: "panel-container" },
-      PropertiesView({
-        object,
-        filterPlaceHolder: COOKIES_FILTER_TEXT,
-        sectionNames: SECTION_NAMES,
-      })
-    )
-  );
-}
-
-CookiesPanel.displayName = "CookiesPanel";
-
-CookiesPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- *
- * @param {Object[]} arr - key-value pair array like cookies or params
- * @returns {Object}
- */
-function getProperties(arr) {
-  return arr.reduce((map, obj) => {
-    // Generally cookies object contains only name and value properties and can
-    // be rendered as name: value pair.
-    // When there are more properties in cookies object such as extra or path,
-    // We will pass the object to display these extra information
-    if (Object.keys(obj).length > 2) {
-      map[obj.name] = Object.assign({}, obj);
-      delete map[obj.name].name;
-    } else {
-      map[obj.name] = obj.value;
-    }
-    return map;
-  }, {});
-}
-
-module.exports = CookiesPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/custom-request-panel.js
+++ /dev/null
@@ -1,257 +0,0 @@
-/* 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 {
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { L10N } = require("../../utils/l10n");
-const Actions = require("../../actions/index");
-const { getSelectedRequest } = require("../../selectors/index");
-const {
-  getUrlQuery,
-  parseQueryString,
-  writeHeaderText,
-} = require("../../utils/request-utils");
-
-const {
-  button,
-  div,
-  input,
-  textarea,
-} = DOM;
-
-const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
-const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
-const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
-const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
-const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
-const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
-
-function CustomRequestPanel({
-  removeSelectedCustomRequest,
-  request = {},
-  sendCustomRequest,
-  updateRequest,
-}) {
-  let {
-    method,
-    customQueryValue,
-    requestHeaders,
-    requestPostData,
-    url,
-  } = request;
-
-  let headers = "";
-  if (requestHeaders) {
-    headers = requestHeaders.customHeadersValue ?
-      requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
-  }
-  let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
-  let params = customQueryValue;
-  if (!params) {
-    params = queryArray ?
-      queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
-  }
-  let postData = requestPostData && requestPostData.postData.text ?
-    requestPostData.postData.text : "";
-
-  return (
-    div({ className: "custom-request-panel" },
-      div({ className: "tabpanel-summary-container custom-request" },
-        div({ className: "custom-request-label custom-header" },
-          CUSTOM_NEW_REQUEST
-        ),
-        button({
-          className: "devtools-button",
-          id: "custom-request-send-button",
-          onClick: sendCustomRequest,
-        },
-          CUSTOM_SEND
-        ),
-        button({
-          className: "devtools-button",
-          id: "custom-request-close-button",
-          onClick: removeSelectedCustomRequest,
-        },
-          CUSTOM_CANCEL
-        ),
-      ),
-      div({
-        className: "tabpanel-summary-container custom-method-and-url",
-        id: "custom-method-and-url",
-      },
-        input({
-          className: "custom-method-value",
-          id: "custom-method-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          value: method || "GET",
-        }),
-        input({
-          className: "custom-url-value",
-          id: "custom-url-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          value: url || "http://",
-        }),
-      ),
-      // Hide query field when there is no params
-      params ? div({
-        className: "tabpanel-summary-container custom-section",
-        id: "custom-query",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_QUERY),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-query-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 4,
-          value: params,
-          wrap: "off",
-        })
-      ) : null,
-      div({
-        id: "custom-headers",
-        className: "tabpanel-summary-container custom-section",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_HEADERS),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-headers-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 8,
-          value: headers,
-          wrap: "off",
-        })
-      ),
-      div({
-        id: "custom-postdata",
-        className: "tabpanel-summary-container custom-section",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-postdata-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 6,
-          value: postData,
-          wrap: "off",
-        })
-      ),
-    )
-  );
-}
-
-CustomRequestPanel.displayName = "CustomRequestPanel";
-
-CustomRequestPanel.propTypes = {
-  removeSelectedCustomRequest: PropTypes.func.isRequired,
-  request: PropTypes.object,
-  sendCustomRequest: PropTypes.func.isRequired,
-  updateRequest: PropTypes.func.isRequired,
-};
-
-/**
- * Parse a text representation of a name[divider]value list with
- * the given name regex and divider character.
- *
- * @param {string} text - Text of list
- * @return {array} array of headers info {name, value}
- */
-function parseRequestText(text, namereg, divider) {
-  let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
-  let pairs = [];
-
-  for (let line of text.split("\n")) {
-    let matches = regex.exec(line);
-    if (matches) {
-      let [, name, value] = matches;
-      pairs.push({ name, value });
-    }
-  }
-  return pairs;
-}
-
-/**
- * Update Custom Request Fields
- *
- * @param {Object} evt click event
- * @param {Object} request current request
- * @param {updateRequest} updateRequest action
- */
-function updateCustomRequestFields(evt, request, updateRequest) {
-  const val = evt.target.value;
-  let data;
-  switch (evt.target.id) {
-    case "custom-headers-value":
-      let customHeadersValue = val || "";
-      // Parse text representation of multiple HTTP headers
-      let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
-      // Remove temp customHeadersValue while query string is parsable
-      if (customHeadersValue === "" ||
-          headersArray.length === customHeadersValue.split("\n").length) {
-        customHeadersValue = null;
-      }
-      data = {
-        requestHeaders: {
-          customHeadersValue,
-          headers: headersArray,
-        },
-      };
-      break;
-    case "custom-method-value":
-      data = { method: val.trim() };
-      break;
-    case "custom-postdata-value":
-      data = {
-        requestPostData: {
-          postData: { text: val },
-        }
-      };
-      break;
-    case "custom-query-value":
-      let customQueryValue = val || "";
-      // Parse readable text list of a query string
-      let queryArray = customQueryValue ?
-        parseRequestText(customQueryValue, ".+?", "=") : [];
-      // Write out a list of query params into a query string
-      let queryString = queryArray.map(
-        ({ name, value }) => name + "=" + value).join("&");
-      let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
-        request.url.split("?")[0];
-      // Remove temp customQueryValue while query string is parsable
-      if (customQueryValue === "" ||
-          queryArray.length === customQueryValue.split("\n").length) {
-        customQueryValue = null;
-      }
-      data = {
-        customQueryValue,
-        url,
-      };
-      break;
-    case "custom-url-value":
-      data = {
-        customQueryValue: null,
-        url: val
-      };
-      break;
-    default:
-      break;
-  }
-  if (data) {
-    // All updateRequest batch mode should be disabled to make UI editing in sync
-    updateRequest(request.id, data, false);
-  }
-}
-
-module.exports = connect(
-  (state) => ({ request: getSelectedRequest(state) }),
-  (dispatch) => ({
-    removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
-    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
-    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
-  })
-)(CustomRequestPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/editor.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/* 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/. */
-
-/* eslint-disable react/prop-types */
-
-"use strict";
-
-const { createClass, DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-const SourceEditor = require("devtools/client/sourceeditor/editor");
-
-const { div } = DOM;
-const SYNTAX_HIGHLIGHT_MAX_SIZE = 102400;
-
-/**
- * CodeMirror editor as a React component
- */
-const Editor = createClass({
-  displayName: "Editor",
-
-  propTypes: {
-    // Source editor syntax hightligh mode, which is a mime type defined in CodeMirror
-    mode: PropTypes.string,
-    // Source editor is displayed if set to true
-    open: PropTypes.bool,
-    // Source editor content
-    text: PropTypes.string,
-  },
-
-  getDefaultProps() {
-    return {
-      mode: null,
-      open: true,
-      text: "",
-    };
-  },
-
-  componentDidMount() {
-    const { mode, text } = this.props;
-
-    this.editor = new SourceEditor({
-      lineNumbers: true,
-      mode: text.length < SYNTAX_HIGHLIGHT_MAX_SIZE ? mode : null,
-      readOnly: true,
-      value: text,
-    });
-
-    this.deferEditor = this.editor.appendTo(this.refs.editorElement);
-  },
-
-  componentDidUpdate(prevProps) {
-    const { mode, open, text } = this.props;
-
-    if (!open) {
-      return;
-    }
-
-    if (prevProps.mode !== mode && text.length < SYNTAX_HIGHLIGHT_MAX_SIZE) {
-      this.deferEditor.then(() => {
-        this.editor.setMode(mode);
-      });
-    }
-
-    if (prevProps.text !== text) {
-      this.deferEditor.then(() => {
-        // FIXME: Workaround for browser_net_accessibility test to
-        // make sure editor node exists while setting editor text.
-        // deferEditor workaround should be removed in bug 1308442
-        if (this.refs.editorElement) {
-          this.editor.setText(text);
-        }
-      });
-    }
-  },
-
-  componentWillUnmount() {
-    this.deferEditor.then(() => {
-      this.editor.destroy();
-      this.editor = null;
-    });
-    this.deferEditor = null;
-  },
-
-  render() {
-    const { open } = this.props;
-
-    return (
-      div({ className: "editor-container devtools-monospace" },
-        div({
-          ref: "editorElement",
-          className: "editor-mount devtools-monospace",
-          // Using visibility instead of display property to avoid breaking
-          // CodeMirror indentation
-          style: { visibility: open ? "visible" : "hidden" },
-        }),
-      )
-    );
-  }
-});
-
-module.exports = Editor;
-
-/* eslint-enable react/prop-types */
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/headers-panel.js
+++ /dev/null
@@ -1,260 +0,0 @@
-/* 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 {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-const { writeHeaderText } = require("../../utils/request-utils");
-const {
-  getHeadersURL,
-  getHTTPStatusCodeURL,
-} = require("../../utils/mdn-utils");
-const { getFormattedSize } = require("../../utils/format-utils");
-const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
-const Rep = createFactory(REPS.Rep);
-
-// Components
-const MDNLink = createFactory(require("./mdn-link"));
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { button, 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.isRequired,
-    renderValue: PropTypes.func
-  },
-
-  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,
-        }),
-      )
-    );
-  },
-
-  renderValue(props) {
-    const member = props.member;
-    const value = props.value;
-
-    if (typeof value !== "string") {
-      return null;
-    }
-
-    let headerDocURL = getHeadersURL(member.name);
-
-    return (
-      div({ className: "treeValueCellDivider" },
-        Rep(Object.assign(props, {
-          // FIXME: A workaround for the issue in StringRep
-          // Force StringRep to crop the text everytime
-          member: Object.assign({}, member, { open: false }),
-          mode: MODE.TINY,
-          cropLimit: 60,
-        })),
-        headerDocURL ? MDNLink({
-          url: headerDocURL,
-        }) : null
-      )
-    );
-  },
-
-  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;
-      }
-
-      let statusCodeDocURL = getHTTPStatusCodeURL(code);
-      let inputWidth = status.length + statusText.length + 1;
-
-      summaryStatus = (
-        div({ className: "tabpanel-summary-container headers-summary" },
-          div({
-            className: "tabpanel-summary-label headers-summary-label",
-          }, SUMMARY_STATUS),
-          div({
-            className: "requests-list-status-icon",
-            "data-code": code,
-          }),
-          input({
-            className: "tabpanel-summary-value textbox-input devtools-monospace"
-              + " status-text",
-            readOnly: true,
-            value: `${status} ${statusText}`,
-            size: `${inputWidth}`,
-          }),
-          statusCodeDocURL ? MDNLink({
-            url: statusCodeDocURL,
-          }) : null,
-          window.NetMonitorController.supportsCustomRequest && button({
-            className: "devtools-button",
-            onClick: cloneSelectedRequest,
-          }, EDIT_AND_RESEND),
-          button({
-            className: "devtools-button",
-            onClick: this.toggleRawHeaders,
-          }, 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({ className: "panel-container" },
-        div({ className: "headers-overview" },
-          summaryUrl,
-          summaryMethod,
-          summaryAddress,
-          summaryStatus,
-          summaryVersion,
-          summaryRawHeaders,
-        ),
-        PropertiesView({
-          object,
-          filterPlaceHolder: HEADERS_FILTER_TEXT,
-          sectionNames: Object.keys(object),
-          renderValue: this.renderValue,
-        }),
-      )
-    );
-  }
-});
-
-module.exports = HeadersPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/mdn-link.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/* 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 Services = require("Services");
-const {
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { gDevTools } = require("devtools/client/framework/devtools");
-const { L10N } = require("../../utils/l10n");
-
-const { a } = DOM;
-
-const LEARN_MORE = L10N.getStr("netmonitor.headers.learnMore");
-
-function MDNLink({ url }) {
-  return (
-    a({
-      className: "learn-more-link",
-      title: url,
-      onClick: (e) => onLearnMoreClick(e, url),
-    }, `[${LEARN_MORE}]`)
-  );
-}
-
-MDNLink.displayName = "MDNLink";
-
-MDNLink.propTypes = {
-  url: PropTypes.string.isRequired,
-};
-
-function onLearnMoreClick(e, url) {
-  e.stopPropagation();
-  e.preventDefault();
-
-  let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-  if (e.button === 1) {
-    win.openUILinkIn(url, "tabshifted");
-  } else {
-    win.openUILinkIn(url, "tab");
-  }
-}
-
-module.exports = MDNLink;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/moz.build
+++ /dev/null
@@ -1,19 +0,0 @@
-# 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(
-    'cookies-panel.js',
-    'custom-request-panel.js',
-    'editor.js',
-    'headers-panel.js',
-    'mdn-link.js',
-    'network-details-panel.js',
-    'params-panel.js',
-    'preview-panel.js',
-    'properties-view.js',
-    'response-panel.js',
-    'security-panel.js',
-    'tabbox-panel.js',
-    'timings-panel.js',
-)
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/network-details-panel.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/* 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 Actions = require("../../actions/index");
-const { getSelectedRequest } = require("../../selectors/index");
-
-// Components
-const CustomRequestPanel = createFactory(require("./custom-request-panel"));
-const TabboxPanel = createFactory(require("./tabbox-panel"));
-
-const { div } = DOM;
-
-/*
- * Network details panel component
- */
-function NetworkDetailsPanel({
-  activeTabId,
-  cloneSelectedRequest,
-  request,
-  selectTab,
-}) {
-  if (!request) {
-    return null;
-  }
-
-  return (
-    div({ className: "network-details-panel" },
-      !request.isCustom ?
-        TabboxPanel({
-          activeTabId,
-          request,
-          selectTab,
-        }) :
-        CustomRequestPanel({
-          cloneSelectedRequest,
-          request,
-        })
-    )
-  );
-}
-
-NetworkDetailsPanel.displayName = "NetworkDetailsPanel";
-
-NetworkDetailsPanel.propTypes = {
-  activeTabId: PropTypes.string,
-  cloneSelectedRequest: PropTypes.func.isRequired,
-  open: PropTypes.bool,
-  request: PropTypes.object,
-  selectTab: PropTypes.func.isRequired,
-};
-
-module.exports = connect(
-  (state) => ({
-    activeTabId: state.ui.detailsPanelSelectedTab,
-    request: getSelectedRequest(state),
-  }),
-  (dispatch) => ({
-    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-    selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
-  }),
-)(NetworkDetailsPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/params-panel.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/* 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 { L10N } = require("../../utils/l10n");
-const { getUrlQuery, parseQueryString } = require("../../utils/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({
-  request,
-}) {
-  let {
-    formDataSections,
-    mimeType,
-    requestPostData,
-    url,
-  } = request;
-  let postData = requestPostData ? requestPostData.postData.text : null;
-  let query = getUrlQuery(url);
-
-  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] = getProperties(parseQueryString(query));
-  }
-
-  // Form Data section
-  if (formDataSections && formDataSections.length > 0) {
-    let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
-    object[PARAMS_FORM_DATA] = getProperties(parseQueryString(sections));
-  }
-
-  // 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 (
-    div({ className: "panel-container" },
-      PropertiesView({
-        object,
-        filterPlaceHolder: PARAMS_FILTER_TEXT,
-        sectionNames: SECTION_NAMES,
-      })
-    )
-  );
-}
-
-ParamsPanel.displayName = "ParamsPanel";
-
-ParamsPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- * This function also deal with duplicate key case
- * (for multiple selection and query params with same keys)
- *
- * @param {Object[]} arr - key-value pair array like query or form params
- * @returns {Object} Rep compatible object
- */
-function getProperties(arr) {
-  return arr.reduce((map, obj) => {
-    let value = map[obj.name];
-    if (value) {
-      if (typeof value !== "object") {
-        map[obj.name] = [value];
-      }
-      map[obj.name].push(obj.value);
-    } else {
-      map[obj.name] = obj.value;
-    }
-    return map;
-  }, {});
-}
-
-module.exports = ParamsPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/preview-panel.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-
-const { div, iframe } = DOM;
-
-/*
- * Preview panel component
- * Display HTML content within a sandbox enabled iframe
- */
-function PreviewPanel({
-  request,
-}) {
-  const htmlBody = request.responseContent ?
-    request.responseContent.content.text : "";
-
-  return (
-    div({ className: "panel-container" },
-      iframe({
-        sandbox: "",
-        srcDoc: typeof htmlBody === "string" ? htmlBody : "",
-      })
-    )
-  );
-}
-
-PreviewPanel.displayName = "PreviewPanel";
-
-PreviewPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-module.exports = PreviewPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/properties-view.js
+++ /dev/null
@@ -1,218 +0,0 @@
-/* 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/. */
-
-/* eslint-disable react/prop-types */
-
-"use strict";
-
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-
-const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
-const Rep = createFactory(REPS.Rep);
-
-const { FILTER_SEARCH_DELAY } = require("../../constants");
-
-// 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 { div, tr, td } = DOM;
-const AUTO_EXPAND_MAX_LEVEL = 7;
-const AUTO_EXPAND_MAX_NODES = 50;
-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 EDITOR_CONFIG_ID.
- * Rep - Default enabled.
- */
-const PropertiesView = createClass({
-  displayName: "PropertiesView",
-
-  propTypes: {
-    object: PropTypes.object,
-    enableInput: PropTypes.bool,
-    expandableStrings: PropTypes.bool,
-    filterPlaceHolder: PropTypes.string,
-    sectionNames: PropTypes.array,
-  },
-
-  getDefaultProps() {
-    return {
-      enableInput: true,
-      enableFilter: true,
-      expandableStrings: false,
-      filterPlaceHolder: "",
-      sectionNames: [],
-    };
-  },
-
-  getInitialState() {
-    return {
-      filterText: "",
-    };
-  },
-
-  getRowClass(object, sectionNames) {
-    return sectionNames.includes(object.name) ? "tree-section" : "";
-  },
-
-  onFilter(object, whiteList) {
-    let { name, value } = object;
-    let filterText = this.state.filterText;
-
-    if (!filterText || whiteList.includes(name)) {
-      return true;
-    }
-
-    let jsonString = JSON.stringify({ [name]: value }).toLowerCase();
-    return jsonString.includes(filterText.toLowerCase());
-  },
-
-  renderRowWithEditor(props) {
-    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({ className: "editor-row-container" },
-          td({ colSpan: 2 },
-            Editor(value)
-          )
-        )
-      );
-    }
-
-    // Skip for editor config
-    if (level >= 1 && path.includes(EDITOR_CONFIG_ID)) {
-      return null;
-    }
-
-    return TreeRow(props);
-  },
-
-  renderValueWithRep(props) {
-    const { member } = props;
-
-    // Hide strings with following conditions
-    // 1. this row is a togglable section and content is object ('cause it shouldn't hide
-    //    when string or number)
-    // 2. the `value` object has a `value` property, only happened in Cookies panel
-    // Put 2 here to not dup this method
-    if (member.level === 0 && member.type === "object" ||
-      (typeof member.value === "object" && member.value && member.value.value)) {
-      return null;
-    }
-
-    return Rep(Object.assign(props, {
-      // FIXME: A workaround for the issue in StringRep
-      // Force StringRep to crop the text everytime
-      member: Object.assign({}, 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) {
-      if (expandedNodes.size > AUTO_EXPAND_MAX_NODES) {
-        // If we reached the limit of expandable nodes, bail out to avoid performance
-        // issues.
-        break;
-      }
-
-      let nodePath = path + "/" + prop;
-      expandedNodes.add(nodePath);
-
-      let nodes = this.getExpandedNodes(object[prop], nodePath, level + 1);
-      if (nodes) {
-        let newSize = expandedNodes.size + nodes.size;
-        if (newSize < AUTO_EXPAND_MAX_NODES) {
-          // Avoid having a subtree half expanded.
-          expandedNodes = new Set([...expandedNodes, ...nodes]);
-        }
-      }
-    }
-    return expandedNodes;
-  },
-
-  render() {
-    const {
-      decorator,
-      enableInput,
-      expandableStrings,
-      filterPlaceHolder,
-      object,
-      renderRow,
-      renderValue,
-      sectionNames,
-    } = this.props;
-
-    return (
-      div({ className: "properties-view" },
-        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: this.getExpandedNodes(object),
-            onFilter: (props) => this.onFilter(props, sectionNames),
-            renderRow: renderRow || this.renderRowWithEditor,
-            renderValue: renderValue || this.renderValueWithRep,
-          }),
-        ),
-      )
-    );
-  }
-});
-
-module.exports = PropertiesView;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/response-panel.js
+++ /dev/null
@@ -1,185 +0,0 @@
-/* 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 {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-const { formDataURI, getUrlBaseName } = require("../../utils/request-utils");
-
-// Components
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div, img } = DOM;
-const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
-const JSON_FILTER_TEXT = L10N.getStr("jsonFilterText");
-const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name");
-const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions");
-const RESPONSE_IMG_MIMETYPE = L10N.getStr("netmonitor.response.mime");
-const RESPONSE_PAYLOAD = L10N.getStr("responsePayload");
-
-/*
- * Response panel component
- * Displays the GET parameters and POST data of a request
- */
-const ResponsePanel = createClass({
-  displayName: "ResponsePanel",
-
-  propTypes: {
-    request: PropTypes.object.isRequired,
-  },
-
-  getInitialState() {
-    return {
-      imageDimensions: {
-        width: 0,
-        height: 0,
-      },
-    };
-  },
-
-  updateImageDimemsions({ target }) {
-    this.setState({
-      imageDimensions: {
-        width: target.naturalWidth,
-        height: target.naturalHeight,
-      },
-    });
-  },
-
-  // Handle json, which we tentatively identify by checking the MIME type
-  // for "json" after any word boundary. This works for the standard
-  // "application/json", and also for custom types like "x-bigcorp-json".
-  // Additionally, we also directly parse the response text content to
-  // verify whether it's json or not, to handle responses incorrectly
-  // labeled as text/plain instead.
-  isJSON(mimeType, response) {
-    let json, error;
-    try {
-      json = JSON.parse(response);
-    } catch (err) {
-      try {
-        json = JSON.parse(atob(response));
-      } catch (err64) {
-        error = err;
-      }
-    }
-
-    if (/\bjson/.test(mimeType) || json) {
-      // Extract the actual json substring in case this might be a "JSONP".
-      // This regex basically parses a function call and captures the
-      // function name and arguments in two separate groups.
-      let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
-      let [, jsonpCallback, jsonp] = response.match(jsonpRegex) || [];
-      let result = {};
-
-      // Make sure this is a valid JSON object first. If so, nicely display
-      // the parsing results in a tree view.
-      if (jsonpCallback && jsonp) {
-        error = null;
-        try {
-          json = JSON.parse(jsonp);
-        } catch (err) {
-          error = err;
-        }
-      }
-
-      // Valid JSON
-      if (json) {
-        result.json = json;
-      }
-      // Valid JSONP
-      if (jsonpCallback) {
-        result.jsonpCallback = jsonpCallback;
-      }
-      // Malformed JSON
-      if (error) {
-        result.error = "" + error;
-      }
-
-      return result;
-    }
-
-    return null;
-  },
-
-  render() {
-    let { responseContent, url } = this.props.request;
-
-    if (!responseContent || typeof responseContent.content.text !== "string") {
-      return null;
-    }
-
-    let { encoding, mimeType, text } = responseContent.content;
-
-    if (mimeType.includes("image/")) {
-      let { width, height } = this.state.imageDimensions;
-
-      return (
-        div({ className: "panel-container response-image-box devtools-monospace" },
-          img({
-            className: "response-image",
-            src: formDataURI(mimeType, encoding, text),
-            onLoad: this.updateImageDimemsions,
-          }),
-          div({ className: "response-summary" },
-            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_NAME),
-            div({ className: "tabpanel-summary-value" }, getUrlBaseName(url)),
-          ),
-          div({ className: "response-summary" },
-            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_DIMENSIONS),
-            div({ className: "tabpanel-summary-value" }, `${width} × ${height}`),
-          ),
-          div({ className: "response-summary" },
-            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_MIMETYPE),
-            div({ className: "tabpanel-summary-value" }, mimeType),
-          ),
-        )
-      );
-    }
-
-    // Display Properties View
-    let { json, jsonpCallback, error } = this.isJSON(mimeType, text) || {};
-    let object = {};
-    let sectionName;
-
-    if (json) {
-      if (jsonpCallback) {
-        sectionName = L10N.getFormatStr("jsonpScopeName", jsonpCallback);
-      } else {
-        sectionName = JSON_SCOPE_NAME;
-      }
-      object[sectionName] = json;
-    } else {
-      sectionName = RESPONSE_PAYLOAD;
-
-      object[sectionName] = {
-        EDITOR_CONFIG: {
-          text,
-          mode: mimeType.replace(/;.+/, ""),
-        },
-      };
-    }
-
-    return (
-      div({ className: "panel-container" },
-        error && div({ className: "response-error-header", title: error },
-          error
-        ),
-        PropertiesView({
-          object,
-          filterPlaceHolder: JSON_FILTER_TEXT,
-          sectionNames: [sectionName],
-        }),
-      )
-    );
-  }
-});
-
-module.exports = ResponsePanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/security-panel.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/* 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 { L10N } = require("../../utils/l10n");
-const { getUrlHost } = require("../../utils/request-utils");
-
-// Components
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div, input, span } = DOM;
-
-/*
- * Security panel component
- * If the site is being served over HTTPS, you get an extra tab labeled "Security".
- * This contains details about the secure connection used including the protocol,
- * the cipher suite, and certificate details
- */
-function SecurityPanel({
-  request,
-}) {
-  const { securityInfo, url } = request;
-
-  if (!securityInfo || !url) {
-    return null;
-  }
-
-  const notAvailable = L10N.getStr("netmonitor.security.notAvailable");
-  let object;
-
-  if (securityInfo.state === "secure" || securityInfo.state === "weak") {
-    const { subject, issuer, validity, fingerprint } = securityInfo.cert;
-    const enabledLabel = L10N.getStr("netmonitor.security.enabled");
-    const disabledLabel = L10N.getStr("netmonitor.security.disabled");
-
-    object = {
-      [L10N.getStr("netmonitor.security.connection")]: {
-        [L10N.getStr("netmonitor.security.protocolVersion")]:
-          securityInfo.protocolVersion || notAvailable,
-        [L10N.getStr("netmonitor.security.cipherSuite")]:
-          securityInfo.cipherSuite || notAvailable,
-      },
-      [L10N.getFormatStr("netmonitor.security.hostHeader", getUrlHost(url))]: {
-        [L10N.getStr("netmonitor.security.hsts")]:
-          securityInfo.hsts ? enabledLabel : disabledLabel,
-        [L10N.getStr("netmonitor.security.hpkp")]:
-          securityInfo.hpkp ? enabledLabel : disabledLabel,
-      },
-      [L10N.getStr("netmonitor.security.certificate")]: {
-        [L10N.getStr("certmgr.subjectinfo.label")]: {
-          [L10N.getStr("certmgr.certdetail.cn")]:
-            subject.commonName || notAvailable,
-          [L10N.getStr("certmgr.certdetail.o")]:
-            subject.organization || notAvailable,
-          [L10N.getStr("certmgr.certdetail.ou")]:
-            subject.organizationUnit || notAvailable,
-        },
-        [L10N.getStr("certmgr.issuerinfo.label")]: {
-          [L10N.getStr("certmgr.certdetail.cn")]:
-            issuer.commonName || notAvailable,
-          [L10N.getStr("certmgr.certdetail.o")]:
-            issuer.organization || notAvailable,
-          [L10N.getStr("certmgr.certdetail.ou")]:
-            issuer.organizationUnit || notAvailable,
-        },
-        [L10N.getStr("certmgr.periodofvalidity.label")]: {
-          [L10N.getStr("certmgr.begins")]:
-            validity.start || notAvailable,
-          [L10N.getStr("certmgr.expires")]:
-            validity.end || notAvailable,
-        },
-        [L10N.getStr("certmgr.fingerprints.label")]: {
-          [L10N.getStr("certmgr.certdetail.sha256fingerprint")]:
-            fingerprint.sha256 || notAvailable,
-          [L10N.getStr("certmgr.certdetail.sha1fingerprint")]:
-            fingerprint.sha1 || notAvailable,
-        },
-      },
-    };
-  } else {
-    object = {
-      [L10N.getStr("netmonitor.security.error")]:
-        new DOMParser().parseFromString(securityInfo.errorMessage, "text/html")
-          .body.textContent || notAvailable
-    };
-  }
-
-  return div({ className: "panel-container security-panel" },
-    PropertiesView({
-      object,
-      renderValue: (props) => renderValue(props, securityInfo.weaknessReasons),
-      enableFilter: false,
-      expandedNodes: getExpandedNodes(object),
-    })
-  );
-}
-
-SecurityPanel.displayName = "SecurityPanel";
-
-SecurityPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-function renderValue(props, weaknessReasons = []) {
-  const { member, value } = props;
-
-  // Hide object summary
-  if (typeof member.value === "object") {
-    return null;
-  }
-
-  return span({ className: "security-info-value" },
-    member.name === L10N.getStr("netmonitor.security.error") ?
-      // Display multiline text for security error
-      value
-      :
-      // Display one line selectable text for security details
-      input({
-        className: "textbox-input",
-        readOnly: "true",
-        value,
-      })
-    ,
-    weaknessReasons.indexOf("cipher") !== -1 &&
-    member.name === L10N.getStr("netmonitor.security.cipherSuite") ?
-      // Display an extra warning icon after the cipher suite
-      div({
-        id: "security-warning-cipher",
-        className: "security-warning-icon",
-        title: L10N.getStr("netmonitor.security.warning.cipher"),
-      })
-      :
-      null
-  );
-}
-
-function getExpandedNodes(object, path = "", level = 0) {
-  if (typeof object !== "object") {
-    return null;
-  }
-
-  let expandedNodes = new Set();
-  for (let prop in object) {
-    let nodePath = path + "/" + prop;
-    expandedNodes.add(nodePath);
-
-    let nodes = getExpandedNodes(object[prop], nodePath, level + 1);
-    if (nodes) {
-      expandedNodes = new Set([...expandedNodes, ...nodes]);
-    }
-  }
-  return expandedNodes;
-}
-
-module.exports = SecurityPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/tabbox-panel.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/* 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,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const Actions = require("../../actions/index");
-const { Filters } = require("../../utils/filter-predicates");
-const { L10N } = require("../../utils/l10n");
-const { getSelectedRequest } = require("../../selectors/index");
-
-// Components
-const Tabbar = createFactory(require("devtools/client/shared/components/tabs/tabbar"));
-const TabPanel = createFactory(require("devtools/client/shared/components/tabs/tabs").TabPanel);
-const CookiesPanel = createFactory(require("./cookies-panel"));
-const HeadersPanel = createFactory(require("./headers-panel"));
-const ParamsPanel = createFactory(require("./params-panel"));
-const PreviewPanel = createFactory(require("./preview-panel"));
-const ResponsePanel = createFactory(require("./response-panel"));
-const SecurityPanel = createFactory(require("./security-panel"));
-const TimingsPanel = createFactory(require("./timings-panel"));
-
-const HEADERS_TITLE = L10N.getStr("netmonitor.tab.headers");
-const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
-const PARAMS_TITLE = L10N.getStr("netmonitor.tab.params");
-const RESPONSE_TITLE = L10N.getStr("netmonitor.tab.response");
-const TIMINGS_TITLE = L10N.getStr("netmonitor.tab.timings");
-const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
-const PREVIEW_TITLE = L10N.getStr("netmonitor.tab.preview");
-
-/*
- * Tabbox panel component
- * Display the network request details
- */
-function TabboxPanel({
-  activeTabId,
-  cloneSelectedRequest,
-  request,
-  selectTab,
-}) {
-  if (!request) {
-    return null;
-  }
-
-  return (
-    Tabbar({
-      activeTabId,
-      onSelect: selectTab,
-      renderOnlySelected: true,
-      showAllTabsMenu: true,
-    },
-      TabPanel({
-        id: "headers",
-        title: HEADERS_TITLE,
-      },
-        HeadersPanel({ request, cloneSelectedRequest }),
-      ),
-      TabPanel({
-        id: "cookies",
-        title: COOKIES_TITLE,
-      },
-        CookiesPanel({ request }),
-      ),
-      TabPanel({
-        id: "params",
-        title: PARAMS_TITLE,
-      },
-        ParamsPanel({ request }),
-      ),
-      TabPanel({
-        id: "response",
-        title: RESPONSE_TITLE,
-      },
-        ResponsePanel({ request }),
-      ),
-      TabPanel({
-        id: "timings",
-        title: TIMINGS_TITLE,
-      },
-        TimingsPanel({ request }),
-      ),
-      request.securityState && request.securityState !== "insecure" &&
-      TabPanel({
-        id: "security",
-        title: SECURITY_TITLE,
-      },
-        SecurityPanel({ request }),
-      ),
-      Filters.html(request) &&
-      TabPanel({
-        id: "preview",
-        title: PREVIEW_TITLE,
-      },
-        PreviewPanel({ request }),
-      ),
-    )
-  );
-}
-
-TabboxPanel.displayName = "TabboxPanel";
-
-TabboxPanel.propTypes = {
-  activeTabId: PropTypes.string,
-  cloneSelectedRequest: PropTypes.func.isRequired,
-  request: PropTypes.object,
-  selectTab: PropTypes.func.isRequired,
-};
-
-module.exports = connect(
-  (state) => ({
-    activeTabId: state.ui.detailsPanelSelectedTab,
-    request: getSelectedRequest(state),
-  }),
-  (dispatch) => ({
-    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-    selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
-  }),
-)(TabboxPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/timings-panel.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-
-const { div, span } = DOM;
-const types = ["blocked", "dns", "connect", "send", "wait", "receive"];
-const TIMINGS_END_PADDING = "80px";
-
-/*
- * Timings panel component
- * Display timeline bars that shows the total wait time for various stages
- */
-function TimingsPanel({
-  request,
-}) {
-  if (!request.eventTimings) {
-    return null;
-  }
-
-  const { timings, totalTime } = request.eventTimings;
-  const timelines = types.map((type, idx) => {
-    // Determine the relative offset for each timings box. For example, the
-    // offset of third timings box will be 0 + blocked offset + dns offset
-    const offset = types
-      .slice(0, idx)
-      .reduce((acc, cur) => (acc + timings[cur] || 0), 0);
-    const offsetScale = offset / totalTime || 0;
-    const timelineScale = timings[type] / totalTime || 0;
-
-    return div({
-      key: type,
-      id: `timings-summary-${type}`,
-      className: "tabpanel-summary-container timings-container",
-    },
-      span({ className: "tabpanel-summary-label timings-label" },
-        L10N.getStr(`netmonitor.timings.${type}`)
-      ),
-      div({ className: "requests-list-timings-container" },
-        span({
-          className: "requests-list-timings-offset",
-          style: {
-            width: `calc(${offsetScale} * (100% - ${TIMINGS_END_PADDING})`,
-          },
-        }),
-        span({
-          className: `requests-list-timings-box ${type}`,
-          style: {
-            width: `calc(${timelineScale} * (100% - ${TIMINGS_END_PADDING}))`,
-          },
-        }),
-        span({ className: "requests-list-timings-total" },
-          L10N.getFormatStr("networkMenu.totalMS", timings[type])
-        )
-      ),
-    );
-  });
-
-  return div({ className: "panel-container" }, timelines);
-}
-
-TimingsPanel.displayName = "TimingsPanel";
-
-TimingsPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-module.exports = TimingsPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/moz.build
+++ /dev/null
@@ -1,7 +0,0 @@
-# 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/.
-
-DIRS += [
-    'components',
-]
rename from devtools/client/netmonitor/actions/batching.js
rename to devtools/client/netmonitor/src/actions/batching.js
rename from devtools/client/netmonitor/actions/filters.js
rename to devtools/client/netmonitor/src/actions/filters.js
rename from devtools/client/netmonitor/actions/index.js
rename to devtools/client/netmonitor/src/actions/index.js
rename from devtools/client/netmonitor/actions/moz.build
rename to devtools/client/netmonitor/src/actions/moz.build
rename from devtools/client/netmonitor/actions/requests.js
rename to devtools/client/netmonitor/src/actions/requests.js
rename from devtools/client/netmonitor/actions/selection.js
rename to devtools/client/netmonitor/src/actions/selection.js
rename from devtools/client/netmonitor/actions/sort.js
rename to devtools/client/netmonitor/src/actions/sort.js
rename from devtools/client/netmonitor/actions/timing-markers.js
rename to devtools/client/netmonitor/src/actions/timing-markers.js
rename from devtools/client/netmonitor/actions/ui.js
rename to devtools/client/netmonitor/src/actions/ui.js
rename from devtools/client/themes/netmonitor.css
rename to devtools/client/netmonitor/src/assets/styles/netmonitor.css
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/netmonitor/src/assets/styles/netmonitor.css
@@ -1303,16 +1303,16 @@
 }
 
 .split-box {
   overflow: hidden;
 }
 
 html,
 body,
-.root,
+#mount,
 .network-monitor,
 .monitor-panel {
   display: flex;
   flex-direction: column;
   height: 100%;
   overflow: hidden;
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/app.js
@@ -0,0 +1,40 @@
+/* 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");
+
+// Components
+const MonitorPanel = createFactory(require("./monitor-panel"));
+const StatisticsPanel = createFactory(require("./statistics-panel"));
+
+const { div } = DOM;
+
+/*
+ * App component
+ * The top level component for representing main panel
+ */
+function App({ statisticsOpen }) {
+  return (
+    div({ className: "network-monitor" },
+      !statisticsOpen ? MonitorPanel() : StatisticsPanel()
+    )
+  );
+}
+
+App.displayName = "App";
+
+App.propTypes = {
+  statisticsOpen: PropTypes.bool.isRequired,
+};
+
+module.exports = connect(
+  (state) => ({ statisticsOpen: state.ui.statisticsOpen }),
+)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/cookies-panel.js
@@ -0,0 +1,99 @@
+/* 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 { L10N } = require("../utils/l10n");
+
+// Component
+const PropertiesView = createFactory(require("./properties-view"));
+
+const { div } = DOM;
+
+const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText");
+const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText");
+const REQUEST_COOKIES = L10N.getStr("requestCookies");
+const RESPONSE_COOKIES = L10N.getStr("responseCookies");
+const SECTION_NAMES = [
+  RESPONSE_COOKIES,
+  REQUEST_COOKIES,
+];
+
+/*
+ * Cookies panel component
+ * This tab lists full details of any cookies sent with the request or response
+ */
+function CookiesPanel({
+  request,
+}) {
+  let {
+    requestCookies = { cookies: [] },
+    responseCookies = { cookies: [] },
+  } = request;
+
+  requestCookies = requestCookies.cookies || requestCookies;
+  responseCookies = responseCookies.cookies || responseCookies;
+
+  if (!requestCookies.length && !responseCookies.length) {
+    return div({ className: "empty-notice" },
+      COOKIES_EMPTY_TEXT
+    );
+  }
+
+  let object = {};
+
+  if (responseCookies.length) {
+    object[RESPONSE_COOKIES] = getProperties(responseCookies);
+  }
+
+  if (requestCookies.length) {
+    object[REQUEST_COOKIES] = getProperties(requestCookies);
+  }
+
+  return (
+    div({ className: "panel-container" },
+      PropertiesView({
+        object,
+        filterPlaceHolder: COOKIES_FILTER_TEXT,
+        sectionNames: SECTION_NAMES,
+      })
+    )
+  );
+}
+
+CookiesPanel.displayName = "CookiesPanel";
+
+CookiesPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+/**
+ * Mapping array to dict for TreeView usage.
+ * Since TreeView only support Object(dict) format.
+ *
+ * @param {Object[]} arr - key-value pair array like cookies or params
+ * @returns {Object}
+ */
+function getProperties(arr) {
+  return arr.reduce((map, obj) => {
+    // Generally cookies object contains only name and value properties and can
+    // be rendered as name: value pair.
+    // When there are more properties in cookies object such as extra or path,
+    // We will pass the object to display these extra information
+    if (Object.keys(obj).length > 2) {
+      map[obj.name] = Object.assign({}, obj);
+      delete map[obj.name].name;
+    } else {
+      map[obj.name] = obj.value;
+    }
+    return map;
+  }, {});
+}
+
+module.exports = CookiesPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/custom-request-panel.js
@@ -0,0 +1,257 @@
+/* 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 {
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../utils/l10n");
+const Actions = require("../actions/index");
+const { getSelectedRequest } = require("../selectors/index");
+const {
+  getUrlQuery,
+  parseQueryString,
+  writeHeaderText,
+} = require("../utils/request-utils");
+
+const {
+  button,
+  div,
+  input,
+  textarea,
+} = DOM;
+
+const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
+const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
+const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
+const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
+const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
+const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
+
+function CustomRequestPanel({
+  removeSelectedCustomRequest,
+  request = {},
+  sendCustomRequest,
+  updateRequest,
+}) {
+  let {
+    method,
+    customQueryValue,
+    requestHeaders,
+    requestPostData,
+    url,
+  } = request;
+
+  let headers = "";
+  if (requestHeaders) {
+    headers = requestHeaders.customHeadersValue ?
+      requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
+  }
+  let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
+  let params = customQueryValue;
+  if (!params) {
+    params = queryArray ?
+      queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
+  }
+  let postData = requestPostData && requestPostData.postData.text ?
+    requestPostData.postData.text : "";
+
+  return (
+    div({ className: "custom-request-panel" },
+      div({ className: "tabpanel-summary-container custom-request" },
+        div({ className: "custom-request-label custom-header" },
+          CUSTOM_NEW_REQUEST
+        ),
+        button({
+          className: "devtools-button",
+          id: "custom-request-send-button",
+          onClick: sendCustomRequest,
+        },
+          CUSTOM_SEND
+        ),
+        button({
+          className: "devtools-button",
+          id: "custom-request-close-button",
+          onClick: removeSelectedCustomRequest,
+        },
+          CUSTOM_CANCEL
+        ),
+      ),
+      div({
+        className: "tabpanel-summary-container custom-method-and-url",
+        id: "custom-method-and-url",
+      },
+        input({
+          className: "custom-method-value",
+          id: "custom-method-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          value: method || "GET",
+        }),
+        input({
+          className: "custom-url-value",
+          id: "custom-url-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          value: url || "http://",
+        }),
+      ),
+      // Hide query field when there is no params
+      params ? div({
+        className: "tabpanel-summary-container custom-section",
+        id: "custom-query",
+      },
+        div({ className: "custom-request-label" }, CUSTOM_QUERY),
+        textarea({
+          className: "tabpanel-summary-input",
+          id: "custom-query-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          rows: 4,
+          value: params,
+          wrap: "off",
+        })
+      ) : null,
+      div({
+        id: "custom-headers",
+        className: "tabpanel-summary-container custom-section",
+      },
+        div({ className: "custom-request-label" }, CUSTOM_HEADERS),
+        textarea({
+          className: "tabpanel-summary-input",
+          id: "custom-headers-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          rows: 8,
+          value: headers,
+          wrap: "off",
+        })
+      ),
+      div({
+        id: "custom-postdata",
+        className: "tabpanel-summary-container custom-section",
+      },
+        div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
+        textarea({
+          className: "tabpanel-summary-input",
+          id: "custom-postdata-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          rows: 6,
+          value: postData,
+          wrap: "off",
+        })
+      ),
+    )
+  );
+}
+
+CustomRequestPanel.displayName = "CustomRequestPanel";
+
+CustomRequestPanel.propTypes = {
+  removeSelectedCustomRequest: PropTypes.func.isRequired,
+  request: PropTypes.object,
+  sendCustomRequest: PropTypes.func.isRequired,
+  updateRequest: PropTypes.func.isRequired,
+};
+
+/**
+ * Parse a text representation of a name[divider]value list with
+ * the given name regex and divider character.
+ *
+ * @param {string} text - Text of list
+ * @return {array} array of headers info {name, value}
+ */
+function parseRequestText(text, namereg, divider) {
+  let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
+  let pairs = [];
+
+  for (let line of text.split("\n")) {
+    let matches = regex.exec(line);
+    if (matches) {
+      let [, name, value] = matches;
+      pairs.push({ name, value });
+    }
+  }
+  return pairs;
+}
+
+/**
+ * Update Custom Request Fields
+ *
+ * @param {Object} evt click event
+ * @param {Object} request current request
+ * @param {updateRequest} updateRequest action
+ */
+function updateCustomRequestFields(evt, request, updateRequest) {
+  const val = evt.target.value;
+  let data;
+  switch (evt.target.id) {
+    case "custom-headers-value":
+      let customHeadersValue = val || "";
+      // Parse text representation of multiple HTTP headers
+      let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
+      // Remove temp customHeadersValue while query string is parsable
+      if (customHeadersValue === "" ||
+          headersArray.length === customHeadersValue.split("\n").length) {
+        customHeadersValue = null;
+      }
+      data = {
+        requestHeaders: {
+          customHeadersValue,
+          headers: headersArray,
+        },
+      };
+      break;
+    case "custom-method-value":
+      data = { method: val.trim() };
+      break;
+    case "custom-postdata-value":
+      data = {
+        requestPostData: {
+          postData: { text: val },
+        }
+      };
+      break;
+    case "custom-query-value":
+      let customQueryValue = val || "";
+      // Parse readable text list of a query string
+      let queryArray = customQueryValue ?
+        parseRequestText(customQueryValue, ".+?", "=") : [];
+      // Write out a list of query params into a query string
+      let queryString = queryArray.map(
+        ({ name, value }) => name + "=" + value).join("&");
+      let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
+        request.url.split("?")[0];
+      // Remove temp customQueryValue while query string is parsable
+      if (customQueryValue === "" ||
+          queryArray.length === customQueryValue.split("\n").length) {
+        customQueryValue = null;
+      }
+      data = {
+        customQueryValue,
+        url,
+      };
+      break;
+    case "custom-url-value":
+      data = {
+        customQueryValue: null,
+        url: val
+      };
+      break;
+    default:
+      break;
+  }
+  if (data) {
+    // All updateRequest batch mode should be disabled to make UI editing in sync
+    updateRequest(request.id, data, false);
+  }
+}
+
+module.exports = connect(
+  (state) => ({ request: getSelectedRequest(state) }),
+  (dispatch) => ({
+    removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
+    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
+    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
+  })
+)(CustomRequestPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/editor.js
@@ -0,0 +1,107 @@
+/* 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/. */
+
+/* eslint-disable react/prop-types */
+
+"use strict";
+
+const {
+  createClass,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const SourceEditor = require("devtools/client/sourceeditor/editor");
+
+const { div } = DOM;
+const SYNTAX_HIGHLIGHT_MAX_SIZE = 102400;
+
+/**
+ * CodeMirror editor as a React component
+ */
+const Editor = createClass({
+  displayName: "Editor",
+
+  propTypes: {
+    // Source editor syntax hightligh mode, which is a mime type defined in CodeMirror
+    mode: PropTypes.string,
+    // Source editor is displayed if set to true
+    open: PropTypes.bool,
+    // Source editor content
+    text: PropTypes.string,
+  },
+
+  getDefaultProps() {
+    return {
+      mode: null,
+      open: true,
+      text: "",
+    };
+  },
+
+  componentDidMount() {
+    const { mode, text } = this.props;
+
+    this.editor = new SourceEditor({
+      lineNumbers: true,
+      mode: text.length < SYNTAX_HIGHLIGHT_MAX_SIZE ? mode : null,
+      readOnly: true,
+      value: text,
+    });
+
+    this.deferEditor = this.editor.appendTo(this.refs.editorElement);
+  },
+
+  componentDidUpdate(prevProps) {
+    const { mode, open, text } = this.props;
+
+    if (!open) {
+      return;
+    }
+
+    if (prevProps.mode !== mode && text.length < SYNTAX_HIGHLIGHT_MAX_SIZE) {
+      this.deferEditor.then(() => {
+        this.editor.setMode(mode);
+      });
+    }
+
+    if (prevProps.text !== text) {
+      this.deferEditor.then(() => {
+        // FIXME: Workaround for browser_net_accessibility test to
+        // make sure editor node exists while setting editor text.
+        // deferEditor workaround should be removed in bug 1308442
+        if (this.refs.editorElement) {
+          this.editor.setText(text);
+        }
+      });
+    }
+  },
+
+  componentWillUnmount() {
+    this.deferEditor.then(() => {
+      this.editor.destroy();
+      this.editor = null;
+    });
+    this.deferEditor = null;
+  },
+
+  render() {
+    const { open } = this.props;
+
+    return (
+      div({ className: "editor-container devtools-monospace" },
+        div({
+          ref: "editorElement",
+          className: "editor-mount devtools-monospace",
+          // Using visibility instead of display property to avoid breaking
+          // CodeMirror indentation
+          style: { visibility: open ? "visible" : "hidden" },
+        }),
+      )
+    );
+  }
+});
+
+module.exports = Editor;
+
+/* eslint-enable react/prop-types */
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/headers-panel.js
@@ -0,0 +1,260 @@
+/* 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 {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { getFormattedSize } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const {
+  getHeadersURL,
+  getHTTPStatusCodeURL,
+} = require("../utils/mdn-utils");
+const { writeHeaderText } = require("../utils/request-utils");
+
+// Components
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const MDNLink = createFactory(require("./mdn-link"));
+const PropertiesView = createFactory(require("./properties-view"));
+
+const Rep = createFactory(REPS.Rep);
+const { button, 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.isRequired,
+    renderValue: PropTypes.func
+  },
+
+  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,
+        }),
+      )
+    );
+  },
+
+  renderValue(props) {
+    const member = props.member;
+    const value = props.value;
+
+    if (typeof value !== "string") {
+      return null;
+    }
+
+    let headerDocURL = getHeadersURL(member.name);
+
+    return (
+      div({ className: "treeValueCellDivider" },
+        Rep(Object.assign(props, {
+          // FIXME: A workaround for the issue in StringRep
+          // Force StringRep to crop the text everytime
+          member: Object.assign({}, member, { open: false }),
+          mode: MODE.TINY,
+          cropLimit: 60,
+        })),
+        headerDocURL ? MDNLink({
+          url: headerDocURL,
+        }) : null
+      )
+    );
+  },
+
+  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;
+      }
+
+      let statusCodeDocURL = getHTTPStatusCodeURL(code);
+      let inputWidth = status.length + statusText.length + 1;
+
+      summaryStatus = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({
+            className: "tabpanel-summary-label headers-summary-label",
+          }, SUMMARY_STATUS),
+          div({
+            className: "requests-list-status-icon",
+            "data-code": code,
+          }),
+          input({
+            className: "tabpanel-summary-value textbox-input devtools-monospace"
+              + " status-text",
+            readOnly: true,
+            value: `${status} ${statusText}`,
+            size: `${inputWidth}`,
+          }),
+          statusCodeDocURL ? MDNLink({
+            url: statusCodeDocURL,
+          }) : null,
+          window.NetMonitorController.supportsCustomRequest && button({
+            className: "devtools-button",
+            onClick: cloneSelectedRequest,
+          }, EDIT_AND_RESEND),
+          button({
+            className: "devtools-button",
+            onClick: this.toggleRawHeaders,
+          }, 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({ className: "panel-container" },
+        div({ className: "headers-overview" },
+          summaryUrl,
+          summaryMethod,
+          summaryAddress,
+          summaryStatus,
+          summaryVersion,
+          summaryRawHeaders,
+        ),
+        PropertiesView({
+          object,
+          filterPlaceHolder: HEADERS_FILTER_TEXT,
+          sectionNames: Object.keys(object),
+          renderValue: this.renderValue,
+        }),
+      )
+    );
+  }
+});
+
+module.exports = HeadersPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/mdn-link.js
@@ -0,0 +1,47 @@
+/* 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 Services = require("Services");
+const {
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { L10N } = require("../utils/l10n");
+
+const { a } = DOM;
+
+const LEARN_MORE = L10N.getStr("netmonitor.headers.learnMore");
+
+function MDNLink({ url }) {
+  return (
+    a({
+      className: "learn-more-link",
+      title: url,
+      onClick: (e) => onLearnMoreClick(e, url),
+    }, `[${LEARN_MORE}]`)
+  );
+}
+
+MDNLink.displayName = "MDNLink";
+
+MDNLink.propTypes = {
+  url: PropTypes.string.isRequired,
+};
+
+function onLearnMoreClick(e, url) {
+  e.stopPropagation();
+  e.preventDefault();
+
+  let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+  if (e.button === 1) {
+    win.openUILinkIn(url, "tabshifted");
+  } else {
+    win.openUILinkIn(url, "tab");
+  }
+}
+
+module.exports = MDNLink;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/monitor-panel.js
@@ -0,0 +1,135 @@
+/* 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 {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+const Actions = require("../actions/index");
+const { getLongString } = require("../utils/client");
+const { Prefs } = require("../utils/prefs");
+const { getFormDataSections } = require("../utils/request-utils");
+const { getSelectedRequest } = require("../selectors/index");
+
+// Components
+const SplitBox = createFactory(require("devtools/client/shared/components/splitter/split-box"));
+const NetworkDetailsPanel = createFactory(require("./network-details-panel"));
+const RequestList = createFactory(require("./request-list"));
+const Toolbar = createFactory(require("./toolbar"));
+
+const { div } = DOM;
+const MediaQueryList = window.matchMedia("(min-width: 700px)");
+
+/*
+ * Monitor panel component
+ * The main panel for displaying various network request information
+ */
+const MonitorPanel = createClass({
+  displayName: "MonitorPanel",
+
+  propTypes: {
+    isEmpty: PropTypes.bool.isRequired,
+    networkDetailsOpen: PropTypes.bool.isRequired,
+    openNetworkDetails: PropTypes.func.isRequired,
+    request: PropTypes.object,
+    updateRequest: PropTypes.func.isRequired,
+  },
+
+  getInitialState() {
+    return {
+      isVerticalSpliter: MediaQueryList.matches,
+    };
+  },
+
+  componentDidMount() {
+    MediaQueryList.addListener(this.onLayoutChange);
+  },
+
+  componentWillReceiveProps(nextProps) {
+    let {
+      request = {},
+      updateRequest,
+    } = nextProps;
+    let {
+      formDataSections,
+      requestHeaders,
+      requestHeadersFromUploadStream,
+      requestPostData,
+    } = request;
+
+    if (!formDataSections && requestHeaders &&
+        requestHeadersFromUploadStream && requestPostData) {
+      getFormDataSections(
+        requestHeaders,
+        requestHeadersFromUploadStream,
+        requestPostData,
+        getLongString,
+      ).then((newFormDataSections) => {
+        updateRequest(
+          request.id,
+          { formDataSections: newFormDataSections },
+          true,
+        );
+      });
+    }
+  },
+
+  componentWillUnmount() {
+    MediaQueryList.removeListener(this.onLayoutChange);
+
+    let { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
+
+    if (this.state.isVerticalSpliter && clientWidth) {
+      Prefs.networkDetailsWidth = clientWidth;
+    }
+    if (!this.state.isVerticalSpliter && clientHeight) {
+      Prefs.networkDetailsHeight = clientHeight;
+    }
+  },
+
+  onLayoutChange() {
+    this.setState({
+      isVerticalSpliter: MediaQueryList.matches,
+    });
+  },
+
+  render() {
+    let { isEmpty, networkDetailsOpen } = this.props;
+    return (
+      div({ className: "monitor-panel" },
+        Toolbar(),
+        SplitBox({
+          className: "devtools-responsive-container",
+          initialWidth: `${Prefs.networkDetailsWidth}px`,
+          initialHeight: `${Prefs.networkDetailsHeight}px`,
+          minSize: "50px",
+          maxSize: "80%",
+          splitterSize: "1px",
+          startPanel: RequestList({ isEmpty }),
+          endPanel: networkDetailsOpen && NetworkDetailsPanel({ ref: "endPanel" }),
+          endPanelControl: true,
+          vert: this.state.isVerticalSpliter,
+        }),
+      )
+    );
+  }
+});
+
+module.exports = connect(
+  (state) => ({
+    isEmpty: state.requests.requests.isEmpty(),
+    networkDetailsOpen: state.ui.networkDetailsOpen,
+    request: getSelectedRequest(state),
+  }),
+  (dispatch) => ({
+    openNetworkDetails: (open) => dispatch(Actions.openNetworkDetails(open)),
+    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
+  }),
+)(MonitorPanel);
rename from devtools/client/netmonitor/components/moz.build
rename to devtools/client/netmonitor/src/components/moz.build
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/src/components/moz.build
@@ -1,15 +1,28 @@
 # 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(
+    'app.js',
+    'cookies-panel.js',
+    'custom-request-panel.js',
+    'editor.js',
+    'headers-panel.js',
+    'mdn-link.js',
     'monitor-panel.js',
-    'network-monitor.js',
+    'network-details-panel.js',
+    'params-panel.js',
+    'preview-panel.js',
+    'properties-view.js',
     'request-list-content.js',
-    'request-list-empty.js',
+    'request-list-empty-notice.js',
     'request-list-header.js',
     'request-list-item.js',
     'request-list.js',
+    'response-panel.js',
+    'security-panel.js',
     'statistics-panel.js',
+    'tabbox-panel.js',
+    'timings-panel.js',
     'toolbar.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/network-details-panel.js
@@ -0,0 +1,70 @@
+/* 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 Actions = require("../actions/index");
+const { getSelectedRequest } = require("../selectors/index");
+
+// Components
+const CustomRequestPanel = createFactory(require("./custom-request-panel"));
+const TabboxPanel = createFactory(require("./tabbox-panel"));
+
+const { div } = DOM;
+
+/*
+ * Network details panel component
+ */
+function NetworkDetailsPanel({
+  activeTabId,
+  cloneSelectedRequest,
+  request,
+  selectTab,
+}) {
+  if (!request) {
+    return null;
+  }
+
+  return (
+    div({ className: "network-details-panel" },
+      !request.isCustom ?
+        TabboxPanel({
+          activeTabId,
+          request,
+          selectTab,
+        }) :
+        CustomRequestPanel({
+          cloneSelectedRequest,
+          request,
+        })
+    )
+  );
+}
+
+NetworkDetailsPanel.displayName = "NetworkDetailsPanel";
+
+NetworkDetailsPanel.propTypes = {
+  activeTabId: PropTypes.string,
+  cloneSelectedRequest: PropTypes.func.isRequired,
+  open: PropTypes.bool,
+  request: PropTypes.object,
+  selectTab: PropTypes.func.isRequired,
+};
+
+module.exports = connect(
+  (state) => ({
+    activeTabId: state.ui.detailsPanelSelectedTab,
+    request: getSelectedRequest(state),
+  }),
+  (dispatch) => ({
+    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+    selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
+  }),
+)(NetworkDetailsPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/params-panel.js
@@ -0,0 +1,130 @@
+/* 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 { L10N } = require("../utils/l10n");
+const { getUrlQuery, parseQueryString } = require("../utils/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({ request }) {
+  let {
+    formDataSections,
+    mimeType,
+    requestPostData,
+    url,
+  } = request;
+  let postData = requestPostData ? requestPostData.postData.text : null;
+  let query = getUrlQuery(url);
+
+  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] = getProperties(parseQueryString(query));
+  }
+
+  // Form Data section
+  if (formDataSections && formDataSections.length > 0) {
+    let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
+    object[PARAMS_FORM_DATA] = getProperties(parseQueryString(sections));
+  }
+
+  // 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 (
+    div({ className: "panel-container" },
+      PropertiesView({
+        object,
+        filterPlaceHolder: PARAMS_FILTER_TEXT,
+        sectionNames: SECTION_NAMES,
+      })
+    )
+  );
+}
+
+ParamsPanel.displayName = "ParamsPanel";
+
+ParamsPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+/**
+ * Mapping array to dict for TreeView usage.
+ * Since TreeView only support Object(dict) format.
+ * This function also deal with duplicate key case
+ * (for multiple selection and query params with same keys)
+ *
+ * @param {Object[]} arr - key-value pair array like query or form params
+ * @returns {Object} Rep compatible object
+ */
+function getProperties(arr) {
+  return arr.reduce((map, obj) => {
+    let value = map[obj.name];
+    if (value) {
+      if (typeof value !== "object") {
+        map[obj.name] = [value];
+      }
+      map[obj.name].push(obj.value);
+    } else {
+      map[obj.name] = obj.value;
+    }
+    return map;
+  }, {});
+}
+
+module.exports = ParamsPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/preview-panel.js
@@ -0,0 +1,35 @@
+/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
+
+const { div, iframe } = DOM;
+
+/*
+ * Preview panel component
+ * Display HTML content within a sandbox enabled iframe
+ */
+function PreviewPanel({ request }) {
+  const htmlBody = request.responseContent ?
+    request.responseContent.content.text : "";
+
+  return (
+    div({ className: "panel-container" },
+      iframe({
+        sandbox: "",
+        srcDoc: typeof htmlBody === "string" ? htmlBody : "",
+      })
+    )
+  );
+}
+
+PreviewPanel.displayName = "PreviewPanel";
+
+PreviewPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+module.exports = PreviewPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/properties-view.js
@@ -0,0 +1,218 @@
+/* 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/. */
+
+/* eslint-disable react/prop-types */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const Rep = createFactory(REPS.Rep);
+
+const { FILTER_SEARCH_DELAY } = require("../constants");
+
+// Components
+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 Editor = createFactory(require("./editor"));
+
+const { div, tr, td } = DOM;
+const AUTO_EXPAND_MAX_LEVEL = 7;
+const AUTO_EXPAND_MAX_NODES = 50;
+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 EDITOR_CONFIG_ID.
+ * Rep - Default enabled.
+ */
+const PropertiesView = createClass({
+  displayName: "PropertiesView",
+
+  propTypes: {
+    object: PropTypes.object,
+    enableInput: PropTypes.bool,
+    expandableStrings: PropTypes.bool,
+    filterPlaceHolder: PropTypes.string,
+    sectionNames: PropTypes.array,
+  },
+
+  getDefaultProps() {
+    return {
+      enableInput: true,
+      enableFilter: true,
+      expandableStrings: false,
+      filterPlaceHolder: "",
+      sectionNames: [],
+    };
+  },
+
+  getInitialState() {
+    return {
+      filterText: "",
+    };
+  },
+
+  getRowClass(object, sectionNames) {
+    return sectionNames.includes(object.name) ? "tree-section" : "";
+  },
+
+  onFilter(object, whiteList) {
+    let { name, value } = object;
+    let filterText = this.state.filterText;
+
+    if (!filterText || whiteList.includes(name)) {
+      return true;
+    }
+
+    let jsonString = JSON.stringify({ [name]: value }).toLowerCase();
+    return jsonString.includes(filterText.toLowerCase());
+  },
+
+  renderRowWithEditor(props) {
+    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({ className: "editor-row-container" },
+          td({ colSpan: 2 },
+            Editor(value)
+          )
+        )
+      );
+    }
+
+    // Skip for editor config
+    if (level >= 1 && path.includes(EDITOR_CONFIG_ID)) {
+      return null;
+    }
+
+    return TreeRow(props);
+  },
+
+  renderValueWithRep(props) {
+    const { member } = props;
+
+    // Hide strings with following conditions
+    // 1. this row is a togglable section and content is object ('cause it shouldn't hide
+    //    when string or number)
+    // 2. the `value` object has a `value` property, only happened in Cookies panel
+    // Put 2 here to not dup this method
+    if (member.level === 0 && member.type === "object" ||
+      (typeof member.value === "object" && member.value && member.value.value)) {
+      return null;
+    }
+
+    return Rep(Object.assign(props, {
+      // FIXME: A workaround for the issue in StringRep
+      // Force StringRep to crop the text everytime
+      member: Object.assign({}, 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) {
+      if (expandedNodes.size > AUTO_EXPAND_MAX_NODES) {
+        // If we reached the limit of expandable nodes, bail out to avoid performance
+        // issues.
+        break;
+      }
+
+      let nodePath = path + "/" + prop;
+      expandedNodes.add(nodePath);
+
+      let nodes = this.getExpandedNodes(object[prop], nodePath, level + 1);
+      if (nodes) {
+        let newSize = expandedNodes.size + nodes.size;
+        if (newSize < AUTO_EXPAND_MAX_NODES) {
+          // Avoid having a subtree half expanded.
+          expandedNodes = new Set([...expandedNodes, ...nodes]);
+        }
+      }
+    }
+    return expandedNodes;
+  },
+
+  render() {
+    const {
+      decorator,
+      enableInput,
+      expandableStrings,
+      filterPlaceHolder,
+      object,
+      renderRow,
+      renderValue,
+      sectionNames,
+    } = this.props;
+
+    return (
+      div({ className: "properties-view" },
+        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: this.getExpandedNodes(object),
+            onFilter: (props) => this.onFilter(props, sectionNames),
+            renderRow: renderRow || this.renderRowWithEditor,
+            renderValue: renderValue || this.renderValueWithRep,
+          }),
+        ),
+      )
+    );
+  }
+});
+
+module.exports = PropertiesView;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/request-list-content.js
@@ -0,0 +1,280 @@
+/* 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 { KeyCodes } = require("devtools/client/shared/keycodes");
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const Actions = require("../actions/index");
+const {
+  setTooltipImageContent,
+  setTooltipStackTraceContent,
+} = require("../request-list-tooltip");
+const {
+  getDisplayedRequests,
+  getWaterfallScale,
+} = require("../selectors/index");
+
+// Components
+const RequestListItem = createFactory(require("./request-list-item"));
+const RequestListContextMenu = require("../request-list-context-menu");
+
+const { div } = DOM;
+
+// tooltip show/hide delay in ms
+const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
+
+/**
+ * Renders the actual contents of the request list.
+ */
+const RequestListContent = createClass({
+  displayName: "RequestListContent",
+
+  propTypes: {
+    dispatch: PropTypes.func.isRequired,
+    displayedRequests: PropTypes.object.isRequired,
+    firstRequestStartedMillis: PropTypes.number.isRequired,
+    fromCache: PropTypes.bool.isRequired,
+    onItemMouseDown: PropTypes.func.isRequired,
+    onSecurityIconClick: PropTypes.func.isRequired,
+    onSelectDelta: PropTypes.func.isRequired,
+    scale: PropTypes.number,
+    selectedRequestId: PropTypes.string,
+  },
+
+  componentWillMount() {
+    const { dispatch } = this.props;
+    this.contextMenu = new RequestListContextMenu({
+      cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+      openStatistics: (open) => dispatch(Actions.openStatistics(open)),
+    });
+    this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
+  },
+
+  componentDidMount() {
+    // Set the CSS variables for waterfall scaling
+    this.setScalingStyles();
+
+    // Install event handler for displaying a tooltip
+    this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
+      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
+      interactive: true
+    });
+
+    // Install event handler to hide the tooltip on scroll
+    this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
+  },
+
+  componentWillUpdate(nextProps) {
+    // Check if the list is scrolled to bottom before the UI update.
+    // The scroll is ever needed only if new rows are added to the list.
+    const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
+    this.shouldScrollBottom = delta > 0 && this.isScrolledToBottom();
+  },
+
+  componentDidUpdate(prevProps) {
+    // Update the CSS variables for waterfall scaling after props change
+    this.setScalingStyles(prevProps);
+
+    // Keep the list scrolled to bottom if a new row was added
+    if (this.shouldScrollBottom) {
+      let node = this.refs.contentEl;
+      node.scrollTop = node.scrollHeight;
+    }
+  },
+
+  componentWillUnmount() {
+    this.refs.contentEl.removeEventListener("scroll", this.onScroll, true);
+
+    // Uninstall the tooltip event handler
+    this.tooltip.stopTogglingOnHover();
+  },
+
+  /**
+   * Set the CSS variables for waterfall scaling. If React supported setting CSS
+   * variables as part of the "style" property of a DOM element, we would use that.
+   *
+   * However, React doesn't support this, so we need to use a hack and update the
+   * DOM element directly: https://github.com/facebook/react/issues/6411
+   */
+  setScalingStyles(prevProps) {
+    const { scale } = this.props;
+    if (prevProps && prevProps.scale === scale) {
+      return;
+    }
+
+    const { style } = this.refs.contentEl;
+    style.removeProperty("--timings-scale");
+    style.removeProperty("--timings-rev-scale");
+    style.setProperty("--timings-scale", scale);
+    style.setProperty("--timings-rev-scale", 1 / scale);
+  },
+
+  isScrolledToBottom() {
+    const { contentEl } = this.refs;
+    const lastChildEl = contentEl.lastElementChild;
+
+    if (!lastChildEl) {
+      return false;
+    }
+
+    let lastChildRect = lastChildEl.getBoundingClientRect();
+    let contentRect = contentEl.getBoundingClientRect();
+
+    return (lastChildRect.height + lastChildRect.top) <= contentRect.bottom;
+  },
+
+  /**
+   * The predicate used when deciding whether a popup should be shown
+   * over a request item or not.
+   *
+   * @param nsIDOMNode target
+   *        The element node currently being hovered.
+   * @param object tooltip
+   *        The current tooltip instance.
+   * @return {Promise}
+   */
+  onHover(target, tooltip) {
+    let itemEl = target.closest(".request-list-item");
+    if (!itemEl) {
+      return false;
+    }
+    let itemId = itemEl.dataset.id;
+    if (!itemId) {
+      return false;
+    }
+    let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
+    if (!requestItem) {
+      return false;
+    }
+
+    if (requestItem.responseContent && target.closest(".requests-list-icon-and-file")) {
+      return setTooltipImageContent(tooltip, itemEl, requestItem);
+    } else if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
+      return setTooltipStackTraceContent(tooltip, requestItem);
+    }
+
+    return false;
+  },
+
+  /**
+   * Scroll listener for the requests menu view.
+   */
+  onScroll() {
+    this.tooltip.hide();
+  },
+
+  /**
+   * Handler for keyboard events. For arrow up/down, page up/down, home/end,
+   * move the selection up or down.
+   */
+  onKeyDown(e) {
+    let delta;
+
+    switch (e.keyCode) {
+      case KeyCodes.DOM_VK_UP:
+      case KeyCodes.DOM_VK_LEFT:
+        delta = -1;
+        break;
+      case KeyCodes.DOM_VK_DOWN:
+      case KeyCodes.DOM_VK_RIGHT:
+        delta = +1;
+        break;
+      case KeyCodes.DOM_VK_PAGE_UP:
+        delta = "PAGE_UP";
+        break;
+      case KeyCodes.DOM_VK_PAGE_DOWN:
+        delta = "PAGE_DOWN";
+        break;
+      case KeyCodes.DOM_VK_HOME:
+        delta = -Infinity;
+        break;
+      case KeyCodes.DOM_VK_END:
+        delta = +Infinity;
+        break;
+    }
+
+    if (delta) {
+      // Prevent scrolling when pressing navigation keys.
+      e.preventDefault();
+      e.stopPropagation();
+      this.props.onSelectDelta(delta);
+    }
+  },
+
+  onContextMenu(evt) {
+    evt.preventDefault();
+    this.contextMenu.open(evt);
+  },
+
+  /**
+   * If selection has just changed (by keyboard navigation), don't keep the list
+   * scrolled to bottom, but allow scrolling up with the selection.
+   */
+  onFocusedNodeChange() {
+    this.shouldScrollBottom = false;
+  },
+
+  render() {
+    const {
+      displayedRequests,
+      firstRequestStartedMillis,
+      selectedRequestId,
+      onItemMouseDown,
+      onSecurityIconClick,
+    } = this.props;
+
+    return (
+      div({
+        ref: "contentEl",
+        className: "requests-list-contents",
+        tabIndex: 0,
+        onKeyDown: this.onKeyDown,
+      },
+        displayedRequests.map((item, index) => RequestListItem({
+          firstRequestStartedMillis,
+          fromCache: item.status === "304" || item.fromCache,
+          item,
+          index,
+          isSelected: item.id === selectedRequestId,
+          key: item.id,
+          onContextMenu: this.onContextMenu,
+          onFocusedNodeChange: this.onFocusedNodeChange,
+          onMouseDown: () => onItemMouseDown(item.id),
+          onSecurityIconClick: () => onSecurityIconClick(item.securityState),
+        }))
+      )
+    );
+  },
+});
+
+module.exports = connect(
+  (state) => ({
+    displayedRequests: getDisplayedRequests(state),
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    selectedRequestId: state.requests.selectedId,
+    scale: getWaterfallScale(state),
+  }),
+  (dispatch) => ({
+    dispatch,
+    onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
+    /**
+     * A handler that opens the security tab in the details view if secure or
+     * broken security indicator is clicked.
+     */
+    onSecurityIconClick: (securityState) => {
+      if (securityState && securityState !== "insecure") {
+        dispatch(Actions.selectDetailsPanelTab("security"));
+      }
+    },
+    onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
+  }),
+)(RequestListContent);
rename from devtools/client/netmonitor/components/request-list-empty.js
rename to devtools/client/netmonitor/src/components/request-list-empty-notice.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/request-list-header.js
@@ -0,0 +1,203 @@
+/* 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 {
+  createClass,
+  PropTypes,
+  DOM,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const Actions = require("../actions/index");
+const { getWaterfallScale } = require("../selectors/index");
+const { getFormattedTime } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const WaterfallBackground = require("../waterfall-background");
+
+const { div, button } = DOM;
+
+const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
+const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
+
+const HEADERS = [
+  { name: "status", label: "status3" },
+  { name: "method" },
+  { name: "file", boxName: "icon-and-file" },
+  { name: "domain", boxName: "security-and-domain" },
+  { name: "cause" },
+  { name: "type" },
+  { name: "transferred" },
+  { name: "size" },
+  { name: "waterfall" }
+];
+
+/**
+ * Render the request list header with sorting arrows for columns.
+ * Displays tick marks in the waterfall column header.
+ * Also draws the waterfall background canvas and updates it when needed.
+ */
+const RequestListHeader = createClass({
+  displayName: "RequestListHeader",
+
+  propTypes: {
+    sort: PropTypes.object,
+    scale: PropTypes.number,
+    waterfallWidth: PropTypes.number,
+    onHeaderClick: PropTypes.func.isRequired,
+    resizeWaterfall: PropTypes.func.isRequired,
+  },
+
+  componentDidMount() {
+    // Create the object that takes care of drawing the waterfall canvas background
+    this.background = new WaterfallBackground(document);
+    this.background.draw(this.props);
+    this.resizeWaterfall();
+    window.addEventListener("resize", this.resizeWaterfall);
+  },
+
+  componentDidUpdate() {
+    this.background.draw(this.props);
+  },
+
+  componentWillUnmount() {
+    this.background.destroy();
+    this.background = null;
+    window.removeEventListener("resize", this.resizeWaterfall);
+  },
+
+  resizeWaterfall() {
+    // Measure its width and update the 'waterfallWidth' property in the store.
+    // The 'waterfallWidth' will be further updated on every window resize.
+    setNamedTimeout("resize-events", 50, () => {
+      const { width } = this.refs.header.getBoundingClientRect();
+      this.props.resizeWaterfall(width);
+    });
+  },
+
+  render() {
+    const { sort, scale, waterfallWidth, onHeaderClick } = this.props;
+
+    return div(
+      { className: "devtools-toolbar requests-list-toolbar" },
+      div({ className: "toolbar-labels" },
+        HEADERS.map(header => {
+          const name = header.name;
+          const boxName = header.boxName || name;
+          const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
+
+          let sorted, sortedTitle;
+          const active = sort.type == name ? true : undefined;
+          if (active) {
+            sorted = sort.ascending ? "ascending" : "descending";
+            sortedTitle = L10N.getStr(sort.ascending
+              ? "networkMenu.sortedAsc"
+              : "networkMenu.sortedDesc");
+          }
+
+          return div(
+            {
+              id: `requests-list-${boxName}-header-box`,
+              className: `requests-list-header requests-list-${boxName}`,
+              key: name,
+              ref: "header",
+              // Used to style the next column.
+              "data-active": active,
+            },
+            button(
+              {
+                id: `requests-list-${name}-button`,
+                className: `requests-list-header-button requests-list-${name}`,
+                "data-sorted": sorted,
+                title: sortedTitle,
+                onClick: () => onHeaderClick(name),
+              },
+              name == "waterfall" ? WaterfallLabel(waterfallWidth, scale, label)
+                                  : div({ className: "button-text" }, label),
+              div({ className: "button-icon" })
+            )
+          );
+        })
+      )
+    );
+  }
+});
+
+/**
+ * Build the waterfall header - timing tick marks with the right spacing
+ */
+function waterfallDivisionLabels(waterfallWidth, scale) {
+  let labels = [];
+
+  // Build new millisecond tick labels...
+  let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
+  let scaledStep = scale * timingStep;
+
+  // Ignore any divisions that would end up being too close to each other.
+  while (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
+    scaledStep *= 2;
+  }
+
+  // Insert one label for each division on the current scale.
+  for (let x = 0; x < waterfallWidth; x += scaledStep) {
+    let millisecondTime = x / scale;
+    let divisionScale = "millisecond";
+
+    // If the division is greater than 1 minute.
+    if (millisecondTime > 60000) {
+      divisionScale = "minute";
+    } else if (millisecondTime > 1000) {
+      // If the division is greater than 1 second.
+      divisionScale = "second";
+    }
+
+    let width = (x + scaledStep | 0) - (x | 0);
+    // Adjust the first marker for the borders
+    if (x == 0) {
+      width -= 2;
+    }
+    // Last marker doesn't need a width specified at all
+    if (x + scaledStep >= waterfallWidth) {
+      width = undefined;
+    }
+
+    labels.push(div(
+      {
+        key: labels.length,
+        className: "requests-list-timings-division",
+        "data-division-scale": divisionScale,
+        style: { width }
+      },
+      getFormattedTime(millisecondTime)
+    ));
+  }
+
+  return labels;
+}
+
+function WaterfallLabel(waterfallWidth, scale, label) {
+  let className = "button-text requests-list-waterfall-label-wrapper";
+
+  if (waterfallWidth != null && scale != null) {
+    label = waterfallDivisionLabels(waterfallWidth, scale);
+    className += " requests-list-waterfall-visible";
+  }
+
+  return div({ className }, label);
+}
+
+module.exports = connect(
+  state => ({
+    sort: state.sort,
+    scale: getWaterfallScale(state),
+    waterfallWidth: state.ui.waterfallWidth,
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    timingMarkers: state.timingMarkers,
+  }),
+  dispatch => ({
+    onHeaderClick: type => dispatch(Actions.sortBy(type)),
+    resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)),
+  })
+)(RequestListHeader);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/request-list-item.js
@@ -0,0 +1,528 @@
+/* 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 {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { getFormattedSize } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const { getAbbreviatedMimeType } = require("../utils/request-utils");
+
+const { div, img, span } = DOM;
+
+/**
+ * Compare two objects on a subset of their properties
+ */
+function propertiesEqual(props, item1, item2) {
+  return item1 === item2 || props.every(p => item1[p] === item2[p]);
+}
+
+/**
+ * Used by shouldComponentUpdate: compare two items, and compare only properties
+ * relevant for rendering the RequestListItem. Other properties (like request and
+ * response headers, cookies, bodies) are ignored. These are very useful for the
+ * network details, but not here.
+ */
+const UPDATED_REQ_ITEM_PROPS = [
+  "mimeType",
+  "eventTimings",
+  "securityState",
+  "responseContentDataUri",
+  "status",
+  "statusText",
+  "fromCache",
+  "fromServiceWorker",
+  "method",
+  "url",
+  "remoteAddress",
+  "cause",
+  "contentSize",
+  "transferredSize",
+  "startedMillis",
+  "totalTime",
+];
+
+const UPDATED_REQ_PROPS = [
+  "index",
+  "isSelected",
+  "firstRequestStartedMillis",
+];
+
+/**
+ * Render one row in the request list.
+ */
+const RequestListItem = createClass({
+  displayName: "RequestListItem",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+    index: PropTypes.number.isRequired,
+    isSelected: PropTypes.bool.isRequired,
+    firstRequestStartedMillis: PropTypes.number.isRequired,
+    fromCache: PropTypes.bool.isRequired,
+    onContextMenu: PropTypes.func.isRequired,
+    onFocusedNodeChange: PropTypes.func,
+    onMouseDown: PropTypes.func.isRequired,
+    onSecurityIconClick: PropTypes.func.isRequired,
+  },
+
+  componentDidMount() {
+    if (this.props.isSelected) {
+      this.refs.el.focus();
+    }
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
+      !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
+  },
+
+  componentDidUpdate(prevProps) {
+    if (!prevProps.isSelected && this.props.isSelected) {
+      this.refs.el.focus();
+      if (this.props.onFocusedNodeChange) {
+        this.props.onFocusedNodeChange();
+      }
+    }
+  },
+
+  render() {
+    const {
+      item,
+      index,
+      isSelected,
+      firstRequestStartedMillis,
+      fromCache,
+      onContextMenu,
+      onMouseDown,
+      onSecurityIconClick
+    } = this.props;
+
+    let classList = ["request-list-item"];
+    if (isSelected) {
+      classList.push("selected");
+    }
+
+    if (fromCache) {
+      classList.push("fromCache");
+    }
+
+    classList.push(index % 2 ? "odd" : "even");
+
+    return (
+      div({
+        ref: "el",
+        className: classList.join(" "),
+        "data-id": item.id,
+        tabIndex: 0,
+        onContextMenu,
+        onMouseDown,
+      },
+        StatusColumn({ item }),
+        MethodColumn({ item }),
+        FileColumn({ item }),
+        DomainColumn({ item, onSecurityIconClick }),
+        CauseColumn({ item }),
+        TypeColumn({ item }),
+        TransferredSizeColumn({ item }),
+        ContentSizeColumn({ item }),
+        WaterfallColumn({ item, firstRequestStartedMillis }),
+      )
+    );
+  }
+});
+
+const UPDATED_STATUS_PROPS = [
+  "status",
+  "statusText",
+  "fromCache",
+  "fromServiceWorker",
+];
+
+const StatusColumn = createFactory(createClass({
+  displayName: "StatusColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { status, statusText, fromCache, fromServiceWorker } = this.props.item;
+
+    let code, title;
+
+    if (status) {
+      if (fromCache) {
+        code = "cached";
+      } else if (fromServiceWorker) {
+        code = "service worker";
+      } else {
+        code = status;
+      }
+
+      if (statusText) {
+        title = `${status} ${statusText}`;
+        if (fromCache) {
+          title += " (cached)";
+        }
+        if (fromServiceWorker) {
+          title += " (service worker)";
+        }
+      }
+    }
+
+    return (
+        div({ className: "requests-list-subitem requests-list-status", title },
+        div({ className: "requests-list-status-icon", "data-code": code }),
+        span({ className: "subitem-label requests-list-status-code" }, status)
+      )
+    );
+  }
+}));
+
+const MethodColumn = createFactory(createClass({
+  displayName: "MethodColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.method !== nextProps.item.method;
+  },
+
+  render() {
+    const { method } = this.props.item;
+    return (
+      div({ className: "requests-list-subitem requests-list-method-box" },
+        span({ className: "subitem-label requests-list-method" }, method)
+      )
+    );
+  }
+}));
+
+const UPDATED_FILE_PROPS = [
+  "urlDetails",
+  "responseContentDataUri",
+];
+
+const FileColumn = createFactory(createClass({
+  displayName: "FileColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { urlDetails, responseContentDataUri } = this.props.item;
+
+    return (
+      div({ className: "requests-list-subitem requests-list-icon-and-file" },
+        img({
+          className: "requests-list-icon",
+          src: responseContentDataUri,
+          hidden: !responseContentDataUri,
+          "data-type": responseContentDataUri ? "thumbnail" : undefined,
+        }),
+        div({
+          className: "subitem-label requests-list-file",
+          title: urlDetails.unicodeUrl,
+        },
+          urlDetails.baseNameWithQuery,
+        ),
+      )
+    );
+  }
+}));
+
+const UPDATED_DOMAIN_PROPS = [
+  "urlDetails",
+  "remoteAddress",
+  "securityState",
+];
+
+const DomainColumn = createFactory(createClass({
+  displayName: "DomainColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+    onSecurityIconClick: PropTypes.func.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { item, onSecurityIconClick } = this.props;
+    const { urlDetails, remoteAddress, securityState } = item;
+
+    let iconClassList = ["requests-security-state-icon"];
+    let iconTitle;
+    if (urlDetails.isLocal) {
+      iconClassList.push("security-state-local");
+      iconTitle = L10N.getStr("netmonitor.security.state.secure");
+    } else if (securityState) {
+      iconClassList.push(`security-state-${securityState}`);
+      iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
+    }
+
+    let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : "");
+
+    return (
+      div({ className: "requests-list-subitem requests-list-security-and-domain" },
+        div({
+          className: iconClassList.join(" "),
+          title: iconTitle,
+          onClick: onSecurityIconClick,
+        }),
+        span({ className: "subitem-label requests-list-domain", title }, urlDetails.host),
+      )
+    );
+  }
+}));
+
+const CauseColumn = createFactory(createClass({
+  displayName: "CauseColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.cause !== nextProps.item.cause;
+  },
+
+  render() {
+    const { cause } = this.props.item;
+
+    let causeType = "";
+    let causeUri = undefined;
+    let causeHasStack = false;
+
+    if (cause) {
+      // Legacy server might send a numeric value. Display it as "unknown"
+      causeType = typeof cause.type === "string" ? cause.type : "unknown";
+      causeUri = cause.loadingDocumentUri;
+      causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
+    }
+
+    return (
+      div({
+        className: "requests-list-subitem requests-list-cause",
+        title: causeUri,
+      },
+        span({
+          className: "requests-list-cause-stack",
+          hidden: !causeHasStack,
+        }, "JS"),
+        span({ className: "subitem-label" }, causeType),
+      )
+    );
+  }
+}));
+
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+  "ecmascript": "js",
+  "javascript": "js",
+  "x-javascript": "js"
+};
+
+const TypeColumn = createFactory(createClass({
+  displayName: "TypeColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.mimeType !== nextProps.item.mimeType;
+  },
+
+  render() {
+    const { mimeType } = this.props.item;
+    let abbrevType;
+    if (mimeType) {
+      abbrevType = getAbbreviatedMimeType(mimeType);
+      abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
+    }
+
+    return (
+      div({
+        className: "requests-list-subitem requests-list-type",
+        title: mimeType,
+      },
+        span({ className: "subitem-label" }, abbrevType),
+      )
+    );
+  }
+}));
+
+const UPDATED_TRANSFERRED_PROPS = [
+  "transferredSize",
+  "fromCache",
+  "fromServiceWorker",
+];
+
+const TransferredSizeColumn = createFactory(createClass({
+  displayName: "TransferredSizeColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { transferredSize, fromCache, fromServiceWorker, status } = this.props.item;
+
+    let text;
+    let className = "subitem-label";
+    if (fromCache || status === "304") {
+      text = L10N.getStr("networkMenu.sizeCached");
+      className += " theme-comment";
+    } else if (fromServiceWorker) {
+      text = L10N.getStr("networkMenu.sizeServiceWorker");
+      className += " theme-comment";
+    } else if (typeof transferredSize == "number") {
+      text = getFormattedSize(transferredSize);
+    } else if (transferredSize === null) {
+      text = L10N.getStr("networkMenu.sizeUnavailable");
+    }
+
+    return (
+      div({
+        className: "requests-list-subitem requests-list-transferred",
+        title: text,
+      },
+        span({ className }, text),
+      )
+    );
+  }
+}));
+
+const ContentSizeColumn = createFactory(createClass({
+  displayName: "ContentSizeColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.contentSize !== nextProps.item.contentSize;
+  },
+
+  render() {
+    const { contentSize } = this.props.item;
+
+    let text;
+    if (typeof contentSize == "number") {
+      text = getFormattedSize(contentSize);
+    }
+
+    return (
+      div({
+        className: "requests-list-subitem subitem-label requests-list-size",
+        title: text,
+      },
+        span({ className: "subitem-label" }, text),
+      )
+    );
+  }
+}));
+
+const UPDATED_WATERFALL_PROPS = [
+  "eventTimings",
+  "totalTime",
+  "fromCache",
+  "fromServiceWorker",
+];
+
+const WaterfallColumn = createFactory(createClass({
+  displayName: "WaterfallColumn",
+
+  propTypes: {
+    firstRequestStartedMillis: PropTypes.number.isRequired,
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis ||
+      !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { item, firstRequestStartedMillis } = this.props;
+
+    return (
+      div({ className: "requests-list-subitem requests-list-waterfall" },
+        div({
+          className: "requests-list-timings",
+          style: {
+            paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`,
+          },
+        },
+          timingBoxes(item),
+        )
+      )
+    );
+  }
+}));
+
+// List of properties of the timing info we want to create boxes for
+const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"];
+
+function timingBoxes(item) {
+  const { eventTimings, totalTime, fromCache, fromServiceWorker } = item;
+  let boxes = [];
+
+  if (fromCache || fromServiceWorker) {
+    return boxes;
+  }
+
+  if (eventTimings) {
+    // Add a set of boxes representing timing information.
+    for (let key of TIMING_KEYS) {
+      let width = eventTimings.timings[key];
+
+      // Don't render anything if it surely won't be visible.
+      // One millisecond == one unscaled pixel.
+      if (width > 0) {
+        boxes.push(div({
+          key,
+          className: "requests-list-timings-box " + key,
+          style: { width }
+        }));
+      }
+    }
+  }
+
+  if (typeof totalTime === "number") {
+    let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
+    boxes.push(div({
+      key: "total",
+      className: "requests-list-timings-total",
+      title: text
+    }, text));
+  }
+
+  return boxes;
+}
+
+module.exports = RequestListItem;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/request-list.js
@@ -0,0 +1,38 @@
+/* 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");
+
+// Components
+const RequestListContent = createFactory(require("./request-list-content"));
+const RequestListEmptyNotice = createFactory(require("./request-list-empty-notice"));
+const RequestListHeader = createFactory(require("./request-list-header"));
+
+const { div } = DOM;
+
+/**
+ * Request panel component
+ */
+function RequestList({ isEmpty }) {
+  return (
+    div({ className: "request-list-container" },
+      RequestListHeader(),
+      isEmpty ? RequestListEmptyNotice() : RequestListContent(),
+    )
+  );
+}
+
+RequestList.displayName = "RequestList";
+
+RequestList.propTypes = {
+  isEmpty: PropTypes.bool.isRequired,
+};
+
+module.exports = RequestList;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/response-panel.js
@@ -0,0 +1,185 @@
+/* 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 {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { formDataURI, getUrlBaseName } = require("../utils/request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./properties-view"));
+
+const { div, img } = DOM;
+const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
+const JSON_FILTER_TEXT = L10N.getStr("jsonFilterText");
+const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name");
+const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions");
+const RESPONSE_IMG_MIMETYPE = L10N.getStr("netmonitor.response.mime");
+const RESPONSE_PAYLOAD = L10N.getStr("responsePayload");
+
+/*
+ * Response panel component
+ * Displays the GET parameters and POST data of a request
+ */
+const ResponsePanel = createClass({
+  displayName: "ResponsePanel",
+
+  propTypes: {
+    request: PropTypes.object.isRequired,
+  },
+
+  getInitialState() {
+    return {
+      imageDimensions: {
+        width: 0,
+        height: 0,
+      },
+    };
+  },
+
+  updateImageDimemsions({ target }) {
+    this.setState({
+      imageDimensions: {
+        width: target.naturalWidth,
+        height: target.naturalHeight,
+      },
+    });
+  },
+
+  // Handle json, which we tentatively identify by checking the MIME type
+  // for "json" after any word boundary. This works for the standard
+  // "application/json", and also for custom types like "x-bigcorp-json".
+  // Additionally, we also directly parse the response text content to
+  // verify whether it's json or not, to handle responses incorrectly
+  // labeled as text/plain instead.
+  isJSON(mimeType, response) {
+    let json, error;
+    try {
+      json = JSON.parse(response);
+    } catch (err) {
+      try {
+        json = JSON.parse(atob(response));
+      } catch (err64) {
+        error = err;
+      }
+    }
+
+    if (/\bjson/.test(mimeType) || json) {
+      // Extract the actual json substring in case this might be a "JSONP".
+      // This regex basically parses a function call and captures the
+      // function name and arguments in two separate groups.
+      let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
+      let [, jsonpCallback, jsonp] = response.match(jsonpRegex) || [];
+      let result = {};
+
+      // Make sure this is a valid JSON object first. If so, nicely display
+      // the parsing results in a tree view.
+      if (jsonpCallback && jsonp) {
+        error = null;
+        try {
+          json = JSON.parse(jsonp);
+        } catch (err) {
+          error = err;
+        }
+      }
+
+      // Valid JSON
+      if (json) {
+        result.json = json;
+      }
+      // Valid JSONP
+      if (jsonpCallback) {
+        result.jsonpCallback = jsonpCallback;
+      }
+      // Malformed JSON
+      if (error) {
+        result.error = "" + error;
+      }
+
+      return result;
+    }
+
+    return null;
+  },
+
+  render() {
+    let { responseContent, url } = this.props.request;
+
+    if (!responseContent || typeof responseContent.content.text !== "string") {
+      return null;
+    }
+
+    let { encoding, mimeType, text } = responseContent.content;
+
+    if (mimeType.includes("image/")) {
+      let { width, height } = this.state.imageDimensions;
+
+      return (
+        div({ className: "panel-container response-image-box devtools-monospace" },
+          img({
+            className: "response-image",
+            src: formDataURI(mimeType, encoding, text),
+            onLoad: this.updateImageDimemsions,
+          }),
+          div({ className: "response-summary" },
+            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_NAME),
+            div({ className: "tabpanel-summary-value" }, getUrlBaseName(url)),
+          ),
+          div({ className: "response-summary" },
+            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_DIMENSIONS),
+            div({ className: "tabpanel-summary-value" }, `${width} × ${height}`),
+          ),
+          div({ className: "response-summary" },
+            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_MIMETYPE),
+            div({ className: "tabpanel-summary-value" }, mimeType),
+          ),
+        )
+      );
+    }
+
+    // Display Properties View
+    let { json, jsonpCallback, error } = this.isJSON(mimeType, text) || {};
+    let object = {};
+    let sectionName;
+
+    if (json) {
+      if (jsonpCallback) {
+        sectionName = L10N.getFormatStr("jsonpScopeName", jsonpCallback);
+      } else {
+        sectionName = JSON_SCOPE_NAME;
+      }
+      object[sectionName] = json;
+    } else {
+      sectionName = RESPONSE_PAYLOAD;
+
+      object[sectionName] = {
+        EDITOR_CONFIG: {
+          text,
+          mode: mimeType.replace(/;.+/, ""),
+        },
+      };
+    }
+
+    return (
+      div({ className: "panel-container" },
+        error && div({ className: "response-error-header", title: error },
+          error
+        ),
+        PropertiesView({
+          object,
+          filterPlaceHolder: JSON_FILTER_TEXT,
+          sectionNames: [sectionName],
+        }),
+      )
+    );
+  }
+});
+
+module.exports = ResponsePanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/security-panel.js
@@ -0,0 +1,160 @@
+/* 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 { L10N } = require("../utils/l10n");
+const { getUrlHost } = require("../utils/request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./properties-view"));
+
+const { div, input, span } = DOM;
+
+/*
+ * Security panel component
+ * If the site is being served over HTTPS, you get an extra tab labeled "Security".
+ * This contains details about the secure connection used including the protocol,
+ * the cipher suite, and certificate details
+ */
+function SecurityPanel({ request }) {
+  const { securityInfo, url } = request;
+
+  if (!securityInfo || !url) {
+    return null;
+  }
+
+  const notAvailable = L10N.getStr("netmonitor.security.notAvailable");
+  let object;
+
+  if (securityInfo.state === "secure" || securityInfo.state === "weak") {
+    const { subject, issuer, validity, fingerprint } = securityInfo.cert;
+    const enabledLabel = L10N.getStr("netmonitor.security.enabled");
+    const disabledLabel = L10N.getStr("netmonitor.security.disabled");
+
+    object = {
+      [L10N.getStr("netmonitor.security.connection")]: {
+        [L10N.getStr("netmonitor.security.protocolVersion")]:
+          securityInfo.protocolVersion || notAvailable,
+        [L10N.getStr("netmonitor.security.cipherSuite")]:
+          securityInfo.cipherSuite || notAvailable,
+      },
+      [L10N.getFormatStr("netmonitor.security.hostHeader", getUrlHost(url))]: {
+        [L10N.getStr("netmonitor.security.hsts")]:
+          securityInfo.hsts ? enabledLabel : disabledLabel,
+        [L10N.getStr("netmonitor.security.hpkp")]:
+          securityInfo.hpkp ? enabledLabel : disabledLabel,
+      },
+      [L10N.getStr("netmonitor.security.certificate")]: {
+        [L10N.getStr("certmgr.subjectinfo.label")]: {
+          [L10N.getStr("certmgr.certdetail.cn")]:
+            subject.commonName || notAvailable,
+          [L10N.getStr("certmgr.certdetail.o")]:
+            subject.organization || notAvailable,
+          [L10N.getStr("certmgr.certdetail.ou")]:
+            subject.organizationUnit || notAvailable,
+        },
+        [L10N.getStr("certmgr.issuerinfo.label")]: {
+          [L10N.getStr("certmgr.certdetail.cn")]:
+            issuer.commonName || notAvailable,
+          [L10N.getStr("certmgr.certdetail.o")]:
+            issuer.organization || notAvailable,
+          [L10N.getStr("certmgr.certdetail.ou")]:
+            issuer.organizationUnit || notAvailable,
+        },
+        [L10N.getStr("certmgr.periodofvalidity.label")]: {
+          [L10N.getStr("certmgr.begins")]:
+            validity.start || notAvailable,
+          [L10N.getStr("certmgr.expires")]:
+            validity.end || notAvailable,
+        },
+        [L10N.getStr("certmgr.fingerprints.label")]: {
+          [L10N.getStr("certmgr.certdetail.sha256fingerprint")]:
+            fingerprint.sha256 || notAvailable,
+          [L10N.getStr("certmgr.certdetail.sha1fingerprint")]:
+            fingerprint.sha1 || notAvailable,
+        },
+      },
+    };
+  } else {
+    object = {
+      [L10N.getStr("netmonitor.security.error")]:
+        new DOMParser().parseFromString(securityInfo.errorMessage, "text/html")
+          .body.textContent || notAvailable
+    };
+  }
+
+  return div({ className: "panel-container security-panel" },
+    PropertiesView({
+      object,
+      renderValue: (props) => renderValue(props, securityInfo.weaknessReasons),
+      enableFilter: false,
+      expandedNodes: getExpandedNodes(object),
+    })
+  );
+}
+
+SecurityPanel.displayName = "SecurityPanel";
+
+SecurityPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+function renderValue(props, weaknessReasons = []) {
+  const { member, value } = props;
+
+  // Hide object summary
+  if (typeof member.value === "object") {
+    return null;
+  }
+
+  return span({ className: "security-info-value" },
+    member.name === L10N.getStr("netmonitor.security.error") ?
+      // Display multiline text for security error
+      value
+      :
+      // Display one line selectable text for security details
+      input({
+        className: "textbox-input",
+        readOnly: "true",
+        value,
+      })
+    ,
+    weaknessReasons.indexOf("cipher") !== -1 &&
+    member.name === L10N.getStr("netmonitor.security.cipherSuite") ?
+      // Display an extra warning icon after the cipher suite
+      div({
+        id: "security-warning-cipher",
+        className: "security-warning-icon",
+        title: L10N.getStr("netmonitor.security.warning.cipher"),
+      })
+      :
+      null
+  );
+}
+
+function getExpandedNodes(object, path = "", level = 0) {
+  if (typeof object !== "object") {
+    return null;
+  }
+
+  let expandedNodes = new Set();
+  for (let prop in object) {
+    let nodePath = path + "/" + prop;
+    expandedNodes.add(nodePath);
+
+    let nodes = getExpandedNodes(object[prop], nodePath, level + 1);
+    if (nodes) {
+      expandedNodes = new Set([...expandedNodes, ...nodes]);
+    }
+  }
+  return expandedNodes;
+}
+
+module.exports = SecurityPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/statistics-panel.js
@@ -0,0 +1,276 @@
+/* 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 {
+  createClass,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { Chart } = require("devtools/client/shared/widgets/Chart");
+const { PluralForm } = require("devtools/shared/plural-form");
+const Actions = require("../actions/index");
+const { Filters } = require("../utils/filter-predicates");
+const {
+  getSizeWithDecimals,
+  getTimeWithDecimals
+} = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+
+const { button, div } = DOM;
+const MediaQueryList = window.matchMedia("(min-width: 700px)");
+
+const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
+const BACK_BUTTON = L10N.getStr("netmonitor.backButton");
+const CHARTS_CACHE_ENABLED = L10N.getStr("charts.cacheEnabled");
+const CHARTS_CACHE_DISABLED = L10N.getStr("charts.cacheDisabled");
+
+/*
+ * Statistics panel component
+ * Performance analysis tool which shows you how long the browser takes to
+ * download the different parts of your site.
+ */
+const StatisticsPanel = createClass({
+  displayName: "StatisticsPanel",
+
+  propTypes: {
+    closeStatistics: PropTypes.func.isRequired,
+    enableRequestFilterTypeOnly: PropTypes.func.isRequired,
+    requests: PropTypes.object,
+  },
+
+  getInitialState() {
+    return {
+      isVerticalSpliter: MediaQueryList.matches,
+    };
+  },
+
+  componentDidUpdate(prevProps) {
+    MediaQueryList.addListener(this.onLayoutChange);
+
+    const { requests } = this.props;
+    let ready = requests && !requests.isEmpty() && requests.every((req) =>
+      req.contentSize !== undefined && req.mimeType && req.responseHeaders &&
+      req.status !== undefined && req.totalTime !== undefined
+    );
+
+    this.createChart({
+      id: "primedCacheChart",
+      title: CHARTS_CACHE_ENABLED,
+      data: ready ? this.sanitizeChartDataSource(requests, false) : null,
+    });
+
+    this.createChart({
+      id: "emptyCacheChart",
+      title: CHARTS_CACHE_DISABLED,
+      data: ready ? this.sanitizeChartDataSource(requests, true) : null,
+    });
+  },
+
+  componentWillUnmount() {
+    MediaQueryList.removeListener(this.onLayoutChange);
+  },
+
+  createChart({ id, title, data }) {
+    // Create a new chart.
+    let chart = Chart.PieTable(document, {
+      diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
+      title,
+      header: {
+        cached: "",
+        count: "",
+        label: L10N.getStr("charts.type"),
+        size: L10N.getStr("charts.size"),
+        transferredSize: L10N.getStr("charts.transferred"),
+        time: L10N.getStr("charts.time"),
+      },
+      data,
+      strings: {
+        size: (value) =>
+          L10N.getFormatStr("charts.sizeKB", getSizeWithDecimals(value / 1024)),
+        transferredSize: (value) =>
+          L10N.getFormatStr("charts.transferredSizeKB",
+            getSizeWithDecimals(value / 1024)),
+        time: (value) =>
+          L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
+      },
+      totals: {
+        cached: (total) => L10N.getFormatStr("charts.totalCached", total),
+        count: (total) => L10N.getFormatStr("charts.totalCount", total),
+        size: (total) =>
+          L10N.getFormatStr("charts.totalSize", getSizeWithDecimals(total / 1024)),
+        transferredSize: total =>
+          L10N.getFormatStr("charts.totalTransferredSize",
+            getSizeWithDecimals(total / 1024)),
+        time: (total) => {
+          let seconds = total / 1000;
+          let string = getTimeWithDecimals(seconds);
+          return PluralForm.get(seconds,
+            L10N.getStr("charts.totalSeconds")).replace("#1", string);
+        },
+      },
+      sorted: true,
+    });
+
+    chart.on("click", (_, { label }) => {
+      // Reset FilterButtons and enable one filter exclusively
+      this.props.closeStatistics();
+      this.props.enableRequestFilterTypeOnly(label);
+    });
+
+    let container = this.refs[id];
+
+    // Nuke all existing charts of the specified type.
+    while (container.hasChildNodes()) {
+      container.firstChild.remove();
+    }
+
+    container.appendChild(chart.node);
+  },
+
+  sanitizeChartDataSource(requests, emptyCache) {
+    const data = [
+      "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
+    ].map((type) => ({
+      cached: 0,
+      count: 0,
+      label: type,
+      size: 0,
+      transferredSize: 0,
+      time: 0,
+    }));
+
+    for (let request of requests) {
+      let type;
+
+      if (Filters.html(request)) {
+        // "html"
+        type = 0;
+      } else if (Filters.css(request)) {
+        // "css"
+        type = 1;
+      } else if (Filters.js(request)) {
+        // "js"
+        type = 2;
+      } else if (Filters.fonts(request)) {
+        // "fonts"
+        type = 4;
+      } else if (Filters.images(request)) {
+        // "images"
+        type = 5;
+      } else if (Filters.media(request)) {
+        // "media"
+        type = 6;
+      } else if (Filters.flash(request)) {
+        // "flash"
+        type = 7;
+      } else if (Filters.ws(request)) {
+        // "ws"
+        type = 8;
+      } else if (Filters.xhr(request)) {
+        // Verify XHR last, to categorize other mime types in their own blobs.
+        // "xhr"
+        type = 3;
+      } else {
+        // "other"
+        type = 9;
+      }
+
+      if (emptyCache || !this.responseIsFresh(request)) {
+        data[type].time += request.totalTime || 0;
+        data[type].size += request.contentSize || 0;
+        data[type].transferredSize += request.transferredSize || 0;
+      } else {
+        data[type].cached++;
+      }
+      data[type].count++;
+    }
+
+    return data.filter(e => e.count > 0);
+  },
+
+  /**
+   * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
+   * "HTTP/1.1: Caching in HTTP" spec holds true for a coll