From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../src/components/request-list/RequestList.js | 50 ++ .../request-list/RequestListColumnContentSize.js | 38 ++ .../request-list/RequestListColumnCookies.js | 61 ++ .../request-list/RequestListColumnDomain.js | 66 ++ .../request-list/RequestListColumnFile.js | 91 +++ .../request-list/RequestListColumnInitiator.js | 65 ++ .../request-list/RequestListColumnMethod.js | 33 + .../request-list/RequestListColumnPriority.js | 36 + .../request-list/RequestListColumnProtocol.js | 43 ++ .../request-list/RequestListColumnRemoteIP.js | 43 ++ .../RequestListColumnResponseHeader.js | 59 ++ .../request-list/RequestListColumnScheme.js | 38 ++ .../request-list/RequestListColumnSetCookies.js | 61 ++ .../request-list/RequestListColumnStatus.js | 39 ++ .../request-list/RequestListColumnTime.js | 91 +++ .../RequestListColumnTransferredSize.js | 99 +++ .../request-list/RequestListColumnType.js | 45 ++ .../request-list/RequestListColumnUrl.js | 89 +++ .../request-list/RequestListColumnWaterfall.js | 209 ++++++ .../components/request-list/RequestListContent.js | 524 +++++++++++++++ .../request-list/RequestListEmptyNotice.js | 107 +++ .../components/request-list/RequestListHeader.js | 731 +++++++++++++++++++++ .../src/components/request-list/RequestListItem.js | 412 ++++++++++++ .../src/components/request-list/moz.build | 29 + 24 files changed, 3059 insertions(+) create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestList.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnContentSize.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnCookies.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnDomain.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnFile.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnInitiator.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnMethod.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnPriority.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnProtocol.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnRemoteIP.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnResponseHeader.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnScheme.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnSetCookies.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnStatus.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnTime.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnTransferredSize.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnType.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnUrl.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListColumnWaterfall.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListContent.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListEmptyNotice.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListHeader.js create mode 100644 devtools/client/netmonitor/src/components/request-list/RequestListItem.js create mode 100644 devtools/client/netmonitor/src/components/request-list/moz.build (limited to 'devtools/client/netmonitor/src/components/request-list') diff --git a/devtools/client/netmonitor/src/components/request-list/RequestList.js b/devtools/client/netmonitor/src/components/request-list/RequestList.js new file mode 100644 index 0000000000..63640e389c --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestList.js @@ -0,0 +1,50 @@ +/* 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 { + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { div } = dom; +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +// Components +const StatusBar = createFactory( + require("resource://devtools/client/netmonitor/src/components/StatusBar.js") +); + +loader.lazyGetter(this, "RequestListContent", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListContent.js") + ); +}); +loader.lazyGetter(this, "RequestListEmptyNotice", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListEmptyNotice.js") + ); +}); + +/** + * Request panel component + */ +function RequestList({ connector, isEmpty }) { + return div( + { className: "request-list-container" }, + isEmpty + ? RequestListEmptyNotice({ connector }) + : RequestListContent({ connector }), + StatusBar({ connector }) + ); +} + +RequestList.displayName = "RequestList"; + +RequestList.propTypes = { + connector: PropTypes.object.isRequired, + isEmpty: PropTypes.bool.isRequired, +}; + +module.exports = RequestList; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnContentSize.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnContentSize.js new file mode 100644 index 0000000000..4c20d4aebe --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnContentSize.js @@ -0,0 +1,38 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getFormattedSize, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); + +class RequestListColumnContentSize extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return this.props.item.contentSize !== nextProps.item.contentSize; + } + + render() { + const { contentSize } = this.props.item; + const size = + typeof contentSize === "number" ? getFormattedSize(contentSize) : null; + return dom.td( + { className: "requests-list-column requests-list-size", title: size }, + size + ); + } +} + +module.exports = RequestListColumnContentSize; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnCookies.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnCookies.js new file mode 100644 index 0000000000..0c1431ed60 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnCookies.js @@ -0,0 +1,61 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + fetchNetworkUpdatePacket, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +class RequestListColumnCookies extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, + }; + } + + componentDidMount() { + const { item, connector } = this.props; + fetchNetworkUpdatePacket(connector.requestData, item, ["requestCookies"]); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { item, connector } = nextProps; + fetchNetworkUpdatePacket(connector.requestData, item, ["requestCookies"]); + } + + shouldComponentUpdate(nextProps) { + let { requestCookies: currRequestCookies = { cookies: [] } } = + this.props.item; + let { requestCookies: nextRequestCookies = { cookies: [] } } = + nextProps.item; + currRequestCookies = currRequestCookies.cookies || currRequestCookies; + nextRequestCookies = nextRequestCookies.cookies || nextRequestCookies; + return currRequestCookies !== nextRequestCookies; + } + + render() { + let { requestCookies = { cookies: [] } } = this.props.item; + requestCookies = requestCookies.cookies || requestCookies; + const requestCookiesLength = requestCookies.length + ? requestCookies.length + : ""; + return dom.td( + { + className: "requests-list-column requests-list-cookies", + title: requestCookiesLength, + }, + requestCookiesLength + ); + } +} + +module.exports = RequestListColumnCookies; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnDomain.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnDomain.js new file mode 100644 index 0000000000..3ca6841da6 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnDomain.js @@ -0,0 +1,66 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const { + td, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getFormattedIPAndPort, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); +const { + propertiesEqual, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const SecurityState = createFactory( + require("resource://devtools/client/netmonitor/src/components/SecurityState.js") +); + +const UPDATED_DOMAIN_PROPS = ["remoteAddress", "securityState", "urlDetails"]; + +class RequestListColumnDomain extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + onSecurityIconMouseDown: PropTypes.func.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return !propertiesEqual( + UPDATED_DOMAIN_PROPS, + this.props.item, + nextProps.item + ); + } + + render() { + const { item, onSecurityIconMouseDown } = this.props; + + const { + remoteAddress, + remotePort, + urlDetails: { host, isLocal }, + } = item; + + const title = + host + + (remoteAddress + ? ` (${getFormattedIPAndPort(remoteAddress, remotePort)})` + : ""); + + return td( + { className: "requests-list-column requests-list-domain", title }, + SecurityState({ item, onSecurityIconMouseDown, isLocal }), + host + ); + } +} + +module.exports = RequestListColumnDomain; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnFile.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnFile.js new file mode 100644 index 0000000000..f824cdb973 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnFile.js @@ -0,0 +1,91 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); +const { + propertiesEqual, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const { + getFormattedTime, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); + +const UPDATED_FILE_PROPS = ["urlDetails", "waitingTime"]; + +class RequestListColumnFile extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + slowLimit: PropTypes.number, + onWaterfallMouseDown: PropTypes.func, + }; + } + + shouldComponentUpdate(nextProps) { + return !propertiesEqual( + UPDATED_FILE_PROPS, + this.props.item, + nextProps.item + ); + } + + render() { + const { + item: { urlDetails, waitingTime }, + slowLimit, + onWaterfallMouseDown, + } = this.props; + + const originalFileURL = urlDetails.url; + const decodedFileURL = urlDetails.unicodeUrl; + const ORIGINAL_FILE_URL = L10N.getFormatStr( + "netRequest.originalFileURL.tooltip", + originalFileURL + ); + const DECODED_FILE_URL = L10N.getFormatStr( + "netRequest.decodedFileURL.tooltip", + decodedFileURL + ); + const requestedFile = urlDetails.baseNameWithQuery; + const fileToolTip = + originalFileURL === decodedFileURL + ? originalFileURL + : ORIGINAL_FILE_URL + "\n\n" + DECODED_FILE_URL; + + const isSlow = slowLimit > 0 && !!waitingTime && waitingTime > slowLimit; + + return dom.td( + { + className: "requests-list-column requests-list-file", + title: fileToolTip, + }, + dom.div({}, requestedFile), + isSlow && + dom.div({ + title: L10N.getFormatStr( + "netmonitor.audits.slowIconTooltip", + getFormattedTime(waitingTime), + getFormattedTime(slowLimit) + ), + onMouseDown: onWaterfallMouseDown, + className: "requests-list-slow-button", + }) + ); + } +} + +module.exports = connect(state => ({ + slowLimit: state.ui.slowLimit, +}))(RequestListColumnFile); diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnInitiator.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnInitiator.js new file mode 100644 index 0000000000..0c08387fa1 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnInitiator.js @@ -0,0 +1,65 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + getUrlBaseName, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class RequestListColumnInitiator extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + onInitiatorBadgeMouseDown: PropTypes.func.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return this.props.item.cause !== nextProps.item.cause; + } + + render() { + const { + item: { cause }, + onInitiatorBadgeMouseDown, + } = this.props; + + let initiator = ""; + let lineNumber = ""; + + const lastFrameExists = cause && cause.lastFrame; + if (lastFrameExists) { + const { filename, lineNumber: _lineNumber } = cause.lastFrame; + initiator = getUrlBaseName(filename); + lineNumber = ":" + _lineNumber; + } + + // Legacy server might send a numeric value. Display it as "unknown" + const causeType = typeof cause.type === "string" ? cause.type : "unknown"; + const causeStr = lastFrameExists ? " (" + causeType + ")" : causeType; + return dom.td( + { + className: "requests-list-column requests-list-initiator", + title: initiator + lineNumber + causeStr, + }, + dom.div( + { + className: "requests-list-initiator-lastframe", + onMouseDown: onInitiatorBadgeMouseDown, + }, + dom.span({ className: "requests-list-initiator-filename" }, initiator), + dom.span({ className: "requests-list-initiator-line" }, lineNumber) + ), + dom.div({ className: "requests-list-initiator-cause" }, causeStr) + ); + } +} + +module.exports = RequestListColumnInitiator; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnMethod.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnMethod.js new file mode 100644 index 0000000000..08434a76fe --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnMethod.js @@ -0,0 +1,33 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class RequestListColumnMethod extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return this.props.item.method !== nextProps.item.method; + } + + render() { + const { method } = this.props.item; + return dom.td( + { className: "requests-list-column requests-list-method" }, + method + ); + } +} + +module.exports = RequestListColumnMethod; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnPriority.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnPriority.js new file mode 100644 index 0000000000..56f33ec3de --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnPriority.js @@ -0,0 +1,36 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getRequestPriorityAsText, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); + +class RequestListColumnPriority extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return this.props.item.method !== nextProps.item.method; + } + + render() { + const { priority } = this.props.item; + return dom.td( + { className: "requests-list-column" }, + getRequestPriorityAsText(priority) + ); + } +} + +module.exports = RequestListColumnPriority; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnProtocol.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnProtocol.js new file mode 100644 index 0000000000..d77ad43406 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnProtocol.js @@ -0,0 +1,43 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getFormattedProtocol, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +class RequestListColumnProtocol extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return ( + getFormattedProtocol(this.props.item) !== + getFormattedProtocol(nextProps.item) + ); + } + + render() { + const protocol = getFormattedProtocol(this.props.item); + + return dom.td( + { + className: "requests-list-column requests-list-protocol", + title: protocol, + }, + protocol + ); + } +} + +module.exports = RequestListColumnProtocol; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnRemoteIP.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnRemoteIP.js new file mode 100644 index 0000000000..4fa22aa2b2 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnRemoteIP.js @@ -0,0 +1,43 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getFormattedIPAndPort, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); + +class RequestListColumnRemoteIP extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return this.props.item.remoteAddress !== nextProps.item.remoteAddress; + } + + render() { + const { remoteAddress, remotePort } = this.props.item; + const remoteIP = remoteAddress + ? getFormattedIPAndPort(remoteAddress, remotePort) + : "unknown"; + + return dom.td( + { + className: "requests-list-column requests-list-remoteip", + title: remoteIP, + }, + remoteIP + ); + } +} + +module.exports = RequestListColumnRemoteIP; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnResponseHeader.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnResponseHeader.js new file mode 100644 index 0000000000..de02da0bc5 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnResponseHeader.js @@ -0,0 +1,59 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getResponseHeader, + fetchNetworkUpdatePacket, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +/** + * Renders a response header column in the requests list. The actual + * header to show is passed as a prop. + */ +class RequestListColumnResponseHeader extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, + header: PropTypes.string.isRequired, + }; + } + + componentDidMount() { + const { item, connector } = this.props; + fetchNetworkUpdatePacket(connector.requestData, item, ["responseHeaders"]); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { item, connector } = nextProps; + fetchNetworkUpdatePacket(connector.requestData, item, ["responseHeaders"]); + } + + shouldComponentUpdate(nextProps) { + const currHeader = getResponseHeader(this.props.item, this.props.header); + const nextHeader = getResponseHeader(nextProps.item, nextProps.header); + return currHeader !== nextHeader; + } + + render() { + const header = getResponseHeader(this.props.item, this.props.header); + return dom.td( + { + className: "requests-list-column requests-list-response-header", + title: header, + }, + header + ); + } +} + +module.exports = RequestListColumnResponseHeader; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnScheme.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnScheme.js new file mode 100644 index 0000000000..7309a17d60 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnScheme.js @@ -0,0 +1,38 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class RequestListColumnScheme extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return ( + this.props.item.urlDetails.scheme !== nextProps.item.urlDetails.scheme + ); + } + + render() { + const { urlDetails } = this.props.item; + return dom.td( + { + className: "requests-list-column requests-list-scheme", + title: urlDetails.scheme, + }, + urlDetails.scheme + ); + } +} + +module.exports = RequestListColumnScheme; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnSetCookies.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnSetCookies.js new file mode 100644 index 0000000000..0ee8164411 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnSetCookies.js @@ -0,0 +1,61 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + fetchNetworkUpdatePacket, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +class RequestListColumnSetCookies extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, + }; + } + + componentDidMount() { + const { item, connector } = this.props; + fetchNetworkUpdatePacket(connector.requestData, item, ["responseCookies"]); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { item, connector } = nextProps; + fetchNetworkUpdatePacket(connector.requestData, item, ["responseCookies"]); + } + + shouldComponentUpdate(nextProps) { + let { responseCookies: currResponseCookies = { cookies: [] } } = + this.props.item; + let { responseCookies: nextResponseCookies = { cookies: [] } } = + nextProps.item; + currResponseCookies = currResponseCookies.cookies || currResponseCookies; + nextResponseCookies = nextResponseCookies.cookies || nextResponseCookies; + return currResponseCookies !== nextResponseCookies; + } + + render() { + let { responseCookies = { cookies: [] } } = this.props.item; + responseCookies = responseCookies.cookies || responseCookies; + const responseCookiesLength = responseCookies.length + ? responseCookies.length + : ""; + return dom.td( + { + className: "requests-list-column requests-list-set-cookies", + title: responseCookiesLength, + }, + responseCookiesLength + ); + } +} + +module.exports = RequestListColumnSetCookies; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnStatus.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnStatus.js new file mode 100644 index 0000000000..180d280b1b --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnStatus.js @@ -0,0 +1,39 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +// Components + +const StatusCode = createFactory( + require("resource://devtools/client/netmonitor/src/components/StatusCode.js") +); + +class RequestListColumnStatus extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + render() { + const { item } = this.props; + + return dom.td( + { + className: "requests-list-column requests-list-status", + }, + StatusCode({ item }) + ); + } +} + +module.exports = RequestListColumnStatus; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnTime.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnTime.js new file mode 100644 index 0000000000..4ee35d196f --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnTime.js @@ -0,0 +1,91 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getFormattedTime, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); +const { + fetchNetworkUpdatePacket, + getResponseTime, + getStartTime, + getEndTime, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +/** + * This component represents a column displaying selected + * timing value. There are following possible values this + * column can render: + * - Start Time + * - End Time + * - Response Time + * - Duration Time + * - Latency Time + */ +class RequestListColumnTime extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + firstRequestStartedMs: PropTypes.number.isRequired, + item: PropTypes.object.isRequired, + type: PropTypes.oneOf(["start", "end", "response", "duration", "latency"]) + .isRequired, + }; + } + + componentDidMount() { + const { item, connector } = this.props; + fetchNetworkUpdatePacket(connector.requestData, item, ["eventTimings"]); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { item, connector } = nextProps; + fetchNetworkUpdatePacket(connector.requestData, item, ["eventTimings"]); + } + + shouldComponentUpdate(nextProps) { + return this.getTime(this.props) !== this.getTime(nextProps); + } + + getTime(props) { + const { firstRequestStartedMs, item, type } = props; + + switch (type) { + case "start": + return getStartTime(item, firstRequestStartedMs); + case "end": + return getEndTime(item, firstRequestStartedMs); + case "response": + return getResponseTime(item, firstRequestStartedMs); + case "duration": + return item.totalTime; + case "latency": + return item.eventTimings ? item.eventTimings.timings.wait : undefined; + } + + return 0; + } + + render() { + const { type } = this.props; + const time = getFormattedTime(this.getTime(this.props)); + + return dom.td( + { + className: "requests-list-column requests-list-" + type + "-time", + title: time, + }, + time + ); + } +} + +module.exports = RequestListColumnTime; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnTransferredSize.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnTransferredSize.js new file mode 100644 index 0000000000..63cc7a7009 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnTransferredSize.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, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getFormattedSize, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + propertiesEqual, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const { + BLOCKED_REASON_MESSAGES, +} = require("resource://devtools/client/netmonitor/src/constants.js"); + +const SIZE_CACHED = L10N.getStr("networkMenu.sizeCached"); +const SIZE_SERVICE_WORKER = L10N.getStr("networkMenu.sizeServiceWorker"); +const SIZE_UNAVAILABLE = L10N.getStr("networkMenu.sizeUnavailable"); +const SIZE_UNAVAILABLE_TITLE = L10N.getStr("networkMenu.sizeUnavailable.title"); +const UPDATED_TRANSFERRED_PROPS = [ + "transferredSize", + "fromCache", + "isRacing", + "fromServiceWorker", +]; + +class RequestListColumnTransferredSize extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return !propertiesEqual( + UPDATED_TRANSFERRED_PROPS, + this.props.item, + nextProps.item + ); + } + + render() { + const { + blockedReason, + blockingExtension, + fromCache, + fromServiceWorker, + status, + transferredSize, + isRacing, + } = this.props.item; + let text; + + if (blockedReason && blockingExtension) { + text = L10N.getFormatStr("networkMenu.blockedby", blockingExtension); + } else if (blockedReason) { + // If we receive a platform error code, print it as-is + if (typeof blockedReason == "string" && blockedReason.startsWith("NS_")) { + text = blockedReason; + } else { + text = + BLOCKED_REASON_MESSAGES[blockedReason] || + L10N.getStr("networkMenu.blocked2"); + } + } else if (fromCache || status === "304") { + text = SIZE_CACHED; + } else if (fromServiceWorker) { + text = SIZE_SERVICE_WORKER; + } else if (typeof transferredSize == "number") { + text = getFormattedSize(transferredSize); + if (isRacing && typeof isRacing == "boolean") { + text = L10N.getFormatStr("networkMenu.raced", text); + } + } else if (transferredSize === null) { + text = SIZE_UNAVAILABLE; + } + + const title = text == SIZE_UNAVAILABLE ? SIZE_UNAVAILABLE_TITLE : text; + + return dom.td( + { + className: "requests-list-column requests-list-transferred", + title, + }, + text + ); + } +} + +module.exports = RequestListColumnTransferredSize; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnType.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnType.js new file mode 100644 index 0000000000..783245dc19 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnType.js @@ -0,0 +1,45 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getAbbreviatedMimeType, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +class RequestListColumnType extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return this.props.item.mimeType !== nextProps.item.mimeType; + } + + render() { + const { mimeType } = this.props.item; + let abbrevType; + + if (mimeType) { + abbrevType = getAbbreviatedMimeType(mimeType); + } + + return dom.td( + { + className: "requests-list-column requests-list-type", + title: mimeType, + }, + abbrevType + ); + } +} + +module.exports = RequestListColumnType; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnUrl.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnUrl.js new file mode 100644 index 0000000000..9782e9e820 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnUrl.js @@ -0,0 +1,89 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const { + td, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + getFormattedIPAndPort, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); +const { + propertiesEqual, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const SecurityState = createFactory( + require("resource://devtools/client/netmonitor/src/components/SecurityState.js") +); +const UPDATED_FILE_PROPS = ["remoteAddress", "securityState", "urlDetails"]; + +class RequestListColumnUrl extends Component { + static get propTypes() { + return { + item: PropTypes.object.isRequired, + onSecurityIconMouseDown: PropTypes.func.isRequired, + }; + } + + shouldComponentUpdate(nextProps) { + return !propertiesEqual( + UPDATED_FILE_PROPS, + this.props.item, + nextProps.item + ); + } + + render() { + const { + item: { urlDetails }, + } = this.props; + + const { item, onSecurityIconMouseDown } = this.props; + + const { + remoteAddress, + remotePort, + urlDetails: { isLocal }, + } = item; + + const title = remoteAddress + ? ` (${getFormattedIPAndPort(remoteAddress, remotePort)})` + : ""; + + // deals with returning whole url + const originalURL = urlDetails.url; + const decodedFileURL = urlDetails.unicodeUrl; + const ORIGINAL_FILE_URL = L10N.getFormatStr( + "netRequest.originalFileURL.tooltip", + originalURL + ); + const DECODED_FILE_URL = L10N.getFormatStr( + "netRequest.decodedFileURL.tooltip", + decodedFileURL + ); + const urlToolTip = + originalURL === decodedFileURL + ? originalURL + : ORIGINAL_FILE_URL + "\n\n" + DECODED_FILE_URL; + + return td( + { + className: "requests-list-column requests-list-url", + title: urlToolTip + title, + }, + SecurityState({ item, onSecurityIconMouseDown, isLocal }), + originalURL + ); + } +} + +module.exports = RequestListColumnUrl; diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListColumnWaterfall.js b/devtools/client/netmonitor/src/components/request-list/RequestListColumnWaterfall.js new file mode 100644 index 0000000000..19523399b6 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListColumnWaterfall.js @@ -0,0 +1,209 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); +const { + getWaterfallScale, +} = require("resource://devtools/client/netmonitor/src/selectors/index.js"); + +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + fetchNetworkUpdatePacket, + propertiesEqual, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +// List of properties of the timing info we want to create boxes for +const { + TIMING_KEYS, +} = require("resource://devtools/client/netmonitor/src/constants.js"); + +const { div } = dom; + +const UPDATED_WATERFALL_ITEM_PROPS = ["eventTimings", "totalTime"]; +const UPDATED_WATERFALL_PROPS = [ + "item", + "firstRequestStartedMs", + "scale", + "isVisible", +]; + +class RequestListColumnWaterfall extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + firstRequestStartedMs: PropTypes.number.isRequired, + item: PropTypes.object.isRequired, + onWaterfallMouseDown: PropTypes.func.isRequired, + scale: PropTypes.number, + isVisible: PropTypes.bool.isRequired, + }; + } + + constructor() { + super(); + this.handleMouseOver = this.handleMouseOver.bind(this); + } + + componentDidMount() { + const { connector, item } = this.props; + fetchNetworkUpdatePacket(connector.requestData, item, ["eventTimings"]); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + if (nextProps.isVisible && nextProps.item.totalTime) { + const { connector, item } = nextProps; + fetchNetworkUpdatePacket(connector.requestData, item, ["eventTimings"]); + } + } + + shouldComponentUpdate(nextProps) { + return ( + nextProps.isVisible && + (!propertiesEqual(UPDATED_WATERFALL_PROPS, this.props, nextProps) || + !propertiesEqual( + UPDATED_WATERFALL_ITEM_PROPS, + this.props.item, + nextProps.item + )) + ); + } + + handleMouseOver({ target }) { + if (!target.title) { + target.title = this.timingTooltip(); + } + } + + timingTooltip() { + const { eventTimings, totalTime } = this.props.item; + const tooltip = []; + + if (eventTimings) { + for (const key of TIMING_KEYS) { + const width = eventTimings.timings[key]; + + if (width > 0) { + tooltip.push( + L10N.getFormatStr("netmonitor.waterfall.tooltip." + key, width) + ); + } + } + } + + if (typeof totalTime === "number") { + tooltip.push( + L10N.getFormatStr("netmonitor.waterfall.tooltip.total", totalTime) + ); + } + + return tooltip.join(L10N.getStr("netmonitor.waterfall.tooltip.separator")); + } + + timingBoxes() { + const { + scale, + item: { eventTimings, totalTime }, + } = this.props; + const boxes = []; + + // Physical pixel as minimum size + const minPixel = 1 / window.devicePixelRatio; + + if (typeof totalTime === "number") { + if (eventTimings) { + // Add a set of boxes representing timing information. + for (const key of TIMING_KEYS) { + if (eventTimings.timings[key] > 0) { + boxes.push( + div({ + key, + className: `requests-list-timings-box ${key}`, + style: { + width: Math.max(eventTimings.timings[key] * scale, minPixel), + }, + }) + ); + } + } + } + // Minimal box to at least show start and total time + if (!boxes.length) { + boxes.push( + div({ + className: "requests-list-timings-box filler", + key: "filler", + style: { width: Math.max(totalTime * scale, minPixel) }, + }) + ); + } + + const title = L10N.getFormatStr("networkMenu.totalMS2", totalTime); + boxes.push( + div( + { + key: "total", + className: "requests-list-timings-total", + title, + }, + title + ) + ); + } else { + // Pending requests are marked for start time + boxes.push( + div({ + className: "requests-list-timings-box filler", + key: "pending", + style: { width: minPixel }, + }) + ); + } + + return boxes; + } + + render() { + const { + firstRequestStartedMs, + item: { startedMs }, + scale, + onWaterfallMouseDown, + } = this.props; + + return dom.td( + { + className: "requests-list-column requests-list-waterfall", + onMouseOver: this.handeMouseOver, + }, + div( + { + className: "requests-list-timings", + style: { + paddingInlineStart: `${ + (startedMs - firstRequestStartedMs) * scale + }px`, + }, + onMouseDown: onWaterfallMouseDown, + }, + this.timingBoxes() + ) + ); + } +} + +module.exports = connect(state => ({ + scale: getWaterfallScale(state), +}))(RequestListColumnWaterfall); diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListContent.js b/devtools/client/netmonitor/src/components/request-list/RequestListContent.js new file mode 100644 index 0000000000..0ee91cc48e --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListContent.js @@ -0,0 +1,524 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); +const { + HTMLTooltip, +} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js"); + +const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); +const { + formDataURI, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const { + getDisplayedRequests, + getColumns, + getSelectedRequest, + getClickedRequest, +} = require("resource://devtools/client/netmonitor/src/selectors/index.js"); + +loader.lazyRequireGetter( + this, + "openRequestInTab", + "resource://devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js", + true +); +loader.lazyGetter(this, "setImageTooltip", function () { + return require("resource://devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js") + .setImageTooltip; +}); +loader.lazyGetter(this, "getImageDimensions", function () { + return require("resource://devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js") + .getImageDimensions; +}); + +// Components +const RequestListHeader = createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListHeader.js") +); +const RequestListItem = createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListItem.js") +); +const RequestListContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestListContextMenu.js"); + +const { div } = dom; + +// Tooltip show / hide delay in ms +const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500; +// Tooltip image maximum dimension in px +const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; + +const LEFT_MOUSE_BUTTON = 0; +const MIDDLE_MOUSE_BUTTON = 1; +const RIGHT_MOUSE_BUTTON = 2; + +/** + * Renders the actual contents of the request list. + */ +class RequestListContent extends Component { + static get propTypes() { + return { + blockedUrls: PropTypes.array.isRequired, + connector: PropTypes.object.isRequired, + columns: PropTypes.object.isRequired, + networkActionOpen: PropTypes.bool, + networkDetailsOpen: PropTypes.bool.isRequired, + networkDetailsWidth: PropTypes.number, + networkDetailsHeight: PropTypes.number, + cloneRequest: PropTypes.func.isRequired, + clickedRequest: PropTypes.object, + openDetailsPanelTab: PropTypes.func.isRequired, + openHTTPCustomRequestTab: PropTypes.func.isRequired, + closeHTTPCustomRequestTab: PropTypes.func.isRequired, + sendCustomRequest: PropTypes.func.isRequired, + sendHTTPCustomRequest: PropTypes.func.isRequired, + displayedRequests: PropTypes.array.isRequired, + firstRequestStartedMs: PropTypes.number.isRequired, + fromCache: PropTypes.bool, + onInitiatorBadgeMouseDown: PropTypes.func.isRequired, + onItemRightMouseButtonDown: PropTypes.func.isRequired, + onItemMouseDown: PropTypes.func.isRequired, + onSecurityIconMouseDown: PropTypes.func.isRequired, + onSelectDelta: PropTypes.func.isRequired, + onWaterfallMouseDown: PropTypes.func.isRequired, + openStatistics: PropTypes.func.isRequired, + openRequestBlockingAndAddUrl: PropTypes.func.isRequired, + openRequestBlockingAndDisableUrls: PropTypes.func.isRequired, + removeBlockedUrl: PropTypes.func.isRequired, + selectedActionBarTabId: PropTypes.string, + selectRequest: PropTypes.func.isRequired, + selectedRequest: PropTypes.object, + requestFilterTypes: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + this.onHover = this.onHover.bind(this); + this.onScroll = this.onScroll.bind(this); + this.onResize = this.onResize.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.openRequestInTab = this.openRequestInTab.bind(this); + this.onDoubleClick = this.onDoubleClick.bind(this); + this.onDragStart = this.onDragStart.bind(this); + this.onContextMenu = this.onContextMenu.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + this.hasOverflow = false; + this.onIntersect = this.onIntersect.bind(this); + this.intersectionObserver = null; + this.state = { + onscreenItems: new Set(), + }; + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillMount() { + this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" }); + window.addEventListener("resize", this.onResize); + } + + componentDidMount() { + // Install event handler for displaying a tooltip + this.tooltip.startTogglingOnHover(this.refs.scrollEl, this.onHover, { + toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY, + interactive: true, + }); + // Install event handler to hide the tooltip on scroll + this.refs.scrollEl.addEventListener("scroll", this.onScroll, true); + this.onResize(); + this.intersectionObserver = new IntersectionObserver(this.onIntersect, { + root: this.refs.scrollEl, + // Render 10% more columns for a scrolling headstart + rootMargin: "10%", + }); + // Prime IntersectionObserver with existing entries + for (const item of this.refs.scrollEl.querySelectorAll( + ".request-list-item" + )) { + this.intersectionObserver.observe(item); + } + } + + componentDidUpdate(prevProps) { + const output = this.refs.scrollEl; + if (!this.hasOverflow && output.scrollHeight > output.clientHeight) { + output.scrollTop = output.scrollHeight; + this.hasOverflow = true; + } + if ( + prevProps.networkDetailsOpen !== this.props.networkDetailsOpen || + prevProps.networkDetailsWidth !== this.props.networkDetailsWidth || + prevProps.networkDetailsHeight !== this.props.networkDetailsHeight + ) { + this.onResize(); + } + } + + componentWillUnmount() { + this.refs.scrollEl.removeEventListener("scroll", this.onScroll, true); + + // Uninstall the tooltip event handler + this.tooltip.stopTogglingOnHover(); + window.removeEventListener("resize", this.onResize); + if (this.intersectionObserver !== null) { + this.intersectionObserver.disconnect(); + this.intersectionObserver = null; + } + } + + /* + * Removing onResize() method causes perf regression - too many repaints of the panel. + * So it is needed in ComponentDidMount and ComponentDidUpdate. See Bug 1532914. + */ + onResize() { + const parent = this.refs.scrollEl.parentNode; + this.refs.scrollEl.style.width = parent.offsetWidth + "px"; + this.refs.scrollEl.style.height = parent.offsetHeight + "px"; + } + + onIntersect(entries) { + // Track when off screen elements moved on screen to ensure updates + let onscreenDidChange = false; + const onscreenItems = new Set(this.state.onscreenItems); + for (const { target, isIntersecting } of entries) { + const { id } = target.dataset; + if (isIntersecting) { + if (onscreenItems.add(id)) { + onscreenDidChange = true; + } + } else { + onscreenItems.delete(id); + } + } + if (onscreenDidChange) { + // Remove ids that are no longer displayed + const itemIds = new Set(this.props.displayedRequests.map(({ id }) => id)); + for (const id of onscreenItems) { + if (!itemIds.has(id)) { + onscreenItems.delete(id); + } + } + this.setState({ onscreenItems }); + } + } + + /** + * The predicate used when deciding whether a popup should be shown + * over a request item or not. + * + * @param Node target + * The element node currently being hovered. + * @param object tooltip + * The current tooltip instance. + * @return {Promise} + */ + async onHover(target, tooltip) { + const itemEl = target.closest(".request-list-item"); + if (!itemEl) { + return false; + } + const itemId = itemEl.dataset.id; + if (!itemId) { + return false; + } + const requestItem = this.props.displayedRequests.find(r => r.id == itemId); + if (!requestItem) { + return false; + } + + if (!target.closest(".requests-list-file")) { + return false; + } + + const { mimeType } = requestItem; + if (!mimeType || !mimeType.includes("image/")) { + return false; + } + + const responseContent = await this.props.connector.requestData( + requestItem.id, + "responseContent" + ); + const { encoding, text } = responseContent.content; + const src = formDataURI(mimeType, encoding, text); + const maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM; + const { naturalWidth, naturalHeight } = await getImageDimensions( + tooltip.doc, + src + ); + const options = { maxDim, naturalWidth, naturalHeight }; + setImageTooltip(tooltip, tooltip.doc, src, options); + + return itemEl.querySelector(".requests-list-file"); + } + + /** + * Scroll listener for the requests menu view. + */ + onScroll() { + this.tooltip.hide(); + } + + onMouseDown(evt, id, request) { + if (evt.button === LEFT_MOUSE_BUTTON) { + this.props.selectRequest(id, request); + } else if (evt.button === RIGHT_MOUSE_BUTTON) { + this.props.onItemRightMouseButtonDown(id); + } else if (evt.button === MIDDLE_MOUSE_BUTTON) { + this.onMiddleMouseButtonDown(request); + } + } + + /** + * Handler for keyboard events. For arrow up/down, page up/down, home/end, + * move the selection up or down. + */ + onKeyDown(evt) { + let delta; + + switch (evt.key) { + case "ArrowUp": + delta = -1; + break; + case "ArrowDown": + delta = +1; + break; + case "PageUp": + delta = "PAGE_UP"; + break; + case "PageDown": + delta = "PAGE_DOWN"; + break; + case "Home": + delta = -Infinity; + break; + case "End": + delta = +Infinity; + break; + } + + if (delta) { + // Prevent scrolling when pressing navigation keys. + evt.preventDefault(); + evt.stopPropagation(); + this.props.onSelectDelta(delta); + } + } + + /** + * Opens selected item in a new tab. + */ + async openRequestInTab(id, url, requestHeaders, requestPostData) { + requestHeaders = + requestHeaders || + (await this.props.connector.requestData(id, "requestHeaders")); + + requestPostData = + requestPostData || + (await this.props.connector.requestData(id, "requestPostData")); + + openRequestInTab(url, requestHeaders, requestPostData); + } + + onDoubleClick({ id, url, requestHeaders, requestPostData }) { + this.openRequestInTab(id, url, requestHeaders, requestPostData); + } + + onMiddleMouseButtonDown({ id, url, requestHeaders, requestPostData }) { + this.openRequestInTab(id, url, requestHeaders, requestPostData); + } + + onDragStart(evt, { url }) { + evt.dataTransfer.setData("text/plain", url); + } + + onContextMenu(evt) { + evt.preventDefault(); + const { clickedRequest, displayedRequests, blockedUrls } = this.props; + + if (!this.contextMenu) { + const { + connector, + cloneRequest, + openDetailsPanelTab, + openHTTPCustomRequestTab, + closeHTTPCustomRequestTab, + sendCustomRequest, + sendHTTPCustomRequest, + openStatistics, + openRequestBlockingAndAddUrl, + openRequestBlockingAndDisableUrls, + removeBlockedUrl, + } = this.props; + this.contextMenu = new RequestListContextMenu({ + connector, + cloneRequest, + openDetailsPanelTab, + openHTTPCustomRequestTab, + closeHTTPCustomRequestTab, + sendCustomRequest, + sendHTTPCustomRequest, + openStatistics, + openRequestBlockingAndAddUrl, + openRequestBlockingAndDisableUrls, + removeBlockedUrl, + openRequestInTab: this.openRequestInTab, + }); + } + + this.contextMenu.open(evt, clickedRequest, displayedRequests, blockedUrls); + } + + render() { + const { + connector, + columns, + displayedRequests, + firstRequestStartedMs, + onInitiatorBadgeMouseDown, + onSecurityIconMouseDown, + onWaterfallMouseDown, + requestFilterTypes, + selectedRequest, + selectedActionBarTabId, + openRequestBlockingAndAddUrl, + openRequestBlockingAndDisableUrls, + networkActionOpen, + networkDetailsOpen, + } = this.props; + + return div( + { + ref: "scrollEl", + className: "requests-list-scroll", + }, + [ + dom.table( + { + className: "requests-list-table", + key: "table", + }, + RequestListHeader(), + dom.tbody( + { + ref: "rowGroupEl", + className: "requests-list-row-group", + tabIndex: 0, + onKeyDown: this.onKeyDown, + }, + displayedRequests.map((item, index) => { + return RequestListItem({ + blocked: !!item.blockedReason, + firstRequestStartedMs, + fromCache: item.status === "304" || item.fromCache, + networkDetailsOpen, + networkActionOpen, + selectedActionBarTabId, + connector, + columns, + item, + index, + isSelected: item.id === selectedRequest?.id, + isVisible: this.state.onscreenItems.has(item.id), + key: item.id, + intersectionObserver: this.intersectionObserver, + onContextMenu: this.onContextMenu, + onDoubleClick: () => this.onDoubleClick(item), + onDragStart: evt => this.onDragStart(evt, item), + onMouseDown: evt => this.onMouseDown(evt, item.id, item), + onInitiatorBadgeMouseDown: () => + onInitiatorBadgeMouseDown(item.cause), + onSecurityIconMouseDown: () => + onSecurityIconMouseDown(item.securityState), + onWaterfallMouseDown, + requestFilterTypes, + openRequestBlockingAndAddUrl, + openRequestBlockingAndDisableUrls, + }); + }) + ) + ), // end of requests-list-row-group"> + dom.div({ + className: "requests-list-anchor", + key: "anchor", + }), + ] + ); + } +} + +module.exports = connect( + state => ({ + blockedUrls: state.requestBlocking.blockedUrls + .map(({ enabled, url }) => (enabled ? url : null)) + .filter(Boolean), + columns: getColumns(state), + networkActionOpen: state.ui.networkActionOpen, + networkDetailsOpen: state.ui.networkDetailsOpen, + networkDetailsWidth: state.ui.networkDetailsWidth, + networkDetailsHeight: state.ui.networkDetailsHeight, + clickedRequest: getClickedRequest(state), + displayedRequests: getDisplayedRequests(state), + firstRequestStartedMs: state.requests.firstStartedMs, + selectedActionBarTabId: state.ui.selectedActionBarTabId, + selectedRequest: getSelectedRequest(state), + requestFilterTypes: state.filters.requestFilterTypes, + }), + (dispatch, props) => ({ + cloneRequest: id => dispatch(Actions.cloneRequest(id)), + openDetailsPanelTab: () => dispatch(Actions.openNetworkDetails(true)), + openHTTPCustomRequestTab: () => + dispatch(Actions.openHTTPCustomRequest(true)), + closeHTTPCustomRequestTab: () => + dispatch(Actions.openHTTPCustomRequest(false)), + sendCustomRequest: () => dispatch(Actions.sendCustomRequest()), + sendHTTPCustomRequest: request => + dispatch(Actions.sendHTTPCustomRequest(request)), + openStatistics: open => + dispatch(Actions.openStatistics(props.connector, open)), + openRequestBlockingAndAddUrl: url => + dispatch(Actions.openRequestBlockingAndAddUrl(url)), + removeBlockedUrl: url => dispatch(Actions.removeBlockedUrl(url)), + openRequestBlockingAndDisableUrls: url => + dispatch(Actions.openRequestBlockingAndDisableUrls(url)), + /** + * A handler that opens the stack trace tab when a stack trace is available + */ + onInitiatorBadgeMouseDown: cause => { + if (cause.lastFrame) { + dispatch(Actions.selectDetailsPanelTab("stack-trace")); + } + }, + selectRequest: (id, request) => + dispatch(Actions.selectRequest(id, request)), + onItemRightMouseButtonDown: id => dispatch(Actions.rightClickRequest(id)), + onItemMouseDown: id => dispatch(Actions.selectRequest(id)), + /** + * A handler that opens the security tab in the details view if secure or + * broken security indicator is clicked. + */ + onSecurityIconMouseDown: securityState => { + if (securityState && securityState !== "insecure") { + dispatch(Actions.selectDetailsPanelTab("security")); + } + }, + onSelectDelta: delta => dispatch(Actions.selectDelta(delta)), + /** + * A handler that opens the timing sidebar panel if the waterfall is clicked. + */ + onWaterfallMouseDown: () => { + dispatch(Actions.selectDetailsPanelTab("timings")); + }, + }) +)(RequestListContent); diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListEmptyNotice.js b/devtools/client/netmonitor/src/components/request-list/RequestListEmptyNotice.js new file mode 100644 index 0000000000..318078b782 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListEmptyNotice.js @@ -0,0 +1,107 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); +const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); +const { + ACTIVITY_TYPE, +} = require("resource://devtools/client/netmonitor/src/constants.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + getPerformanceAnalysisURL, +} = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js"); + +// Components +const MDNLink = createFactory( + require("resource://devtools/client/shared/components/MdnLink.js") +); + +const { button, div, span } = dom; + +const RELOAD_NOTICE_1 = L10N.getStr("netmonitor.reloadNotice1"); +const RELOAD_NOTICE_2 = L10N.getStr("netmonitor.reloadNotice2"); +const RELOAD_NOTICE_3 = L10N.getStr("netmonitor.reloadNotice3"); +const RELOAD_NOTICE_BT = L10N.getStr("netmonitor.emptyBrowserToolbox"); +const PERFORMANCE_NOTICE_1 = L10N.getStr("netmonitor.perfNotice1"); +const PERFORMANCE_NOTICE_2 = L10N.getStr("netmonitor.perfNotice2"); +const PERFORMANCE_NOTICE_3 = L10N.getStr("netmonitor.perfNotice3"); +const PERFORMANCE_LEARN_MORE = L10N.getStr("charts.learnMore"); + +/** + * UI displayed when the request list is empty. Contains instructions on reloading + * the page and on triggering performance analysis of the page. + */ +class RequestListEmptyNotice extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + onReloadClick: PropTypes.func.isRequired, + onPerfClick: PropTypes.func.isRequired, + }; + } + + render() { + const { connector } = this.props; + const toolbox = connector.getToolbox(); + + return div( + { + className: "request-list-empty-notice", + }, + !toolbox.isBrowserToolbox + ? div( + { className: "notice-reload-message empty-notice-element" }, + span(null, RELOAD_NOTICE_1), + button( + { + className: "devtools-button requests-list-reload-notice-button", + "data-standalone": true, + onClick: this.props.onReloadClick, + }, + RELOAD_NOTICE_2 + ), + span(null, RELOAD_NOTICE_3) + ) + : div( + { className: "notice-reload-message empty-notice-element" }, + span(null, RELOAD_NOTICE_BT) + ), + !toolbox.isBrowserToolbox + ? div( + { className: "notice-perf-message empty-notice-element" }, + span(null, PERFORMANCE_NOTICE_1), + button({ + title: PERFORMANCE_NOTICE_3, + className: "devtools-button requests-list-perf-notice-button", + "data-standalone": true, + onClick: this.props.onPerfClick, + }), + span(null, PERFORMANCE_NOTICE_2), + MDNLink({ + url: getPerformanceAnalysisURL(), + title: PERFORMANCE_LEARN_MORE, + }) + ) + : null + ); + } +} + +module.exports = connect(undefined, (dispatch, props) => ({ + onPerfClick: () => dispatch(Actions.openStatistics(props.connector, true)), + onReloadClick: () => + props.connector.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT), +}))(RequestListEmptyNotice); diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js b/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js new file mode 100644 index 0000000000..d02e993b02 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListHeader.js @@ -0,0 +1,731 @@ +/* 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 { + createRef, + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); +const { + getTheme, + addThemeObserver, + removeThemeObserver, +} = require("resource://devtools/client/shared/theme.js"); +const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); +const { + HEADERS, + REQUESTS_WATERFALL, + MIN_COLUMN_WIDTH, + DEFAULT_COLUMN_WIDTH, +} = require("resource://devtools/client/netmonitor/src/constants.js"); +const { + getColumns, + getWaterfallScale, +} = require("resource://devtools/client/netmonitor/src/selectors/index.js"); +const { + getFormattedTime, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const RequestListHeaderContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js"); +const WaterfallBackground = require("resource://devtools/client/netmonitor/src/widgets/WaterfallBackground.js"); +const Draggable = createFactory( + require("resource://devtools/client/shared/components/splitter/Draggable.js") +); + +const { div, button } = dom; + +/** + * Render the request list header with sorting arrows for columns. + * Displays tick marks in the waterfall column header. + * Also draws the waterfall background canvas and updates it when needed. + */ +class RequestListHeader extends Component { + static get propTypes() { + return { + columns: PropTypes.object.isRequired, + resetColumns: PropTypes.func.isRequired, + resetSorting: PropTypes.func.isRequired, + resizeWaterfall: PropTypes.func.isRequired, + scale: PropTypes.number, + sort: PropTypes.object, + sortBy: PropTypes.func.isRequired, + toggleColumn: PropTypes.func.isRequired, + waterfallWidth: PropTypes.number, + columnsData: PropTypes.object.isRequired, + setColumnsWidth: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + this.requestListHeader = createRef(); + + this.onContextMenu = this.onContextMenu.bind(this); + this.drawBackground = this.drawBackground.bind(this); + this.resizeWaterfall = this.resizeWaterfall.bind(this); + this.waterfallDivisionLabels = this.waterfallDivisionLabels.bind(this); + this.waterfallLabel = this.waterfallLabel.bind(this); + this.onHeaderClick = this.onHeaderClick.bind(this); + this.resizeColumnToFitContent = this.resizeColumnToFitContent.bind(this); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillMount() { + const { resetColumns, resetSorting, toggleColumn } = this.props; + this.contextMenu = new RequestListHeaderContextMenu({ + resetColumns, + resetSorting, + toggleColumn, + resizeColumnToFitContent: this.resizeColumnToFitContent, + }); + } + + componentDidMount() { + // Create the object that takes care of drawing the waterfall canvas background + this.background = new WaterfallBackground(document); + this.drawBackground(); + // When visible columns add up to less or more than 100% => update widths in prefs. + if (this.shouldUpdateWidths()) { + this.updateColumnsWidth(); + } + this.resizeWaterfall(); + window.addEventListener("resize", this.resizeWaterfall); + addThemeObserver(this.drawBackground); + } + + componentDidUpdate() { + this.drawBackground(); + // check if the widths in prefs need to be updated + // e.g. after hide/show column + if (this.shouldUpdateWidths()) { + this.updateColumnsWidth(); + this.resizeWaterfall(); + } + } + + componentWillUnmount() { + this.background.destroy(); + this.background = null; + window.removeEventListener("resize", this.resizeWaterfall); + removeThemeObserver(this.drawBackground); + } + + /** + * Helper method to get the total width of cell's content. + * Used for resizing columns to fit their content. + */ + totalCellWidth(cellEl) { + return [...cellEl.childNodes] + .map(cNode => { + if (cNode.nodeType === 3) { + // if it's text node + return Math.ceil( + cNode.getBoxQuads()[0].p2.x - cNode.getBoxQuads()[0].p1.x + ); + } + return cNode.getBoundingClientRect().width; + }) + .reduce((a, b) => a + b, 0); + } + + /** + * Resize column to fit its content. + * Additionally, resize other columns (starting from last) to compensate. + */ + resizeColumnToFitContent(name) { + const headerRef = this.refs[`${name}Header`]; + const parentEl = headerRef.closest(".requests-list-table"); + const width = headerRef.getBoundingClientRect().width; + const parentWidth = parentEl.getBoundingClientRect().width; + const items = parentEl.querySelectorAll(".request-list-item"); + const columnIndex = headerRef.cellIndex; + const widths = [...items].map(item => + this.totalCellWidth(item.children[columnIndex]) + ); + + const minW = this.getMinWidth(name); + + // Add 11 to account for cell padding (padding-right + padding-left = 9px), not accurate. + let maxWidth = 11 + Math.max.apply(null, widths); + + if (maxWidth < minW) { + maxWidth = minW; + } + + // Pixel value which, if added to this column's width, will fit its content. + let change = maxWidth - width; + + // Max change we can do while taking other columns into account. + let maxAllowedChange = 0; + const visibleColumns = this.getVisibleColumns(); + const newWidths = []; + + // Calculate new widths for other columns to compensate. + // Start from the 2nd last column if last column is waterfall. + // This is done to comply with the existing resizing behavior. + const delta = + visibleColumns[visibleColumns.length - 1].name === "waterfall" ? 2 : 1; + + for (let i = visibleColumns.length - delta; i > 0; i--) { + if (i !== columnIndex) { + const columnName = visibleColumns[i].name; + const columnHeaderRef = this.refs[`${columnName}Header`]; + const columnWidth = columnHeaderRef.getBoundingClientRect().width; + const minWidth = this.getMinWidth(columnName); + const newWidth = columnWidth - change; + + // If this column can compensate for all the remaining change. + if (newWidth >= minWidth) { + maxAllowedChange += change; + change = 0; + newWidths.push({ + name: columnName, + width: this.px2percent(newWidth, parentWidth), + }); + break; + } else { + // Max change we can do in this column. + let maxColumnChange = columnWidth - minWidth; + maxColumnChange = maxColumnChange > change ? change : maxColumnChange; + maxAllowedChange += maxColumnChange; + change -= maxColumnChange; + newWidths.push({ + name: columnName, + width: this.px2percent(columnWidth - maxColumnChange, parentWidth), + }); + } + } + } + newWidths.push({ + name, + width: this.px2percent(width + maxAllowedChange, parentWidth), + }); + this.props.setColumnsWidth(newWidths); + } + + onContextMenu(evt) { + evt.preventDefault(); + this.contextMenu.open(evt, this.props.columns); + } + + onHeaderClick(evt, headerName) { + const { sortBy, resetSorting } = this.props; + if (evt.button == 1) { + // reset sort state on middle click + resetSorting(); + } else { + sortBy(headerName); + } + } + + drawBackground() { + // The background component is theme dependent, so add the current theme to the props. + const props = Object.assign({}, this.props, { + theme: getTheme(), + }); + this.background.draw(props); + } + + resizeWaterfall() { + const { waterfallHeader } = this.refs; + if (waterfallHeader) { + // Measure its width and update the 'waterfallWidth' property in the store. + // The 'waterfallWidth' will be further updated on every window resize. + window.cancelIdleCallback(this._resizeTimerId); + this._resizeTimerId = window.requestIdleCallback(() => + this.props.resizeWaterfall( + waterfallHeader.getBoundingClientRect().width + ) + ); + } + } + + /** + * Build the waterfall header - timing tick marks with the right spacing + */ + waterfallDivisionLabels(waterfallWidth, scale) { + const labels = []; + + // Build new millisecond tick labels... + const timingStep = REQUESTS_WATERFALL.HEADER_TICKS_MULTIPLE; + let scaledStep = scale * timingStep; + + // Ignore any divisions that would end up being too close to each other. + while (scaledStep < REQUESTS_WATERFALL.HEADER_TICKS_SPACING_MIN) { + scaledStep *= 2; + } + + // Insert one label for each division on the current scale. + for (let x = 0; x < waterfallWidth; x += scaledStep) { + const millisecondTime = x / scale; + let divisionScale = "millisecond"; + + // If the division is greater than 1 minute. + if (millisecondTime > 60000) { + divisionScale = "minute"; + } else if (millisecondTime > 1000) { + // If the division is greater than 1 second. + divisionScale = "second"; + } + + let width = ((x + scaledStep) | 0) - (x | 0); + // Adjust the first marker for the borders + if (x == 0) { + width -= 2; + } + // Last marker doesn't need a width specified at all + if (x + scaledStep >= waterfallWidth) { + width = undefined; + } + + labels.push( + div( + { + key: labels.length, + className: "requests-list-timings-division", + "data-division-scale": divisionScale, + style: { width }, + }, + getFormattedTime(millisecondTime) + ) + ); + } + + return labels; + } + + waterfallLabel(waterfallWidth, scale, label) { + let className = "button-text requests-list-waterfall-label-wrapper"; + + if (waterfallWidth !== null && scale !== null) { + label = this.waterfallDivisionLabels(waterfallWidth, scale); + className += " requests-list-waterfall-visible"; + } + + return div({ className }, label); + } + + // Dragging Events + + /** + * Set 'resizing' cursor on entire container dragging. + * This avoids cursor-flickering when the mouse leaves + * the column-resizer area (happens frequently). + */ + onStartMove() { + // Set cursor to dragging + const container = document.querySelector(".request-list-container"); + container.style.cursor = "ew-resize"; + // Class .dragging is used to disable pointer events while dragging - see css. + this.requestListHeader.classList.add("dragging"); + } + + /** + * A handler that calculates the new width of the columns + * based on mouse position and adjusts the width. + */ + onMove(name, x) { + const parentEl = document.querySelector(".requests-list-headers"); + const parentWidth = parentEl.getBoundingClientRect().width; + + // Get the current column handle and save its old width + // before changing so we can compute the adjustment in width + const headerRef = this.refs[`${name}Header`]; + const headerRefRect = headerRef.getBoundingClientRect(); + const oldWidth = headerRefRect.width; + + // Get the column handle that will compensate the width change. + const compensateHeaderName = this.getCompensateHeader(); + + if (name === compensateHeaderName) { + // this is the case where we are resizing waterfall + this.moveWaterfall(x, parentWidth); + return; + } + + const compensateHeaderRef = this.refs[`${compensateHeaderName}Header`]; + const compensateHeaderRefRect = compensateHeaderRef.getBoundingClientRect(); + const oldCompensateWidth = compensateHeaderRefRect.width; + const sumOfBothColumns = oldWidth + oldCompensateWidth; + + // Get minimal widths for both changed columns (in px). + const minWidth = this.getMinWidth(name); + const minCompensateWidth = this.getMinWidth(compensateHeaderName); + + // Calculate new width (according to the mouse x-position) and set to style. + // Do not allow to set it below minWidth. + let newWidth = + document.dir == "ltr" ? x - headerRefRect.left : headerRefRect.right - x; + newWidth = Math.max(newWidth, minWidth); + headerRef.style.width = `${this.px2percent(newWidth, parentWidth)}%`; + const adjustment = oldWidth - newWidth; + + // Calculate new compensate width as the original width + adjustment. + // Do not allow to set it below minCompensateWidth. + const newCompensateWidth = Math.max( + adjustment + oldCompensateWidth, + minCompensateWidth + ); + compensateHeaderRef.style.width = `${this.px2percent( + newCompensateWidth, + parentWidth + )}%`; + + // Do not allow to reset size of column when compensate column is at minWidth. + if (newCompensateWidth === minCompensateWidth) { + headerRef.style.width = `${this.px2percent( + sumOfBothColumns - newCompensateWidth, + parentWidth + )}%`; + } + } + + /** + * After resizing - we get the width for each 'column' + * and convert it into % and store it in user prefs. + * Also resets the 'resizing' cursor back to initial. + */ + onStopMove() { + this.updateColumnsWidth(); + // If waterfall is visible and width has changed, call resizeWaterfall. + const waterfallRef = this.refs.waterfallHeader; + if (waterfallRef) { + const { waterfallWidth } = this.props; + const realWaterfallWidth = waterfallRef.getBoundingClientRect().width; + if (Math.round(waterfallWidth) !== Math.round(realWaterfallWidth)) { + this.resizeWaterfall(); + } + } + + // Restore cursor back to default. + const container = document.querySelector(".request-list-container"); + container.style.cursor = "initial"; + this.requestListHeader.classList.remove("dragging"); + } + + /** + * Helper method to get the name of the column that will compensate + * the width change. It should be the last column before waterfall, + * (if waterfall visible) otherwise it is simply the last visible column. + */ + getCompensateHeader() { + const visibleColumns = this.getVisibleColumns(); + const lastColumn = visibleColumns[visibleColumns.length - 1].name; + const delta = lastColumn === "waterfall" ? 2 : 1; + return visibleColumns[visibleColumns.length - delta].name; + } + + /** + * Called from onMove() when resizing waterfall column + * because waterfall is a special case, where ALL other + * columns are made smaller when waterfall is bigger and vice versa. + */ + moveWaterfall(x, parentWidth) { + const visibleColumns = this.getVisibleColumns(); + const minWaterfall = this.getMinWidth("waterfall"); + const waterfallRef = this.refs.waterfallHeader; + + // Compute and set style.width for waterfall. + const waterfallRefRect = waterfallRef.getBoundingClientRect(); + const oldWidth = waterfallRefRect.width; + const adjustment = + document.dir == "ltr" + ? waterfallRefRect.left - x + : x - waterfallRefRect.right; + if (this.allColumnsAtMinWidth() && adjustment > 0) { + // When we want to make waterfall wider but all + // other columns are already at minWidth => return. + return; + } + + const newWidth = Math.max(oldWidth + adjustment, minWaterfall); + + // Now distribute evenly the change in width to all other columns except waterfall. + const changeInWidth = oldWidth - newWidth; + const widths = this.autoSizeWidths(changeInWidth, visibleColumns); + + // Set the new computed width for waterfall into array widths. + widths[widths.length - 1] = newWidth; + + // Update style for all columns from array widths. + let i = 0; + visibleColumns.forEach(col => { + const { name } = col; + const headerRef = this.refs[`${name}Header`]; + headerRef.style.width = `${this.px2percent(widths[i], parentWidth)}%`; + i++; + }); + } + + /** + * Helper method that checks if all columns have reached their minWidth. + * This can happen when making waterfall column wider. + */ + allColumnsAtMinWidth() { + const visibleColumns = this.getVisibleColumns(); + // Do not check width for waterfall because + // when all are getting smaller, waterfall is getting bigger. + for (let i = 0; i < visibleColumns.length - 1; i++) { + const { name } = visibleColumns[i]; + const headerRef = this.refs[`${name}Header`]; + const minColWidth = this.getMinWidth(name); + if (headerRef.getBoundingClientRect().width > minColWidth) { + return false; + } + } + return true; + } + + /** + * Method takes the total change in width for waterfall column + * and distributes it among all other columns. Returns an array + * where all visible columns have newly computed width in pixels. + */ + autoSizeWidths(changeInWidth, visibleColumns) { + const widths = visibleColumns.map(col => { + const headerRef = this.refs[`${col.name}Header`]; + const colWidth = headerRef.getBoundingClientRect().width; + return colWidth; + }); + + // Divide changeInWidth among all columns but waterfall (that's why -1). + const changeInWidthPerColumn = changeInWidth / (widths.length - 1); + + while (changeInWidth) { + const lastChangeInWidth = changeInWidth; + // In the loop adjust all columns except last one - waterfall + for (let i = 0; i < widths.length - 1; i++) { + const { name } = visibleColumns[i]; + const minColWidth = this.getMinWidth(name); + const newColWidth = Math.max( + widths[i] + changeInWidthPerColumn, + minColWidth + ); + + widths[i] = newColWidth; + if (changeInWidth > 0) { + changeInWidth -= newColWidth - widths[i]; + } else { + changeInWidth += newColWidth - widths[i]; + } + if (!changeInWidth) { + break; + } + } + if (lastChangeInWidth == changeInWidth) { + break; + } + } + return widths; + } + + /** + * Method returns 'true' - if the column widths need to be updated + * when the total % is less or more than 100%. + * It returns 'false' if they add up to 100% => no need to update. + */ + shouldUpdateWidths() { + const visibleColumns = this.getVisibleColumns(); + let totalPercent = 0; + + visibleColumns.forEach(col => { + const { name } = col; + const headerRef = this.refs[`${name}Header`]; + // Get column width from style. + let widthFromStyle = 0; + // In case the column is in visibleColumns but has display:none + // we don't want to count its style.width into totalPercent. + if (headerRef.getBoundingClientRect().width > 0) { + widthFromStyle = headerRef.style.width.slice(0, -1); + } + totalPercent += +widthFromStyle; // + converts it to a number + }); + + // Do not update if total percent is from 99-101% or when it is 0 + // - it means that no columns are displayed (e.g. other panel is currently selected). + return Math.round(totalPercent) !== 100 && totalPercent !== 0; + } + + /** + * Method reads real width of each column header + * and updates the style.width for that header. + * It returns updated columnsData. + */ + updateColumnsWidth() { + const visibleColumns = this.getVisibleColumns(); + const parentEl = document.querySelector(".requests-list-headers"); + const parentElRect = parentEl.getBoundingClientRect(); + const parentWidth = parentElRect.width; + const newWidths = []; + visibleColumns.forEach(col => { + const { name } = col; + const headerRef = this.refs[`${name}Header`]; + const headerWidth = headerRef.getBoundingClientRect().width; + + // Get actual column width, change into %, update style + const width = this.px2percent(headerWidth, parentWidth); + + if (width > 0) { + // This prevents saving width 0 for waterfall when it is not showing for + // @media (max-width: 700px) + newWidths.push({ name, width }); + } + }); + this.props.setColumnsWidth(newWidths); + } + + /** + * Helper method to convert pixels into percent based on parent container width + */ + px2percent(pxWidth, parentWidth) { + const percent = Math.round(((100 * pxWidth) / parentWidth) * 100) / 100; + return percent; + } + + /** + * Helper method to get visibleColumns; + */ + getVisibleColumns() { + const { columns } = this.props; + return HEADERS.filter(header => columns[header.name]); + } + + /** + * Helper method to get minWidth from columnsData; + */ + getMinWidth(colName) { + const { columnsData } = this.props; + if (columnsData.has(colName)) { + return columnsData.get(colName).minWidth; + } + return MIN_COLUMN_WIDTH; + } + + /** + * Render one column header from the table headers. + */ + renderColumn(header) { + const { columnsData } = this.props; + const visibleColumns = this.getVisibleColumns(); + const lastVisibleColumn = visibleColumns[visibleColumns.length - 1].name; + const { name } = header; + const boxName = header.boxName || name; + const label = header.noLocalization + ? name + : L10N.getStr(`netmonitor.toolbar.${header.label || name}`); + + const { scale, sort, waterfallWidth } = this.props; + let sorted, sortedTitle; + const active = sort.type == name ? true : undefined; + + if (active) { + sorted = sort.ascending ? "ascending" : "descending"; + sortedTitle = L10N.getStr( + sort.ascending ? "networkMenu.sortedAsc" : "networkMenu.sortedDesc" + ); + } + + // If the pref for this column width exists, set the style + // otherwise use default. + let colWidth = DEFAULT_COLUMN_WIDTH; + if (columnsData.has(name)) { + const oneColumnEl = columnsData.get(name); + colWidth = oneColumnEl.width; + } + const columnStyle = { + width: colWidth + "%", + }; + + // Support for columns resizing is currently hidden behind a pref. + const draggable = Draggable({ + className: "column-resizer ", + title: L10N.getStr("netmonitor.toolbar.resizeColumnToFitContent.title"), + onStart: () => this.onStartMove(), + onStop: () => this.onStopMove(), + onMove: x => this.onMove(name, x), + onDoubleClick: () => this.resizeColumnToFitContent(name), + }); + + return dom.th( + { + id: `requests-list-${boxName}-header-box`, + className: `requests-list-column requests-list-${boxName}`, + scope: "col", + style: columnStyle, + key: name, + ref: `${name}Header`, + // Used to style the next column. + "data-active": active, + }, + button( + { + id: `requests-list-${name}-button`, + className: `requests-list-header-button`, + "data-sorted": sorted, + "data-name": name, + title: sortedTitle ? `${label} (${sortedTitle})` : label, + onClick: evt => this.onHeaderClick(evt, name), + }, + name === "waterfall" + ? this.waterfallLabel(waterfallWidth, scale, label) + : div({ className: "button-text" }, label), + div({ className: "button-icon" }) + ), + name !== lastVisibleColumn && draggable + ); + } + + /** + * Render all columns in the table header + */ + renderColumns() { + const visibleColumns = this.getVisibleColumns(); + return visibleColumns.map(header => this.renderColumn(header)); + } + + render() { + return dom.thead( + { className: "requests-list-headers-group" }, + dom.tr( + { + className: "requests-list-headers", + onContextMenu: this.onContextMenu, + ref: node => { + this.requestListHeader = node; + }, + }, + this.renderColumns() + ) + ); + } +} + +module.exports = connect( + state => ({ + columns: getColumns(state), + columnsData: state.ui.columnsData, + firstRequestStartedMs: state.requests.firstStartedMs, + scale: getWaterfallScale(state), + sort: state.sort, + timingMarkers: state.timingMarkers, + waterfallWidth: state.ui.waterfallWidth, + }), + dispatch => ({ + resetColumns: () => dispatch(Actions.resetColumns()), + resetSorting: () => dispatch(Actions.sortBy(null)), + resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)), + sortBy: type => dispatch(Actions.sortBy(type)), + toggleColumn: column => dispatch(Actions.toggleColumn(column)), + setColumnsWidth: widths => dispatch(Actions.setColumnsWidth(widths)), + }) +)(RequestListHeader); diff --git a/devtools/client/netmonitor/src/components/request-list/RequestListItem.js b/devtools/client/netmonitor/src/components/request-list/RequestListItem.js new file mode 100644 index 0000000000..5c70b44e00 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/RequestListItem.js @@ -0,0 +1,412 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + fetchNetworkUpdatePacket, + propertiesEqual, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const { + PANELS, + RESPONSE_HEADERS, +} = require("resource://devtools/client/netmonitor/src/constants.js"); + +// Components +/* global + RequestListColumnInitiator, + RequestListColumnContentSize, + RequestListColumnCookies, + RequestListColumnDomain, + RequestListColumnFile, + RequestListColumnMethod, + RequestListColumnProtocol, + RequestListColumnRemoteIP, + RequestListColumnResponseHeader, + RequestListColumnScheme, + RequestListColumnSetCookies, + RequestListColumnStatus, + RequestListColumnTime, + RequestListColumnTransferredSize, + RequestListColumnType, + RequestListColumnUrl, + RequestListColumnWaterfall, + RequestListColumnPriority +*/ +loader.lazyGetter(this, "RequestListColumnInitiator", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnInitiator.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnContentSize", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnContentSize.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnCookies", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnCookies.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnDomain", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnDomain.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnFile", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnFile.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnUrl", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnUrl.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnMethod", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnMethod.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnProtocol", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnProtocol.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnRemoteIP", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnRemoteIP.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnResponseHeader", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnResponseHeader.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnTime", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnTime.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnScheme", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnScheme.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnSetCookies", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnSetCookies.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnStatus", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnStatus.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnTransferredSize", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnTransferredSize.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnType", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnType.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnWaterfall", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnWaterfall.js") + ); +}); +loader.lazyGetter(this, "RequestListColumnPriority", function () { + return createFactory( + require("resource://devtools/client/netmonitor/src/components/request-list/RequestListColumnPriority.js") + ); +}); + +/** + * Used by shouldComponentUpdate: compare two items, and compare only properties + * relevant for rendering the RequestListItem. Other properties (like request and + * response headers, cookies, bodies) are ignored. These are very useful for the + * network details, but not here. + */ +const UPDATED_REQ_ITEM_PROPS = [ + "mimeType", + "eventTimings", + "securityState", + "status", + "statusText", + "fromCache", + "isRacing", + "fromServiceWorker", + "method", + "url", + "remoteAddress", + "cause", + "contentSize", + "transferredSize", + "startedMs", + "totalTime", + "requestCookies", + "requestHeaders", + "responseCookies", + "responseHeaders", + "waitingTime", + "isEventStream", + "priority", +]; + +const UPDATED_REQ_PROPS = [ + "firstRequestStartedMs", + "index", + "networkDetailsOpen", + "isSelected", + "isVisible", + "requestFilterTypes", +]; + +/** + * Used by render: renders the given ColumnComponent if the flag for this column + * is set in the columns prop. The list of props are used to determine which of + * RequestListItem's need to be passed to the ColumnComponent. Any objects contained + * in that list are passed as props verbatim. + */ +const COLUMN_COMPONENTS = [ + { column: "status", ColumnComponent: RequestListColumnStatus }, + { column: "method", ColumnComponent: RequestListColumnMethod }, + { + column: "domain", + ColumnComponent: RequestListColumnDomain, + props: ["onSecurityIconMouseDown"], + }, + { + column: "file", + ColumnComponent: RequestListColumnFile, + props: ["onWaterfallMouseDown"], + }, + { + column: "url", + ColumnComponent: RequestListColumnUrl, + props: ["onSecurityIconMouseDown"], + }, + { column: "protocol", ColumnComponent: RequestListColumnProtocol }, + { column: "scheme", ColumnComponent: RequestListColumnScheme }, + { column: "remoteip", ColumnComponent: RequestListColumnRemoteIP }, + { + column: "initiator", + ColumnComponent: RequestListColumnInitiator, + props: ["onInitiatorBadgeMouseDown"], + }, + { column: "type", ColumnComponent: RequestListColumnType }, + { + column: "cookies", + ColumnComponent: RequestListColumnCookies, + props: ["connector"], + }, + { + column: "setCookies", + ColumnComponent: RequestListColumnSetCookies, + props: ["connector"], + }, + { column: "transferred", ColumnComponent: RequestListColumnTransferredSize }, + { column: "contentSize", ColumnComponent: RequestListColumnContentSize }, + { column: "priority", ColumnComponent: RequestListColumnPriority }, + { + column: "startTime", + ColumnComponent: RequestListColumnTime, + props: ["connector", "firstRequestStartedMs", { type: "start" }], + }, + { + column: "endTime", + ColumnComponent: RequestListColumnTime, + props: ["connector", "firstRequestStartedMs", { type: "end" }], + }, + { + column: "responseTime", + ColumnComponent: RequestListColumnTime, + props: ["connector", "firstRequestStartedMs", { type: "response" }], + }, + { + column: "duration", + ColumnComponent: RequestListColumnTime, + props: ["connector", "firstRequestStartedMs", { type: "duration" }], + }, + { + column: "latency", + ColumnComponent: RequestListColumnTime, + props: ["connector", "firstRequestStartedMs", { type: "latency" }], + }, +]; + +/** + * Render one row in the request list. + */ +class RequestListItem extends Component { + static get propTypes() { + return { + blocked: PropTypes.bool, + connector: PropTypes.object.isRequired, + columns: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + isSelected: PropTypes.bool.isRequired, + isVisible: PropTypes.bool.isRequired, + firstRequestStartedMs: PropTypes.number.isRequired, + fromCache: PropTypes.bool, + networkActionOpen: PropTypes.bool, + networkDetailsOpen: PropTypes.bool, + onInitiatorBadgeMouseDown: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, + onDragStart: PropTypes.func.isRequired, + onContextMenu: PropTypes.func.isRequired, + onFocusedNodeChange: PropTypes.func, + onMouseDown: PropTypes.func.isRequired, + onSecurityIconMouseDown: PropTypes.func.isRequired, + onWaterfallMouseDown: PropTypes.func.isRequired, + requestFilterTypes: PropTypes.object.isRequired, + selectedActionBarTabId: PropTypes.string, + intersectionObserver: PropTypes.object, + }; + } + + componentDidMount() { + if (this.props.isSelected) { + this.refs.listItem.focus(); + } + if (this.props.intersectionObserver) { + this.props.intersectionObserver.observe(this.refs.listItem); + } + + const { connector, item, requestFilterTypes } = this.props; + // Filtering XHR & WS require to lazily fetch requestHeaders & responseHeaders + if (requestFilterTypes.xhr || requestFilterTypes.ws) { + fetchNetworkUpdatePacket(connector.requestData, item, [ + "requestHeaders", + "responseHeaders", + ]); + } + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + const { connector, item, requestFilterTypes } = nextProps; + // Filtering XHR & WS require to lazily fetch requestHeaders & responseHeaders + if (requestFilterTypes.xhr || requestFilterTypes.ws) { + fetchNetworkUpdatePacket(connector.requestData, item, [ + "requestHeaders", + "responseHeaders", + ]); + } + } + + shouldComponentUpdate(nextProps) { + return ( + !propertiesEqual( + UPDATED_REQ_ITEM_PROPS, + this.props.item, + nextProps.item + ) || + !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps) || + this.props.columns !== nextProps.columns + ); + } + + componentDidUpdate(prevProps) { + if (!prevProps.isSelected && this.props.isSelected) { + this.refs.listItem.focus(); + if (this.props.onFocusedNodeChange) { + this.props.onFocusedNodeChange(); + } + } + } + + componentWillUnmount() { + if (this.props.intersectionObserver) { + this.props.intersectionObserver.unobserve(this.refs.listItem); + } + } + + render() { + const { + blocked, + connector, + columns, + item, + index, + isSelected, + isVisible, + firstRequestStartedMs, + fromCache, + networkActionOpen, + onDoubleClick, + onDragStart, + onContextMenu, + onMouseDown, + onWaterfallMouseDown, + selectedActionBarTabId, + } = this.props; + + const classList = ["request-list-item", index % 2 ? "odd" : "even"]; + isSelected && classList.push("selected"); + fromCache && classList.push("fromCache"); + blocked && classList.push("blocked"); + + return dom.tr( + { + ref: "listItem", + className: classList.join(" "), + "data-id": item.id, + draggable: + !blocked && + networkActionOpen && + selectedActionBarTabId === PANELS.BLOCKING, + tabIndex: 0, + onContextMenu, + onMouseDown, + onDoubleClick, + onDragStart, + }, + ...COLUMN_COMPONENTS.filter(({ column }) => columns[column]).map( + ({ column, ColumnComponent, props: columnProps }) => { + return ColumnComponent({ + key: column, + item, + ...(columnProps || []).reduce((acc, keyOrObject) => { + if (typeof keyOrObject == "string") { + acc[keyOrObject] = this.props[keyOrObject]; + } else { + Object.assign(acc, keyOrObject); + } + return acc; + }, {}), + }); + } + ), + ...RESPONSE_HEADERS.filter(header => columns[header]).map(header => + RequestListColumnResponseHeader({ + connector, + item, + header, + }) + ), + // The last column is Waterfall (aka Timeline) + columns.waterfall && + RequestListColumnWaterfall({ + connector, + firstRequestStartedMs, + item, + onWaterfallMouseDown, + isVisible, + }) + ); + } +} + +module.exports = RequestListItem; diff --git a/devtools/client/netmonitor/src/components/request-list/moz.build b/devtools/client/netmonitor/src/components/request-list/moz.build new file mode 100644 index 0000000000..22f0602b00 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list/moz.build @@ -0,0 +1,29 @@ +# 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( + "RequestList.js", + "RequestListColumnContentSize.js", + "RequestListColumnCookies.js", + "RequestListColumnDomain.js", + "RequestListColumnFile.js", + "RequestListColumnInitiator.js", + "RequestListColumnMethod.js", + "RequestListColumnPriority.js", + "RequestListColumnProtocol.js", + "RequestListColumnRemoteIP.js", + "RequestListColumnResponseHeader.js", + "RequestListColumnScheme.js", + "RequestListColumnSetCookies.js", + "RequestListColumnStatus.js", + "RequestListColumnTime.js", + "RequestListColumnTransferredSize.js", + "RequestListColumnType.js", + "RequestListColumnUrl.js", + "RequestListColumnWaterfall.js", + "RequestListContent.js", + "RequestListEmptyNotice.js", + "RequestListHeader.js", + "RequestListItem.js", +) -- cgit v1.2.3