Bug 1353319 - Render HTML preview within Response side-panel. r?Honza draft
authorBrandon Cheng <brandon.cheng@protonmail.com>
Sat, 28 Oct 2017 23:33:02 -0400
changeset 696855 feca08607a0a46bfb46fdb5d91d8bef828754d17
parent 696850 aabfc14671b55983e1c3053989a4c3b7c5691aaa
child 696856 e14abefe4e49f438993b10fbe57d955a2f159932
push id88809
push userbmo:brandon.cheng@protonmail.com
push dateSun, 12 Nov 2017 02:18:53 +0000
reviewersHonza
bugs1353319
milestone58.0a1
Bug 1353319 - Render HTML preview within Response side-panel. r?Honza Restoring the HTML preview in the response panel was requested by many users who rely on it to debug erroring AJAX requests. Many web backend frameworks display an HTML stacktrace helping users trace down the problem. The html-preview.js component was taken from a previous commit of the source code before the preview panel was removed. A few modifications with its name and CSS classname were made. MozReview-Commit-ID: JFyF6cBMaNf
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/assets/styles/NetworkDetailsPanel.css
devtools/client/netmonitor/src/components/HtmlPreview.js
devtools/client/netmonitor/src/components/PropertiesView.js
devtools/client/netmonitor/src/components/ResponsePanel.js
devtools/client/netmonitor/src/components/moz.build
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -130,16 +130,20 @@ jsonFilterText=Filter properties
 # LOCALIZATION NOTE (jsonScopeName): This is the text displayed
 # in the response tab of the network details pane for a JSON scope.
 jsonScopeName=JSON
 
 # LOCALIZATION NOTE (jsonpScopeName): This is the text displayed
 # in the response tab of the network details pane for a JSONP scope.
 jsonpScopeName=JSONP → callback %S()
 
+# LOCALIZATION NOTE (responsePreview): This is the text displayed
+# in the response tab of the network details pane for an HTML preview.
+responsePreview=Preview
+
 # LOCALIZATION NOTE (networkMenu.sortedAsc): This is the tooltip displayed
 # in the network table toolbar, for any column that is sorted ascending.
 networkMenu.sortedAsc=Sorted ascending
 
 # LOCALIZATION NOTE (networkMenu.sortedDesc): This is the tooltip displayed
 # in the network table toolbar, for any column that is sorted descending.
 networkMenu.sortedDesc=Sorted descending
 
--- a/devtools/client/netmonitor/src/assets/styles/NetworkDetailsPanel.css
+++ b/devtools/client/netmonitor/src/assets/styles/NetworkDetailsPanel.css
@@ -250,16 +250,37 @@
 .network-monitor .response-image {
   background: #fff;
   border: 1px dashed GrayText;
   margin-bottom: 10px;
   max-width: 300px;
   max-height: 100px;
 }
 
+.network-monitor .tree-container .treeTable tr.response-preview-container {
+  flex: 1;
+  min-height: 0;
+}
+
+.network-monitor .tree-container .treeTable tr.response-preview-container td {
+  display: block;
+  height: 100%;
+}
+
+.network-monitor .html-preview {
+  height: 100%;
+}
+
+.network-monitor .html-preview iframe {
+  background-color: #fff;
+  border: none;
+  height: 100%;
+  width: 100%;
+}
+
 /* Timings tabpanel */
 
 .network-monitor .timings-container {
   display: flex;
 }
 
 .network-monitor .timings-label {
   width: 10em;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/HtmlPreview.js
@@ -0,0 +1,33 @@
+/* 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;
+
+/*
+ * Response preview component
+ * Display HTML content within a sandbox enabled iframe
+ */
+function HTMLPreview({ responseContent }) {
+  const htmlBody = responseContent ? responseContent.content.text : "";
+
+  return (
+    div({ className: "html-preview" },
+      iframe({
+        sandbox: "",
+        srcDoc: typeof htmlBody === "string" ? htmlBody : "",
+      })
+    )
+  );
+}
+
+HTMLPreview.displayName = "HTMLPreview";
+
+HTMLPreview.propTypes = {
+  requestContent: PropTypes.object.isRequired,
+};
+
+module.exports = HTMLPreview;
--- a/devtools/client/netmonitor/src/components/PropertiesView.js
+++ b/devtools/client/netmonitor/src/components/PropertiesView.js
@@ -19,30 +19,33 @@ const { Rep } = REPS;
 const { FILTER_SEARCH_DELAY } = require("../constants");
 
 // Components
 const SearchBox = createFactory(require("devtools/client/shared/components/SearchBox"));
 const TreeViewClass = require("devtools/client/shared/components/tree/TreeView");
 const TreeView = createFactory(TreeViewClass);
 const TreeRow = createFactory(require("devtools/client/shared/components/tree/TreeRow"));
 const SourceEditor = createFactory(require("./SourceEditor"));
+const HTMLPreview = createFactory(require("./HtmlPreview"));
 
 const { div, tr, td } = DOM;
 const AUTO_EXPAND_MAX_LEVEL = 7;
 const AUTO_EXPAND_MAX_NODES = 50;
 const EDITOR_CONFIG_ID = "EDITOR_CONFIG";
+const HTML_PREVIEW_ID = "HTML_PREVIEW";
 
 /*
  * 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.
+ * HTML preview - Enable by specifying object level 1 property name to HTML_PREVIEW_ID.
  * Rep - Default enabled.
  */
 class PropertiesView extends Component {
   static get propTypes() {
     return {
       object: PropTypes.object,
       enableInput: PropTypes.bool,
       expandableStrings: PropTypes.bool,
@@ -68,17 +71,17 @@ class PropertiesView extends Component {
     super(props);
 
     this.state = {
       filterText: "",
     };
 
     this.getRowClass = this.getRowClass.bind(this);
     this.onFilter = this.onFilter.bind(this);
-    this.renderRowWithEditor = this.renderRowWithEditor.bind(this);
+    this.renderRowWithExtras = this.renderRowWithExtras.bind(this);
     this.renderValueWithRep = this.renderValueWithRep.bind(this);
     this.shouldRenderSearchBox = this.shouldRenderSearchBox.bind(this);
     this.updateFilterText = this.updateFilterText.bind(this);
   }
 
   getRowClass(object, sectionNames) {
     return sectionNames.includes(object.name) ? "tree-section" : "";
   }
@@ -90,32 +93,44 @@ class PropertiesView extends Component {
     if (!filterText || whiteList.includes(name)) {
       return true;
     }
 
     let jsonString = JSON.stringify({ [name]: value }).toLowerCase();
     return jsonString.includes(filterText.toLowerCase());
   }
 
-  renderRowWithEditor(props) {
+  renderRowWithExtras(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({ key: EDITOR_CONFIG_ID, className: "editor-row-container" },
           td({ colSpan: 2 },
             SourceEditor(value)
           )
         )
       );
     }
 
-    // Skip for editor config
-    if (level >= 1 && path.includes(EDITOR_CONFIG_ID)) {
+    // Similar to the source editor, display a preview when specifying HTML_PREVIEW_ID
+    if (level === 1 && name === HTML_PREVIEW_ID) {
+      return (
+        tr({ key: HTML_PREVIEW_ID, className: "response-preview-container" },
+          td({ colSpan: 2 },
+            HTMLPreview(value)
+          )
+        )
+      );
+    }
+
+    // Skip for editor config and HTML previews
+    if ((path.includes(EDITOR_CONFIG_ID) || path.includes(HTML_PREVIEW_ID))
+      && level >= 1) {
       return null;
     }
 
     return TreeRow(props);
   }
 
   renderValueWithRep(props) {
     const { member } = props;
@@ -134,19 +149,23 @@ class PropertiesView extends Component {
       // FIXME: A workaround for the issue in StringRep
       // Force StringRep to crop the text every time
       member: Object.assign({}, member, { open: false }),
       mode: MODE.TINY,
       cropLimit: this.props.cropLimit,
     }));
   }
 
+  sectionIsSearchable(object, section) {
+    return !(object[section][EDITOR_CONFIG_ID] || object[section][HTML_PREVIEW_ID]);
+  }
+
   shouldRenderSearchBox(object) {
     return this.props.enableFilter && object && Object.keys(object)
-      .filter((section) => !object[section][EDITOR_CONFIG_ID]).length > 0;
+      .filter((section) => this.sectionIsSearchable(object, section)).length > 0;
   }
 
   updateFilterText(filterText) {
     this.setState({
       filterText,
     });
   }
 
@@ -187,17 +206,17 @@ class PropertiesView extends Component {
             enableInput,
             expandableStrings,
             useQuotes: false,
             expandedNodes: TreeViewClass.getExpandedNodes(
               object,
               {maxLevel: AUTO_EXPAND_MAX_LEVEL, maxNodes: AUTO_EXPAND_MAX_NODES}
             ),
             onFilter: (props) => this.onFilter(props, sectionNames),
-            renderRow: renderRow || this.renderRowWithEditor,
+            renderRow: renderRow || this.renderRowWithExtras,
             renderValue: renderValue || this.renderValueWithRep,
             openLink,
           }),
         ),
       )
     );
   }
 }
--- a/devtools/client/netmonitor/src/components/ResponsePanel.js
+++ b/devtools/client/netmonitor/src/components/ResponsePanel.js
@@ -11,27 +11,29 @@ const {
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { L10N } = require("../utils/l10n");
 const {
   decodeUnicodeBase64,
   formDataURI,
   getUrlBaseName,
 } = require("../utils/request-utils");
+const { Filters } = require("../utils/filter-predicates");
 
 // Components
 const PropertiesView = createFactory(require("./PropertiesView"));
 
 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");
+const RESPONSE_PREVIEW = L10N.getStr("responsePreview");
 
 const JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view";
 
 /*
  * Response panel component
  * Displays the GET parameters and POST data of a request
  */
 class ResponsePanel extends Component {
@@ -171,16 +173,23 @@ class ResponsePanel extends Component {
       if (jsonpCallback) {
         sectionName = L10N.getFormatStr("jsonpScopeName", jsonpCallback);
       } else {
         sectionName = JSON_SCOPE_NAME;
       }
       object[sectionName] = json;
     }
 
+    // Display HTML under Properties View
+    if (Filters.html(this.props.request)) {
+      object[RESPONSE_PREVIEW] = {
+        HTML_PREVIEW: { responseContent }
+      };
+    }
+
     // Others like text/html, text/plain, application/javascript
     object[RESPONSE_PAYLOAD] = {
       EDITOR_CONFIG: {
         text,
         mode: json ? "application/json" : mimeType.replace(/;.+/, ""),
       },
     };
 
--- a/devtools/client/netmonitor/src/components/moz.build
+++ b/devtools/client/netmonitor/src/components/moz.build
@@ -2,16 +2,17 @@
 # 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',
     'CookiesPanel.js',
     'CustomRequestPanel.js',
     'HeadersPanel.js',
+    'HtmlPreview.js',
     'MdnLink.js',
     'MonitorPanel.js',
     'NetworkDetailsPanel.js',
     'ParamsPanel.js',
     'PropertiesView.js',
     'RequestList.js',
     'RequestListColumnCause.js',
     'RequestListColumnContentSize.js',