Bug 1559398 - Implement table and preview sections in WebSocket side panel. r=Honza
authortanhengyeow <E0032242@u.nus.edu>
Fri, 28 Jun 2019 07:24:59 +0000
changeset 480484 42fa48dca98885b5da0568284a5e1a0456ffd289
parent 480483 bed7cc8564f4b4af14bc08bc70a1645afcc9ef31
child 480485 097eedc2b14f7adcc8d4d1cbd151ff5135d0a112
push id36214
push usercsabou@mozilla.com
push dateFri, 28 Jun 2019 16:12:48 +0000
treeherdermozilla-central@8537d24d8aa8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza
bugs1559398
milestone69.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 1559398 - Implement table and preview sections in WebSocket side panel. r=Honza Implement table and preview sections in WebSocket side panel. Differential Revision: https://phabricator.services.mozilla.com/D35983
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/actions/web-sockets.js
devtools/client/netmonitor/src/assets/styles/RequestList.css
devtools/client/netmonitor/src/components/TabboxPanel.js
devtools/client/netmonitor/src/components/WebSocketsPanel.js
devtools/client/netmonitor/src/components/moz.build
devtools/client/netmonitor/src/components/websockets/FrameListColumnFinBit.js
devtools/client/netmonitor/src/components/websockets/FrameListColumnMaskBit.js
devtools/client/netmonitor/src/components/websockets/FrameListColumnOpCode.js
devtools/client/netmonitor/src/components/websockets/FrameListColumnPayload.js
devtools/client/netmonitor/src/components/websockets/FrameListColumnSize.js
devtools/client/netmonitor/src/components/websockets/FrameListColumnTime.js
devtools/client/netmonitor/src/components/websockets/FrameListColumnType.js
devtools/client/netmonitor/src/components/websockets/FrameListContent.js
devtools/client/netmonitor/src/components/websockets/FrameListHeader.js
devtools/client/netmonitor/src/components/websockets/FrameListItem.js
devtools/client/netmonitor/src/components/websockets/FramePayload.js
devtools/client/netmonitor/src/components/websockets/WebSocketsPanel.js
devtools/client/netmonitor/src/components/websockets/moz.build
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/reducers/index.js
devtools/client/netmonitor/src/reducers/web-sockets.js
devtools/client/netmonitor/src/selectors/web-sockets.js
devtools/client/netmonitor/src/utils/request-utils.js
devtools/client/preferences/devtools-client.js
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -632,16 +632,44 @@ netmonitor.toolbar.transferred=Transferr
 # in the network table toolbar, above the "size" column, which is the
 # uncompressed / decoded size.
 netmonitor.toolbar.contentSize=Size
 
 # LOCALIZATION NOTE (netmonitor.toolbar.waterfall): This is the label displayed
 # in the network table toolbar, above the "waterfall" column.
 netmonitor.toolbar.waterfall=Timeline
 
+# LOCALIZATION NOTE (netmonitor.ws.toolbar.frameType): This is the label displayed
+# in the websocket frame table header, above the "type" column.
+netmonitor.ws.toolbar.frameType=Type
+
+# LOCALIZATION NOTE (netmonitor.ws.toolbar.size): This is the label displayed
+# in the websocket frame table header, above the "size" column.
+netmonitor.ws.toolbar.size=Size
+
+# LOCALIZATION NOTE (netmonitor.ws.toolbar.payload): This is the label displayed
+# in the websocket frame table header, above the "payload" column.
+netmonitor.ws.toolbar.payload=Payload
+
+# LOCALIZATION NOTE (netmonitor.ws.toolbar.opCode): This is the label displayed
+# in the websocket frame table header, above the "opCode" column.
+netmonitor.ws.toolbar.opCode=OpCode
+
+# LOCALIZATION NOTE (netmonitor.ws.toolbar.maskBit): This is the label displayed
+# in the websocket frame table header, above the "maskBit" column.
+netmonitor.ws.toolbar.maskBit=MaskBit
+
+# LOCALIZATION NOTE (netmonitor.ws.toolbar.finBit): This is the label displayed
+# in the websocket frame table header, above the "finBit" column.
+netmonitor.ws.toolbar.finBit=FinBit
+
+# LOCALIZATION NOTE (netmonitor.ws.toolbar.time): This is the label displayed
+# in the websocket frame table header, above the "time" column.
+netmonitor.ws.toolbar.time=Time
+
 # LOCALIZATION NOTE (netmonitor.tab.headers): This is the label displayed
 # in the network details pane identifying the headers tab.
 netmonitor.tab.headers=Headers
 
 # LOCALIZATION NOTE (netmonitor.tab.webSockets): This is the label displayed
 # in the network details pane identifying the webSockets tab.
 netmonitor.tab.webSockets=WebSockets
 
--- a/devtools/client/netmonitor/src/actions/web-sockets.js
+++ b/devtools/client/netmonitor/src/actions/web-sockets.js
@@ -1,19 +1,48 @@
 /* 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 { WS_ADD_FRAME } = require("../constants");
+const {
+  WS_ADD_FRAME,
+  WS_SELECT_FRAME,
+  WS_OPEN_FRAME_DETAILS,
+} = require("../constants");
 
 function addFrame(httpChannelId, data) {
   return {
     type: WS_ADD_FRAME,
     httpChannelId,
     data,
   };
 }
 
+/**
+ * Select frame.
+ */
+function selectFrame(frame) {
+  return {
+    type: WS_SELECT_FRAME,
+    open: true,
+    frame,
+  };
+}
+
+/**
+ * Open frame details panel.
+ *
+ * @param {boolean} open - expected frame details panel open state
+ */
+function openFrameDetails(open) {
+  return {
+    type: WS_OPEN_FRAME_DETAILS,
+    open,
+  };
+}
+
 module.exports = {
   addFrame,
+  selectFrame,
+  openFrameDetails,
 };
--- a/devtools/client/netmonitor/src/assets/styles/RequestList.css
+++ b/devtools/client/netmonitor/src/assets/styles/RequestList.css
@@ -5,16 +5,20 @@
 /* Request list empty panel */
 
 .request-list-empty-notice {
   margin: 0;
   flex: 1;
   overflow-x: hidden;
 }
 
+.ws-frame-list-empty-notice {
+  width: 100%;
+}
+
 .empty-notice-element {
   padding-top: 12px;
   padding-left: 12px;
   padding-right: 12px;
   font-size: 1.2rem;
 }
 
 .notice-perf-message {
@@ -51,63 +55,69 @@
   color: var(--table-text-color);
 }
 
 .requests-list-scroll {
   overflow-x: hidden;
   overflow-y: auto;
 }
 
-.requests-list-table {
+.requests-list-table,
+.ws-frames-list-table {
   /* Reset default browser style of <table> */
   border-spacing: 0;
   width: 100%;
   /* The layout must be fixed for resizing of columns to work.
   The layout is based on the first row.
   Set the width of those cells, and the rest of the table follows. */
   table-layout: fixed;
 }
 
-.requests-list-column {
+.requests-list-column,
+.ws-frames-list-column {
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
   vertical-align: middle;
 
   /* Reset default browser style of <td> */
   padding: 0;
 }
 
 .requests-list-column > * {
   display: inline-block;
 }
 
 /* Requests list headers */
 
-.requests-list-headers-group {
+.requests-list-headers-group,
+.ws-frames-list-headers-group {
   /* Avoid .devtools-toolbar to override <thead> display type */
   display: table-header-group;
 
   position: sticky;
   top: 0;
   left: 0;
   width: 100%;
   z-index: 1;
 }
 
-.requests-list-headers {
+.requests-list-headers,
+.ws-frames-list-headers {
   height: 24px;
   padding: 0;
 }
 
-.requests-list-headers .requests-list-column:first-child .requests-list-header-button {
+.requests-list-headers .requests-list-column:first-child .requests-list-header-button,
+.ws-frames-list-headers .ws-frames-list-column:first-child .ws-frames-list-header-button {
   border-width: 0;
 }
 
-.requests-list-header-button {
+.requests-list-header-button,
+.ws-frames-list-header-button {
   background-color: transparent;
   border-image: linear-gradient(transparent 15%,
                                 var(--theme-splitter-color) 15%,
                                 var(--theme-splitter-color) 85%,
                                 transparent 85%) 1 1;
   border-width: 0;
   border-inline-start-width: 1px;
   width: 100%;
@@ -121,25 +131,27 @@
   border: 0;
   padding: 0;
 }
 
 .requests-list-header-button:hover {
   background-color: rgba(0, 0, 0, 0.1);
 }
 
-.requests-list-header-button > .button-text {
+.requests-list-header-button > .button-text,
+.ws-frames-list-header-button > .button-text {
   display: inline-block;
   vertical-align: middle;
   width: 100%;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
-.requests-list-header-button > .button-icon {
+.requests-list-header-button > .button-icon,
+.ws-frames-list-header-button > .button-icon {
   /* display icon only when column sorted otherwise display:none */
   display: none;
   width: 7px;
   height: 4px;
   margin-inline-start: 3px;
   margin-inline-end: 6px;
   vertical-align: middle;
 }
@@ -304,17 +316,18 @@
   background-image: url(chrome://devtools/content/netmonitor/src/assets/icons/shield.svg);
   background-repeat: no-repeat;
 }
 
 .selected .tracking-resource {
   filter: brightness(500%);
 }
 
-.request-list-item .requests-list-column {
+.request-list-item .requests-list-column,
+.ws-frame-list-item .ws-frames-list-column {
   padding-inline-start: 4px;
 }
 
 .request-list-item .requests-list-waterfall {
   padding-inline-start: 0px;
 }
 
 .request-list-item .status-code {
@@ -459,42 +472,46 @@
 }
 
 .requests-list-timings-total:dir(rtl) {
   transform-origin: right center;
 }
 
 /* Request list item */
 
-.request-list-item {
+.request-list-item,
+.ws-frame-list-item {
   height: 24px;
   line-height: 24px;
 }
 
-.request-list-item:not(.selected).odd {
+.request-list-item:not(.selected).odd,
+.ws-frame-list-item:not(.selected).odd {
   background-color: var(--table-zebra-background);
 }
 
-.request-list-item:not(.selected):hover {
+.request-list-item:not(.selected):hover,
+.ws-frame-list-item:not(.selected):hover {
   background-color: var(--table-selection-background-hover);
 }
 
 .request-list-item:not(.selected).fromCache > .requests-list-column:not(.requests-list-waterfall) {
   opacity: 0.7;
 }
 
 .request-list-item.blocked {
   color: var(--timing-blocked-color);
 }
 
 /*
  * Put ahead of .request-list-item.blocked to avoid specificity conflict.
  * Bug 1530914 - Highlighted Security Value is difficult to read.
  */
-.request-list-item.selected {
+.request-list-item.selected,
+.ws-frame-list-item.selected {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
 /* Responsive web design support */
 
 @media (max-width: 700px) {
   .requests-list-status-code {
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -13,17 +13,17 @@ const PropTypes = require("devtools/clie
 const { L10N } = require("../utils/l10n");
 const { PANELS } = require("../constants");
 
 // Components
 const Tabbar = createFactory(require("devtools/client/shared/components/tabs/TabBar"));
 const TabPanel = createFactory(require("devtools/client/shared/components/tabs/Tabs").TabPanel);
 const CookiesPanel = createFactory(require("./CookiesPanel"));
 const HeadersPanel = createFactory(require("./HeadersPanel"));
-const WebSocketsPanel = createFactory(require("./WebSocketsPanel"));
+const WebSocketsPanel = createFactory(require("./websockets/WebSocketsPanel"));
 const ParamsPanel = createFactory(require("./ParamsPanel"));
 const CachePanel = createFactory(require("./CachePanel"));
 const ResponsePanel = createFactory(require("./ResponsePanel"));
 const SecurityPanel = createFactory(require("./SecurityPanel"));
 const StackTracePanel = createFactory(require("./StackTracePanel"));
 const TimingsPanel = createFactory(require("./TimingsPanel"));
 
 const COLLAPSE_DETAILS_PANE = L10N.getStr("collapseDetailsPane");
@@ -126,16 +126,17 @@ class TabboxPanel extends Component {
           }),
         ),
         showWebSocketsPanel && TabPanel({
           id: PANELS.WEBSOCKETS,
           title: WEBSOCKETS_TITLE,
         },
           WebSocketsPanel({
             channelId,
+            connector,
           }),
         ),
         TabPanel({
           id: PANELS.COOKIES,
           title: COOKIES_TITLE,
         },
           CookiesPanel({
             connector,
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/WebSocketsPanel.js
+++ /dev/null
@@ -1,85 +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 } = require("devtools/client/shared/vendor/react");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const {
-  connect,
-} = require("devtools/client/shared/redux/visibility-handler-connect");
-const { getFramesByChannelId } = require("../selectors/index");
-
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const { table, tbody, thead, tr, td, th, div } = dom;
-
-const { L10N } = require("../utils/l10n");
-const FRAMES_EMPTY_TEXT = L10N.getStr("webSocketsEmptyText");
-
-class WebSocketsPanel extends Component {
-  static get propTypes() {
-    return {
-      channelId: PropTypes.number,
-      frames: PropTypes.array,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    const { frames } = this.props;
-
-    if (!frames) {
-      return div({ className: "empty-notice" },
-        FRAMES_EMPTY_TEXT
-      );
-    }
-
-    const rows = [];
-    frames.forEach((frame, index) => {
-      rows.push(
-        tr(
-          { key: index,
-            className: "frames-row" },
-          td({ className: "frames-cell" }, frame.type),
-          td({ className: "frames-cell" }, frame.httpChannelId),
-          td({ className: "frames-cell" }, frame.payload),
-          td({ className: "frames-cell" }, frame.opCode),
-          td({ className: "frames-cell" }, frame.maskBit.toString()),
-          td({ className: "frames-cell" }, frame.finBit.toString()),
-          td({ className: "frames-cell" }, frame.timeStamp)
-        )
-      );
-    });
-
-    return table(
-      { className: "frames-list-table" },
-      thead(
-        { className: "frames-head" },
-        tr(
-          { className: "frames-row" },
-          th({ className: "frames-headerCell" }, "Type"),
-          th({ className: "frames-headerCell" }, "Channel ID"),
-          th({ className: "frames-headerCell" }, "Payload"),
-          th({ className: "frames-headerCell" }, "OpCode"),
-          th({ className: "frames-headerCell" }, "MaskBit"),
-          th({ className: "frames-headerCell" }, "FinBit"),
-          th({ className: "frames-headerCell" }, "Time")
-        )
-      ),
-      tbody(
-        {
-          className: "frames-list-tableBody",
-        },
-        rows
-      )
-    );
-  }
-}
-
-module.exports = connect((state, props) => ({
-  frames: getFramesByChannelId(state, props.channelId),
-}))(WebSocketsPanel);
--- a/devtools/client/netmonitor/src/components/moz.build
+++ b/devtools/client/netmonitor/src/components/moz.build
@@ -1,12 +1,16 @@
 # 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/.
 
+DIRS += [
+    'websockets',
+]
+
 DevToolsModules(
     'App.js',
     'CachePanel.js',
     'CookiesPanel.js',
     'CustomRequestPanel.js',
     'DropHarHandler.js',
     'HeadersPanel.js',
     'HtmlPreview.js',
@@ -42,10 +46,9 @@ DevToolsModules(
     'SourceEditor.js',
     'StackTracePanel.js',
     'StatisticsPanel.js',
     'StatusBar.js',
     'StatusCode.js',
     'TabboxPanel.js',
     'TimingsPanel.js',
     'Toolbar.js',
-    'WebSocketsPanel.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListColumnFinBit.js
@@ -0,0 +1,41 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * Renders the "FinBit" column of a WebSocket frame.
+ */
+class FrameListColumnFinBit extends Component {
+  static get propTypes() {
+    return {
+      item: PropTypes.object.isRequired,
+      index: PropTypes.number.isRequired,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.finBit !== nextProps.item.finBit;
+  }
+
+  render() {
+    const { finBit } = this.props.item;
+    const { index } = this.props;
+
+    return dom.td(
+      {
+        key: index,
+        className: "ws-frames-list-column ws-frames-list-finBit",
+        title: finBit.toString(),
+      },
+      finBit.toString()
+    );
+  }
+}
+
+module.exports = FrameListColumnFinBit;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListColumnMaskBit.js
@@ -0,0 +1,41 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * Renders the "MaskBit" column of a WebSocket frame.
+ */
+class FrameListColumnMaskBit extends Component {
+  static get propTypes() {
+    return {
+      item: PropTypes.object.isRequired,
+      index: PropTypes.number.isRequired,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.maskBit !== nextProps.item.maskBit;
+  }
+
+  render() {
+    const { maskBit } = this.props.item;
+    const { index } = this.props;
+
+    return dom.td(
+      {
+        key: index,
+        className: "ws-frames-list-column ws-frames-list-maskBit",
+        title: maskBit.toString(),
+      },
+      maskBit.toString()
+    );
+  }
+}
+
+module.exports = FrameListColumnMaskBit;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListColumnOpCode.js
@@ -0,0 +1,41 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * Renders the "OpCode" column of a WebSocket frame.
+ */
+class FrameListColumnOpCode extends Component {
+  static get propTypes() {
+    return {
+      item: PropTypes.object.isRequired,
+      index: PropTypes.number.isRequired,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.opCode !== nextProps.item.opCode;
+  }
+
+  render() {
+    const { opCode } = this.props.item;
+    const { index } = this.props;
+
+    return dom.td(
+      {
+        key: index,
+        className: "ws-frames-list-column ws-frames-list-opCode",
+        title: opCode,
+      },
+      opCode
+    );
+  }
+}
+
+module.exports = FrameListColumnOpCode;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListColumnPayload.js
@@ -0,0 +1,64 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { getFramePayload } = require("../../utils/request-utils");
+
+/**
+ * Renders the "Payload" column of a WebSocket frame.
+ */
+class FrameListColumnPayload extends Component {
+  static get propTypes() {
+    return {
+      item: PropTypes.object.isRequired,
+      index: PropTypes.number.isRequired,
+      connector: PropTypes.object.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      payload: "",
+    };
+  }
+
+  componentDidMount() {
+    const { item, connector } = this.props;
+    getFramePayload(item.payload, connector.getLongString).then(payload => {
+      this.setState({
+        payload,
+      });
+    });
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const { item, connector } = nextProps;
+    getFramePayload(item.payload, connector.getLongString).then(payload => {
+      this.setState({
+        payload,
+      });
+    });
+  }
+
+  render() {
+    const { index } = this.props;
+
+    return dom.td(
+      {
+        key: index,
+        className: "ws-frames-list-column ws-frames-list-payload",
+        title: this.state.payload,
+      },
+      this.state.payload
+    );
+  }
+}
+
+module.exports = FrameListColumnPayload;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListColumnSize.js
@@ -0,0 +1,42 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { getFormattedSize } = require("../../utils/format-utils");
+
+/**
+ * Renders the "Size" column of a WebSocket frame.
+ */
+class FrameListColumnSize extends Component {
+  static get propTypes() {
+    return {
+      item: PropTypes.object.isRequired,
+      index: PropTypes.number.isRequired,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.payload !== nextProps.item.payload;
+  }
+
+  render() {
+    const { payload } = this.props.item;
+    const { index } = this.props;
+
+    return dom.td(
+      {
+        key: index,
+        className: "ws-frames-list-column ws-frames-list-size",
+        title: getFormattedSize(payload.length),
+      },
+      getFormattedSize(payload.length)
+    );
+  }
+}
+
+module.exports = FrameListColumnSize;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListColumnTime.js
@@ -0,0 +1,44 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * Renders the "Time" column of a WebSocket frame.
+ */
+class FrameListColumnTime extends Component {
+  static get propTypes() {
+    return {
+      item: PropTypes.object.isRequired,
+      index: PropTypes.number.isRequired,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.timeStamp !== nextProps.item.timeStamp;
+  }
+
+  render() {
+    const { timeStamp } = this.props.item;
+    const { index } = this.props;
+
+    // Convert microseconds (DOMHighResTimeStamp) to milliseconds
+    const time = timeStamp / 1000;
+
+    return dom.td(
+      {
+        key: index,
+        className: "ws-frames-list-column ws-frames-list-time",
+        title: timeStamp,
+      },
+      new Date(time).toLocaleTimeString()
+    );
+  }
+}
+
+module.exports = FrameListColumnTime;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListColumnType.js
@@ -0,0 +1,41 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * Renders the "Type" column of a WebSocket frame.
+ */
+class FrameListColumnType extends Component {
+  static get propTypes() {
+    return {
+      item: PropTypes.object.isRequired,
+      index: PropTypes.number.isRequired,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.type !== nextProps.item.type;
+  }
+
+  render() {
+    const { type } = this.props.item;
+    const { index } = this.props;
+
+    return dom.td(
+      {
+        key: index,
+        className: "ws-frames-list-column ws-frames-list-type",
+        title: type,
+      },
+      type
+    );
+  }
+}
+
+module.exports = FrameListColumnType;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListContent.js
@@ -0,0 +1,99 @@
+/* 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 {
+  connect,
+} = require("devtools/client/shared/redux/visibility-handler-connect");
+const { getFramesByChannelId } = require("../../selectors/index");
+
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { table, tbody, div } = dom;
+
+const { L10N } = require("../../utils/l10n");
+const FRAMES_EMPTY_TEXT = L10N.getStr("webSocketsEmptyText");
+const Actions = require("../../actions/index");
+
+const { getSelectedFrame } = require("../../selectors/index");
+
+loader.lazyGetter(this, "FrameListHeader", function() {
+  return createFactory(require("./FrameListHeader"));
+});
+loader.lazyGetter(this, "FrameListItem", function() {
+  return createFactory(require("./FrameListItem"));
+});
+
+const LEFT_MOUSE_BUTTON = 0;
+
+/**
+ * Renders the actual contents of the WebSocket frame list.
+ */
+class FrameListContent extends Component {
+  static get propTypes() {
+    return {
+      channelId: PropTypes.number,
+      connector: PropTypes.object.isRequired,
+      frames: PropTypes.array,
+      selectedFrame: PropTypes.object,
+      selectFrame: PropTypes.func.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+  }
+
+  onMouseDown(evt, item) {
+    if (evt.button === LEFT_MOUSE_BUTTON) {
+      this.props.selectFrame(item);
+    }
+  }
+
+  render() {
+    const { frames, selectedFrame, connector } = this.props;
+
+    if (!frames) {
+      return div(
+        { className: "empty-notice ws-frame-list-empty-notice" },
+        FRAMES_EMPTY_TEXT
+      );
+    }
+
+    return table(
+      { className: "ws-frames-list-table" },
+      FrameListHeader(),
+      tbody(
+        {
+          className: "ws-frames-list-body",
+        },
+        frames.map((item, index) =>
+          FrameListItem({
+            key: "ws-frame-list-item-" + index,
+            item,
+            index,
+            isSelected: item === selectedFrame,
+            onMouseDown: evt => this.onMouseDown(evt, item),
+            connector,
+          })
+        )
+      )
+    );
+  }
+}
+
+module.exports = connect(
+  (state, props) => ({
+    selectedFrame: getSelectedFrame(state),
+    frames: getFramesByChannelId(state, props.channelId),
+  }),
+  dispatch => ({
+    selectFrame: item => dispatch(Actions.selectFrame(item)),
+  })
+)(FrameListContent);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListHeader.js
@@ -0,0 +1,67 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { WS_FRAMES_HEADERS } = require("../../constants");
+const { L10N } = require("../../utils/l10n");
+
+const { div, button } = dom;
+
+/**
+ * Renders the frame list header.
+ */
+class FrameListHeader extends Component {
+  constructor(props) {
+    super(props);
+  }
+
+  /**
+   * Render one column header from the table headers.
+   */
+  renderColumn(header) {
+    const name = header.name;
+    const label = L10N.getStr(`netmonitor.ws.toolbar.${name}`);
+
+    return dom.td(
+      {
+        id: `ws-frames-list-${name}-header-box`,
+        className: `ws-frames-list-column ws-frames-list-${name}`,
+        key: name,
+      },
+      button(
+        {
+          id: `ws-frames-list-${name}-button`,
+          className: `ws-frames-list-header-button`,
+          title: label,
+        },
+        div({ className: "button-text" }, label),
+        div({ className: "button-icon" })
+      )
+    );
+  }
+
+  /**
+   * Render all columns in the table header
+   */
+  renderColumns() {
+    return WS_FRAMES_HEADERS.map(header => this.renderColumn(header));
+  }
+
+  render() {
+    return dom.thead(
+      { className: "devtools-toolbar ws-frames-list-headers-group" },
+      dom.tr(
+        {
+          className: "ws-frames-list-headers",
+        },
+        this.renderColumns()
+      )
+    );
+  }
+}
+
+module.exports = FrameListHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FrameListItem.js
@@ -0,0 +1,87 @@
+/* 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 dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { tr } = dom;
+
+loader.lazyGetter(this, "FrameListColumnType", function() {
+  return createFactory(require("./FrameListColumnType"));
+});
+loader.lazyGetter(this, "FrameListColumnSize", function() {
+  return createFactory(require("./FrameListColumnSize"));
+});
+loader.lazyGetter(this, "FrameListColumnPayload", function() {
+  return createFactory(require("./FrameListColumnPayload"));
+});
+loader.lazyGetter(this, "FrameListColumnOpCode", function() {
+  return createFactory(require("./FrameListColumnOpCode"));
+});
+loader.lazyGetter(this, "FrameListColumnMaskBit", function() {
+  return createFactory(require("./FrameListColumnMaskBit"));
+});
+loader.lazyGetter(this, "FrameListColumnFinBit", function() {
+  return createFactory(require("./FrameListColumnFinBit"));
+});
+loader.lazyGetter(this, "FrameListColumnTime", function() {
+  return createFactory(require("./FrameListColumnTime"));
+});
+
+const COLUMN_COMPONENTS = [
+  { column: "type", ColumnComponent: FrameListColumnType },
+  { column: "size", ColumnComponent: FrameListColumnSize },
+  { column: "payload", ColumnComponent: FrameListColumnPayload },
+  { column: "opCode", ColumnComponent: FrameListColumnOpCode },
+  { column: "maskBit", ColumnComponent: FrameListColumnMaskBit },
+  { column: "finBit", ColumnComponent: FrameListColumnFinBit },
+  { column: "time", ColumnComponent: FrameListColumnTime },
+];
+
+/**
+ * Renders one row in the frame list.
+ */
+class FrameListItem extends Component {
+  static get propTypes() {
+    return {
+      item: PropTypes.object.isRequired,
+      index: PropTypes.number.isRequired,
+      isSelected: PropTypes.bool.isRequired,
+      onMouseDown: PropTypes.func.isRequired,
+      connector: PropTypes.object.isRequired,
+    };
+  }
+
+  render() {
+    const { item, index, isSelected, onMouseDown, connector } = this.props;
+
+    const classList = ["ws-frame-list-item", index % 2 ? "odd" : "even"];
+    if (isSelected) {
+      classList.push("selected");
+    }
+
+    return tr(
+      {
+        className: classList.join(" "),
+        tabIndex: 0,
+        onMouseDown,
+      },
+      COLUMN_COMPONENTS.map(({ ColumnComponent, column }) =>
+        ColumnComponent({
+          key: column + "-" + index,
+          connector,
+          item,
+          index,
+        })
+      )
+    );
+  }
+}
+
+module.exports = FrameListItem;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/FramePayload.js
@@ -0,0 +1,60 @@
+/* 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 } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { div } = dom;
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { getFramePayload } = require("../../utils/request-utils");
+
+/**
+ * Shows the full payload of a WebSocket frame.
+ * The payload is unwrapped from the LongStringActor object.
+ */
+class FramePayload extends Component {
+  static get propTypes() {
+    return {
+      connector: PropTypes.object.isRequired,
+      selectedFrame: PropTypes.object,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      payload: "",
+    };
+  }
+
+  componentDidMount() {
+    const { selectedFrame, connector } = this.props;
+    getFramePayload(selectedFrame.payload, connector.getLongString).then(
+      payload => {
+        this.setState({
+          payload,
+        });
+      }
+    );
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const { selectedFrame, connector } = nextProps;
+    getFramePayload(selectedFrame.payload, connector.getLongString).then(
+      payload => {
+        this.setState({
+          payload,
+        });
+      }
+    );
+  }
+
+  render() {
+    return div({ className: "ws-frame-payload" }, this.state.payload);
+  }
+}
+
+module.exports = FramePayload;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/WebSocketsPanel.js
@@ -0,0 +1,109 @@
+/* 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 {
+  Component,
+  createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const {
+  connect,
+} = require("devtools/client/shared/redux/visibility-handler-connect");
+const Actions = require("../../actions/index");
+
+const {
+  getSelectedFrame,
+  isSelectedFrameVisible,
+} = require("../../selectors/index");
+
+// Components
+const SplitBox = createFactory(
+  require("devtools/client/shared/components/splitter/SplitBox")
+);
+const FrameListContent = createFactory(require("./FrameListContent"));
+
+loader.lazyGetter(this, "FramePayload", function() {
+  return createFactory(require("./FramePayload"));
+});
+
+/**
+ * Renders a list of WebSocket frames in table view.
+ * Full payload is separated using a SplitBox.
+ */
+class WebSocketsPanel extends Component {
+  static get propTypes() {
+    return {
+      channelId: PropTypes.number,
+      connector: PropTypes.object.isRequired,
+      selectedFrame: PropTypes.object,
+      frameDetailsOpen: PropTypes.bool.isRequired,
+      openFrameDetailsTab: PropTypes.func.isRequired,
+      selectedFrameVisible: PropTypes.bool.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+  }
+
+  componentDidUpdate() {
+    const { selectedFrameVisible, openFrameDetailsTab } = this.props;
+    if (!selectedFrameVisible) {
+      openFrameDetailsTab(false);
+    }
+  }
+
+  render() {
+    const {
+      frameDetailsOpen,
+      channelId,
+      connector,
+      selectedFrame,
+    } = this.props;
+
+    const initialWidth = Services.prefs.getIntPref(
+      "devtools.netmonitor.ws.payload-preview-width"
+    );
+    const initialHeight = Services.prefs.getIntPref(
+      "devtools.netmonitor.ws.payload-preview-height"
+    );
+
+    return SplitBox({
+      className: "devtools-responsive-container",
+      initialWidth: initialWidth,
+      initialHeight: initialHeight,
+      minSize: "50px",
+      maxSize: "50%",
+      splitterSize: frameDetailsOpen ? 1 : 0,
+      startPanel: FrameListContent({ channelId, connector }),
+      endPanel:
+        frameDetailsOpen &&
+        FramePayload({
+          connector,
+          selectedFrame,
+        }),
+      endPanelCollapsed: !frameDetailsOpen,
+      endPanelControl: true,
+      vert: false,
+    });
+  }
+}
+
+module.exports = connect(
+  (state, props) => ({
+    selectedFrame: getSelectedFrame(state),
+    frameDetailsOpen: state.webSockets.frameDetailsOpen,
+    selectedFrameVisible: isSelectedFrameVisible(
+      state,
+      props.channelId,
+      getSelectedFrame(state)
+    ),
+  }),
+  dispatch => ({
+    openFrameDetailsTab: open => dispatch(Actions.openFrameDetails(open)),
+  })
+)(WebSocketsPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/websockets/moz.build
@@ -0,0 +1,18 @@
+# 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(
+    'FrameListColumnFinBit.js',
+    'FrameListColumnMaskBit.js',
+    'FrameListColumnOpCode.js',
+    'FrameListColumnPayload.js',
+    'FrameListColumnSize.js',
+    'FrameListColumnTime.js',
+    'FrameListColumnType.js',
+    'FrameListContent.js',
+    'FrameListHeader.js',
+    'FrameListItem.js',
+    'FramePayload.js',
+    'WebSocketsPanel.js',
+)
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -30,16 +30,18 @@ const actionTypes = {
   SORT_BY: "SORT_BY",
   TOGGLE_COLUMN: "TOGGLE_COLUMN",
   TOGGLE_RECORDING: "TOGGLE_RECORDING",
   TOGGLE_REQUEST_FILTER_TYPE: "TOGGLE_REQUEST_FILTER_TYPE",
   UPDATE_REQUEST: "UPDATE_REQUEST",
   WATERFALL_RESIZE: "WATERFALL_RESIZE",
   SET_COLUMNS_WIDTH: "SET_COLUMNS_WIDTH",
   WS_ADD_FRAME: "WS_ADD_FRAME",
+  WS_SELECT_FRAME: "WS_SELECT_FRAME",
+  WS_OPEN_FRAME_DETAILS: "WS_OPEN_FRAME_DETAILS",
 };
 
 // Descriptions for what this frontend is currently doing.
 const ACTIVITY_TYPE = {
   // Standing by and handling requests normally.
   NONE: 0,
 
   // Forcing the target to reload with cache enabled or disabled.
@@ -311,16 +313,40 @@ const FILTER_TAGS = [
   "xhr",
   "fonts",
   "images",
   "media",
   "ws",
   "other",
 ];
 
+const WS_FRAMES_HEADERS = [
+  {
+    name: "frameType",
+  },
+  {
+    name: "size",
+  },
+  {
+    name: "payload",
+  },
+  {
+    name: "opCode",
+  },
+  {
+    name: "maskBit",
+  },
+  {
+    name: "finBit",
+  },
+  {
+    name: "time",
+  },
+];
+
 const REQUESTS_WATERFALL = {
   BACKGROUND_TICKS_MULTIPLE: 5, // ms
   BACKGROUND_TICKS_SCALES: 3,
   BACKGROUND_TICKS_SPACING_MIN: 10, // px
   BACKGROUND_TICKS_COLOR_RGB: [128, 136, 144],
   // 8-bit value of the alpha component of the tick color
   BACKGROUND_TICKS_OPACITY_MIN: 32,
   BACKGROUND_TICKS_OPACITY_ADD: 32,
@@ -439,16 +465,17 @@ const BLOCKED_REASON_MESSAGES = {
 };
 
 const general = {
   ACTIVITY_TYPE,
   EVENTS,
   FILTER_SEARCH_DELAY: 200,
   UPDATE_PROPS,
   HEADERS,
+  WS_FRAMES_HEADERS,
   RESPONSE_HEADERS,
   FILTER_FLAGS,
   FILTER_TAGS,
   REQUESTS_WATERFALL,
   PANELS,
   TIMING_KEYS,
   MIN_COLUMN_WIDTH,
   DEFAULT_COLUMN_WIDTH,
--- a/devtools/client/netmonitor/src/reducers/index.js
+++ b/devtools/client/netmonitor/src/reducers/index.js
@@ -6,22 +6,22 @@
 
 const { combineReducers } = require("devtools/client/shared/vendor/redux");
 const batchingReducer = require("./batching");
 const { requestsReducer } = require("./requests");
 const { sortReducer } = require("./sort");
 const { filters } = require("./filters");
 const { timingMarkers } = require("./timing-markers");
 const { ui } = require("./ui");
-const { webSocketsReducer } = require("./web-sockets");
+const { webSockets } = require("./web-sockets");
 const networkThrottling = require("devtools/client/shared/components/throttling/reducer");
 
 module.exports = batchingReducer(
   combineReducers({
     requests: requestsReducer,
     sort: sortReducer,
-    webSockets: webSocketsReducer,
+    webSockets,
     filters,
     timingMarkers,
     ui,
     networkThrottling,
   })
 );
--- a/devtools/client/netmonitor/src/reducers/web-sockets.js
+++ b/devtools/client/netmonitor/src/reducers/web-sockets.js
@@ -1,64 +1,90 @@
 /* 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 {
   WS_ADD_FRAME,
+  WS_SELECT_FRAME,
+  WS_OPEN_FRAME_DETAILS,
 } = require("../constants");
 
 /**
  * This structure stores list of all WebSocket frames received
  * from the backend.
  */
 function WebSockets() {
   return {
     // Map with all requests (key = channelId, value = array of frame objects)
     frames: new Map(),
+    selectedFrame: null,
+    frameDetailsOpen: false,
   };
 }
 
-/**
- * This reducer is responsible for maintaining list of
- * WebSocket frames within the Network panel.
- */
-function webSocketsReducer(state = WebSockets(), action) {
-  switch (action.type) {
-    // Appending new frame into the map.
-    case WS_ADD_FRAME: {
-      const nextState = { ...state };
+// Appending new frame into the map.
+function addFrame(state, action) {
+  const nextState = { ...state };
+
+  const newFrame = {
+    httpChannelId: action.httpChannelId,
+    ...action.data,
+  };
+
+  nextState.frames = mapSet(state.frames, newFrame.httpChannelId, newFrame);
+
+  return nextState;
+}
 
-      const newFrame = {
-        httpChannelId: action.httpChannelId,
-        ...action.data,
-      };
-
-      nextState.frames = mapSet(state.frames, newFrame.httpChannelId, newFrame);
+// Select specific frame.
+function selectFrame(state, action) {
+  return {
+    ...state,
+    selectedFrame: action.frame,
+    frameDetailsOpen: action.open,
+  };
+}
 
-      return nextState;
-    }
-
-    default:
-      return state;
-  }
+function openFrameDetails(state, action) {
+  return {
+    ...state,
+    frameDetailsOpen: action.open,
+  };
 }
 
 /**
  * Append new item into existing map and return new map.
  */
 function mapSet(map, key, value) {
   const newMap = new Map(map);
   if (newMap.has(key)) {
     const framesArray = [...newMap.get(key)];
     framesArray.push(value);
     newMap.set(key, framesArray);
     return newMap;
   }
   return newMap.set(key, [value]);
 }
 
+/**
+ * This reducer is responsible for maintaining list of
+ * WebSocket frames within the Network panel.
+ */
+function webSockets(state = WebSockets(), action) {
+  switch (action.type) {
+    case WS_ADD_FRAME:
+      return addFrame(state, action);
+    case WS_SELECT_FRAME:
+      return selectFrame(state, action);
+    case WS_OPEN_FRAME_DETAILS:
+      return openFrameDetails(state, action);
+    default:
+      return state;
+  }
+}
+
 module.exports = {
   WebSockets,
-  webSocketsReducer,
+  webSockets,
 };
--- a/devtools/client/netmonitor/src/selectors/web-sockets.js
+++ b/devtools/client/netmonitor/src/selectors/web-sockets.js
@@ -1,13 +1,35 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const { createSelector } = require("devtools/client/shared/vendor/reselect");
+
 function getFramesByChannelId(state, channelId) {
   return state.webSockets.frames.get(channelId);
 }
 
+/**
+ * Checks if the selected frame is visible.
+ * If the selected frame is not visible, the SplitBox component
+ * should not show the FramePayload component.
+ */
+function isSelectedFrameVisible(state, channelId, targetFrame) {
+  const displayedFrames = getFramesByChannelId(state, channelId);
+  if (displayedFrames && targetFrame) {
+    return displayedFrames.some(frame => frame === targetFrame);
+  }
+  return false;
+}
+
+const getSelectedFrame = createSelector(
+  state => state.webSockets,
+  ({ selectedFrame }) => (selectedFrame ? selectedFrame : undefined)
+);
+
 module.exports = {
   getFramesByChannelId,
+  getSelectedFrame,
+  isSelectedFrameVisible,
 };
--- a/devtools/client/netmonitor/src/utils/request-utils.js
+++ b/devtools/client/netmonitor/src/utils/request-utils.js
@@ -516,16 +516,25 @@ async function updateFormDataSections(pr
       connector.getLongString,
     );
 
     updateRequest(request.id, { formDataSections }, true);
   }
 }
 
 /**
+ * This helper function helps to resolve the full payload of a WebSocket frame
+ * that is wrapped in a LongStringActor object.
+ */
+async function getFramePayload(payload, getLongString) {
+  const result = await getLongString(payload);
+  return result;
+}
+
+/**
  * This helper function is used for additional processing of
  * incoming network update packets. It's used by Network and
  * Console panel reducers.
  */
 function processNetworkUpdates(update, request) {
   const result = {};
   for (const [key, value] of Object.entries(update)) {
     if (UPDATE_PROPS.includes(key)) {
@@ -556,16 +565,17 @@ module.exports = {
   fetchHeaders,
   fetchNetworkUpdatePacket,
   formDataURI,
   writeHeaderText,
   getAbbreviatedMimeType,
   getFileName,
   getEndTime,
   getFormattedProtocol,
+  getFramePayload,
   getResponseHeader,
   getResponseTime,
   getStartTime,
   getUrlBaseName,
   getUrlBaseNameWithQuery,
   getUrlDetails,
   getUrlHost,
   getUrlHostName,
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -164,16 +164,18 @@ pref("devtools.application.enabled", fal
 pref("devtools.netmonitor.panes-network-details-width", 550);
 pref("devtools.netmonitor.panes-network-details-height", 450);
 pref("devtools.netmonitor.filters", "[\"all\"]");
 pref("devtools.netmonitor.visibleColumns",
   "[\"status\",\"method\",\"domain\",\"file\",\"cause\",\"type\",\"transferred\",\"contentSize\",\"waterfall\"]"
 );
 pref("devtools.netmonitor.columnsData",
   '[{"name":"status","minWidth":30,"width":5}, {"name":"method","minWidth":30,"width":5}, {"name":"domain","minWidth":30,"width":10}, {"name":"file","minWidth":30,"width":25}, {"name":"url","minWidth":30,"width":25}, {"name":"cause","minWidth":30,"width":10},{"name":"type","minWidth":30,"width":5},{"name":"transferred","minWidth":30,"width":10},{"name":"contentSize","minWidth":30,"width":5},{"name":"waterfall","minWidth":150,"width":25}]');
+pref("devtools.netmonitor.ws.payload-preview-width", 550);
+pref("devtools.netmonitor.ws.payload-preview-height", 450);
 
 // Support for columns resizing pref is now enabled (after merge date 03/18/19).
 pref("devtools.netmonitor.features.resizeColumns", true);
 
 pref("devtools.netmonitor.response.ui.limit", 10240);
 
 // Save request/response bodies yes/no.
 pref("devtools.netmonitor.saveRequestAndResponseBodies", true);