From 44c6145f6ef66abe7e786dace0cd6e0c0d5c5e79 Mon Sep 17 00:00:00 2001 From: tanhengyeow Date: Fri, 28 Jun 2019 07:24:59 +0000 Subject: [PATCH] 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 --HG-- extra : moz-landing-system : lando --- .../locales/en-US/netmonitor.properties | 28 +++++ .../netmonitor/src/actions/web-sockets.js | 31 ++++- .../src/assets/styles/RequestList.css | 43 ++++--- .../netmonitor/src/components/TabboxPanel.js | 3 +- .../src/components/WebSocketsPanel.js | 85 -------------- .../netmonitor/src/components/moz.build | 5 +- .../websockets/FrameListColumnFinBit.js | 41 +++++++ .../websockets/FrameListColumnMaskBit.js | 41 +++++++ .../websockets/FrameListColumnOpCode.js | 41 +++++++ .../websockets/FrameListColumnPayload.js | 64 ++++++++++ .../websockets/FrameListColumnSize.js | 42 +++++++ .../websockets/FrameListColumnTime.js | 44 +++++++ .../websockets/FrameListColumnType.js | 41 +++++++ .../components/websockets/FrameListContent.js | 99 ++++++++++++++++ .../components/websockets/FrameListHeader.js | 67 +++++++++++ .../components/websockets/FrameListItem.js | 87 ++++++++++++++ .../src/components/websockets/FramePayload.js | 60 ++++++++++ .../components/websockets/WebSocketsPanel.js | 109 ++++++++++++++++++ .../src/components/websockets/moz.build | 18 +++ devtools/client/netmonitor/src/constants.js | 27 +++++ .../client/netmonitor/src/reducers/index.js | 4 +- .../netmonitor/src/reducers/web-sockets.js | 66 +++++++---- .../netmonitor/src/selectors/web-sockets.js | 22 ++++ .../netmonitor/src/utils/request-utils.js | 10 ++ .../client/preferences/devtools-client.js | 2 + 25 files changed, 957 insertions(+), 123 deletions(-) delete mode 100644 devtools/client/netmonitor/src/components/WebSocketsPanel.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListColumnFinBit.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListColumnMaskBit.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListColumnOpCode.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListColumnPayload.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListColumnSize.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListColumnTime.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListColumnType.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListContent.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListHeader.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FrameListItem.js create mode 100644 devtools/client/netmonitor/src/components/websockets/FramePayload.js create mode 100644 devtools/client/netmonitor/src/components/websockets/WebSocketsPanel.js create mode 100644 devtools/client/netmonitor/src/components/websockets/moz.build diff --git a/devtools/client/locales/en-US/netmonitor.properties b/devtools/client/locales/en-US/netmonitor.properties index 7d2386934cca..c0a006d4b0c7 100644 --- a/devtools/client/locales/en-US/netmonitor.properties +++ b/devtools/client/locales/en-US/netmonitor.properties @@ -637,6 +637,34 @@ netmonitor.toolbar.contentSize=Size # 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 diff --git a/devtools/client/netmonitor/src/actions/web-sockets.js b/devtools/client/netmonitor/src/actions/web-sockets.js index 0105635997c7..d450a4f52561 100644 --- a/devtools/client/netmonitor/src/actions/web-sockets.js +++ b/devtools/client/netmonitor/src/actions/web-sockets.js @@ -4,7 +4,11 @@ "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 { @@ -14,6 +18,31 @@ function addFrame(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, }; diff --git a/devtools/client/netmonitor/src/assets/styles/RequestList.css b/devtools/client/netmonitor/src/assets/styles/RequestList.css index 892772e24abd..67b69043914f 100644 --- a/devtools/client/netmonitor/src/assets/styles/RequestList.css +++ b/devtools/client/netmonitor/src/assets/styles/RequestList.css @@ -10,6 +10,10 @@ overflow-x: hidden; } +.ws-frame-list-empty-notice { + width: 100%; +} + .empty-notice-element { padding-top: 12px; padding-left: 12px; @@ -56,7 +60,8 @@ overflow-y: auto; } -.requests-list-table { +.requests-list-table, +.ws-frames-list-table { /* Reset default browser style of */ border-spacing: 0; width: 100%; @@ -66,7 +71,8 @@ table-layout: fixed; } -.requests-list-column { +.requests-list-column, +.ws-frames-list-column { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -82,7 +88,8 @@ /* Requests list headers */ -.requests-list-headers-group { +.requests-list-headers-group, +.ws-frames-list-headers-group { /* Avoid .devtools-toolbar to override display type */ display: table-header-group; @@ -93,16 +100,19 @@ 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%, @@ -126,7 +136,8 @@ 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%; @@ -134,7 +145,8 @@ 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; @@ -309,7 +321,8 @@ 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; } @@ -464,16 +477,19 @@ /* 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); } @@ -489,7 +505,8 @@ * 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); } diff --git a/devtools/client/netmonitor/src/components/TabboxPanel.js b/devtools/client/netmonitor/src/components/TabboxPanel.js index d608580eaf57..75e573767454 100644 --- a/devtools/client/netmonitor/src/components/TabboxPanel.js +++ b/devtools/client/netmonitor/src/components/TabboxPanel.js @@ -18,7 +18,7 @@ const Tabbar = createFactory(require("devtools/client/shared/components/tabs/Tab 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")); @@ -131,6 +131,7 @@ class TabboxPanel extends Component { }, WebSocketsPanel({ channelId, + connector, }), ), TabPanel({ diff --git a/devtools/client/netmonitor/src/components/WebSocketsPanel.js b/devtools/client/netmonitor/src/components/WebSocketsPanel.js deleted file mode 100644 index 0d8ca9f4d7f2..000000000000 --- 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); diff --git a/devtools/client/netmonitor/src/components/moz.build b/devtools/client/netmonitor/src/components/moz.build index 9326f8d49b8f..7a8027ab39e3 100644 --- a/devtools/client/netmonitor/src/components/moz.build +++ b/devtools/client/netmonitor/src/components/moz.build @@ -2,6 +2,10 @@ # 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', @@ -47,5 +51,4 @@ DevToolsModules( 'TabboxPanel.js', 'TimingsPanel.js', 'Toolbar.js', - 'WebSocketsPanel.js', ) diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListColumnFinBit.js b/devtools/client/netmonitor/src/components/websockets/FrameListColumnFinBit.js new file mode 100644 index 000000000000..8e2313698af2 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListColumnMaskBit.js b/devtools/client/netmonitor/src/components/websockets/FrameListColumnMaskBit.js new file mode 100644 index 000000000000..6631b2318197 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListColumnOpCode.js b/devtools/client/netmonitor/src/components/websockets/FrameListColumnOpCode.js new file mode 100644 index 000000000000..593105e318c6 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListColumnPayload.js b/devtools/client/netmonitor/src/components/websockets/FrameListColumnPayload.js new file mode 100644 index 000000000000..f8d7d1a9ec98 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListColumnSize.js b/devtools/client/netmonitor/src/components/websockets/FrameListColumnSize.js new file mode 100644 index 000000000000..a03ca745c906 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListColumnTime.js b/devtools/client/netmonitor/src/components/websockets/FrameListColumnTime.js new file mode 100644 index 000000000000..908c212c81ae --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListColumnType.js b/devtools/client/netmonitor/src/components/websockets/FrameListColumnType.js new file mode 100644 index 000000000000..d46fb4f190c0 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListContent.js b/devtools/client/netmonitor/src/components/websockets/FrameListContent.js new file mode 100644 index 000000000000..c229cfde9845 --- /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); diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListHeader.js b/devtools/client/netmonitor/src/components/websockets/FrameListHeader.js new file mode 100644 index 000000000000..9c197b560e60 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FrameListItem.js b/devtools/client/netmonitor/src/components/websockets/FrameListItem.js new file mode 100644 index 000000000000..1591f7b44a26 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/FramePayload.js b/devtools/client/netmonitor/src/components/websockets/FramePayload.js new file mode 100644 index 000000000000..549547042526 --- /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; diff --git a/devtools/client/netmonitor/src/components/websockets/WebSocketsPanel.js b/devtools/client/netmonitor/src/components/websockets/WebSocketsPanel.js new file mode 100644 index 000000000000..6890adca2e54 --- /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); diff --git a/devtools/client/netmonitor/src/components/websockets/moz.build b/devtools/client/netmonitor/src/components/websockets/moz.build new file mode 100644 index 000000000000..0874f3a5d3ed --- /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', +) diff --git a/devtools/client/netmonitor/src/constants.js b/devtools/client/netmonitor/src/constants.js index 908ae95bbe2c..ce280605078f 100644 --- a/devtools/client/netmonitor/src/constants.js +++ b/devtools/client/netmonitor/src/constants.js @@ -35,6 +35,8 @@ const actionTypes = { 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. @@ -316,6 +318,30 @@ const FILTER_TAGS = [ "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, @@ -444,6 +470,7 @@ const general = { FILTER_SEARCH_DELAY: 200, UPDATE_PROPS, HEADERS, + WS_FRAMES_HEADERS, RESPONSE_HEADERS, FILTER_FLAGS, FILTER_TAGS, diff --git a/devtools/client/netmonitor/src/reducers/index.js b/devtools/client/netmonitor/src/reducers/index.js index 3b44ea823a88..6742136adbb2 100644 --- a/devtools/client/netmonitor/src/reducers/index.js +++ b/devtools/client/netmonitor/src/reducers/index.js @@ -11,14 +11,14 @@ 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, diff --git a/devtools/client/netmonitor/src/reducers/web-sockets.js b/devtools/client/netmonitor/src/reducers/web-sockets.js index cd6fb23763f9..1a712a54ccd3 100644 --- a/devtools/client/netmonitor/src/reducers/web-sockets.js +++ b/devtools/client/netmonitor/src/reducers/web-sockets.js @@ -6,6 +6,8 @@ const { WS_ADD_FRAME, + WS_SELECT_FRAME, + WS_OPEN_FRAME_DETAILS, } = require("../constants"); /** @@ -16,32 +18,39 @@ 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, - }; + const newFrame = { + httpChannelId: action.httpChannelId, + ...action.data, + }; - nextState.frames = mapSet(state.frames, newFrame.httpChannelId, newFrame); + nextState.frames = mapSet(state.frames, newFrame.httpChannelId, newFrame); - return nextState; - } + return nextState; +} - default: - return state; - } +// Select specific frame. +function selectFrame(state, action) { + return { + ...state, + selectedFrame: action.frame, + frameDetailsOpen: action.open, + }; +} + +function openFrameDetails(state, action) { + return { + ...state, + frameDetailsOpen: action.open, + }; } /** @@ -58,7 +67,24 @@ function mapSet(map, key, value) { 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, }; diff --git a/devtools/client/netmonitor/src/selectors/web-sockets.js b/devtools/client/netmonitor/src/selectors/web-sockets.js index 4c5d97d3223e..dbee2f66e11b 100644 --- a/devtools/client/netmonitor/src/selectors/web-sockets.js +++ b/devtools/client/netmonitor/src/selectors/web-sockets.js @@ -4,10 +4,32 @@ "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, }; diff --git a/devtools/client/netmonitor/src/utils/request-utils.js b/devtools/client/netmonitor/src/utils/request-utils.js index e70fe0f77262..513a941fc94e 100644 --- a/devtools/client/netmonitor/src/utils/request-utils.js +++ b/devtools/client/netmonitor/src/utils/request-utils.js @@ -520,6 +520,15 @@ async function updateFormDataSections(props) { } } +/** + * 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 @@ -561,6 +570,7 @@ module.exports = { getFileName, getEndTime, getFormattedProtocol, + getFramePayload, getResponseHeader, getResponseTime, getStartTime, diff --git a/devtools/client/preferences/devtools-client.js b/devtools/client/preferences/devtools-client.js index 9248b4bcf537..8570ef0402c0 100644 --- a/devtools/client/preferences/devtools-client.js +++ b/devtools/client/preferences/devtools-client.js @@ -169,6 +169,8 @@ pref("devtools.netmonitor.visibleColumns", ); 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);