Bug 1358507 - Simplify component tree. r=bgrins draft
authornchevobbe <nchevobbe@mozilla.com>
Fri, 28 Apr 2017 11:38:13 +0200
changeset 570075 c3bf1874f85f50f02d0adbbd1e498a604c3b5ebe
parent 570044 45facddc6737aed4ddc0986b023f350e3bb61a1e
child 570076 d07006beec1fb2ccb1a7c89ae4ec3067e830ab0a
child 570531 c825430a92c7346edee720f54a242b65467025c5
push id56384
push userbmo:nchevobbe@mozilla.com
push dateFri, 28 Apr 2017 10:17:28 +0000
reviewersbgrins
bugs1358507
milestone55.0a1
Bug 1358507 - Simplify component tree. r=bgrins MozReview-Commit-ID: 3os1JlDVeEI
devtools/client/webconsole/new-console-output/components/collapse-button.js
devtools/client/webconsole/new-console-output/components/console-output.js
devtools/client/webconsole/new-console-output/components/grip-message-body.js
devtools/client/webconsole/new-console-output/components/message-container.js
devtools/client/webconsole/new-console-output/components/message-icon.js
devtools/client/webconsole/new-console-output/components/message-indent.js
devtools/client/webconsole/new-console-output/components/message-repeat.js
devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
devtools/client/webconsole/new-console-output/components/message-types/console-command.js
devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
devtools/client/webconsole/new-console-output/components/message-types/page-error.js
devtools/client/webconsole/new-console-output/components/message.js
devtools/client/webconsole/new-console-output/components/variables-view-link.js
--- a/devtools/client/webconsole/new-console-output/components/collapse-button.js
+++ b/devtools/client/webconsole/new-console-output/components/collapse-button.js
@@ -3,49 +3,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";
 
 // React & Redux
 const {
-  createClass,
   DOM: dom,
-  PropTypes,
 } = require("devtools/client/shared/vendor/react");
 
 const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
-
-const CollapseButton = createClass({
-
-  displayName: "CollapseButton",
+const messageToggleDetails = l10n.getStr("messageToggleDetails");
 
-  propTypes: {
-    onClick: PropTypes.func.isRequired,
-    open: PropTypes.bool.isRequired,
-    title: PropTypes.string,
-  },
+function CollapseButton(props) {
+  const {
+    open,
+    onClick,
+    title = messageToggleDetails,
+  } = props;
 
-  getDefaultProps: function () {
-    return {
-      title: l10n.getStr("messageToggleDetails")
-    };
-  },
+  let classes = ["theme-twisty"];
 
-  render: function () {
-    const { open, onClick, title } = this.props;
-
-    let classes = ["theme-twisty"];
+  if (open) {
+    classes.push("open");
+  }
 
-    if (open) {
-      classes.push("open");
-    }
-
-    return dom.a({
-      className: classes.join(" "),
-      onClick,
-      title: title,
-    });
-  }
-});
+  return dom.a({
+    className: classes.join(" "),
+    onClick,
+    title: title,
+  });
+}
 
 module.exports = CollapseButton;
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -96,29 +96,24 @@ const ConsoleOutput = createClass({
           dispatch,
           message,
           key: message.id,
           serviceContainer,
           open: messagesUi.includes(message.id),
           tableData: messagesTableData.get(message.id),
           autoscroll,
           indent: parentGroups.length,
+          timestampsVisible,
         })
       );
     });
 
-    let classList = ["webconsole-output"];
-
-    if (!timestampsVisible) {
-      classList.push("hideTimestamps");
-    }
-
     return (
       dom.div({
-        className: classList.join(" "),
+        className: "webconsole-output",
         onContextMenu: this.onContextMenu,
         ref: node => {
           this.outputNode = node;
         },
       }, messageNodes
       )
     );
   }
--- a/devtools/client/webconsole/new-console-output/components/grip-message-body.js
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -9,21 +9,20 @@
 // If this is being run from Mocha, then the browser loader hasn't set up
 // define. We need to do that before loading Rep.
 if (typeof define === "undefined") {
   require("amd-loader");
 }
 
 // React
 const {
-  createFactory,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 
-const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link"));
+const VariablesViewLink = require("devtools/client/webconsole/new-console-output/components/variables-view-link");
 
 const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
 const Rep = REPS.Rep;
 const Grip = REPS.Grip;
 const StringRep = REPS.StringRep.rep;
 
 GripMessageBody.displayName = "GripMessageBody";
 
@@ -89,33 +88,33 @@ function GripMessageBody(props) {
         onDOMNodeMouseOut,
         onInspectIconClick,
         defaultRep: Grip,
         mode: props.mode,
       })
   );
 }
 
-function cleanupStyle(userProvidedStyle, createElement) {
-  // Regular expression that matches the allowed CSS property names.
-  const allowedStylesRegex = new RegExp(
-    "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
-    "margin|padding|text|transition|outline|white-space|word|writing|" +
-    "(?:min-|max-)?width|(?:min-|max-)?height)"
-  );
+// Regular expression that matches the allowed CSS property names.
+const allowedStylesRegex = new RegExp(
+  "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
+  "margin|padding|text|transition|outline|white-space|word|writing|" +
+  "(?:min-|max-)?width|(?:min-|max-)?height)"
+);
 
-  // Regular expression that matches the forbidden CSS property values.
-  const forbiddenValuesRegexs = [
-    // url(), -moz-element()
-    /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
+// Regular expression that matches the forbidden CSS property values.
+const forbiddenValuesRegexs = [
+  // url(), -moz-element()
+  /\b(?:url|(?:-moz-)?element)[\s('"]+/gi,
 
-    // various URL protocols
-    /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
-  ];
+  // various URL protocols
+  /['"(]*(?:chrome|resource|about|app|data|https?|ftp|file):+\/*/gi,
+];
 
+function cleanupStyle(userProvidedStyle, createElement) {
   // Use a dummy element to parse the style string.
   let dummy = createElement("div");
   dummy.style = userProvidedStyle;
 
   // Return a style object as expected by React DOM components, e.g.
   // {color: "red"}
   // without forbidden properties and values.
   return [...dummy.style]
--- a/devtools/client/webconsole/new-console-output/components/message-container.js
+++ b/devtools/client/webconsole/new-console-output/components/message-container.js
@@ -4,17 +4,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/. */
 
 "use strict";
 
 // React & Redux
 const {
   createClass,
-  createFactory,
+
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 
 const {
   MESSAGE_SOURCE,
   MESSAGE_TYPE
 } = require("devtools/client/webconsole/new-console-output/constants");
 
@@ -32,39 +32,47 @@ const MessageContainer = createClass({
 
   propTypes: {
     message: PropTypes.object.isRequired,
     open: PropTypes.bool.isRequired,
     serviceContainer: PropTypes.object.isRequired,
     autoscroll: PropTypes.bool.isRequired,
     indent: PropTypes.number.isRequired,
     tableData: PropTypes.object,
+    timestampsVisible: PropTypes.bool.isRequired,
   },
 
   getDefaultProps: function () {
     return {
       open: false,
       indent: 0,
     };
   },
 
   shouldComponentUpdate(nextProps, nextState) {
     const repeatChanged = this.props.message.repeat !== nextProps.message.repeat;
     const openChanged = this.props.open !== nextProps.open;
     const tableDataChanged = this.props.tableData !== nextProps.tableData;
     const responseChanged = this.props.message.response !== nextProps.message.response;
     const totalTimeChanged = this.props.message.totalTime !== nextProps.message.totalTime;
-    return repeatChanged || openChanged || tableDataChanged || responseChanged ||
-      totalTimeChanged;
+    const timestampVisibleChanged =
+      this.props.timestampsVisible !== nextProps.timestampsVisible;
+
+    return repeatChanged
+      || openChanged
+      || tableDataChanged
+      || responseChanged
+      || totalTimeChanged
+      || timestampVisibleChanged;
   },
 
   render() {
     const { message } = this.props;
 
-    let MessageComponent = createFactory(getMessageComponent(message));
+    let MessageComponent = getMessageComponent(message);
     return MessageComponent(this.props);
   }
 });
 
 function getMessageComponent(message) {
   switch (message.source) {
     case MESSAGE_SOURCE.CONSOLE_API:
       return componentMap.get("ConsoleApiCall");
--- a/devtools/client/webconsole/new-console-output/components/message-icon.js
+++ b/devtools/client/webconsole/new-console-output/components/message-icon.js
@@ -18,15 +18,15 @@ MessageIcon.displayName = "MessageIcon";
 MessageIcon.propTypes = {
   level: PropTypes.string.isRequired,
 };
 
 function MessageIcon(props) {
   const { level } = props;
 
   const title = l10n.getStr("level." + level);
-  return dom.div({
+  return dom.span({
     className: "icon",
     title
   });
 }
 
 module.exports = MessageIcon;
--- a/devtools/client/webconsole/new-console-output/components/message-indent.js
+++ b/devtools/client/webconsole/new-console-output/components/message-indent.js
@@ -3,35 +3,25 @@
 /* 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";
 
 // React & Redux
 const {
-  createClass,
   DOM: dom,
-  PropTypes,
 } = require("devtools/client/shared/vendor/react");
 
 const INDENT_WIDTH = 12;
-const MessageIndent = createClass({
 
-  displayName: "MessageIndent",
-
-  propTypes: {
-    indent: PropTypes.number.isRequired,
-  },
-
-  render: function () {
-    const { indent } = this.props;
-    return dom.span({
-      className: "indent",
-      style: {"width": indent * INDENT_WIDTH}
-    });
-  }
-});
+function MessageIndent(props) {
+  const { indent } = props;
+  return dom.span({
+    className: "indent",
+    style: {"width": indent * INDENT_WIDTH}
+  });
+}
 
 module.exports.MessageIndent = MessageIndent;
 
 // Exported so we can test it with unit tests.
 module.exports.INDENT_WIDTH = INDENT_WIDTH;
--- a/devtools/client/webconsole/new-console-output/components/message-repeat.js
+++ b/devtools/client/webconsole/new-console-output/components/message-repeat.js
@@ -9,28 +9,30 @@
 
 // React & Redux
 const {
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { PluralForm } = require("devtools/shared/plural-form");
 const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const messageRepeatsTooltip = l10n.getStr("messageRepeats.tooltip2");
 
 MessageRepeat.displayName = "MessageRepeat";
 
 MessageRepeat.propTypes = {
   repeat: PropTypes.number.isRequired
 };
 
 function MessageRepeat(props) {
   const { repeat } = props;
-  const visibility = repeat > 1 ? "visible" : "hidden";
+
+  if (!repeat || repeat < 2) {
+    return null;
+  }
 
   return dom.span({
     className: "message-repeats",
-    style: {visibility},
-    title: PluralForm.get(repeat, l10n.getStr("messageRepeats.tooltip2"))
-      .replace("#1", repeat)
+    title: PluralForm.get(repeat, messageRepeatsTooltip).replace("#1", repeat)
   }, repeat);
 }
 
 module.exports = MessageRepeat;
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
@@ -7,44 +7,46 @@
 "use strict";
 
 // React & Redux
 const {
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
-const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+const GripMessageBody = require("devtools/client/webconsole/new-console-output/components/grip-message-body");
 const ConsoleTable = createFactory(require("devtools/client/webconsole/new-console-output/components/console-table"));
 const {isGroupType, l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
 
 const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
 
 ConsoleApiCall.displayName = "ConsoleApiCall";
 
 ConsoleApiCall.propTypes = {
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   serviceContainer: PropTypes.object.isRequired,
   indent: PropTypes.number.isRequired,
+  timestampsVisible: PropTypes.bool.isRequired,
 };
 
 ConsoleApiCall.defaultProps = {
   open: false,
   indent: 0,
 };
 
 function ConsoleApiCall(props) {
   const {
     dispatch,
     message,
     open,
     tableData,
     serviceContainer,
     indent,
+    timestampsVisible,
   } = props;
   const {
     id: messageId,
     source,
     type,
     level,
     repeat,
     stacktrace,
@@ -103,16 +105,17 @@ function ConsoleApiCall(props) {
     repeat,
     frame,
     stacktrace,
     attachment,
     serviceContainer,
     dispatch,
     indent,
     timeStamp,
+    timestampsVisible,
   });
 }
 
 function formatReps(parameters, userProvidedStyles, serviceContainer) {
   return (
     parameters
       // Get all the grips.
       .map((grip, key) => GripMessageBody({
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
@@ -14,44 +14,51 @@ const {
 const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
 
 ConsoleCommand.displayName = "ConsoleCommand";
 
 ConsoleCommand.propTypes = {
   message: PropTypes.object.isRequired,
   autoscroll: PropTypes.bool.isRequired,
   indent: PropTypes.number.isRequired,
+  timestampsVisible: PropTypes.bool.isRequired,
 };
 
 ConsoleCommand.defaultProps = {
   indent: 0,
 };
 
 /**
  * Displays input from the console.
  */
 function ConsoleCommand(props) {
-  const { autoscroll, indent, message } = props;
+  const {
+    autoscroll,
+    indent,
+    message,
+    timestampsVisible,
+  } = props;
+
   const {
     source,
     type,
     level,
     messageText: messageBody,
   } = message;
 
   const {
     serviceContainer,
   } = props;
 
-  const childProps = {
+  return Message({
     source,
     type,
     level,
     topLevelClasses: [],
     messageBody,
     scrollToMessage: autoscroll,
     serviceContainer,
-    indent: indent,
-  };
-  return Message(childProps);
+    indent,
+    timestampsVisible,
+  });
 }
 
 module.exports = ConsoleCommand;
--- a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
@@ -7,31 +7,38 @@
 "use strict";
 
 // React & Redux
 const {
   createFactory,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
-const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body"));
+const GripMessageBody = require("devtools/client/webconsole/new-console-output/components/grip-message-body");
 
 EvaluationResult.displayName = "EvaluationResult";
 
 EvaluationResult.propTypes = {
   message: PropTypes.object.isRequired,
   indent: PropTypes.number.isRequired,
+  timestampsVisible: PropTypes.bool.isRequired,
 };
 
 EvaluationResult.defaultProps = {
   indent: 0,
 };
 
 function EvaluationResult(props) {
-  const { message, serviceContainer, indent } = props;
+  const {
+    message,
+    serviceContainer,
+    indent,
+    timestampsVisible,
+  } = props;
+
   const {
     source,
     type,
     level,
     id: messageId,
     exceptionDocURL,
     frame,
     timeStamp,
@@ -55,28 +62,28 @@ function EvaluationResult(props) {
       serviceContainer,
       useQuotes: true,
       escapeWhitespace: false,
     });
   }
 
   const topLevelClasses = ["cm-s-mozilla"];
 
-  const childProps = {
+  return Message({
     source,
     type,
     level,
     indent,
     topLevelClasses,
     messageBody,
     messageId,
     scrollToMessage: props.autoscroll,
     serviceContainer,
     exceptionDocURL,
     frame,
     timeStamp,
     parameters,
     notes,
-  };
-  return Message(childProps);
+    timestampsVisible,
+  });
 }
 
 module.exports = EvaluationResult;
--- a/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
@@ -18,26 +18,28 @@ const { l10n } = require("devtools/clien
 NetworkEventMessage.displayName = "NetworkEventMessage";
 
 NetworkEventMessage.propTypes = {
   message: PropTypes.object.isRequired,
   serviceContainer: PropTypes.shape({
     openNetworkPanel: PropTypes.func.isRequired,
   }),
   indent: PropTypes.number.isRequired,
+  timestampsVisible: PropTypes.bool.isRequired,
 };
 
 NetworkEventMessage.defaultProps = {
   indent: 0,
 };
 
 function NetworkEventMessage({
   indent,
   message = {},
   serviceContainer,
+  timestampsVisible,
 }) {
   const {
     actor,
     source,
     type,
     level,
     request,
     response: {
@@ -68,23 +70,23 @@ function NetworkEventMessage({
   const url = dom.a({ className: "url", title: request.url, onClick: openNetworkMonitor },
     request.url.replace(/\?.+/, ""));
   const statusBody = statusInfo
     ? dom.a({ className: "status", onClick: openNetworkMonitor }, statusInfo)
     : null;
 
   const messageBody = [method, xhr, url, statusBody];
 
-  const childProps = {
+  return Message({
     source,
     type,
     level,
     indent,
     topLevelClasses,
     timeStamp,
     messageBody,
     serviceContainer,
     request,
-  };
-  return Message(childProps);
+    timestampsVisible,
+  });
 }
 
 module.exports = NetworkEventMessage;
--- a/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
@@ -14,30 +14,32 @@ const {
 const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
 
 PageError.displayName = "PageError";
 
 PageError.propTypes = {
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   indent: PropTypes.number.isRequired,
+  timestampsVisible: PropTypes.bool.isRequired,
 };
 
 PageError.defaultProps = {
   open: false,
   indent: 0,
 };
 
 function PageError(props) {
   const {
     dispatch,
     message,
     open,
     serviceContainer,
     indent,
+    timestampsVisible,
   } = props;
   const {
     id: messageId,
     source,
     type,
     level,
     messageText,
     repeat,
@@ -50,17 +52,17 @@ function PageError(props) {
 
   let messageBody;
   if (typeof messageText === "string") {
     messageBody = messageText;
   } else if (typeof messageText === "object" && messageText.type === "longString") {
     messageBody = `${message.messageText.initial}…`;
   }
 
-  const childProps = {
+  return Message({
     dispatch,
     messageId,
     open,
     collapsible: Array.isArray(stacktrace),
     source,
     type,
     level,
     topLevelClasses: [],
@@ -68,13 +70,13 @@ function PageError(props) {
     messageBody,
     repeat,
     frame,
     stacktrace,
     serviceContainer,
     exceptionDocURL,
     timeStamp,
     notes,
-  };
-  return Message(childProps);
+    timestampsVisible,
+  });
 }
 
 module.exports = PageError;
--- a/devtools/client/webconsole/new-console-output/components/message.js
+++ b/devtools/client/webconsole/new-console-output/components/message.js
@@ -11,20 +11,20 @@ const {
   createClass,
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const {MESSAGE_SOURCE} = require("devtools/client/webconsole/new-console-output/constants");
-const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button"));
-const MessageIndent = createFactory(require("devtools/client/webconsole/new-console-output/components/message-indent").MessageIndent);
-const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon"));
-const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat"));
+const CollapseButton = require("devtools/client/webconsole/new-console-output/components/collapse-button");
+const MessageIndent = require("devtools/client/webconsole/new-console-output/components/message-indent").MessageIndent;
+const MessageIcon = require("devtools/client/webconsole/new-console-output/components/message-icon");
+const MessageRepeat = require("devtools/client/webconsole/new-console-output/components/message-repeat");
 const FrameView = createFactory(require("devtools/client/shared/components/frame"));
 const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
 
 const Message = createClass({
   displayName: "Message",
 
   propTypes: {
     open: PropTypes.bool,
@@ -42,16 +42,17 @@ const Message = createClass({
     stacktrace: PropTypes.any,
     messageId: PropTypes.string,
     scrollToMessage: PropTypes.bool,
     exceptionDocURL: PropTypes.string,
     parameters: PropTypes.object,
     request: PropTypes.object,
     dispatch: PropTypes.func,
     timeStamp: PropTypes.number,
+    timestampsVisible: PropTypes.bool.isRequired,
     serviceContainer: PropTypes.shape({
       emitNewMessage: PropTypes.func.isRequired,
       onViewSourceInDebugger: PropTypes.func,
       onViewSourceInScratchpad: PropTypes.func,
       onViewSourceInStyleEditor: PropTypes.func,
       openContextMenu: PropTypes.func.isRequired,
       openLink: PropTypes.func.isRequired,
       sourceMapService: PropTypes.any,
@@ -111,41 +112,49 @@ const Message = createClass({
       topLevelClasses,
       messageBody,
       frame,
       stacktrace,
       serviceContainer,
       dispatch,
       exceptionDocURL,
       timeStamp = Date.now(),
+      timestampsVisible,
       notes,
     } = this.props;
 
     topLevelClasses.push("message", source, type, level);
     if (open) {
       topLevelClasses.push("open");
     }
 
-    const timestampEl = dom.span({
-      className: "timestamp devtools-monospace"
-    }, l10n.timestampString(timeStamp));
+    let timestampEl;
+    if (timestampsVisible === true) {
+      timestampEl = dom.span({
+        className: "timestamp devtools-monospace"
+      }, l10n.timestampString(timeStamp));
+    }
 
     const icon = MessageIcon({level});
 
     // Figure out if there is an expandable part to the message.
     let attachment = null;
     if (this.props.attachment) {
       attachment = this.props.attachment;
-    } else if (stacktrace) {
-      const child = open ? StackTrace({
-        stacktrace: stacktrace,
-        onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger,
-        onViewSourceInScratchpad: serviceContainer.onViewSourceInScratchpad,
-      }) : null;
-      attachment = dom.div({ className: "stacktrace devtools-monospace" }, child);
+    } else if (stacktrace && open) {
+      attachment = dom.div(
+        {
+          className: "stacktrace devtools-monospace"
+        },
+        StackTrace({
+          stacktrace: stacktrace,
+          onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger,
+          onViewSourceInScratchpad: serviceContainer.onViewSourceInScratchpad,
+        })
+      );
     }
 
     // If there is an expandable part, make it collapsible.
     let collapse = null;
     if (collapsible) {
       collapse = CollapseButton({
         open,
         title: collapseTitle,
@@ -177,17 +186,17 @@ const Message = createClass({
               ? serviceContainer.sourceMapService
               : undefined
           }) : null
         )));
     } else {
       notesNodes = [];
     }
 
-    const repeat = this.props.repeat ? MessageRepeat({repeat: this.props.repeat}) : null;
+    const repeat = MessageRepeat({repeat: this.props.repeat});
 
     let onFrameClick;
     if (serviceContainer && frame) {
       if (source === MESSAGE_SOURCE.CSS) {
         onFrameClick = serviceContainer.onViewSourceInStyleEditor;
       } else if (/^Scratchpad\/\d+$/.test(frame.source)) {
         onFrameClick = serviceContainer.onViewSourceInScratchpad;
       } else {
@@ -225,21 +234,23 @@ const Message = createClass({
     },
       timestampEl,
       MessageIndent({indent}),
       icon,
       collapse,
       dom.span({ className: "message-body-wrapper" },
         dom.span({ className: "message-flex-body" },
           // Add whitespaces for formatting when copying to the clipboard.
-          " ", dom.span({ className: "message-body devtools-monospace" },
+          timestampEl ? " " : null,
+          dom.span({ className: "message-body devtools-monospace" },
             messageBody,
             learnMore
           ),
-          " ", repeat,
+          repeat ? " " : null,
+          repeat,
           " ", location
         ),
         // Add a newline for formatting when copying to the clipboard.
         "\n",
         // If an attachment is displayed, the final newline is handled by the attachment.
         attachment,
         ...notesNodes
       )
--- a/devtools/client/webconsole/new-console-output/components/variables-view-link.js
+++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js
@@ -14,26 +14,26 @@ const {
 const {openVariablesView} = require("devtools/client/webconsole/new-console-output/utils/variables-view");
 
 VariablesViewLink.displayName = "VariablesViewLink";
 
 VariablesViewLink.propTypes = {
   object: PropTypes.object.isRequired
 };
 
-function VariablesViewLink(props) {
-  const { className, object, children } = props;
+function VariablesViewLink(props, ...children) {
+  const { className, object } = props;
   const classes = ["cm-variable"];
   if (className) {
     classes.push(className);
   }
   return (
     dom.a({
       onClick: openVariablesView.bind(null, object),
       // Context menu can use this actor id information to enable additional menu items.
       "data-link-actor-id": object.actor,
       className: classes.join(" "),
       draggable: false,
-    }, children)
+    }, ...children)
   );
 }
 
 module.exports = VariablesViewLink;