Backed out changeset 8d0caf759080 (bug 1425538) for failing devtools/client/webconsole/test/mochitest/browser_jsterm_selfxss.js on a CLOSED TREE
authorAndreea Pavel <apavel@mozilla.com>
Wed, 16 May 2018 09:49:32 +0300
changeset 472657 dedd25bfd2794eaba95225361f82c701e49c9339
parent 472656 89a073a0ff2c179b604a11879c4c1408a42a4b63
child 472684 3c9d69736f4a421218e5eb01b6571d535d38318a
child 472686 e5bf2e9ff60a28306fcdc73cf82f13faa4f1ce4b
push id9374
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:43:20 +0000
treeherdermozilla-beta@160e085dfb0b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1425538
milestone62.0a1
backs out8d0caf759080d0c325637824bd21ca90230b73fd
first release with
nightly linux32
dedd25bfd279 / 62.0a1 / 20180516100125 / files
nightly linux64
dedd25bfd279 / 62.0a1 / 20180516100125 / files
nightly mac
dedd25bfd279 / 62.0a1 / 20180516100125 / files
nightly win32
dedd25bfd279 / 62.0a1 / 20180516100125 / files
nightly win64
dedd25bfd279 / 62.0a1 / 20180516100125 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out changeset 8d0caf759080 (bug 1425538) for failing devtools/client/webconsole/test/mochitest/browser_jsterm_selfxss.js on a CLOSED TREE
devtools/client/shared/components/NotificationBox.js
devtools/client/webconsole/actions/index.js
devtools/client/webconsole/actions/moz.build
devtools/client/webconsole/actions/notifications.js
devtools/client/webconsole/components/App.js
devtools/client/webconsole/components/JSTerm.js
devtools/client/webconsole/components/moz.build
devtools/client/webconsole/constants.js
devtools/client/webconsole/new-console-output-wrapper.js
devtools/client/webconsole/reducers/index.js
devtools/client/webconsole/reducers/moz.build
devtools/client/webconsole/reducers/notifications.js
devtools/client/webconsole/selectors/moz.build
devtools/client/webconsole/selectors/notifications.js
devtools/client/webconsole/utils.js
--- a/devtools/client/shared/components/NotificationBox.js
+++ b/devtools/client/shared/components/NotificationBox.js
@@ -28,27 +28,20 @@ const PriorityLevels = {
 };
 
 /**
  * This component represents Notification Box - HTML alternative for
  * <xul:notificationbox> binding.
  *
  * See also MDN for more info about <xul:notificationbox>:
  * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
- *
- * This component can maintain its own state (list of notifications)
- * as well as consume list of notifications provided as a prop
- * (coming e.g. from Redux store).
  */
 class NotificationBox extends Component {
   static get propTypes() {
     return {
-      // Optional box ID (used for mounted node ID attribute)
-      id: PropTypes.string,
-
       // List of notifications appended into the box.
       notifications: PropTypes.arrayOf(PropTypes.shape({
         // label to appear on the notification.
         label: PropTypes.string.isRequired,
 
         // Value used to identify the notification
         value: PropTypes.string.isRequired,
 
@@ -111,26 +104,53 @@ class NotificationBox extends Component 
   }
 
   /**
    * Create a new notification and display it. If another notification is
    * already present with a higher priority, the new notification will be
    * added behind it. See `propTypes` for arguments description.
    */
   appendNotification(label, value, image, priority, buttons = [], eventCallback) {
-    const newState = appendNotification(this.state, {
-      label,
-      value,
-      image,
-      priority,
-      buttons,
-      eventCallback,
+    // Priority level must be within expected interval
+    // (see priority levels at the top of this file).
+    if (priority < PriorityLevels.PRIORITY_INFO_LOW ||
+      priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK) {
+      throw new Error("Invalid notification priority " + priority);
+    }
+
+    // Custom image URL is not supported yet.
+    if (image) {
+      throw new Error("Custom image URL is not supported yet");
+    }
+
+    let type = "warning";
+    if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
+      type = "critical";
+    } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
+      type = "info";
+    }
+
+    let notifications = this.state.notifications.set(value, {
+      label: label,
+      value: value,
+      image: image,
+      priority: priority,
+      type: type,
+      buttons: Array.isArray(buttons) ? buttons : [],
+      eventCallback: eventCallback,
     });
 
-    this.setState(newState);
+    // High priorities must be on top.
+    notifications = notifications.sortBy((val, key) => {
+      return -val.priority;
+    });
+
+    this.setState({
+      notifications: notifications
+    });
   }
 
   /**
    * Remove specific notification from the list.
    */
   removeNotification(notification) {
     if (notification) {
       this.close(this.state.notifications.get(notification.value));
@@ -167,20 +187,16 @@ class NotificationBox extends Component 
     if (!notification) {
       return;
     }
 
     if (notification.eventCallback) {
       notification.eventCallback("removed");
     }
 
-    if (!this.state.notifications.get(notification.value)) {
-      return;
-    }
-
     this.setState({
       notifications: this.state.notifications.remove(notification.value)
     });
   }
 
   /**
    * Render a button. A notification can have a set of custom buttons.
    * These are used to execute custom callback.
@@ -239,97 +255,21 @@ class NotificationBox extends Component 
     );
   }
 
   /**
    * Render the top (highest priority) notification. Only one
    * notification is rendered at a time.
    */
   render() {
-    const notifications = this.props.notifications || this.state.notifications;
-    const notification = notifications ? notifications.first() : null;
-    const content = notification ?
+    let notification = this.state.notifications.first();
+    let content = notification ?
       this.renderNotification(notification) :
       null;
 
-    return div({
-      className: "notificationbox",
-      id: this.props.id},
+    return div({className: "notificationbox"},
       content
     );
   }
 }
 
-// Helpers
-
-/**
- * Create a new notification. If another notification is already present with
- * a higher priority, the new notification will be added behind it.
- * See `propTypes` for arguments description.
- */
-function appendNotification(state, props) {
-  const {
-    label,
-    value,
-    image,
-    priority,
-    buttons,
-    eventCallback
-  } = props;
-
-  // Priority level must be within expected interval
-  // (see priority levels at the top of this file).
-  if (priority < PriorityLevels.PRIORITY_INFO_LOW ||
-    priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK) {
-    throw new Error("Invalid notification priority " + priority);
-  }
-
-  // Custom image URL is not supported yet.
-  if (image) {
-    throw new Error("Custom image URL is not supported yet");
-  }
-
-  let type = "warning";
-  if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
-    type = "critical";
-  } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
-    type = "info";
-  }
-
-  if (!state.notifications) {
-    state.notifications = new Immutable.OrderedMap();
-  }
-
-  let notifications = state.notifications.set(value, {
-    label: label,
-    value: value,
-    image: image,
-    priority: priority,
-    type: type,
-    buttons: Array.isArray(buttons) ? buttons : [],
-    eventCallback: eventCallback,
-  });
-
-  // High priorities must be on top.
-  notifications = notifications.sortBy((val, key) => {
-    return -val.priority;
-  });
-
-  return {
-    notifications: notifications
-  };
-}
-
-function getNotificationWithValue(notifications, value) {
-  return notifications ? notifications.get(value) : null;
-}
-
-function removeNotificationWithValue(notifications, value) {
-  return {
-    notifications: notifications.remove(value)
-  };
-}
-
 module.exports.NotificationBox = NotificationBox;
 module.exports.PriorityLevels = PriorityLevels;
-module.exports.appendNotification = appendNotification;
-module.exports.getNotificationWithValue = getNotificationWithValue;
-module.exports.removeNotificationWithValue = removeNotificationWithValue;
--- a/devtools/client/webconsole/actions/index.js
+++ b/devtools/client/webconsole/actions/index.js
@@ -5,14 +5,13 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const actionModules = [
   require("./filters"),
   require("./messages"),
   require("./ui"),
-  require("./notifications"),
 ];
 
 const actions = Object.assign({}, ...actionModules);
 
 module.exports = actions;
--- a/devtools/client/webconsole/actions/moz.build
+++ b/devtools/client/webconsole/actions/moz.build
@@ -2,11 +2,10 @@
 # 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(
     'filters.js',
     'index.js',
     'messages.js',
-    'notifications.js',
     'ui.js',
 )
deleted file mode 100644
--- a/devtools/client/webconsole/actions/notifications.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* 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 {
-  APPEND_NOTIFICATION,
-  REMOVE_NOTIFICATION,
-} = require("devtools/client/webconsole/constants");
-
-/**
- * Append a notification into JSTerm notification list.
- */
-function appendNotification(label, value, image, priority, buttons = [], eventCallback) {
-  return {
-    type: APPEND_NOTIFICATION,
-    label,
-    value,
-    image,
-    priority,
-    buttons,
-    eventCallback,
-  };
-}
-
-/**
- * Remove notification with specified value from JSTerm
- * notification list.
- */
-function removeNotification(value) {
-  return {
-    type: REMOVE_NOTIFICATION,
-    value,
-  };
-}
-
-module.exports = {
-  appendNotification,
-  removeNotification,
-};
deleted file mode 100644
--- a/devtools/client/webconsole/components/App.js
+++ /dev/null
@@ -1,170 +0,0 @@
-/* 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 { Component, createFactory } = require("devtools/client/shared/vendor/react");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
-
-const actions = require("devtools/client/webconsole/actions/index");
-const ConsoleOutput = createFactory(require("devtools/client/webconsole/components/ConsoleOutput"));
-const FilterBar = createFactory(require("devtools/client/webconsole/components/FilterBar"));
-const SideBar = createFactory(require("devtools/client/webconsole/components/SideBar"));
-const JSTerm = createFactory(require("devtools/client/webconsole/components/JSTerm"));
-const NotificationBox = createFactory(require("devtools/client/shared/components/NotificationBox").NotificationBox);
-
-const l10n = require("devtools/client/webconsole/webconsole-l10n");
-const { Utils: WebConsoleUtils } = require("devtools/client/webconsole/utils");
-
-const SELF_XSS_OK = l10n.getStr("selfxss.okstring");
-const SELF_XSS_MSG = l10n.getFormatStr("selfxss.msg", [SELF_XSS_OK]);
-
-const {
-  getNotificationWithValue,
-  PriorityLevels,
-} = require("devtools/client/shared/components/NotificationBox");
-
-const { getAllNotifications } = require("devtools/client/webconsole/selectors/notifications");
-
-const { div } = dom;
-
-/**
- * Console root Application component.
- */
-class App extends Component {
-  static get propTypes() {
-    return {
-      attachRefToHud: PropTypes.func.isRequired,
-      dispatch: PropTypes.func.isRequired,
-      hud: PropTypes.object.isRequired,
-      notifications: PropTypes.array,
-      onFirstMeaningfulPaint: PropTypes.func.isRequired,
-      serviceContainer: PropTypes.object.isRequired,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-
-    this.onPaste = this.onPaste.bind(this);
-  }
-
-  onPaste(event) {
-    event.preventDefault();
-    event.stopPropagation();
-
-    const {
-      dispatch,
-      notifications,
-    } = this.props;
-
-    // Bail out if self-xss notification is already there.
-    if (getNotificationWithValue(notifications, "selfxss-notification")) {
-      return;
-    }
-
-    const inputField = this.node.querySelector(".jsterm-input-node");
-
-    // Cleanup function if notification is closed by the user.
-    const removeCallback = (eventType) => {
-      if (eventType == "removed") {
-        inputField.removeEventListener("keyup", pasteKeyUpHandler);
-        dispatch(actions.removeNotification("selfxss-notification"));
-      }
-    };
-
-    // Create self-xss notification
-    dispatch(actions.appendNotification(
-      SELF_XSS_MSG,
-      "selfxss-notification",
-      null,
-      PriorityLevels.PRIORITY_WARNING_HIGH,
-      null,
-      removeCallback
-    ));
-
-    // Remove notification automatically when the user
-    // types "allows pasting".
-    function pasteKeyUpHandler() {
-      let value = inputField.value || inputField.textContent;
-      if (value.includes(SELF_XSS_OK)) {
-        dispatch(actions.removeNotification("selfxss-notification"));
-        inputField.removeEventListener("keyup", pasteKeyUpHandler);
-        WebConsoleUtils.usageCount = WebConsoleUtils.CONSOLE_ENTRY_THRESHOLD;
-      }
-    }
-    inputField.addEventListener("keyup", pasteKeyUpHandler);
-  }
-
-  // Rendering
-
-  render() {
-    const {
-      attachRefToHud,
-      hud,
-      notifications,
-      onFirstMeaningfulPaint,
-      serviceContainer,
-    } = this.props;
-
-    const {
-      usageCount,
-      CONSOLE_ENTRY_THRESHOLD
-    } = WebConsoleUtils;
-
-    // Paste event handler is used to prevent self-xss attacks
-    // (supported in WebConsole only).
-    const onPaste = !hud.isBrowserConsole && usageCount < CONSOLE_ENTRY_THRESHOLD
-      ? this.onPaste
-      : null;
-
-    // Render the entire Console panel. The panel consists
-    // from the following parts:
-    // * FilterBar - Buttons & free text for content filtering
-    // * Content - List of logs & messages
-    // * SideBar - Object inspector
-    // * NotificationBox - Notifications for JSTerm (self-xss warning at the moment)
-    // * JSTerm - Input command line.
-    return (
-      div({
-        className: "webconsole-output-wrapper",
-        ref: node => {
-          this.node = node;
-        }},
-        FilterBar({
-          hidePersistLogsCheckbox: hud.isBrowserConsole,
-          serviceContainer: {
-            attachRefToHud
-          }
-        }),
-        ConsoleOutput({
-          serviceContainer,
-          onFirstMeaningfulPaint,
-        }),
-        SideBar({
-          serviceContainer,
-        }),
-        NotificationBox({
-          id: "webconsole-notificationbox",
-          notifications,
-        }),
-        JSTerm({
-          hud,
-          onPaste,
-        }),
-      )
-    );
-  }
-}
-
-const mapStateToProps = state => ({
-  notifications: getAllNotifications(state),
-});
-
-const mapDispatchToProps = dispatch => ({
-  dispatch,
-});
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(App);
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -13,16 +13,18 @@ const PropTypes = require("devtools/clie
 
 loader.lazyServiceGetter(this, "clipboardHelper",
                          "@mozilla.org/widget/clipboardhelper;1",
                          "nsIClipboardHelper");
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup");
 loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "NotificationBox", "devtools/client/shared/components/NotificationBox", true);
+loader.lazyRequireGetter(this, "PriorityLevels", "devtools/client/shared/components/NotificationBox", true);
 
 const l10n = require("devtools/client/webconsole/webconsole-l10n");
 
 // Constants used for defining the direction of JSTerm input history navigation.
 const HISTORY_BACK = -1;
 const HISTORY_FORWARD = 1;
 
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
@@ -46,28 +48,25 @@ const dom = require("devtools/client/sha
  * @constructor
  * @param object webConsoleFrame
  *        The WebConsoleFrame object that owns this JSTerm instance.
  */
 class JSTerm extends Component {
   static get propTypes() {
     return {
       hud: PropTypes.object.isRequired,
-      // Handler for clipboard 'paste' event (also used for 'drop' event).
-      onPaste: PropTypes.func,
     };
   }
 
   constructor(props) {
     super(props);
 
     const {
       hud,
     } = props;
-
     this.hud = hud;
     this.hudId = this.hud.hudId;
     this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT);
     this._loadHistory();
 
     /**
      * Stores the data for the last completion.
      * @type object
@@ -172,16 +171,26 @@ class JSTerm extends Component {
     // Update the character width and height needed for the popup offset
     // calculations.
     this._updateCharSize();
 
     this.inputNode.addEventListener("keypress", this._keyPress);
     this.inputNode.addEventListener("input", this._inputEventHandler);
     this.inputNode.addEventListener("keyup", this._inputEventHandler);
     this.inputNode.addEventListener("focus", this._focusEventHandler);
+
+    if (!this.hud.isBrowserConsole) {
+      let okstring = l10n.getStr("selfxss.okstring");
+      let msg = l10n.getFormatStr("selfxss.msg", [okstring]);
+      this._onPaste = WebConsoleUtils.pasteHandlerGen(this.inputNode,
+          this.getNotificationBox(), msg, okstring);
+      this.inputNode.addEventListener("paste", this._onPaste);
+      this.inputNode.addEventListener("drop", this._onPaste);
+    }
+
     this.hud.window.addEventListener("blur", this._blurEventHandler);
     this.lastInputValue && this.setInputValue(this.lastInputValue);
 
     this.focus();
   }
 
   shouldComponentUpdate() {
     // XXX: For now, everything is handled in an imperative way and we only want React
@@ -1265,58 +1274,79 @@ class JSTerm extends Component {
     style.width = "auto";
     style.color = "transparent";
     WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
     tempLabel.textContent = "x";
     doc.documentElement.appendChild(tempLabel);
     this._inputCharWidth = tempLabel.offsetWidth;
     tempLabel.remove();
     // Calculate the width of the chevron placed at the beginning of the input
-    // box. Remove 4 more pixels to accommodate the padding of the popup.
+    // box. Remove 4 more pixels to accomodate the padding of the popup.
     this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
                              .paddingLeft.replace(/[^0-9.]/g, "") - 4;
   }
 
+  /**
+   * Build the notification box as soon as needed.
+   */
+  getNotificationBox() {
+    if (this._notificationBox) {
+      return this._notificationBox;
+    }
+
+    let box = this.hud.document.getElementById("webconsole-notificationbox");
+    let toolbox = gDevTools.getToolbox(this.hud.owner.target);
+
+    // Render NotificationBox and assign priority levels to it.
+    this._notificationBox = Object.assign(
+      toolbox.ReactDOM.render(toolbox.React.createElement(NotificationBox), box),
+      PriorityLevels);
+    return this._notificationBox;
+  }
+
   destroy() {
     this.clearCompletion();
 
     this.webConsoleClient.clearNetworkRequests();
     if (this.hud.outputNode) {
       // We do this because it's much faster than letting React handle the ConsoleOutput
       // unmounting.
       this.hud.outputNode.innerHTML = "";
     }
 
     if (this.autocompletePopup) {
       this.autocompletePopup.destroy();
       this.autocompletePopup = null;
     }
 
     if (this.inputNode) {
+      if (this._onPaste) {
+        this.inputNode.removeEventListener("paste", this._onPaste);
+        this.inputNode.removeEventListener("drop", this._onPaste);
+        this._onPaste = null;
+      }
+
       this.inputNode.removeEventListener("keypress", this._keyPress);
       this.inputNode.removeEventListener("input", this._inputEventHandler);
       this.inputNode.removeEventListener("keyup", this._inputEventHandler);
       this.inputNode.removeEventListener("focus", this._focusEventHandler);
       this.hud.window.removeEventListener("blur", this._blurEventHandler);
     }
 
     this.hud = null;
   }
 
   render() {
     if (this.props.hud.isBrowserConsole &&
         !Services.prefs.getBoolPref("devtools.chrome.enabled")) {
       return null;
     }
 
-    let {
-      onPaste
-    } = this.props;
-
-    return (
+    return [
+      dom.div({id: "webconsole-notificationbox", key: "notification"}),
       dom.div({
         className: "jsterm-input-container",
         key: "jsterm-container",
         style: {direction: "ltr"},
         "aria-live": "off",
       },
         dom.textarea({
           className: "jsterm-complete-node devtools-monospace",
@@ -1330,17 +1360,15 @@ class JSTerm extends Component {
           className: "jsterm-input-node devtools-monospace",
           key: "input",
           tabIndex: "0",
           rows: "1",
           "aria-autocomplete": "list",
           ref: node => {
             this.inputNode = node;
           },
-          onPaste: onPaste,
-          onDrop: onPaste,
         })
-      )
-    );
+      ),
+    ];
   }
 }
 
 module.exports = JSTerm;
--- a/devtools/client/webconsole/components/moz.build
+++ b/devtools/client/webconsole/components/moz.build
@@ -3,17 +3,16 @@
 # 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/.
 
 DIRS += [
     'message-types'
 ]
 
 DevToolsModules(
-    'App.js',
     'CollapseButton.js',
     'ConsoleOutput.js',
     'ConsoleTable.js',
     'FilterBar.js',
     'FilterButton.js',
     'FilterCheckbox.js',
     'GripMessageBody.js',
     'JSTerm.js',
--- a/devtools/client/webconsole/constants.js
+++ b/devtools/client/webconsole/constants.js
@@ -22,18 +22,16 @@ const actionTypes = {
   NETWORK_UPDATE_REQUEST: "NETWORK_UPDATE_REQUEST",
   PERSIST_TOGGLE: "PERSIST_TOGGLE",
   PRIVATE_MESSAGES_CLEAR: "PRIVATE_MESSAGES_CLEAR",
   REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
   SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
   SIDEBAR_CLOSE: "SIDEBAR_CLOSE",
   SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR",
   TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
-  APPEND_NOTIFICATION: "APPEND_NOTIFICATION",
-  REMOVE_NOTIFICATION: "REMOVE_NOTIFICATION",
 };
 
 const prefs = {
   PREFS: {
     // Filter preferences only have the suffix since they can be used either for the
     // webconsole or the browser console.
     FILTER: {
       ERROR: "filter.error",
--- a/devtools/client/webconsole/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output-wrapper.js
@@ -1,26 +1,30 @@
 /* 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 { createElement, createFactory } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const actions = require("devtools/client/webconsole/actions/index");
 const { createContextMenu } = require("devtools/client/webconsole/utils/context-menu");
 const { configureStore } = require("devtools/client/webconsole/store");
 const { isPacketPrivate } = require("devtools/client/webconsole/utils/messages");
 const { getAllMessagesById, getMessage } = require("devtools/client/webconsole/selectors/messages");
 const Telemetry = require("devtools/client/shared/telemetry");
 
 const EventEmitter = require("devtools/shared/event-emitter");
-const App = createFactory(require("devtools/client/webconsole/components/App"));
+const ConsoleOutput = createFactory(require("devtools/client/webconsole/components/ConsoleOutput"));
+const FilterBar = createFactory(require("devtools/client/webconsole/components/FilterBar"));
+const SideBar = createFactory(require("devtools/client/webconsole/components/SideBar"));
+const JSTerm = createFactory(require("devtools/client/webconsole/components/JSTerm"));
 
 let store = null;
 
 function NewConsoleOutputWrapper(parentNode, hud, toolbox, owner, document) {
   EventEmitter.decorate(this);
 
   this.parentNode = parentNode;
   this.hud = hud;
@@ -34,17 +38,16 @@ function NewConsoleOutputWrapper(parentN
   this.queuedMessageUpdates = [];
   this.queuedRequestUpdates = [];
   this.throttledDispatchPromise = null;
 
   this._telemetry = new Telemetry();
 
   store = configureStore(this.hud);
 }
-
 NewConsoleOutputWrapper.prototype = {
   init: function() {
     return new Promise((resolve) => {
       const attachRefToHud = (id, node) => {
         this.hud[id] = node;
       };
       // Focus the input line whenever the output area is clicked.
       this.parentNode.addEventListener("click", (event) => {
@@ -203,25 +206,38 @@ NewConsoleOutputWrapper.prototype = {
             let onNodeFrontSet = this.toolbox.selection
               .setNodeFront(front, { reason: "console" });
 
             return Promise.all([onNodeFrontSet, onInspectorUpdated]);
           }
         });
       }
 
-      const app = App({
-        attachRefToHud,
-        serviceContainer,
-        hud,
-        onFirstMeaningfulPaint: resolve,
-      });
-
-      // Render the root Application component.
-      let provider = createElement(Provider, { store }, app);
+      let provider = createElement(
+        Provider,
+        { store },
+        dom.div(
+          {className: "webconsole-output-wrapper"},
+          FilterBar({
+            hidePersistLogsCheckbox: this.hud.isBrowserConsole,
+            serviceContainer: {
+              attachRefToHud
+            }
+          }),
+          ConsoleOutput({
+            serviceContainer,
+            onFirstMeaningfulPaint: resolve
+          }),
+          SideBar({
+            serviceContainer,
+          }),
+          JSTerm({
+            hud: this.hud,
+          }),
+        ));
       this.body = ReactDOM.render(provider, this.parentNode);
     });
   },
 
   dispatchMessageAdd: function(packet, waitForResponse) {
     // Wait for the message to render to resolve with the DOM node.
     // This is just for backwards compatibility with old tests, and should
     // be removed once it's not needed anymore.
--- a/devtools/client/webconsole/reducers/index.js
+++ b/devtools/client/webconsole/reducers/index.js
@@ -4,17 +4,15 @@
  * 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 { filters } = require("./filters");
 const { messages } = require("./messages");
 const { prefs } = require("./prefs");
 const { ui } = require("./ui");
-const { notifications } = require("./notifications");
 
 exports.reducers = {
   filters,
   messages,
   prefs,
   ui,
-  notifications,
 };
--- a/devtools/client/webconsole/reducers/moz.build
+++ b/devtools/client/webconsole/reducers/moz.build
@@ -2,12 +2,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(
     'filters.js',
     'index.js',
     'messages.js',
-    'notifications.js',
     'prefs.js',
     'ui.js',
 )
deleted file mode 100644
--- a/devtools/client/webconsole/reducers/notifications.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* 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 {
-  APPEND_NOTIFICATION,
-  REMOVE_NOTIFICATION,
-} = require("devtools/client/webconsole/constants");
-
-const {
-  appendNotification,
-  removeNotificationWithValue
-} = require("devtools/client/shared/components/NotificationBox");
-
-/**
- * Create default initial state for this reducer. The state is composed
- * from list of notifications.
- */
-function getInitialState() {
-  return {
-    notifications: undefined,
-  };
-}
-
-/**
- * Reducer function implementation. This reducers is responsible
- * for maintaining list of notifications. It's consumed by
- * `NotificationBox` component.
- */
-function notifications(state = getInitialState(), action) {
-  switch (action.type) {
-    case APPEND_NOTIFICATION:
-      return append(state, action);
-    case REMOVE_NOTIFICATION:
-      return remove(state, action);
-  }
-
-  return state;
-}
-
-// Helpers
-
-function append(state, action) {
-  return appendNotification(state, action);
-}
-
-function remove(state, action) {
-  return removeNotificationWithValue(state.notifications, action.value);
-}
-
-// Exports
-
-module.exports = {
-  notifications,
-};
--- a/devtools/client/webconsole/selectors/moz.build
+++ b/devtools/client/webconsole/selectors/moz.build
@@ -1,12 +1,11 @@
 # vim: set filetype=python:
 # 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(
     'filters.js',
     'messages.js',
-    'notifications.js',
     'prefs.js',
     'ui.js',
 )
deleted file mode 100644
--- a/devtools/client/webconsole/selectors/notifications.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* 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";
-
-function getAllNotifications(state) {
-  return state.notifications.notifications;
-}
-
-module.exports = {
-  getAllNotifications,
-};
--- a/devtools/client/webconsole/utils.js
+++ b/devtools/client/webconsole/utils.js
@@ -23,18 +23,16 @@ const CONSOLE_ENTRY_THRESHOLD = 5;
 exports.CONSOLE_WORKER_IDS = [
   "SharedWorker",
   "ServiceWorker",
   "Worker"
 ];
 
 var WebConsoleUtils = {
 
-  CONSOLE_ENTRY_THRESHOLD,
-
   /**
    * Wrap a string in an nsISupportsString object.
    *
    * @param string string
    * @return nsISupportsString
    */
   supportsString: function(string) {
     let str = Cc["@mozilla.org/supports-string;1"]