Bug 1256757 - Use shared tree component for JSON Viewer. r=jryans
authorJan Odvarko <odvarko@gmail.com>
Mon, 21 Mar 2016 13:45:06 +0100
changeset 313370 3ddb1c19d1003e3eccc9f82f4664349e8d6b1213
parent 313369 3be9cf3907ad5bbcb0b5cde7dfca5597b4b8096e
child 313371 f450e16672e4abfa30e9b9f9b764d8d9ca7f7bcc
push id9480
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 17:12:58 +0000
treeherdermozilla-aurora@0d6a91c76a9e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1256757
milestone48.0a1
Bug 1256757 - Use shared tree component for JSON Viewer. r=jryans
devtools/client/jsonview/components/headers-panel.js
devtools/client/jsonview/components/headers.js
devtools/client/jsonview/components/json-panel.js
devtools/client/jsonview/components/main-tabbed-area.js
devtools/client/jsonview/components/reps/moz.build
devtools/client/jsonview/components/reps/tree-view.js
devtools/client/jsonview/components/search-box.js
devtools/client/jsonview/components/text-panel.js
devtools/client/jsonview/css/dom-tree.css
devtools/client/jsonview/css/main.css
devtools/client/jsonview/css/moz.build
devtools/client/jsonview/json-viewer.js
devtools/client/shared/components/reps/reps.css
devtools/client/shared/components/tree/tree-cell.js
--- a/devtools/client/jsonview/components/headers-panel.js
+++ b/devtools/client/jsonview/components/headers-panel.js
@@ -2,62 +2,62 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 define(function(require, exports, module) {
-  const React = require("devtools/client/shared/vendor/react");
+  const { DOM: dom, createFactory, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
   const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
   const { Headers } = createFactories(require("./headers"));
   const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
 
-  const DOM = React.DOM;
+  const { div } = dom;
 
   /**
    * This template represents the 'Headers' panel
    * s responsible for rendering its content.
    */
-  let HeadersPanel = React.createClass({
+  let HeadersPanel = createClass({
     propTypes: {
-      actions: React.PropTypes.object,
-      data: React.PropTypes.object,
+      actions: PropTypes.object,
+      data: PropTypes.object,
     },
 
     displayName: "HeadersPanel",
 
     getInitialState: function() {
       return {
         data: {}
       };
     },
 
     render: function() {
       let data = this.props.data;
 
       return (
-        DOM.div({className: "headersPanelBox"},
+        div({className: "headersPanelBox"},
           HeadersToolbar({actions: this.props.actions}),
-          DOM.div({className: "panelContent"},
+          div({className: "panelContent"},
             Headers({data: data})
           )
         )
       );
     }
   });
 
   /**
    * This template is responsible for rendering a toolbar
    * within the 'Headers' panel.
    */
-  let HeadersToolbar = React.createFactory(React.createClass({
+  let HeadersToolbar = createFactory(createClass({
     propTypes: {
-      actions: React.PropTypes.object,
+      actions: PropTypes.object,
     },
 
     displayName: "HeadersToolbar",
 
     // Commands
 
     onCopy: function(event) {
       this.props.actions.onCopyHeaders();
--- a/devtools/client/jsonview/components/headers.js
+++ b/devtools/client/jsonview/components/headers.js
@@ -2,73 +2,71 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 define(function(require, exports, module) {
-  const React = require("devtools/client/shared/vendor/react");
+  const { DOM: dom, createFactory, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
 
-  // Constants
-  const DOM = React.DOM;
-  const PropTypes = React.PropTypes;
+  const { div, span, table, tbody, tr, td, code } = dom;
 
   /**
    * This template is responsible for rendering basic layout
    * of the 'Headers' panel. It displays HTTP headers groups such as
    * received or response headers.
    */
-  let Headers = React.createClass({
+  let Headers = createClass({
     propTypes: {
       data: PropTypes.object,
     },
 
     displayName: "Headers",
 
     getInitialState: function() {
       return {};
     },
 
     render: function() {
       let data = this.props.data;
 
       return (
-        DOM.div({className: "netInfoHeadersTable"},
-          DOM.div({className: "netHeadersGroup"},
-            DOM.div({className: "netInfoHeadersGroup"},
-              DOM.span({className: "netHeader twisty"},
+        div({className: "netInfoHeadersTable"},
+          div({className: "netHeadersGroup"},
+            div({className: "netInfoHeadersGroup"},
+              span({className: "netHeader twisty"},
                 Locale.$STR("jsonViewer.responseHeaders")
               )
             ),
-            DOM.table({cellPadding: 0, cellSpacing: 0},
+            table({cellPadding: 0, cellSpacing: 0},
               HeaderList({headers: data.response})
             )
           ),
-          DOM.div({className: "netHeadersGroup"},
-            DOM.div({className: "netInfoHeadersGroup"},
-              DOM.span({className: "netHeader twisty"},
+          div({className: "netHeadersGroup"},
+            div({className: "netInfoHeadersGroup"},
+              span({className: "netHeader twisty"},
                 Locale.$STR("jsonViewer.requestHeaders")
               )
             ),
-            DOM.table({cellPadding: 0, cellSpacing: 0},
+            table({cellPadding: 0, cellSpacing: 0},
               HeaderList({headers: data.request})
             )
           )
         )
       );
     }
   });
 
   /**
    * This template renders headers list,
    * name + value pairs.
    */
-  let HeaderList = React.createFactory(React.createClass({
+  let HeaderList = createFactory(createClass({
     propTypes: {
       headers: PropTypes.arrayOf(PropTypes.shape({
         name: PropTypes.string,
         value: PropTypes.string
       }))
     },
 
     displayName: "HeaderList",
@@ -84,29 +82,29 @@ define(function(require, exports, module
 
       headers.sort(function(a, b) {
         return a.name > b.name ? 1 : -1;
       });
 
       let rows = [];
       headers.forEach(header => {
         rows.push(
-          DOM.tr({key: header.name},
-            DOM.td({className: "netInfoParamName"},
-              DOM.span({title: header.name}, header.name)
+          tr({key: header.name},
+            td({className: "netInfoParamName"},
+              span({title: header.name}, header.name)
             ),
-            DOM.td({className: "netInfoParamValue"},
-              DOM.code({}, header.value)
+            td({className: "netInfoParamValue"},
+              code({}, header.value)
             )
           )
         );
       });
 
       return (
-        DOM.tbody({},
+        tbody({},
           rows
         )
       );
     }
   }));
 
   // Exports from this module
   exports.Headers = Headers;
--- a/devtools/client/jsonview/components/json-panel.js
+++ b/devtools/client/jsonview/components/json-panel.js
@@ -2,37 +2,39 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 define(function(require, exports, module) {
-  const React = require("devtools/client/shared/vendor/react");
+  const { DOM: dom, createFactory, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
   const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
-  const { TreeView } = createFactories(require("./reps/tree-view"));
+  const TreeView = createFactory(require("devtools/client/shared/components/tree/tree-view"));
+  const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
   const { SearchBox } = createFactories(require("./search-box"));
   const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
-  const DOM = React.DOM;
+
+  const { div } = dom;
 
   /**
    * This template represents the 'JSON' panel. The panel is
    * responsible for rendering an expandable tree that allows simple
    * inspection of JSON structure.
    */
-  let JsonPanel = React.createClass({
+  let JsonPanel = createClass({
     propTypes: {
-      data: React.PropTypes.oneOfType([
-        React.PropTypes.string,
-        React.PropTypes.array,
-        React.PropTypes.object
+      data: PropTypes.oneOfType([
+        PropTypes.string,
+        PropTypes.array,
+        PropTypes.object
       ]),
-      searchFilter: React.PropTypes.string,
-      actions: React.PropTypes.object,
+      searchFilter: PropTypes.string,
+      actions: PropTypes.object,
     },
 
     displayName: "JsonPanel",
 
     getInitialState: function() {
       return {};
     },
 
@@ -43,55 +45,76 @@ define(function(require, exports, module
     componentWillUnmount: function() {
       document.removeEventListener("keypress", this.onKeyPress, true);
     },
 
     onKeyPress: function(e) {
       // XXX shortcut for focusing the Filter field (see Bug 1178771).
     },
 
+    onFilter: function(object) {
+      if (!this.props.searchFilter) {
+        return true;
+      }
+
+      let json = JSON.stringify(object).toLowerCase();
+      return json.indexOf(this.props.searchFilter) >= 0;
+    },
+
     render: function() {
       let content;
       let data = this.props.data;
 
+      // Append custom column for displaying values. This column
+      // Take all available horizontal space.
+      let columns = [{
+        id: "value",
+        width: "100%"
+      }];
+
       try {
         if (typeof data == "object") {
+          // Render tree component. Use Reps to render JSON values.
           content = TreeView({
-            data: this.props.data,
+            object: this.props.data,
             mode: "tiny",
-            searchFilter: this.props.searchFilter
+            onFilter: this.onFilter.bind(this),
+            columns: columns,
+            renderValue: props => {
+              return Rep(props);
+            }
           });
         } else {
-          content = DOM.div({className: "jsonParseError"},
+          content = div({className: "jsonParseError"},
             data + ""
           );
         }
       } catch (err) {
-        content = DOM.div({className: "jsonParseError"},
+        content = div({className: "jsonParseError"},
           err + ""
         );
       }
 
       return (
-        DOM.div({className: "jsonPanelBox"},
+        div({className: "jsonPanelBox"},
           JsonToolbar({actions: this.props.actions}),
-          DOM.div({className: "panelContent"},
+          div({className: "panelContent"},
             content
           )
         )
       );
     }
   });
 
   /**
    * This template represents a toolbar within the 'JSON' panel.
    */
-  let JsonToolbar = React.createFactory(React.createClass({
+  let JsonToolbar = createFactory(createClass({
     propTypes: {
-      actions: React.PropTypes.object,
+      actions: PropTypes.object,
     },
 
     displayName: "JsonToolbar",
 
     // Commands
 
     onSave: function(event) {
       this.props.actions.onSaveJson();
--- a/devtools/client/jsonview/components/main-tabbed-area.js
+++ b/devtools/client/jsonview/components/main-tabbed-area.js
@@ -2,38 +2,38 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 define(function(require, exports, module) {
-  const React = require("devtools/client/shared/vendor/react");
+  const { createClass, PropTypes } = require("devtools/client/shared/vendor/react");
   const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
   const { JsonPanel } = createFactories(require("./json-panel"));
   const { TextPanel } = createFactories(require("./text-panel"));
   const { HeadersPanel } = createFactories(require("./headers-panel"));
   const { Tabs, TabPanel } = createFactories(require("./reps/tabs"));
 
   /**
    * This object represents the root application template
    * responsible for rendering the basic tab layout.
    */
-  let MainTabbedArea = React.createClass({
+  let MainTabbedArea = createClass({
     propTypes: {
-      jsonText: React.PropTypes.string,
-      tabActive: React.PropTypes.number,
-      actions: React.PropTypes.object,
-      headers: React.PropTypes.object,
-      searchFilter: React.PropTypes.string,
-      json: React.PropTypes.oneOfType([
-        React.PropTypes.string,
-        React.PropTypes.object,
-        React.PropTypes.array
+      jsonText: PropTypes.string,
+      tabActive: PropTypes.number,
+      actions: PropTypes.object,
+      headers: PropTypes.object,
+      searchFilter: PropTypes.string,
+      json: PropTypes.oneOfType([
+        PropTypes.string,
+        PropTypes.object,
+        PropTypes.array
       ])
     },
 
     displayName: "MainTabbedArea",
 
     getInitialState: function() {
       return {
         json: {},
--- a/devtools/client/jsonview/components/reps/moz.build
+++ b/devtools/client/jsonview/components/reps/moz.build
@@ -2,10 +2,9 @@
 # vim: set filetype=python:
 # 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(
     'tabs.js',
     'toolbar.js',
-    'tree-view.js',
 )
deleted file mode 100644
--- a/devtools/client/jsonview/components/reps/tree-view.js
+++ /dev/null
@@ -1,267 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* 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";
-
-define(function(require, exports, module) {
-  // Dependencies
-  const React = require("devtools/client/shared/vendor/react");
-  const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
-  const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
-  const { StringRep } = require("devtools/client/shared/components/reps/string");
-  const DOM = React.DOM;
-
-  let uid = 0;
-
-  /**
-   * Renders a tree view with expandable/collapsible items.
-   */
-  let TreeView = React.createClass({
-    propTypes: {
-      searchFilter: React.PropTypes.string,
-      data: React.PropTypes.any,
-      mode: React.PropTypes.string,
-    },
-
-    displayName: "TreeView",
-
-    getInitialState: function() {
-      return {
-        data: {},
-        searchFilter: null
-      };
-    },
-
-    // Data
-
-    componentDidMount: function() {
-      let members = initMembers(this.props.data, 0);
-      this.setState({ // eslint-disable-line
-        data: members,
-        searchFilter:
-        this.props.searchFilter
-      });
-    },
-
-    componentWillReceiveProps: function(nextProps) {
-      let updatedState = {
-        searchFilter: nextProps.searchFilter
-      };
-
-      if (this.props.data !== nextProps.data) {
-        updatedState.data = initMembers(nextProps.data, 0);
-      }
-
-      this.setState(updatedState);
-    },
-
-    // Rendering
-
-    render: function() {
-      let mode = this.props.mode;
-      let root = this.state.data;
-
-      let children = [];
-
-      if (Array.isArray(root)) {
-        for (let i = 0; i < root.length; i++) {
-          let child = root[i];
-          children.push(TreeNode({
-            key: child.key,
-            data: child,
-            mode: mode,
-            searchFilter: this.state.searchFilter || this.props.searchFilter
-          }));
-        }
-      } else {
-        children.push(React.addons.createFragment(root));
-      }
-
-      return (
-        DOM.div({className: "domTable", cellPadding: 0, cellSpacing: 0},
-          children
-        )
-      );
-    },
-  });
-
-  /**
-   * Represents a node within the tree.
-   */
-  let TreeNode = React.createFactory(React.createClass({
-    propTypes: {
-      searchFilter: React.PropTypes.string,
-      data: React.PropTypes.object,
-      mode: React.PropTypes.string,
-    },
-
-    displayName: "TreeNode",
-
-    getInitialState: function() {
-      return {
-        data: this.props.data,
-        searchFilter: null
-      };
-    },
-
-    onClick: function(e) {
-      let member = this.state.data;
-      member.open = !member.open;
-
-      this.setState({data: member});
-
-      e.stopPropagation();
-    },
-
-    render: function() {
-      let member = this.state.data;
-      let mode = this.props.mode;
-
-      let classNames = ["memberRow"];
-      classNames.push(member.type + "Row");
-
-      if (member.hasChildren) {
-        classNames.push("hasChildren");
-      }
-
-      if (member.open) {
-        classNames.push("opened");
-      }
-
-      if (!member.children) {
-        // Cropped strings are expandable, but they don't have children.
-        let isString = typeof (member.value) == "string";
-        if (member.hasChildren && !isString) {
-          member.children = initMembers(member.value);
-        } else {
-          member.children = [];
-        }
-      }
-
-      let children = [];
-      if (member.open && member.children.length) {
-        for (let i in member.children) {
-          let child = member.children[i];
-          children.push(TreeNode({
-            key: child.key,
-            data: child,
-            mode: mode,
-            searchFilter: this.state.searchFilter || this.props.searchFilter
-          }));
-        }
-      }
-
-      let filter = this.props.searchFilter || "";
-      let name = member.name || "";
-      let value = member.value || "";
-
-      // Filtering is case-insensitive
-      filter = filter.toLowerCase();
-      name = name.toLowerCase();
-
-      if (filter && (name.indexOf(filter) < 0)) {
-        // Cache the stringify result, so the filtering is fast
-        // the next time.
-        if (!member.valueString) {
-          member.valueString = JSON.stringify(value).toLowerCase();
-        }
-
-        if (member.valueString && member.valueString.indexOf(filter) < 0) {
-          classNames.push("hidden");
-        }
-      }
-
-      return (
-        DOM.div({className: classNames.join(" ")},
-          DOM.span({className: "memberLabelCell", onClick: this.onClick},
-            DOM.span({className: "memberIcon"}),
-            DOM.span({className: "memberLabel " + member.type + "Label"},
-              member.name)
-          ),
-          DOM.span({className: "memberValueCell"},
-            DOM.span({},
-              Rep({
-                object: member.value,
-                mode: this.props.mode,
-                member: member
-              })
-            )
-          ),
-          DOM.div({className: "memberChildren"},
-            children
-          )
-        )
-      );
-    },
-  }));
-
-  // Helpers
-
-  function initMembers(parent) {
-    let members = getMembers(parent);
-    return members;
-  }
-
-  function getMembers(object) {
-    let members = [];
-    getObjectProperties(object, function(prop, value) {
-      let valueType = typeof (value);
-      let hasChildren = (valueType === "object" && hasProperties(value));
-
-      // Cropped strings are expandable, so the user can see the
-      // entire original value.
-      if (StringRep.isCropped(value)) {
-        hasChildren = true;
-      }
-
-      let type = getType(value);
-      let member = createMember(type, prop, value, hasChildren);
-      members.push(member);
-    });
-
-    return members;
-  }
-
-  function createMember(type, name, value, hasChildren) {
-    let member = {
-      name: name,
-      type: type,
-      rowClass: "memberRow-" + type,
-      hasChildren: hasChildren,
-      value: value,
-      open: false,
-      key: uid++
-    };
-
-    return member;
-  }
-
-  function getObjectProperties(obj, callback) {
-    for (let p in obj) {
-      try {
-        callback.call(this, p, obj[p]);
-      } catch (e) {
-        // Ignore
-      }
-    }
-  }
-
-  function hasProperties(obj) {
-    if (typeof (obj) == "string") {
-      return false;
-    }
-
-    return Object.keys(obj).length > 1;
-  }
-
-  function getType(object) {
-    // A type provider (or a decorator) should be used here.
-    return "dom";
-  }
-
-  // Exports from this module
-  exports.TreeView = TreeView;
-});
--- a/devtools/client/jsonview/components/search-box.js
+++ b/devtools/client/jsonview/components/search-box.js
@@ -2,30 +2,30 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 define(function(require, exports, module) {
-  const React = require("devtools/client/shared/vendor/react");
+  const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
 
-  const DOM = React.DOM;
+  const { input } = dom;
 
   // For smooth incremental searching (in case the user is typing quickly).
   const searchDelay = 250;
 
   /**
    * This object represents a search box located at the
    * top right corner of the application.
    */
-  let SearchBox = React.createClass({
+  let SearchBox = createClass({
     propTypes: {
-      actions: React.PropTypes.object,
+      actions: PropTypes.object,
     },
 
     displayName: "SearchBox",
 
     onSearch: function(event) {
       let searchBox = event.target;
       let win = searchBox.ownerDocument.defaultView;
 
@@ -38,17 +38,17 @@ define(function(require, exports, module
     },
 
     doSearch: function(searchBox) {
       this.props.actions.onSearch(searchBox.value);
     },
 
     render: function() {
       return (
-        DOM.input({className: "searchBox",
+        input({className: "searchBox",
           placeholder: Locale.$STR("jsonViewer.filterJSON"),
           onChange: this.onSearch})
       );
     },
   });
 
   // Exports from this module
   exports.SearchBox = SearchBox;
--- a/devtools/client/jsonview/components/text-panel.js
+++ b/devtools/client/jsonview/components/text-panel.js
@@ -2,58 +2,58 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 define(function(require, exports, module) {
-  const React = require("devtools/client/shared/vendor/react");
+  const { DOM: dom, createFactory, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
   const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
   const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
-  const DOM = React.DOM;
+  const { div, pre } = dom;
 
   /**
    * This template represents the 'Raw Data' panel displaying
    * JSON as a text received from the server.
    */
-  let TextPanel = React.createClass({
+  let TextPanel = createClass({
     propTypes: {
-      actions: React.PropTypes.object,
-      data: React.PropTypes.string
+      actions: PropTypes.object,
+      data: PropTypes.string
     },
 
     displayName: "TextPanel",
 
     getInitialState: function() {
       return {};
     },
 
     render: function() {
       return (
-        DOM.div({className: "textPanelBox"},
+        div({className: "textPanelBox"},
           TextToolbar({actions: this.props.actions}),
-          DOM.div({className: "panelContent"},
-            DOM.pre({className: "data"},
+          div({className: "panelContent"},
+            pre({className: "data"},
               this.props.data
             )
           )
         )
       );
     }
   });
 
   /**
    * This object represents a toolbar displayed within the
    * 'Raw Data' panel.
    */
-  let TextToolbar = React.createFactory(React.createClass({
+  let TextToolbar = createFactory(createClass({
     propTypes: {
-      actions: React.PropTypes.object,
+      actions: PropTypes.object,
     },
 
     displayName: "TextToolbar",
 
     // Commands
 
     onPrettify: function(event) {
       this.props.actions.onPrettify();
deleted file mode 100644
--- a/devtools/client/jsonview/css/dom-tree.css
+++ /dev/null
@@ -1,216 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* 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/. */
-
-.domTable {
-  font-size: 11px;
-  font-family: Lucida Grande, Tahoma, sans-serif;
-  line-height: 15px;
-  width: 100%;
-}
-
-.domTable > tbody > tr > td {
-  border-bottom: 1px solid #EFEFEF;
-}
-
-.hidden {
-  display: none;
-}
-
-.memberLabelCell {
-  padding: 2px 0 2px 0px;
-}
-
-.memberValueCell {
-  padding: 2px 0 2px 5px;
-  overflow: hidden;
-}
-
-.memberLabel {
-  cursor: default;
-  overflow: hidden;
-  white-space: nowrap;
-}
-
-.memberLabelPrefix {
-  color: gray;
-  margin-right: 3px;
-  font-weight: normal;
-}
-
-.memberValueIcon > div {
-  width: 15px;
-}
-
-/******************************************************************************/
-/* Read Only Properties */
-
-.memberValueCell.readOnly {
-  opacity: 0.5;
-}
-
-.memberValueIcon.readOnly {
-  background: url("read-only-prop.svg") no-repeat;
-  background-position: 4px 4px;
-  background-size: 10px 10px;
-}
-
-/******************************************************************************/
-
-.memberRow.hasChildren > .memberLabelCell > .memberIcon:hover,
-.memberRow.cropped > .memberLabelCell > .memberIcon:hover {
-  cursor: pointer;
-}
-
-.memberRow.hasChildren > .memberLabelCell > .memberLabel:hover,
-.memberRow.cropped > .memberLabelCell > .memberLabel:hover {
-  cursor: pointer;
-  color: blue;
-  text-decoration: underline;
-}
-
-.memberRow:hover {
-  background-color: #EFEFEF;
-}
-
-.memberRow {
-  padding: 3px 0 3px 0;
-}
-
-.panelNode-dom .memberRow td,
-.panelNode-domSide .memberRow td {
-  border-bottom: 1px solid #EFEFEF;
-}
-
-/******************************************************************************/
-
-.userLabel,
-.userClassLabel,
-.userFunctionLabel {
-  font-weight: bold;
-}
-
-.userLabel {
-  color: #000000;
-}
-
-.userClassLabel {
-  color: #E90000;
-}
-
-.userFunctionLabel {
-  color: #025E2A;
-}
-
-.domLabel {
-  color: #000000;
-}
-
-.domClassLabel {
-  color: #E90000;
-}
-
-.domFunctionLabel {
-  color: #025E2A;
-}
-
-.ordinalLabel {
-  color: SlateBlue;
-  font-weight: bold;
-}
-
-/******************************************************************************/
-/* Twisties */
-
-.memberRow > .memberLabelCell > .memberIcon {
-  height: 14px;
-  width: 14px;
-  display: inline-block;
-  line-height: 15px;
-  vertical-align: bottom;
-  padding-right: 2px;
-  margin-left: 3px;
-}
-
-.memberRow.hasChildren > .memberLabelCell > .memberIcon,
-.memberRow.cropped > .memberLabelCell > .memberIcon {
-  background-image: url("./twisty-closed.svg");
-  background-repeat: no-repeat;
-}
-
-.memberRow.hasChildren.opened > .memberLabelCell > .memberIcon,
-.memberRow.cropped.opened > .memberLabelCell > .memberIcon {
-  background-image: url("./twisty-open.svg");
-  background-repeat: no-repeat;
-}
-
-@media (min-resolution: 1.1dppx) {
-.memberRow.hasChildren > .memberLabelCell > .memberIcon,
-.memberRow.cropped > .memberLabelCell > .memberIcon {
-  background-image: url("./controls@2x.png");
-}
-
-.memberRow.hasChildren.opened > .memberLabelCell > .memberIcon,
-.memberRow.cropped.opened > .memberLabelCell > .memberIcon {
-  background-image: url("./controls@2x.png");
-}
-}
-
-/******************************************************************************/
-/* Layout support */
-
-.memberChildren {
-  padding-left: 16px;
-}
-
-.memberLabelCell,
-.memberValueCell {
-}
-
-.memberLabelCell {
-  min-width: 30px;
-}
-
-.memberRow:hover {
-  background-color: transparent !important;
-}
-
-/******************************************************************************/
-/* Themes */
-
-.theme-light .memberRow.hasChildren > .memberLabelCell > .memberIcon,
-.theme-light .memberRow.cropped > .memberLabelCell > .memberIcon {
-  background-image: url("./controls.png");
-  background-size: 56px 28px;
-  background-repeat: no-repeat;
-  background-position: 0 -14px;
-}
-
-.theme-light .memberRow.hasChildren.opened > .memberLabelCell > .memberIcon,
-.theme-light .memberRow.cropped.opened > .memberLabelCell > .memberIcon {
-  background-image: url("./controls.png");
-  background-size: 56px 28px;
-  background-repeat: no-repeat;
-  background-position: -14px -14px;
-}
-
-.theme-dark .memberRow.hasChildren > .memberLabelCell > .memberIcon,
-.theme-dark .memberRow.cropped > .memberLabelCell > .memberIcon {
-  background-image: url("./controls.png");
-  background-size: 56px 28px;
-  background-repeat: no-repeat;
-  background-position: -28px -14px;
-}
-
-.theme-dark .memberRow.hasChildren.opened > .memberLabelCell > .memberIcon,
-.theme-dark .memberRow.cropped.opened > .memberLabelCell > .memberIcon {
-  background-image: url("./controls.png");
-  background-size: 56px 28px;
-  background-repeat: no-repeat;
-  background-position: -42px -14px;
-}
-
-.theme-dark .memberRow:hover {
-  background-color: var(--theme-selection-background-semitransparent);
-}
--- a/devtools/client/jsonview/css/main.css
+++ b/devtools/client/jsonview/css/main.css
@@ -1,30 +1,42 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 @import "resource://devtools/client/shared/components/reps/reps.css";
+@import "resource://devtools/client/shared/components/tree/tree-view.css";
 
 @import "general.css";
-@import "dom-tree.css";
 @import "search-box.css";
 @import "tabs.css";
 @import "toolbar.css";
 @import "json-panel.css";
 @import "text-panel.css";
 @import "headers-panel.css";
 
 
 /******************************************************************************/
 /* Panel Content */
 
 .panelContent {
   overflow-y: auto;
+  font-size: 11px;
+  font-family: monospace;
+}
+
+/* The tree takes the entire horizontal space within the panel content. */
+.panelContent .treeTable {
+  width: 100%;
+}
+
+/* Make sure there is a little space between label and value columns. */
+.panelContent .treeTable .treeLabelCell {
+  padding-right: 17px;
 }
 
 /******************************************************************************/
 /* Theme Firebug */
 
 .theme-firebug .panelContent {
   height: calc(100% - 30px);
 }
--- a/devtools/client/jsonview/css/moz.build
+++ b/devtools/client/jsonview/css/moz.build
@@ -3,17 +3,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/.
 
 
 DevToolsModules(
     'controls.png',
     'controls@2x.png',
-    'dom-tree.css',
     'general.css',
     'headers-panel.css',
     'json-panel.css',
     'main.css',
     'read-only-prop.svg',
     'search-box.css',
     'search.svg',
     'tabs.css',
--- a/devtools/client/jsonview/json-viewer.js
+++ b/devtools/client/jsonview/json-viewer.js
@@ -3,18 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* global postChromeMessage */
 
 "use strict";
 
 define(function(require, exports, module) {
-  // ReactJS
-  const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+  const { render } = require("devtools/client/shared/vendor/react-dom");
   const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
   const { MainTabbedArea } = createFactories(require("./components/main-tabbed-area"));
 
   const json = document.getElementById("json");
   const headers = document.getElementById("headers");
 
   let jsonData;
 
@@ -74,17 +73,17 @@ define(function(require, exports, module
     },
   };
 
   /**
    * Render the main application component. It's the main tab bar displayed
    * at the top of the window. This component also represents ReacJS root.
    */
   let content = document.getElementById("content");
-  let theApp = ReactDOM.render(MainTabbedArea(input), content);
+  let theApp = render(MainTabbedArea(input), content);
 
   let onResize = event => {
     window.document.body.style.height = window.innerHeight + "px";
     window.document.body.style.width = window.innerWidth + "px";
   };
 
   window.addEventListener("resize", onResize);
   onResize();
--- a/devtools/client/shared/components/reps/reps.css
+++ b/devtools/client/shared/components/reps/reps.css
@@ -11,36 +11,23 @@
 /******************************************************************************/
 
 .inline {
   display: inline;
   white-space: normal;
 }
 
 .objectBox-object {
-  font-family: Lucida Grande, sans-serif;
   font-weight: bold;
   color: DarkGreen;
   white-space: pre-wrap;
 }
 
 .objectBox-string,
 .objectBox-text,
-.objectBox-number,
-.objectLink-element,
-.objectLink-textNode,
-.objectLink-function,
-.objectBox-stackTrace,
-.objectLink-profile,
-.objectBox-table {
-  font-family: monospace;
-}
-
-.objectBox-string,
-.objectBox-text,
 .objectLink-textNode,
 .objectBox-table {
   white-space: pre-wrap;
 }
 
 .objectBox-number,
 .objectLink-styleRule,
 .objectLink-element,
@@ -74,34 +61,32 @@
   color: #909090;
 }
 
 .objectLink-sourceLink {
   position: absolute;
   right: 4px;
   top: 2px;
   padding-left: 8px;
-  font-family: Lucida Grande, sans-serif;
   font-weight: bold;
   color: #0000FF;
 }
 
 .objectLink-sourceLink > .systemLink {
   float: right;
   color: #FF0000;
 }
 
 /******************************************************************************/
 
 .objectLink-event,
 .objectLink-eventLog,
 .objectLink-regexp,
 .objectLink-object,
 .objectLink-Date {
-  font-family: Lucida Grande, sans-serif;
   font-weight: bold;
   color: DarkGreen;
   white-space: pre-wrap;
 }
 
 /******************************************************************************/
 
 .objectLink-object .nodeName,
@@ -117,26 +102,16 @@
 .objectLink-object .nodeName {
   font-weight: normal;
 }
 
 /******************************************************************************/
 
 .objectLeftBrace,
 .objectRightBrace,
-.objectEqual,
-.objectComma,
-.arrayLeftBracket,
-.arrayRightBracket,
-.arrayComma {
-  font-family: monospace;
-}
-
-.objectLeftBrace,
-.objectRightBrace,
 .arrayLeftBracket,
 .arrayRightBracket {
   cursor: pointer;
   font-weight: bold;
 }
 
 .objectLeftBrace,
 .arrayLeftBracket {
@@ -147,30 +122,28 @@
 .arrayRightBracket {
   margin-left: 4px;
 }
 
 /******************************************************************************/
 /* Cycle reference*/
 
 .objectLink-Reference {
-  font-family: monospace;
   font-weight: bold;
   color: rgb(102, 102, 255);
 }
 
 .objectBox-array > .objectTitle {
   font-weight: bold;
   color: DarkGreen;
 }
 
 /******************************************************************************/
 
 .caption {
-  font-family: Lucida Grande, Tahoma, sans-serif;
   font-weight: bold;
   color:  #444444;
 }
 
 /******************************************************************************/
 /* Light Theme & Dark Theme */
 
 .theme-dark .domLabel,
@@ -200,20 +173,18 @@
 
 .theme-dark .objectBox-array,
 .theme-light .objectBox-array {
   color: var(--theme-body-color);
 }
 
 .theme-dark .objectBox-object,
 .theme-light .objectBox-object {
-  font-family: Lucida Grande, sans-serif;
   font-weight: normal;
   color: var(--theme-highlight-blue);
   white-space: pre-wrap;
 }
 
 .theme-dark .caption,
 .theme-light .caption {
-  font-family: Lucida Grande, Tahoma, sans-serif;
   font-weight: normal;
   color: var(--theme-highlight-blue);
 }
--- a/devtools/client/shared/components/tree/tree-cell.js
+++ b/devtools/client/shared/components/tree/tree-cell.js
@@ -25,20 +25,22 @@ define(function(require, exports, module
       id: PropTypes.string.isRequired,
       member: PropTypes.object.isRequired,
       renderValue: PropTypes.func.isRequired
     },
 
     displayName: "TreeCell",
 
     /**
-     * Optimize cell rendering. If value is the same do not render.
+     * Optimize cell rendering. Rerender cell content only if
+     * the value or expanded state changes.
      */
     shouldComponentUpdate: function(nextProps) {
-      return (this.props.value != nextProps.value);
+      return (this.props.value != nextProps.value) ||
+        (this.props.member.open != nextProps.member.open);
     },
 
     getCellClass: function(object, id) {
       let decorator = this.props.decorator;
       if (!decorator || !decorator.getCellClass) {
         return [];
       }