Bug 1350234 - Add stack trace panel. r=Honza
authorTim Nguyen <ntim.bugs@gmail.com>
Mon, 03 Apr 2017 18:04:25 +0200
changeset 555303 2c08f896d669994754d2998b4a4b6e7188d26709
parent 555302 3600f94a4c71ff70ed941e21dc497e4ad73e2ec4
child 555304 1379bfa0f9217effd4c0a8cc70c1c1f1c2afb339
push id52211
push userbmo:jaws@mozilla.com
push dateTue, 04 Apr 2017 01:40:08 +0000
reviewersHonza
bugs1350234
milestone55.0a1
Bug 1350234 - Add stack trace panel. r=Honza MozReview-Commit-ID: A2koumbZFbO
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/assets/styles/netmonitor.css
devtools/client/netmonitor/src/components/moz.build
devtools/client/netmonitor/src/components/request-list-content.js
devtools/client/netmonitor/src/components/request-list-item.js
devtools/client/netmonitor/src/components/stack-trace-panel.js
devtools/client/netmonitor/src/components/tabbox-panel.js
devtools/client/netmonitor/src/request-list-tooltip.js
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -448,16 +448,20 @@ netmonitor.tab.params=Params
 # LOCALIZATION NOTE (netmonitor.tab.response): This is the label displayed
 # in the network details pane identifying the response tab.
 netmonitor.tab.response=Response
 
 # LOCALIZATION NOTE (netmonitor.tab.timings): This is the label displayed
 # in the network details pane identifying the timings tab.
 netmonitor.tab.timings=Timings
 
+# LOCALIZATION NOTE (netmonitor.tab.stackTrace): This is the label displayed
+# in the network details pane identifying the stack-trace tab.
+netmonitor.tab.stackTrace=Stack Trace
+
 # LOCALIZATION NOTE (netmonitor.tab.preview): This is the label displayed
 # in the network details pane identifying the preview tab.
 netmonitor.tab.preview=Preview
 
 # LOCALIZATION NOTE (netmonitor.tab.security): This is the label displayed
 # in the network details pane identifying the security tab.
 netmonitor.tab.security=Security
 
--- a/devtools/client/netmonitor/src/assets/styles/netmonitor.css
+++ b/devtools/client/netmonitor/src/assets/styles/netmonitor.css
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @import "resource://devtools/client/shared/components/splitter/split-box.css";
 @import "resource://devtools/client/shared/components/tree/tree-view.css";
 @import "resource://devtools/client/shared/components/tabs/tabs.css";
 @import "resource://devtools/client/shared/components/tabs/tabbar.css";
+@import "chrome://devtools/skin/components-frame.css";
 
 * {
   box-sizing: border-box;
 }
 
 .toolbar-labels {
   overflow: hidden;
   display: flex;
@@ -741,16 +742,49 @@
   min-width: 1px;
   transition: width 0.2s ease-out;
 }
 
 .theme-firebug .requests-list-timings-total {
   color: var(--theme-body-color);
 }
 
+/* Stack trace panel */
+
+.stack-trace {
+  font-family: var(--monospace-font-family);
+  /* The markup contains extra whitespace to improve formatting of clipboard text.
+     Make sure this whitespace doesn't affect the HTML rendering */
+  white-space: normal;
+  padding: 5px;
+}
+
+.stack-trace .frame-link-source,
+.message-location .frame-link-source {
+  /* Makes the file name truncated (and ellipsis shown) on the left side */
+  direction: rtl;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.stack-trace .frame-link-source-inner,
+.message-location .frame-link-source-inner {
+  /* Enforce LTR direction for the file name - fixes bug 1290056 */
+  direction: ltr;
+  unicode-bidi: embed;
+}
+
+.stack-trace .frame-link-function-display-name {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-inline-end: 1ch;
+}
+
 /* Security tabpanel */
 
 /* Overwrite tree-view cell colon `:` for security panel and tree section */
 .security-panel .treeTable .treeLabelCell::after,
 .treeTable .tree-section .treeLabelCell::after {
   content: "";
 }
 
--- a/devtools/client/netmonitor/src/components/moz.build
+++ b/devtools/client/netmonitor/src/components/moz.build
@@ -16,13 +16,14 @@ DevToolsModules(
     'properties-view.js',
     'request-list-content.js',
     'request-list-empty-notice.js',
     'request-list-header.js',
     'request-list-item.js',
     'request-list.js',
     'response-panel.js',
     'security-panel.js',
+    'stack-trace-panel.js',
     'statistics-panel.js',
     'tabbox-panel.js',
     'timings-panel.js',
     'toolbar.js',
 )
--- a/devtools/client/netmonitor/src/components/request-list-content.js
+++ b/devtools/client/netmonitor/src/components/request-list-content.js
@@ -8,20 +8,17 @@ 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 { setTooltipImageContent } = 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");
@@ -37,16 +34,17 @@ const REQUESTS_TOOLTIP_TOGGLE_DELAY = 50
 const RequestListContent = createClass({
   displayName: "RequestListContent",
 
   propTypes: {
     dispatch: PropTypes.func.isRequired,
     displayedRequests: PropTypes.object.isRequired,
     firstRequestStartedMillis: PropTypes.number.isRequired,
     fromCache: PropTypes.bool.isRequired,
+    onCauseBadgeClick: PropTypes.func.isRequired,
     onItemMouseDown: PropTypes.func.isRequired,
     onSecurityIconClick: PropTypes.func.isRequired,
     onSelectDelta: PropTypes.func.isRequired,
     scale: PropTypes.number,
     selectedRequestId: PropTypes.string,
   },
 
   componentWillMount() {
@@ -152,18 +150,16 @@ const RequestListContent = createClass({
     }
     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.
    */
@@ -222,16 +218,17 @@ const RequestListContent = createClass({
     this.shouldScrollBottom = false;
   },
 
   render() {
     const {
       displayedRequests,
       firstRequestStartedMillis,
       selectedRequestId,
+      onCauseBadgeClick,
       onItemMouseDown,
       onSecurityIconClick,
     } = this.props;
 
     return (
       div({
         ref: "contentEl",
         className: "requests-list-contents",
@@ -243,16 +240,17 @@ const RequestListContent = createClass({
           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),
+          onCauseBadgeClick: () => onCauseBadgeClick(item.cause),
           onSecurityIconClick: () => onSecurityIconClick(item.securityState),
         }))
       )
     );
   },
 });
 
 module.exports = connect(
@@ -269,11 +267,19 @@ module.exports = connect(
      * 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"));
       }
     },
+    /**
+     * A handler that opens the stack trace tab when a stack trace is available
+     */
+    onCauseBadgeClick: (cause) => {
+      if (cause.stacktrace && cause.stacktrace.length > 0) {
+        dispatch(Actions.selectDetailsPanelTab("stack-trace"));
+      }
+    },
     onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
   }),
 )(RequestListContent);
--- a/devtools/client/netmonitor/src/components/request-list-item.js
+++ b/devtools/client/netmonitor/src/components/request-list-item.js
@@ -61,16 +61,17 @@ 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,
+    onCauseBadgeClick: PropTypes.func.isRequired,
     onContextMenu: PropTypes.func.isRequired,
     onFocusedNodeChange: PropTypes.func,
     onMouseDown: PropTypes.func.isRequired,
     onSecurityIconClick: PropTypes.func.isRequired,
   },
 
   componentDidMount() {
     if (this.props.isSelected) {
@@ -96,16 +97,17 @@ const RequestListItem = createClass({
     const {
       item,
       index,
       isSelected,
       firstRequestStartedMillis,
       fromCache,
       onContextMenu,
       onMouseDown,
+      onCauseBadgeClick,
       onSecurityIconClick
     } = this.props;
 
     let classList = ["request-list-item"];
     if (isSelected) {
       classList.push("selected");
     }
 
@@ -123,17 +125,17 @@ const RequestListItem = createClass({
         tabIndex: 0,
         onContextMenu,
         onMouseDown,
       },
         StatusColumn({ item }),
         MethodColumn({ item }),
         FileColumn({ item }),
         DomainColumn({ item, onSecurityIconClick }),
-        CauseColumn({ item }),
+        CauseColumn({ item, onCauseBadgeClick }),
         TypeColumn({ item }),
         TransferredSizeColumn({ item }),
         ContentSizeColumn({ item }),
         WaterfallColumn({ item, firstRequestStartedMillis }),
       )
     );
   }
 });
@@ -296,24 +298,30 @@ const DomainColumn = createFactory(creat
   }
 }));
 
 const CauseColumn = createFactory(createClass({
   displayName: "CauseColumn",
 
   propTypes: {
     item: PropTypes.object.isRequired,
+    onCauseBadgeClick: PropTypes.func.isRequired,
   },
 
   shouldComponentUpdate(nextProps) {
     return this.props.item.cause !== nextProps.item.cause;
   },
 
   render() {
-    const { cause } = this.props.item;
+    const {
+      item,
+      onCauseBadgeClick,
+    } = this.props;
+
+    const { cause } = 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";
@@ -324,16 +332,17 @@ const CauseColumn = createFactory(create
     return (
       div({
         className: "requests-list-subitem requests-list-cause",
         title: causeUri,
       },
         span({
           className: "requests-list-cause-stack",
           hidden: !causeHasStack,
+          onClick: onCauseBadgeClick,
         }, "JS"),
         span({ className: "subitem-label" }, causeType),
       )
     );
   }
 }));
 
 const CONTENT_MIME_TYPE_ABBREVIATIONS = {
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/stack-trace-panel.js
@@ -0,0 +1,39 @@
+/* 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 { div } = DOM;
+
+// Components
+const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
+
+function StackTracePanel({ request }) {
+  let { stacktrace } = request.cause;
+
+  return (
+    div({ className: "panel-container" },
+      StackTrace({
+        stacktrace,
+        onViewSourceInDebugger: (name, line) => {
+          window.NetMonitorController.viewSourceInDebugger(name, line);
+        },
+      }),
+    )
+  );
+}
+
+StackTracePanel.displayName = "StackTracePanel";
+
+StackTracePanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+module.exports = StackTracePanel;
--- a/devtools/client/netmonitor/src/components/tabbox-panel.js
+++ b/devtools/client/netmonitor/src/components/tabbox-panel.js
@@ -18,25 +18,27 @@ const { getSelectedRequest } = require("
 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 StackTracePanel = createFactory(require("./stack-trace-panel"));
 const TimingsPanel = createFactory(require("./timings-panel"));
 
+const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
 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 PREVIEW_TITLE = L10N.getStr("netmonitor.tab.preview");
 const RESPONSE_TITLE = L10N.getStr("netmonitor.tab.response");
+const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
+const STACK_TRACE_TITLE = L10N.getStr("netmonitor.tab.stackTrace");
 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,
@@ -79,16 +81,23 @@ function TabboxPanel({
         ResponsePanel({ request }),
       ),
       TabPanel({
         id: "timings",
         title: TIMINGS_TITLE,
       },
         TimingsPanel({ request }),
       ),
+      request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
+      TabPanel({
+        id: "stack-trace",
+        title: STACK_TRACE_TITLE,
+      },
+        StackTracePanel({ request }),
+      ),
       request.securityState && request.securityState !== "insecure" &&
       TabPanel({
         id: "security",
         title: SECURITY_TITLE,
       },
         SecurityPanel({ request }),
       ),
       Filters.html(request) &&
--- a/devtools/client/netmonitor/src/request-list-tooltip.js
+++ b/devtools/client/netmonitor/src/request-list-tooltip.js
@@ -4,23 +4,19 @@
 
 "use strict";
 
 const {
   setImageTooltip,
   getImageDimensions,
 } = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const { getLongString } = require("./utils/client");
-const { WEBCONSOLE_L10N } = require("./utils/l10n");
 const { formDataURI } = require("./utils/request-utils");
 
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px
-const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600; // px
-
-const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 async function setTooltipImageContent(tooltip, itemEl, requestItem) {
   let { mimeType, text, encoding } = requestItem.responseContent.content;
 
   if (!mimeType || !mimeType.includes("image/")) {
     return false;
   }
 
@@ -29,77 +25,11 @@ async function setTooltipImageContent(to
   let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
   let { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
   let options = { maxDim, naturalWidth, naturalHeight };
   setImageTooltip(tooltip, tooltip.doc, src, options);
 
   return itemEl.querySelector(".requests-list-icon");
 }
 
-async function setTooltipStackTraceContent(tooltip, requestItem) {
-  let {stacktrace} = requestItem.cause;
-
-  if (!stacktrace || stacktrace.length == 0) {
-    return false;
-  }
-
-  let doc = tooltip.doc;
-  let el = doc.createElementNS(HTML_NS, "div");
-  el.className = "stack-trace-tooltip devtools-monospace";
-
-  for (let f of stacktrace) {
-    let { functionName, filename, lineNumber, columnNumber, asyncCause } = f;
-
-    if (asyncCause) {
-      // if there is asyncCause, append a "divider" row into the trace
-      let asyncFrameEl = doc.createElementNS(HTML_NS, "div");
-      asyncFrameEl.className = "stack-frame stack-frame-async";
-      asyncFrameEl.textContent =
-        WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause);
-      el.appendChild(asyncFrameEl);
-    }
-
-    // Parse a source name in format "url -> url"
-    let sourceUrl = filename.split(" -> ").pop();
-
-    let frameEl = doc.createElementNS(HTML_NS, "div");
-    frameEl.className = "stack-frame stack-frame-call";
-
-    let funcEl = doc.createElementNS(HTML_NS, "span");
-    funcEl.className = "stack-frame-function-name";
-    funcEl.textContent =
-      functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction");
-    frameEl.appendChild(funcEl);
-
-    let sourceEl = doc.createElementNS(HTML_NS, "span");
-    sourceEl.className = "stack-frame-source-name";
-    frameEl.appendChild(sourceEl);
-
-    let sourceInnerEl = doc.createElementNS(HTML_NS, "span");
-    sourceInnerEl.className = "stack-frame-source-name-inner";
-    sourceEl.appendChild(sourceInnerEl);
-
-    sourceInnerEl.textContent = sourceUrl;
-    sourceInnerEl.title = sourceUrl;
-
-    let lineEl = doc.createElementNS(HTML_NS, "span");
-    lineEl.className = "stack-frame-line";
-    lineEl.textContent = `:${lineNumber}:${columnNumber}`;
-    sourceInnerEl.appendChild(lineEl);
-
-    frameEl.addEventListener("click", () => {
-      // hide the tooltip immediately, not after delay
-      tooltip.hide();
-      window.NetMonitorController.viewSourceInDebugger(filename, lineNumber);
-    });
-
-    el.appendChild(frameEl);
-  }
-
-  tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
-
-  return true;
-}
-
 module.exports = {
   setTooltipImageContent,
-  setTooltipStackTraceContent,
 };