Bug 1523868 - Add a doorhanger options menu in console toolbar - Part 1.
authorArmando Ferreira <armando.ferreira@edu.uag.mx>
Tue, 15 Oct 2019 17:04:30 +0000
changeset 497725 a58b1f87957170a558be3f7eea1834bf54c59f03
parent 497724 df7e0599db69ac1067d4d909ca5726aa2e132620
child 497726 3b3e905c1425f9a29978e30753d377bf371e5583
push id98020
push usernchevobbe@mozilla.com
push dateTue, 15 Oct 2019 18:27:09 +0000
treeherderautoland@a58b1f879571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1523868
milestone71.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 1523868 - Add a doorhanger options menu in console toolbar - Part 1. Differential Revision: https://phabricator.services.mozilla.com/D47979
devtools/client/framework/test/metrics/browser_metrics_webconsole.js
devtools/client/framework/toolbox-options.xhtml
devtools/client/locales/en-US/toolbox.dtd
devtools/client/locales/en-US/webconsole.properties
devtools/client/webconsole/actions/ui.js
devtools/client/webconsole/components/App.css
devtools/client/webconsole/components/App.js
devtools/client/webconsole/components/FilterBar/ConsoleSettings.js
devtools/client/webconsole/components/FilterBar/FilterBar.js
devtools/client/webconsole/components/FilterBar/moz.build
devtools/client/webconsole/reducers/prefs.js
devtools/client/webconsole/reducers/ui.js
devtools/client/webconsole/selectors/prefs.js
devtools/client/webconsole/test/browser/browser_console_chrome_context_message.js
devtools/client/webconsole/test/browser/browser_console_webconsole_console_api_calls.js
devtools/client/webconsole/test/browser/browser_jsterm_no_input_and_tab_key_pressed.js
devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_entire_message.js
devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js
devtools/client/webconsole/test/browser/browser_webconsole_persist.js
devtools/client/webconsole/test/browser/browser_webconsole_telemetry_persist_toggle_changed.js
devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js
devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js
devtools/client/webconsole/test/browser/head.js
devtools/client/webconsole/test/node/components/filter-bar.test.js
devtools/client/webconsole/test/node/fixtures/DevToolsUtils.js
devtools/client/webconsole/test/node/mocha-test-setup.js
devtools/client/webconsole/test/node/package.json
devtools/client/webconsole/webconsole-ui.js
devtools/client/webconsole/webconsole-wrapper.js
--- a/devtools/client/framework/test/metrics/browser_metrics_webconsole.js
+++ b/devtools/client/framework/test/metrics/browser_metrics_webconsole.js
@@ -31,16 +31,19 @@ add_task(async function() {
     "@loader/options.js",
     "chrome.js",
     "resource://devtools/client/webconsole/constants.js",
     "resource://devtools/client/webconsole/utils.js",
     "resource://devtools/client/webconsole/utils/messages.js",
     "resource://devtools/client/webconsole/utils/l10n.js",
     "resource://devtools/client/netmonitor/src/utils/request-utils.js",
     "resource://devtools/client/webconsole/types.js",
+    "resource://devtools/client/shared/components/menu/MenuButton.js",
+    "resource://devtools/client/shared/components/menu/MenuItem.js",
+    "resource://devtools/client/shared/components/menu/MenuList.js",
     "resource://devtools/client/shared/vendor/react.js",
     "resource://devtools/client/shared/vendor/react-dom.js",
     "resource://devtools/client/shared/vendor/react-prop-types.js",
     "resource://devtools/client/shared/vendor/react-dom-factories.js",
   ];
   runDuplicatedModulesTest(loaders, whitelist);
 
   runMetricsTest({
--- a/devtools/client/framework/toolbox-options.xhtml
+++ b/devtools/client/framework/toolbox-options.xhtml
@@ -61,32 +61,16 @@
             <option value="hex">&options.defaultColorUnit.hex;</option>
             <option value="hsl">&options.defaultColorUnit.hsl;</option>
             <option value="rgb">&options.defaultColorUnit.rgb;</option>
             <option value="name">&options.defaultColorUnit.name;</option>
           </select>
         </label>
       </fieldset>
 
-      <fieldset id="webconsole-options" class="options-groupbox">
-        <legend>&options.webconsole.label;</legend>
-        <label title="&options.timestampMessages.tooltip;">
-          <input type="checkbox"
-                 id="webconsole-timestamp-messages"
-                 data-pref="devtools.webconsole.timestampMessages"/>
-          <span>&options.timestampMessages.label;</span>
-        </label>
-        <label title="&options.warningGroups.tooltip;">
-          <input type="checkbox"
-                 id="webconsole-warning-groups"
-                 data-pref="devtools.webconsole.groupWarningMessages"/>
-          <span>&options.warningGroups.label;</span>
-        </label>
-      </fieldset>
-
       <fieldset id="debugger-options" class="options-groupbox" hidden="true">
         <legend>&options.debugger.label;</legend>
       </fieldset>
 
       <fieldset id="styleeditor-options" class="options-groupbox">
         <legend>&options.styleeditor.label;</legend>
         <label title="&options.stylesheetAutocompletion.tooltip;">
           <input type="checkbox"
--- a/devtools/client/locales/en-US/toolbox.dtd
+++ b/devtools/client/locales/en-US/toolbox.dtd
@@ -110,30 +110,16 @@
   -  for the target of the toolbox. -->
 <!ENTITY options.toolNotSupported.label  "* Not supported for current toolbox target">
 
 <!-- LOCALIZATION NOTE (options.selectDevToolsTheme.label2): This is the label for
   -  the heading of the radiobox corresponding to the theme of the developer
   -  tools. -->
 <!ENTITY options.selectDevToolsTheme.label2   "Themes">
 
-<!-- LOCALIZATION NOTE (options.webconsole.label): This is the label for the
-  -  heading of the group of Web Console preferences in the options panel. -->
-<!ENTITY options.webconsole.label            "Web Console">
-
-<!-- LOCALIZATION NOTE (options.timestampMessages.label): This is the
-   - label for the checkbox that toggles timestamps in the Web Console -->
-<!ENTITY options.timestampMessages.label      "Enable timestamps">
-<!ENTITY options.timestampMessages.tooltip    "If you enable this option commands and output in the Web Console will display a timestamp">
-
-<!-- LOCALIZATION NOTE (options.warningGroups.label): This is the
-   - label for the checkbox that toggles the warningGroups feature in the Web Console -->
-<!ENTITY options.warningGroups.label      "Group similar messages">
-<!ENTITY options.warningGroups.tooltip    "When enabled, similar messages are placed into groups">
-
 <!-- LOCALIZATION NOTE (options.debugger.label): This is the label for the
   -  heading of the group of Debugger preferences in the options panel. -->
 <!ENTITY options.debugger.label            "Debugger">
 
 <!-- LOCALIZATION NOTE (options.sourceMaps.label): This is the
    - label for the checkbox that toggles source maps in all tools -->
 <!ENTITY options.sourceMaps.label      "Enable Source Maps">
 <!ENTITY options.sourceMaps.tooltip1   "If you enable this option sources will be mapped in the tools.">
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -303,20 +303,42 @@ webconsole.filteredMessagesByText.label=
 # LOCALIZATION NOTE (webconsole.filteredMessagesByText.tooltip)
 # Tooltip on the filter input "hidden" text, displayed when some console messages are
 # hidden because the user has filled in the input.
 # This is a semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # example: 345 items hidden by text filter.
 webconsole.filteredMessagesByText.tooltip=#1 item hidden by text filter;#1 items hidden by text filter
 
-# LOCALIZATION NOTE (webconsole.enablePersistentLogs.label)
-webconsole.enablePersistentLogs.label=Persist Logs
-# LOCALIZATION NOTE (webconsole.enablePersistentLogs.tooltip)
-webconsole.enablePersistentLogs.tooltip=If you enable this option the output will not be cleared each time you navigate to a new page
+# LOCALIZATION NOTE (webconsole.console.settings.menu.menuButton.tooltip)
+# Tooltip for the filter bar preferences menu. This menu will display multiple perefences for the
+# filter bar, such as enabling the compact toolbar mode, enable the timestamps, persist logs, etc
+webconsole.console.settings.menu.button.tooltip=Console Settings
+
+# LOCALIZATION NOTE (webconsole.console.settings.menu.item.compactToolbar.label)
+# Label for the `Compact Toolbar` preference option. This will turn the message filters buttons
+# into a Menu Button, making the filter bar more compact.
+webconsole.console.settings.menu.item.compactToolbar.label=Compact Toolbar
+
+# LOCALIZATION NOTE (webconsole.console.settings.menu.item.timestamps.label)
+# Label for enabling the timestamps in the Web Console.
+webconsole.console.settings.menu.item.timestamps.label=Show Timestamps
+# LOCALIZATION NOTE (webconsole.console.settings.menu.item.timestamps.tooltip)
+webconsole.console.settings.menu.item.timestamps.tooltip=If you enable this option commands and output in the Web Console will display a timestamp
+
+# LOCALIZATION NOTE (webconsole.console.settings.menu.item.warningGroups.label)
+# Label for grouping the similar messages in the Web Console
+webconsole.console.settings.menu.item.warningGroups.label=Group Similar Messages
+# LOCALIZATION NOTE (webconsole.console.settings.menu.item.warningGroups.tooltip)
+webconsole.console.settings.menu.item.warningGroups.tooltip=When enabled, similar messages are placed into groups
+
+# LOCALIZATION NOTE (webconsole.console.settings.menu.item.enablePersistentLogs.label)
+webconsole.console.settings.menu.item.enablePersistentLogs.label=Persist Logs
+# LOCALIZATION NOTE (webconsole.console.settings.menu.item.enablePersistentLogs.tooltip)
+webconsole.console.settings.menu.item.enablePersistentLogs.tooltip=If you enable this option the output will not be cleared each time you navigate to a new page
 
 # LOCALIZATION NOTE (browserconsole.contentMessagesCheckbox.label)
 # Label used in the browser console filter bar. This label is used for a checkbox that
 # allows the user to show or hide console messages from the content process in the browser
 # console.
 browserconsole.contentMessagesCheckbox.label=Show Content Messages
 # LOCALIZATION NOTE (browserconsole.contentMessagesCheckbox.tooltip)
 # Tooltip for the "Show content messages" checkbox in the Browser Console filter bar.
--- a/devtools/client/webconsole/actions/ui.js
+++ b/devtools/client/webconsole/actions/ui.js
@@ -1,14 +1,15 @@
 /* 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 { getAllPrefs } = require("devtools/client/webconsole/selectors/prefs");
 const { getAllUi } = require("devtools/client/webconsole/selectors/ui");
 const { getMessage } = require("devtools/client/webconsole/selectors/messages");
 
 const {
   INITIALIZE,
   PERSIST_TOGGLE,
   PREFS,
   REVERSE_SEARCH_INPUT_TOGGLE,
@@ -49,27 +50,39 @@ function contentMessagesToggle() {
     const uiState = getAllUi(getState());
     prefsService.setBoolPref(
       PREFS.UI.CONTENT_MESSAGES,
       uiState.showContentMessages
     );
   };
 }
 
-function timestampsToggle(visible) {
-  return {
-    type: TIMESTAMPS_TOGGLE,
-    visible,
+function timestampsToggle() {
+  return ({ dispatch, getState, prefsService }) => {
+    dispatch({
+      type: TIMESTAMPS_TOGGLE,
+    });
+    const uiState = getAllUi(getState());
+    prefsService.setBoolPref(
+      PREFS.UI.MESSAGE_TIMESTAMP,
+      uiState.timestampsVisible
+    );
   };
 }
 
-function warningGroupsToggle(value) {
-  return {
-    type: WARNING_GROUPS_TOGGLE,
-    value,
+function warningGroupsToggle() {
+  return ({ dispatch, getState, prefsService }) => {
+    dispatch({
+      type: WARNING_GROUPS_TOGGLE,
+    });
+    const prefsState = getAllPrefs(getState());
+    prefsService.setBoolPref(
+      PREFS.FEATURES.GROUP_WARNINGS,
+      prefsState.groupWarnings
+    );
   };
 }
 
 function selectNetworkMessageTab(id) {
   return {
     type: SELECT_NETWORK_MESSAGE_TAB,
     id,
   };
--- a/devtools/client/webconsole/components/App.css
+++ b/devtools/client/webconsole/components/App.css
@@ -96,16 +96,30 @@ body {
   flex-shrink: 100000;
   overflow-x: hidden;
 }
 
 .flexible-output-input > .webconsole-output:not(:empty) {
   min-height: 19px;
 }
 
+/* webconsole.css | chrome://devtools/skin/webconsole.css */
+.webconsole-filteringbar-wrapper .devtools-toolbar {
+  padding-inline-end: 0;
+}
+
+.devtools-button.webconsole-console-settings-menu-button {
+  height: 100%;
+  margin: 0;
+}
+
+.webconsole-console-settings-menu-button::before {
+  background-image: url("chrome://devtools/skin/images/settings.svg");
+}
+
 .webconsole-app .jsterm-input-container {
   min-height: 28px;
   padding-block-start: 2px;
   overflow-y: auto;
   overflow-x: hidden;
   flex-grow: 1;
   /* We display the open editor button at the end of the input */
   display: grid;
@@ -165,17 +179,17 @@ body {
   overflow: auto;
 }
 
 .webconsole-sidebar-toolbar .sidebar-close-button {
   margin: 0;
 }
 
 .sidebar-close-button::before {
-  background-image: url(chrome://devtools/skin/images/close.svg);
+  background-image: url("chrome://devtools/skin/images/close.svg");
 }
 
 .sidebar-contents .object-inspector {
   min-width: 100%;
 }
 
 /** EDITOR MODE */
 .webconsole-app.jsterm-editor {
@@ -244,17 +258,17 @@ body {
      * +------------------------------------------+
      *
      */
   grid-template-columns: auto 1fr auto auto auto auto auto;
   height: unset;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-executeButton {
-  background-image: url(chrome://devtools/skin/images/webconsole/run.svg);
+  background-image: url("chrome://devtools/skin/images/webconsole/run.svg");
   -moz-context-properties: fill;
   fill: currentColor;
   padding-inline-start: 22px;
   background-repeat: no-repeat;
   background-position: 4px center;
   padding-inline-end: 8px;
   height: 20px;
   margin-inline-start: 5px;
@@ -277,32 +291,32 @@ body {
   grid-column: -2 / -3;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-closeButton {
   grid-column: -1 / -2;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-prevExpressionButton::before {
-  background-image: url(chrome://devtools/skin/images/arrowhead-up.svg);
+  background-image: url("chrome://devtools/skin/images/arrowhead-up.svg");
   background-size: 16px;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-history-nextExpressionButton::before {
-  background-image: url(chrome://devtools/skin/images/arrowhead-down.svg);
+  background-image: url("chrome://devtools/skin/images/arrowhead-down.svg");
   background-size: 16px;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-reverseSearchButton::before {
-  background-image: url(chrome://devtools/skin/images/webconsole/reverse-search.svg);
+  background-image: url("chrome://devtools/skin/images/webconsole/reverse-search.svg");
   background-size: 14px;
 }
 
 .jsterm-editor .webconsole-editor-toolbar .webconsole-editor-toolbar-closeButton::before {
-  background-image: url(chrome://devtools/skin/images/close.svg);
+  background-image: url("chrome://devtools/skin/images/close.svg");
 }
 
 .jsterm-editor .webconsole-input-openEditorButton {
   display: none;
 }
 
 .jsterm-editor .webconsole-output {
   grid-column: 2 / 3;
--- a/devtools/client/webconsole/components/App.js
+++ b/devtools/client/webconsole/components/App.js
@@ -245,24 +245,26 @@ class App extends Component {
   }
 
   renderFilterBar() {
     const {
       closeSplitConsole,
       filterBarDisplayMode,
       hidePersistLogsCheckbox,
       hideShowContentMessagesCheckbox,
+      webConsoleUI,
     } = this.props;
 
     return FilterBar({
       key: "filterbar",
       hidePersistLogsCheckbox,
       hideShowContentMessagesCheckbox,
       closeSplitConsole,
       displayMode: filterBarDisplayMode,
+      webConsoleUI,
     });
   }
 
   renderEditorToolbar() {
     const {
       editorMode,
       editorFeatureEnabled,
       dispatch,
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/components/FilterBar/ConsoleSettings.js
@@ -0,0 +1,144 @@
+/* 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 { Component } = require("devtools/client/shared/vendor/react");
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const actions = require("devtools/client/webconsole/actions/index");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+
+// Additional Components
+const MenuButton = createFactory(
+  require("devtools/client/shared/components/menu/MenuButton")
+);
+const MenuItem = createFactory(
+  require("devtools/client/shared/components/menu/MenuItem")
+);
+const MenuList = createFactory(
+  require("devtools/client/shared/components/menu/MenuList")
+);
+
+class ConsoleSettings extends Component {
+  static get propTypes() {
+    return {
+      compactToolbar: PropTypes.bool.isRequired,
+      dispatch: PropTypes.func.isRequired,
+      groupWarnings: PropTypes.bool.isRequired,
+      hideCompactToolbarCheckbox: PropTypes.bool.isRequired,
+      hidePersistLogsCheckbox: PropTypes.bool.isRequired,
+      hideShowContentMessagesCheckbox: PropTypes.bool.isRequired,
+      persistLogs: PropTypes.bool.isRequired,
+      showContentMessages: PropTypes.bool.isRequired,
+      timestampsVisible: PropTypes.bool.isRequired,
+      webConsoleUI: PropTypes.object.isRequired,
+    };
+  }
+
+  renderMenuItems() {
+    const {
+      dispatch,
+      groupWarnings,
+      hidePersistLogsCheckbox,
+      hideShowContentMessagesCheckbox,
+      persistLogs,
+      showContentMessages,
+      timestampsVisible,
+    } = this.props;
+
+    const items = [];
+
+    // Persist Logs
+    if (!hidePersistLogsCheckbox) {
+      items.push(
+        MenuItem({
+          key: "webconsole-console-settings-menu-item-persistent-logs",
+          checked: persistLogs,
+          className:
+            "menu-item webconsole-console-settings-menu-item-persistentLogs",
+          label: l10n.getStr(
+            "webconsole.console.settings.menu.item.enablePersistentLogs.label"
+          ),
+          tooltip: l10n.getStr(
+            "webconsole.console.settings.menu.item.enablePersistentLogs.tooltip"
+          ),
+          onClick: () => dispatch(actions.persistToggle()),
+        })
+      );
+    }
+
+    // Show Content Messages
+    if (!hideShowContentMessagesCheckbox) {
+      items.push(
+        MenuItem({
+          key: "webconsole-console-settings-menu-item-content-messages",
+          checked: showContentMessages,
+          className:
+            "menu-item webconsole-console-settings-menu-item-contentMessages",
+          label: l10n.getStr("browserconsole.contentMessagesCheckbox.label"),
+          tooltip: l10n.getStr(
+            "browserconsole.contentMessagesCheckbox.tooltip"
+          ),
+          onClick: () => dispatch(actions.contentMessagesToggle()),
+        })
+      );
+    }
+
+    // Timestamps
+    items.push(
+      MenuItem({
+        key: "webconsole-console-settings-menu-item-timestamps",
+        checked: timestampsVisible,
+        className: "menu-item webconsole-console-settings-menu-item-timestamps",
+        label: l10n.getStr(
+          "webconsole.console.settings.menu.item.timestamps.label"
+        ),
+        tooltip: l10n.getStr(
+          "webconsole.console.settings.menu.item.timestamps.tooltip"
+        ),
+        onClick: () => dispatch(actions.timestampsToggle()),
+      })
+    );
+
+    // Warning Groups
+    items.push(
+      MenuItem({
+        key: "webconsole-console-settings-menu-item-warning-groups",
+        checked: groupWarnings,
+        className:
+          "menu-item webconsole-console-settings-menu-item-warning-groups",
+        label: l10n.getStr(
+          "webconsole.console.settings.menu.item.warningGroups.label"
+        ),
+        tooltip: l10n.getStr(
+          "webconsole.console.settings.menu.item.warningGroups.tooltip"
+        ),
+        onClick: () => dispatch(actions.warningGroupsToggle()),
+      })
+    );
+
+    return MenuList({ id: "webconsole-console-settings-menu-list" }, items);
+  }
+
+  render() {
+    const { webConsoleUI } = this.props;
+    const doc = webConsoleUI.document;
+    const toolbox = webConsoleUI.wrapper.toolbox;
+
+    return MenuButton(
+      {
+        menuId: "webconsole-console-settings-menu-button",
+        doc: toolbox ? toolbox.doc : doc,
+        className: "devtools-button webconsole-console-settings-menu-button",
+        title: l10n.getStr("webconsole.console.settings.menu.button.tooltip"),
+      },
+      this.renderMenuItems()
+    );
+  }
+}
+
+module.exports = ConsoleSettings;
--- a/devtools/client/webconsole/components/FilterBar/FilterBar.js
+++ b/devtools/client/webconsole/components/FilterBar/FilterBar.js
@@ -1,36 +1,49 @@
 /* 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 {
   Component,
   createFactory,
 } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+// Actions
+const actions = require("devtools/client/webconsole/actions/index");
+
+// Selectors
 const {
   getAllFilters,
 } = require("devtools/client/webconsole/selectors/filters");
 const {
   getFilteredMessagesCount,
 } = require("devtools/client/webconsole/selectors/messages");
+const { getAllPrefs } = require("devtools/client/webconsole/selectors/prefs");
 const { getAllUi } = require("devtools/client/webconsole/selectors/ui");
-const actions = require("devtools/client/webconsole/actions/index");
+
+// Utilities
 const { l10n } = require("devtools/client/webconsole/utils/messages");
 const { PluralForm } = require("devtools/shared/plural-form");
+
+// Constants
 const {
   FILTERS,
   FILTERBAR_DISPLAY_MODES,
 } = require("devtools/client/webconsole/constants");
 
+// Additional Components
 const FilterButton = require("devtools/client/webconsole/components/FilterBar/FilterButton");
-const FilterCheckbox = require("devtools/client/webconsole/components/FilterBar/FilterCheckbox");
+const ConsoleSettings = createFactory(
+  require("devtools/client/webconsole/components/FilterBar/ConsoleSettings")
+);
 const SearchBox = createFactory(
   require("devtools/client/shared/components/SearchBox")
 );
 
 loader.lazyRequireGetter(
   this,
   "PropTypes",
   "devtools/client/shared/vendor/react-prop-types"
@@ -38,46 +51,43 @@ loader.lazyRequireGetter(
 
 const disabledCssFilterButtonTitle = l10n.getStr(
   "webconsole.cssFilterButton.inactive.tooltip"
 );
 
 class FilterBar extends Component {
   static get propTypes() {
     return {
-      dispatch: PropTypes.func.isRequired,
-      filter: PropTypes.object.isRequired,
-      persistLogs: PropTypes.bool.isRequired,
-      hidePersistLogsCheckbox: PropTypes.bool.isRequired,
-      showContentMessages: PropTypes.bool.isRequired,
-      hideShowContentMessagesCheckbox: PropTypes.bool.isRequired,
-      filteredMessagesCount: PropTypes.object.isRequired,
       closeButtonVisible: PropTypes.bool,
       closeSplitConsole: PropTypes.func,
+      dispatch: PropTypes.func.isRequired,
       displayMode: PropTypes.oneOf([...Object.values(FILTERBAR_DISPLAY_MODES)])
         .isRequired,
+      filter: PropTypes.object.isRequired,
+      filteredMessagesCount: PropTypes.object.isRequired,
+      groupWarnings: PropTypes.bool.isRequired,
+      hidePersistLogsCheckbox: PropTypes.bool.isRequired,
+      hideShowContentMessagesCheckbox: PropTypes.bool.isRequired,
+      persistLogs: PropTypes.bool.isRequired,
+      showContentMessages: PropTypes.bool.isRequired,
+      timestampsVisible: PropTypes.bool.isRequired,
+      webConsoleUI: PropTypes.object.isRequired,
     };
   }
 
   static get defaultProps() {
     return {
       hidePersistLogsCheckbox: false,
       hideShowContentMessagesCheckbox: true,
     };
   }
 
   constructor(props) {
     super(props);
-
-    this.onClickMessagesClear = this.onClickMessagesClear.bind(this);
-    this.onSearchBoxChange = this.onSearchBoxChange.bind(this);
-    this.onChangePersistToggle = this.onChangePersistToggle.bind(this);
-    this.onChangeShowContent = this.onChangeShowContent.bind(this);
     this.renderFiltersConfigBar = this.renderFiltersConfigBar.bind(this);
-
     this.maybeUpdateLayout = this.maybeUpdateLayout.bind(this);
     this.resizeObserver = new ResizeObserver(this.maybeUpdateLayout);
   }
 
   componentDidMount() {
     this.filterInputMinWidth = 150;
     try {
       const filterInput = this.wrapperNode.querySelector(".devtools-searchbox");
@@ -91,51 +101,45 @@ class FilterBar extends Component {
     }
 
     this.maybeUpdateLayout();
     this.resizeObserver.observe(this.wrapperNode);
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     const {
+      closeButtonVisible,
+      displayMode,
       filter,
+      filteredMessagesCount,
+      groupWarnings,
       persistLogs,
       showContentMessages,
-      filteredMessagesCount,
-      closeButtonVisible,
-      displayMode,
+      timestampsVisible,
     } = this.props;
 
-    if (nextProps.filter !== filter) {
-      return true;
-    }
-
-    if (nextProps.persistLogs !== persistLogs) {
-      return true;
-    }
-
-    if (nextProps.showContentMessages !== showContentMessages) {
+    if (
+      nextProps.closeButtonVisible !== closeButtonVisible ||
+      nextProps.displayMode !== displayMode ||
+      nextProps.filter !== filter ||
+      nextProps.groupWarnings !== groupWarnings ||
+      nextProps.persistLogs !== persistLogs ||
+      nextProps.showContentMessages !== showContentMessages ||
+      nextProps.timestampsVisible !== timestampsVisible
+    ) {
       return true;
     }
 
     if (
       JSON.stringify(nextProps.filteredMessagesCount) !==
       JSON.stringify(filteredMessagesCount)
     ) {
       return true;
     }
 
-    if (nextProps.closeButtonVisible != closeButtonVisible) {
-      return true;
-    }
-
-    if (nextProps.displayMode != displayMode) {
-      return true;
-    }
-
     return false;
   }
 
   /**
    * Update the boolean state that informs where the filter buttons should be rendered.
    * If the filter buttons are rendered inline with the filter input and the filter
    * input width is reduced below a threshold, the filter buttons are rendered on a new
    * row. When the filter buttons are on a separate row and the filter input grows
@@ -183,30 +187,28 @@ class FilterBar extends Component {
         filterInputWidth - this.filterInputMinWidth >
         filterButtonsToolbarWidth
       ) {
         dispatch(actions.filterBarDisplayModeSet(FILTERBAR_DISPLAY_MODES.WIDE));
       }
     }
   }
 
-  onClickMessagesClear() {
-    this.props.dispatch(actions.messagesClear());
+  renderSeparator() {
+    return dom.div({
+      className: "devtools-separator",
+    });
   }
 
-  onSearchBoxChange(text) {
-    this.props.dispatch(actions.filterTextSet(text));
-  }
-
-  onChangePersistToggle() {
-    this.props.dispatch(actions.persistToggle());
-  }
-
-  onChangeShowContent() {
-    this.props.dispatch(actions.contentMessagesToggle());
+  renderClearButton() {
+    return dom.button({
+      className: "devtools-button devtools-clear-icon",
+      title: l10n.getStr("webconsole.clearButton.tooltip"),
+      onClick: () => this.props.dispatch(actions.messagesClear()),
+    });
   }
 
   renderFiltersConfigBar() {
     const { dispatch, filter, filteredMessagesCount } = this.props;
 
     const getLabel = (baseLabel, filterKey) => {
       const count = filteredMessagesCount[filterKey];
       if (filter[filterKey] || count === 0) {
@@ -285,29 +287,18 @@ class FilterBar extends Component {
         active: filter[FILTERS.NET],
         label: l10n.getStr("webconsole.requestsFilterButton.label"),
         filterKey: FILTERS.NET,
         dispatch,
       })
     );
   }
 
-  render() {
-    const {
-      persistLogs,
-      hidePersistLogsCheckbox,
-      hideShowContentMessagesCheckbox,
-      closeSplitConsole,
-      displayMode,
-      showContentMessages,
-      filteredMessagesCount,
-    } = this.props;
-
-    const isNarrow = displayMode === FILTERBAR_DISPLAY_MODES.NARROW;
-    const isWide = displayMode === FILTERBAR_DISPLAY_MODES.WIDE;
+  renderSearchBox() {
+    const { dispatch, filteredMessagesCount } = this.props;
 
     let searchBoxSummary;
     let searchBoxSummaryTooltip;
     if (filteredMessagesCount.text > 0) {
       searchBoxSummary = l10n.getStr("webconsole.filteredMessagesByText.label");
       searchBoxSummary = PluralForm.get(
         filteredMessagesCount.text,
         searchBoxSummary
@@ -317,90 +308,105 @@ class FilterBar extends Component {
         "webconsole.filteredMessagesByText.tooltip"
       );
       searchBoxSummaryTooltip = PluralForm.get(
         filteredMessagesCount.text,
         searchBoxSummaryTooltip
       ).replace("#1", filteredMessagesCount.text);
     }
 
+    return SearchBox({
+      type: "filter",
+      placeholder: l10n.getStr("webconsole.filterInput.placeholder"),
+      keyShortcut: l10n.getStr("webconsole.find.key"),
+      onChange: text => dispatch(actions.filterTextSet(text)),
+      summary: searchBoxSummary,
+      summaryTooltip: searchBoxSummaryTooltip,
+    });
+  }
+
+  renderSettingsButton() {
+    const {
+      dispatch,
+      groupWarnings,
+      hidePersistLogsCheckbox,
+      hideShowContentMessagesCheckbox,
+      persistLogs,
+      showContentMessages,
+      timestampsVisible,
+      webConsoleUI,
+    } = this.props;
+
+    return ConsoleSettings({
+      dispatch,
+      groupWarnings,
+      hidePersistLogsCheckbox,
+      hideShowContentMessagesCheckbox,
+      persistLogs,
+      showContentMessages,
+      timestampsVisible,
+      webConsoleUI,
+    });
+  }
+
+  renderCloseButton() {
+    const { closeSplitConsole } = this.props;
+
+    return dom.div(
+      {
+        className: "devtools-toolbar split-console-close-button-wrapper",
+        key: "wrapper",
+      },
+      dom.button({
+        id: "split-console-close-button",
+        key: "split-console-close-button",
+        className: "devtools-button",
+        title: l10n.getStr("webconsole.closeSplitConsoleButton.tooltip"),
+        onClick: () => {
+          closeSplitConsole();
+        },
+      })
+    );
+  }
+
+  render() {
+    const { closeButtonVisible, displayMode } = this.props;
+
+    const isNarrow = displayMode === FILTERBAR_DISPLAY_MODES.NARROW;
+    const isWide = displayMode === FILTERBAR_DISPLAY_MODES.WIDE;
+
+    const separator = this.renderSeparator();
+    const clearButton = this.renderClearButton();
+    const searchBox = this.renderSearchBox();
+    const filtersConfigBar = this.renderFiltersConfigBar();
+    const settingsButton = this.renderSettingsButton();
+
     const children = [
       dom.div(
         {
           className:
             "devtools-toolbar devtools-input-toolbar webconsole-filterbar-primary",
           key: "primary-bar",
         },
-        dom.button({
-          className: "devtools-button devtools-clear-icon",
-          title: l10n.getStr("webconsole.clearButton.tooltip"),
-          onClick: this.onClickMessagesClear,
-        }),
-        dom.div({
-          className: "devtools-separator",
-        }),
-        SearchBox({
-          type: "filter",
-          placeholder: l10n.getStr("webconsole.filterInput.placeholder"),
-          keyShortcut: l10n.getStr("webconsole.find.key"),
-          onChange: this.onSearchBoxChange,
-          summary: searchBoxSummary,
-          summaryTooltip: searchBoxSummaryTooltip,
-        }),
-        isWide &&
-          dom.div({
-            className: "devtools-separator",
-          }),
-        isWide && this.renderFiltersConfigBar(),
-        !(hidePersistLogsCheckbox && hideShowContentMessagesCheckbox)
-          ? dom.div({
-              className: "devtools-separator",
-            })
-          : null,
-        !hidePersistLogsCheckbox &&
-          FilterCheckbox({
-            label: l10n.getStr("webconsole.enablePersistentLogs.label"),
-            title: l10n.getStr("webconsole.enablePersistentLogs.tooltip"),
-            onChange: this.onChangePersistToggle,
-            checked: persistLogs,
-          }),
-        !hideShowContentMessagesCheckbox &&
-          FilterCheckbox({
-            label: l10n.getStr("browserconsole.contentMessagesCheckbox.label"),
-            title: l10n.getStr(
-              "browserconsole.contentMessagesCheckbox.tooltip"
-            ),
-            onChange: this.onChangeShowContent,
-            checked: showContentMessages,
-          })
+        clearButton,
+        separator,
+        searchBox,
+        isWide && separator,
+        isWide && filtersConfigBar,
+        separator,
+        settingsButton
       ),
     ];
 
-    if (this.props.closeButtonVisible) {
-      children.push(
-        dom.div(
-          {
-            className: "devtools-toolbar split-console-close-button-wrapper",
-            key: "wrapper",
-          },
-          dom.button({
-            id: "split-console-close-button",
-            key: "split-console-close-button",
-            className: "devtools-button",
-            title: l10n.getStr("webconsole.closeSplitConsoleButton.tooltip"),
-            onClick: () => {
-              closeSplitConsole();
-            },
-          })
-        )
-      );
+    if (closeButtonVisible) {
+      children.push(this.renderCloseButton());
     }
 
     if (isNarrow) {
-      children.push(this.renderFiltersConfigBar());
+      children.push(filtersConfigBar);
     }
 
     return dom.div(
       {
         className: `webconsole-filteringbar-wrapper ${displayMode}`,
         "aria-live": "off",
         ref: node => {
           this.wrapperNode = node;
@@ -408,18 +414,21 @@ class FilterBar extends Component {
       },
       children
     );
   }
 }
 
 function mapStateToProps(state) {
   const uiState = getAllUi(state);
+  const prefsState = getAllPrefs(state);
   return {
+    closeButtonVisible: uiState.closeButtonVisible,
     filter: getAllFilters(state),
+    filteredMessagesCount: getFilteredMessagesCount(state),
+    groupWarnings: prefsState.groupWarnings,
     persistLogs: uiState.persistLogs,
     showContentMessages: uiState.showContentMessages,
-    filteredMessagesCount: getFilteredMessagesCount(state),
-    closeButtonVisible: uiState.closeButtonVisible,
+    timestampsVisible: uiState.timestampsVisible,
   };
 }
 
 module.exports = connect(mapStateToProps)(FilterBar);
--- a/devtools/client/webconsole/components/FilterBar/moz.build
+++ b/devtools/client/webconsole/components/FilterBar/moz.build
@@ -1,10 +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(
+    'ConsoleSettings.js',
     'FilterBar.js',
     'FilterButton.js',
     'FilterCheckbox.js',
 )
\ No newline at end of file
--- a/devtools/client/webconsole/reducers/prefs.js
+++ b/devtools/client/webconsole/reducers/prefs.js
@@ -20,16 +20,18 @@ const PrefState = overrides =>
       overrides
     )
   );
 
 function prefs(state = PrefState(), action) {
   if (action.type === WARNING_GROUPS_TOGGLE) {
     return {
       ...state,
-      groupWarnings: action.value,
+      groupWarnings: !state.groupWarnings,
     };
   }
   return state;
 }
 
-exports.PrefState = PrefState;
-exports.prefs = prefs;
+module.exports = {
+  PrefState,
+  prefs,
+};
--- a/devtools/client/webconsole/reducers/ui.js
+++ b/devtools/client/webconsole/reducers/ui.js
@@ -48,17 +48,17 @@ const UiState = overrides =>
 
 function ui(state = UiState(), action) {
   switch (action.type) {
     case PERSIST_TOGGLE:
       return { ...state, persistLogs: !state.persistLogs };
     case SHOW_CONTENT_MESSAGES_TOGGLE:
       return { ...state, showContentMessages: !state.showContentMessages };
     case TIMESTAMPS_TOGGLE:
-      return { ...state, timestampsVisible: action.visible };
+      return { ...state, timestampsVisible: !state.timestampsVisible };
     case SELECT_NETWORK_MESSAGE_TAB:
       return { ...state, networkMessageActiveTabId: action.id };
     case SIDEBAR_CLOSE:
       return {
         ...state,
         sidebarVisible: false,
         gripInSidebar: null,
       };
--- a/devtools/client/webconsole/selectors/prefs.js
+++ b/devtools/client/webconsole/selectors/prefs.js
@@ -1,10 +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/. */
 "use strict";
 
+function getAllPrefs(state) {
+  return state.prefs;
+}
+
 function getLogLimit(state) {
   return state.prefs.logLimit;
 }
 
-exports.getLogLimit = getLogLimit;
+module.exports = {
+  getAllPrefs,
+  getLogLimit,
+};
--- a/devtools/client/webconsole/test/browser/browser_console_chrome_context_message.js
+++ b/devtools/client/webconsole/test/browser/browser_console_chrome_context_message.js
@@ -25,20 +25,20 @@ add_task(async function() {
   await waitFor(() =>
     expectedMessages.every(expectedMessage => findMessage(hud, expectedMessage))
   );
   await waitFor(() => findMessage(hud, "hello from content"));
 
   ok(true, "Expected messages are displayed in the browser console");
 
   info("Uncheck the Show content messages checkbox");
-  const checkbox = hud.ui.outputNode.querySelector(
-    ".webconsole-filterbar-primary .filter-checkbox"
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-contentMessages"
   );
-  checkbox.click();
   await waitFor(() => !findMessage(hud, "hello from content"));
 
   info("Check the expected messages are still visiable in the browser console");
   for (const expectedMessage of expectedMessages) {
     ok(
       findMessage(hud, expectedMessage),
       `"${expectedMessage}" should be still visible`
     );
--- a/devtools/client/webconsole/test/browser/browser_console_webconsole_console_api_calls.js
+++ b/devtools/client/webconsole/test/browser/browser_console_webconsole_console_api_calls.js
@@ -117,31 +117,34 @@ async function checkContentConsoleApiMes
       "Table has expected content"
     );
   }
 
   info("Uncheck the Show content messages checkbox");
   const onContentMessagesHidden = waitFor(
     () => !findMessage(hud, contentArgs.log)
   );
-  const checkbox = hud.ui.outputNode.querySelector(
-    ".webconsole-filterbar-primary .filter-checkbox"
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-contentMessages"
   );
-  checkbox.click();
   await onContentMessagesHidden;
 
   for (const expectedMessage of expectedMessages) {
     ok(!findMessage(hud, expectedMessage), `"${expectedMessage}" is hidden`);
   }
 
   info("Check the Show content messages checkbox");
   const onContentMessagesDisplayed = waitFor(() =>
     expectedMessages.every(expectedMessage => findMessage(hud, expectedMessage))
   );
-  checkbox.click();
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-contentMessages"
+  );
   await onContentMessagesDisplayed;
 
   for (const expectedMessage of expectedMessages) {
     ok(findMessage(hud, expectedMessage), `"${expectedMessage}" is visible`);
   }
 
   info("Clear and close the Browser Console");
   await clearOutput(hud);
--- a/devtools/client/webconsole/test/browser/browser_jsterm_no_input_and_tab_key_pressed.js
+++ b/devtools/client/webconsole/test/browser/browser_jsterm_no_input_and_tab_key_pressed.js
@@ -24,18 +24,22 @@ add_task(async function() {
   ok(!isInputFocused(hud), "input isn't focused anymore");
 
   info("Check that hitting Shift+Tab when input is empty blur the input");
   jsterm.focus();
   EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
   is(getInputValue(hud), "", "inputnode is empty - matched");
   ok(!isInputFocused(hud), "input isn't focused anymore");
   ok(
-    hasFocus(hud.ui.outputNode.querySelector(".filter-checkbox input")),
-    `The "Persist Logs" checkbox is now focused`
+    hasFocus(
+      hud.ui.outputNode.querySelector(
+        ".webconsole-console-settings-menu-button"
+      )
+    ),
+    `The "Console Settings" menu button is now focused`
   );
 
   info("Check that hitting Tab when input is not empty insert a tab");
   jsterm.focus();
 
   const testString = "window.Bug583816";
   await setInputValueForAutocompletion(hud, testString, 0);
   checkInputValueAndCursorPosition(
--- a/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_entire_message.js
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_context_menu_copy_entire_message.js
@@ -46,17 +46,20 @@ add_task(async function() {
   await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
     content.wrappedJSObject.logStuff();
   });
 
   info("Test copy menu item with timestamp");
   await testMessagesCopy(hud, true);
 
   // Disable timestamp and wait until timestamp are not displayed anymore.
-  await pushPref(PREF_MESSAGE_TIMESTAMP, false);
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-timestamps"
+  );
   await waitFor(
     () => hud.ui.outputNode.querySelector(".message .timestamp") === null
   );
 
   info("Test copy menu item without timestamp");
   await testMessagesCopy(hud, false);
 });
 
--- a/devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_filter_buttons_overflow.js
@@ -21,17 +21,17 @@ add_task(async function() {
   ok(true, "The filter bar has the wide layout");
 
   info("Check filter buttons overflow when window is small.");
   resizeWindow(400, win);
   await waitForFilterBarLayout(hud, ".narrow");
   ok(true, "The filter bar has the narrow layout");
 
   info("Check that the filter bar layout changes when opening the sidebar");
-  resizeWindow(800, win);
+  resizeWindow(750, win);
   await waitForFilterBarLayout(hud, ".wide");
   const onMessage = waitForMessage(hud, "world");
   ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
     content.console.log({ hello: "world" });
   });
   const { node } = await onMessage;
   const object = node.querySelector(".object-inspector .objectBox-object");
   info("Ctrl+click on an object to put it in the sidebar");
--- a/devtools/client/webconsole/test/browser/browser_webconsole_persist.js
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_persist.js
@@ -37,19 +37,20 @@ add_task(async function() {
   await closeToolbox();
 });
 
 add_task(async function() {
   info("Testing that messages persist on a refresh if logs are persisted");
 
   const hud = await openNewTabAndConsole(TEST_URI);
 
-  hud.ui.outputNode
-    .querySelector(".webconsole-filterbar-primary .filter-checkbox")
-    .click();
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-persistentLogs"
+  );
 
   const INITIAL_LOGS_NUMBER = 5;
   await ContentTask.spawn(
     gBrowser.selectedBrowser,
     INITIAL_LOGS_NUMBER,
     count => {
       content.wrappedJSObject.doLogs(count);
     }
--- a/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_persist_toggle_changed.js
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_telemetry_persist_toggle_changed.js
@@ -17,29 +17,30 @@ add_task(async function() {
   // Let's reset the counts.
   Services.telemetry.clearEvents();
 
   // Ensure no events have been logged
   TelemetryTestUtils.assertNumberOfEvents(0);
 
   const hud = await openNewTabAndConsole(TEST_URI);
 
-  // Get log persistence toggle button
-  const logPersistToggle = await waitFor(() =>
-    hud.ui.window.document.querySelector(".filter-checkbox")
+  // Toggle persistent logs - "true"
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-persistentLogs"
   );
-
-  // Click on the toggle - "true"
-  logPersistToggle.click();
   await waitUntil(
     () => hud.ui.wrapper.getStore().getState().ui.persistLogs === true
   );
 
-  // Click a second time - "false"
-  logPersistToggle.click();
+  // Toggle persistent logs - "false"
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-persistentLogs"
+  );
   await waitUntil(
     () => hud.ui.wrapper.getStore().getState().ui.persistLogs === false
   );
 
   const expectedEvents = [
     {
       category: "devtools.main",
       method: "persist_changed",
--- a/devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_timestamps.js
@@ -24,34 +24,28 @@ add_task(async function() {
 
   const prefValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
   ok(!prefValue, "Messages should have no timestamp by default (pref check)");
   ok(
     !message.node.querySelector(".timestamp"),
     "Messages should have no timestamp by default (element check)"
   );
 
-  info("Open the settings panel");
   const observer = new PrefObserver("");
-  const toolbox = hud.toolbox;
-  const { panelDoc, panelWin } = await toolbox.selectTool("options");
 
   info("Change Timestamp preference");
   const prefChanged = observer.once(PREF_MESSAGE_TIMESTAMP, () => {});
-  const checkbox = panelDoc.getElementById("webconsole-timestamp-messages");
 
-  // We use executeSoon here to ensure that the element is in view and clickable.
-  checkbox.scrollIntoView();
-  executeSoon(() => EventUtils.synthesizeMouseAtCenter(checkbox, {}, panelWin));
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-timestamps"
+  );
 
   await prefChanged;
   observer.destroy();
 
-  // Switch back to the console as it won't update when it is in background
-  info("Go back to console");
-  await toolbox.selectTool("webconsole");
   ok(
     message.node.querySelector(".timestamp"),
     "Messages should have timestamp"
   );
 
   Services.prefs.clearUserPref(PREF_MESSAGE_TIMESTAMP);
 });
--- a/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js
+++ b/devtools/client/webconsole/test/browser/browser_webconsole_warning_groups_toggle.js
@@ -104,17 +104,18 @@ add_task(async function testContentBlock
     `${BLOCKED_URL}?1`,
     `simple message 1`,
     `${BLOCKED_URL}?2`,
     `${BLOCKED_URL}?3`,
     `${BLOCKED_URL}?4`,
   ]);
 
   info("Re-disable the warningGroup feature pref");
-  await toggleWarningGroupPreference(hud, false);
+  await toggleWarningGroupPreference(hud);
+  console.log("toggle successful");
   warningGroupMessage1 = await waitFor(() =>
     findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)
   );
 
   checkConsoleOutputForWarningGroup(hud, [
     `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
     `simple message 1`,
   ]);
@@ -134,17 +135,17 @@ add_task(async function testContentBlock
 
   info("Reload the page and wait for it to be ready");
   await reloadPage();
 
   // Wait for the navigation message to be displayed.
   await waitFor(() => findMessage(hud, "Navigated to"));
 
   info("Disable the warningGroup feature pref again");
-  await toggleWarningGroupPreference(hud, false);
+  await toggleWarningGroupPreference(hud);
 
   info("Add one warning message and one simple message");
   await waitFor(() => findMessage(hud, `${BLOCKED_URL}?4`));
   onContentBlockingWarningMessage = waitForMessage(hud, BLOCKED_URL, ".warn");
   emitContentBlockedMessage(hud);
   await onContentBlockingWarningMessage;
   await logString(hud, "simple message 2");
 
@@ -158,17 +159,17 @@ add_task(async function testContentBlock
     `Navigated to`,
     `${BLOCKED_URL}?5`,
     `simple message 2`,
   ]);
 
   info(
     "Enable the warningGroup feature pref to check that the group is still expanded"
   );
-  await toggleWarningGroupPreference(hud, false);
+  await toggleWarningGroupPreference(hud);
   await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
 
   checkConsoleOutputForWarningGroup(hud, [
     `▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
     `| ${BLOCKED_URL}?1`,
     `| ${BLOCKED_URL}?2`,
     `| ${BLOCKED_URL}?3`,
     `| ${BLOCKED_URL}?4`,
@@ -201,17 +202,17 @@ add_task(async function testContentBlock
     `Navigated to`,
     `▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
     `simple message 2`,
   ]);
 
   info(
     "Disable the warningGroup pref and check all warning messages are visible"
   );
-  await toggleWarningGroupPreference(hud, false);
+  await toggleWarningGroupPreference(hud);
   await waitFor(() => findMessage(hud, `${BLOCKED_URL}?6`));
 
   checkConsoleOutputForWarningGroup(hud, [
     `${BLOCKED_URL}?1`,
     `simple message 1`,
     `${BLOCKED_URL}?2`,
     `${BLOCKED_URL}?3`,
     `${BLOCKED_URL}?4`,
@@ -255,44 +256,23 @@ function logString(hud, str) {
 function waitForBadgeNumber(message, expectedNumber) {
   return waitFor(
     () =>
       message.querySelector(".warning-group-badge").textContent ==
       expectedNumber
   );
 }
 
-/**
- *
- * @param {WebConsole} hud
- * @param {Boolean} fromUI: By default, we change the pref by going to the settings panel
- *                          and clicking the checkbox. If fromUI is set to false, we'll
- *                          change the pref through Services.prefs to speed-up things.
- */
-async function toggleWarningGroupPreference(hud, fromUI = true) {
-  if (!fromUI) {
-    await pushPref(
-      WARNING_GROUP_PREF,
-      !Services.prefs.getBoolPref(WARNING_GROUP_PREF)
-    );
-    return;
-  }
-
+async function toggleWarningGroupPreference(hud) {
   info("Open the settings panel");
   const observer = new PrefObserver("");
-  const toolbox = hud.toolbox;
-  const { panelDoc, panelWin } = await toolbox.selectTool("options");
 
   info("Change warning preference");
   const prefChanged = observer.once(WARNING_GROUP_PREF, () => {});
-  const checkbox = panelDoc.getElementById("webconsole-warning-groups");
 
-  // We use executeSoon here to ensure that the element is in view and clickable.
-  checkbox.scrollIntoView();
-  executeSoon(() => EventUtils.synthesizeMouseAtCenter(checkbox, {}, panelWin));
+  await toggleConsoleSetting(
+    hud,
+    ".webconsole-console-settings-menu-item-warning-groups"
+  );
 
   await prefChanged;
   observer.destroy();
-
-  // Switch back to the console as it won't update when it is in background
-  info("Go back to console");
-  await toolbox.selectTool("webconsole");
 }
--- a/devtools/client/webconsole/test/browser/head.js
+++ b/devtools/client/webconsole/test/browser/head.js
@@ -383,16 +383,24 @@ function _getContextMenu(hud) {
   return doc.getElementById("webconsole-menu");
 }
 
 function loadDocument(url, browser = gBrowser.selectedBrowser) {
   BrowserTestUtils.loadURI(browser, url);
   return BrowserTestUtils.browserLoaded(browser);
 }
 
+async function toggleConsoleSetting(hud, node) {
+  const toolbox = hud.toolbox;
+  const doc = toolbox ? toolbox.doc : hud.chromeWindow.document;
+
+  const menuItem = doc.querySelector(node);
+  menuItem.click();
+}
+
 /**
  * Returns a promise that resolves when the node passed as an argument mutate
  * according to the passed configuration.
  *
  * @param {Node} node - The node to observe mutations on.
  * @param {Object} observeConfig - A configuration object for MutationObserver.observe.
  * @returns {Promise}
  */
--- a/devtools/client/webconsole/test/node/components/filter-bar.test.js
+++ b/devtools/client/webconsole/test/node/components/filter-bar.test.js
@@ -10,38 +10,39 @@ const { createFactory } = require("devto
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const Provider = createFactory(require("react-redux").Provider);
 
 const actions = require("devtools/client/webconsole/actions/index");
 const FilterButton = require("devtools/client/webconsole/components/FilterBar/FilterButton");
 const FilterBar = createFactory(
   require("devtools/client/webconsole/components/FilterBar/FilterBar")
 );
-const { getAllUi } = require("devtools/client/webconsole/selectors/ui");
 const {
   FILTERBAR_DISPLAY_MODES,
 } = require("devtools/client/webconsole/constants");
 const {
   MESSAGES_CLEAR,
   FILTERS,
-  PREFS,
 } = require("devtools/client/webconsole/constants");
 
 const {
   setupStore,
-  prefsService,
   clearPrefs,
 } = require("devtools/client/webconsole/test/node/helpers");
 const serviceContainer = require("devtools/client/webconsole/test/node/fixtures/serviceContainer");
 
 function getFilterBar(overrides = {}) {
   return FilterBar({
     serviceContainer,
     hidePersistLogsCheckbox: false,
     attachRefToWebConsoleUI: () => {},
+    webConsoleUI: {
+      document,
+      wrapper: {},
+    },
     ...overrides,
   });
 }
 
 describe("FilterBar component:", () => {
   afterEach(() => {
     clearPrefs();
   });
@@ -81,18 +82,20 @@ describe("FilterBar component:", () => {
     expect(textFilter.attr("value")).toBe("");
 
     // Text filter input clear button
     const textFilterClearButton = textInput.children().eq(1);
     expect(textFilterClearButton.attr("class")).toBe(
       "devtools-searchinput-clear"
     );
 
-    // "Persist logs" checkbox
-    expect(wrapper.find(".filter-checkbox input").length).toBe(1);
+    // Settings menu icon
+    expect(
+      wrapper.find(".webconsole-console-settings-menu-button").length
+    ).toBe(1);
   });
 
   it("displays the number of hidden messages when a search hide messages", () => {
     const store = setupStore([
       "console.log('foobar', 'test')",
       "console.info('info message');",
       "console.warn('danger, will robinson!')",
       "console.debug('debug message');",
@@ -216,36 +219,9 @@ describe("FilterBar component:", () => {
   it("sets filter text when text is typed", () => {
     const store = setupStore();
 
     const wrapper = mount(Provider({ store }, getFilterBar()));
     const input = wrapper.find(".devtools-filterinput");
     input.simulate("change", { target: { value: "a" } });
     expect(store.getState().filters.text).toBe("a");
   });
-
-  it("toggles persist logs when checkbox is clicked", () => {
-    const store = setupStore();
-
-    expect(getAllUi(store.getState()).persistLogs).toBe(false);
-    expect(prefsService.getBoolPref(PREFS.UI.PERSIST), false);
-
-    const wrapper = mount(Provider({ store }, getFilterBar()));
-    wrapper.find(".filter-checkbox input").simulate("change");
-
-    expect(getAllUi(store.getState()).persistLogs).toBe(true);
-    expect(prefsService.getBoolPref(PREFS.UI.PERSIST), true);
-  });
-
-  it(`doesn't render "Persist logs" input when "hidePersistLogsCheckbox" is true`, () => {
-    const store = setupStore();
-
-    const wrapper = render(
-      Provider(
-        { store },
-        getFilterBar({
-          hidePersistLogsCheckbox: true,
-        })
-      )
-    );
-    expect(wrapper.find(".filter-checkbox input").length).toBe(0);
-  });
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/node/fixtures/DevToolsUtils.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+  getTopWindow: () => ({}),
+};
--- a/devtools/client/webconsole/test/node/mocha-test-setup.js
+++ b/devtools/client/webconsole/test/node/mocha-test-setup.js
@@ -3,19 +3,17 @@
 
 /* eslint-env node */
 
 "use strict";
 
 const mcRoot = `${__dirname}/../../../../../`;
 const getModule = mcPath => `module.exports = require("${mcRoot}${mcPath}");`;
 
-const {
-  Services: { pref },
-} = require("devtools-modules");
+const { pref } = require("devtools-services");
 pref("devtools.debugger.remote-timeout", 10000);
 pref("devtools.hud.loglimit", 10000);
 pref("devtools.webconsole.filter.error", true);
 pref("devtools.webconsole.filter.warn", true);
 pref("devtools.webconsole.filter.info", true);
 pref("devtools.webconsole.filter.log", true);
 pref("devtools.webconsole.filter.debug", true);
 pref("devtools.webconsole.filter.css", false);
@@ -83,16 +81,18 @@ if (!global.ResizeObserver) {
 }
 
 // Mock ChromeUtils.
 global.ChromeUtils = {
   import: () => {},
   defineModuleGetter: () => {},
 };
 
+global.define = function() {};
+
 // Point to vendored-in files and mocks when needed.
 const requireHacker = require("require-hacker");
 requireHacker.global_hook("default", (path, module) => {
   const paths = {
     // For Enzyme
     "react-dom": () => getModule("devtools/client/shared/vendor/react-dom"),
     "react-dom/server": () =>
       getModule("devtools/client/shared/vendor/react-dom-server"),
@@ -107,33 +107,32 @@ requireHacker.global_hook("default", (pa
     // Some modules depend on Chrome APIs which don't work in mocha. When such a module
     // is required, replace it with a mock version.
     "devtools/shared/l10n": () =>
       getModule(
         "devtools/client/webconsole/test/node/fixtures/LocalizationHelper"
       ),
     "devtools/shared/plural-form": () =>
       getModule("devtools/client/webconsole/test/node/fixtures/PluralForm"),
-    Services: () => `module.exports = require("devtools-modules/src/Services")`,
-    "Services.default": () =>
-      `module.exports = require("devtools-modules/src/Services")`,
+    Services: () => `module.exports = require("devtools-services")`,
     "devtools/shared/client/object-client": () => `() => {}`,
     "devtools/shared/client/long-string-client": () => `() => {}`,
     "devtools/client/shared/components/SmartTrace": () => "{}",
     "devtools/client/netmonitor/src/components/TabboxPanel": () => "{}",
     "devtools/client/webconsole/utils/context-menu": () => "{}",
     "devtools/client/shared/telemetry": () => `module.exports = function() {
       this.recordEvent = () => {};
       this.getKeyedHistogramById = () => ({add: () => {}});
     }`,
     "devtools/shared/event-emitter": () =>
       `module.exports = require("devtools-modules/src/utils/event-emitter")`,
     "devtools/client/shared/unicode-url": () =>
       `module.exports = require("devtools-modules/src/unicode-url")`,
-    "devtools/shared/DevToolsUtils": () => "{}",
+    "devtools/shared/DevToolsUtils": () =>
+      getModule("devtools/client/webconsole/test/node/fixtures/DevToolsUtils"),
     "devtools/server/actors/reflow": () => "{}",
     "devtools/shared/layout/utils": () => "{getCurrentZoom = () => {}}",
   };
 
   if (paths.hasOwnProperty(path)) {
     return paths[path]();
   }
 
--- a/devtools/client/webconsole/test/node/package.json
+++ b/devtools/client/webconsole/test/node/package.json
@@ -14,16 +14,17 @@
       " * Finally we require mocha-test-setup where we configure Enzyme and",
       "   intercept require() calls with require-hacker and modify them if needed."
     ],
     "test": "mocha \"./{,!(node_modules)/**}/*.test.js\" -r mock-local-storage -r jsdom-global/register -r ./mocha-test-setup.js",
     "test-ci": "mocha \"./{,!(node_modules)/**}/*.test.js\" -r mock-local-storage -r jsdom-global/register -r ./mocha-test-setup.js --reporter json"
   },
   "dependencies": {
     "devtools-modules": "0.0.37",
+    "devtools-services": "^0.0.3",
     "enzyme": "^3.3.0",
     "enzyme-adapter-react-16": "^1.1.1",
     "expect": "^1.16.0",
     "jsdom": "^9.4.1",
     "jsdom-global": "^2.0.0",
     "mocha": "^5.0.1",
     "mock-local-storage": "^1.0.5",
     "require-hacker": "^2.1.4",
--- a/devtools/client/webconsole/webconsole-ui.js
+++ b/devtools/client/webconsole/webconsole-ui.js
@@ -156,20 +156,16 @@ class WebConsoleUI {
       this.outputNode.innerHTML = "";
     }
 
     if (this.jsterm) {
       this.jsterm.destroy();
       this.jsterm = null;
     }
 
-    if (this.wrapper) {
-      this.wrapper.destroy();
-    }
-
     const toolbox = this.hud.toolbox;
     if (toolbox) {
       toolbox.off("webconsole-selected", this._onPanelSelected);
       toolbox.off("split-console", this._onChangeSplitConsoleState);
       toolbox.off("select", this._onChangeSplitConsoleState);
     }
 
     const target = this.hud.currentTarget;
--- a/devtools/client/webconsole/webconsole-wrapper.js
+++ b/devtools/client/webconsole/webconsole-wrapper.js
@@ -1,24 +1,22 @@
 /* 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 Services = require("Services");
 const {
   createElement,
   createFactory,
 } = require("devtools/client/shared/vendor/react");
 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 { configureStore } = require("devtools/client/webconsole/store");
-const { PREFS } = require("devtools/client/webconsole/constants");
 
 const {
   isPacketPrivate,
 } = require("devtools/client/webconsole/utils/messages");
 const {
   getAllMessagesById,
   getMessage,
 } = require("devtools/client/webconsole/selectors/messages");
@@ -112,33 +110,16 @@ class WebConsoleWrapper {
         this.toolbox.threadFront.on("paused", this.dispatchPaused);
         this.toolbox.threadFront.on("progress", this.dispatchProgress);
       }
 
       const { prefs } = store.getState();
       const autocomplete = prefs.autocomplete;
       const editorFeatureEnabled = prefs.editor;
 
-      this.prefsObservers = new Map();
-      this.prefsObservers.set(PREFS.UI.MESSAGE_TIMESTAMP, () => {
-        const enabled = Services.prefs.getBoolPref(PREFS.UI.MESSAGE_TIMESTAMP);
-        store.dispatch(actions.timestampsToggle(enabled));
-      });
-
-      this.prefsObservers.set(PREFS.FEATURES.GROUP_WARNINGS, () => {
-        const enabled = Services.prefs.getBoolPref(
-          PREFS.FEATURES.GROUP_WARNINGS
-        );
-        store.dispatch(actions.warningGroupsToggle(enabled));
-      });
-
-      for (const [pref, observer] of this.prefsObservers) {
-        Services.prefs.addObserver(pref, observer);
-      }
-
       const app = App({
         serviceContainer,
         webConsoleUI,
         onFirstMeaningfulPaint: resolve,
         closeSplitConsole: this.closeSplitConsole.bind(this),
         autocomplete,
         editorFeatureEnabled,
         hidePersistLogsCheckbox:
@@ -450,20 +431,12 @@ class WebConsoleWrapper {
   createElement(nodename) {
     return this.document.createElement(nodename);
   }
 
   // Called by pushing close button.
   closeSplitConsole() {
     this.toolbox.closeSplitConsole();
   }
-
-  destroy() {
-    if (this.prefsObservers) {
-      for (const [pref, observer] of this.prefsObservers) {
-        Services.prefs.removeObserver(pref, observer);
-      }
-    }
-  }
 }
 
 // Exports from this module
 module.exports = WebConsoleWrapper;