Bug 1328828 - Implement Properties View r?jsnajdr,honza draft
authorRicky Chien <rchien@mozilla.com>
Thu, 05 Jan 2017 15:14:45 +0800
changeset 457430 a760dc7dbfb4610216b5883248a914bf405483df
parent 457425 4ece183563a44eab8f410818aa2c7984992dbb8d
child 457435 ebf1cc52654d54756462f075af37d5a23d088242
child 457439 bfb8b8cbf1ecddd9434d3dbe794aece88ba210bf
child 457551 63a50efe3455b30f0d23aa21bfbae8f37217858f
push id40751
push userbmo:rchien@mozilla.com
push dateSun, 08 Jan 2017 04:55:37 +0000
reviewersjsnajdr, honza
bugs1328828
milestone53.0a1
Bug 1328828 - Implement Properties View r?jsnajdr,honza MozReview-Commit-ID: EuFGC12V6BU
devtools/client/netmonitor/shared/components/editor.js
devtools/client/netmonitor/shared/components/moz.build
devtools/client/netmonitor/shared/components/properties-view.js
devtools/client/themes/netmonitor.css
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/shared/components/editor.js
@@ -0,0 +1,92 @@
+/* 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 SourceEditor = require("devtools/client/sourceeditor/editor");
+
+const { div } = DOM;
+
+/**
+ * CodeMirror editor as a React component
+ */
+const Editor = createClass({
+  displayName: "Editor",
+
+  propTypes: {
+    open: PropTypes.bool,
+    text: PropTypes.string,
+  },
+
+  getDefaultProps() {
+    return {
+      open: true,
+      text: "",
+    };
+  },
+
+  componentDidMount() {
+    const { text } = this.props;
+
+    this.editor = new SourceEditor({
+      lineNumbers: true,
+      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) {
+      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 workround should be removed in bug 1308442
+        if (this.refs.editor) {
+          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;
--- a/devtools/client/netmonitor/shared/components/moz.build
+++ b/devtools/client/netmonitor/shared/components/moz.build
@@ -1,9 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'editor.js',
     'preview-panel.js',
+    'properties-view.js',
     'security-panel.js',
     'timings-panel.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/shared/components/properties-view.js
@@ -0,0 +1,162 @@
+/* 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 { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { MODE } = require("devtools/client/shared/components/reps/constants");
+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 { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+
+const { div, tr, td } = DOM;
+
+/*
+ * Properties View component
+ * A scrollable tree view component which provides some useful features for
+ * representing object properties.
+ *
+ * Search filter - Set enableFilter to enable / disable SearchBox feature.
+ * Tree view - Default enabled.
+ * Source editor - Enable by specifying object level 1 property name to "editorText".
+ * 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: "",
+    };
+  },
+
+  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 } = props.member;
+    // Display source editor when prop name specify to editorText
+    if (level === 1 && name === "editorText") {
+      return (
+        tr({},
+          td({ colSpan: 2 },
+            Editor({ text: value })
+          )
+        )
+      );
+    }
+
+    return TreeRow(props);
+  },
+
+  renderValueWithRep(props) {
+    // Hide rep summary for sections
+    if (props.member.level === 0) {
+      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({}, props.member, { open: false }),
+      mode: MODE.TINY,
+      cropLimit: 60,
+    }));
+  },
+
+  updateFilterText(filterText) {
+    this.setState({
+      filterText,
+    });
+  },
+
+  render() {
+    const {
+      object,
+      decorator,
+      enableInput,
+      enableFilter,
+      expandableStrings,
+      filterPlaceHolder,
+      renderRow,
+      renderValue,
+      sectionNames,
+    } = this.props;
+
+    return (
+      div({ className: "properties-view" },
+        enableFilter && div({ className: "searchbox-section" },
+          SearchBox({
+            delay: FILTER_SEARCH_DELAY,
+            type: "filter",
+            onChange: this.updateFilterText,
+            placeholder: filterPlaceHolder,
+          }),
+        ),
+        div({ className: "tree-container" },
+          TreeView({
+            object,
+            columns: [{
+              id: "value",
+              width: "100%",
+            }],
+            decorator: decorator || {
+              getRowClass: (rowObject) => this.getRowClass(rowObject, sectionNames),
+            },
+            enableInput,
+            expandableStrings,
+            expandedNodes: new Set(sectionNames.map((sec) => "/" + sec)),
+            onFilter: (props) => this.onFilter(props, sectionNames),
+            renderRow: renderRow || this.renderRowWithEditor,
+            renderValue: renderValue || this.renderValueWithRep,
+          }),
+        ),
+      )
+    );
+  }
+
+});
+
+module.exports = PropertiesView;
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -1115,34 +1115,83 @@
   outline: 0;
   box-shadow: var(--theme-focus-box-shadow-textbox);
 }
 
 .treeTable .treeLabel {
   font-weight: 600;
 }
 
-/* Customize default tree table style to align with devtools theme  */
-.theme-light .treeTable .treeLabel,
-.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
-  color: var(--theme-highlight-red);
+.properties-view {
+  /* FIXME: Minus 24px * 2 for toolbox height + panel height
+   * Give a fixed panel container height in order to force tree view scrollable */
+  height: calc(100vh - 48px);
+  display: flex;
+  flex-direction: column;
+}
+
+.properties-view .searchbox-section {
+  flex: 0 1 auto;
+}
+
+.properties-view .devtools-searchbox {
+  padding: 0;
+}
+
+.properties-view .devtools-searchbox input {
+  margin: 1px 3px;
+}
+
+.tree-container {
+  position: relative;
+  height: 100%;
 }
 
-.theme-dark .treeTable .treeLabel,
-.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
-  color: var(--theme-highlight-purple);
+/* Make treeTable fill parent element and scrollable */
+.tree-container .treeTable {
+  position: absolute;
+  display: block;
+  overflow-y: auto;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.properties-view .devtools-searchbox,
+.tree-container .treeTable .tree-section {
+  width: 100%;
+  background-color: var(--theme-toolbar-background);
+}
+
+.tree-container .treeTable .tree-section > * {
+  vertical-align: middle;
 }
 
-.theme-firebug .treeTable .treeLabel {
-  color: var(--theme-body-color);
+.tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel,
+.tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel:hover {
+  color: var(--theme-body-color-alt);
+}
+
+.tree-container .treeTable .treeValueCell {
+  /* FIXME: Make value cell can be reduced to shorter width */
+  max-width: 0;
+  padding-inline-end: 5px;
 }
 
-.treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
-  cursor: default;
-  text-decoration: none;
+.tree-container .objectBox {
+  white-space: nowrap;
+}
+
+.editor-container,
+.editor-mount,
+.editor-mount iframe {
+  border: none;
+  width: 100%;
+  height: 100%;
 }
 
 /*
  * FIXME: normal html block element cannot fill outer XUL element
  * This workaround should be removed after netmonitor is migrated to react
  */
 #react-preview-tabpanel-hook,
 #react-security-tabpanel-hook,