Bug 1358507 - Simplify component tree. r=bgrins
authornchevobbe <nchevobbe@mozilla.com>
Fri, 28 Apr 2017 11:38:13 +0200
changeset 355758 44e82e1cf34adaeb4867bf62387e271601dad424
parent 355757 acca706f1eda2801d357f1dbb832ee15944d2136
child 355759 00e46048a3b4b109658b3c0b04a59382f591774a
push id31739
push userarchaeopteryx@coole-files.de
push dateSat, 29 Apr 2017 19:26:25 +0000
treeherdermozilla-central@52b69565a24d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1358507
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;