summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/request-list
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/netmonitor/src/components/request-list')
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestList.js50
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnContentSize.js38
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnCookies.js61
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnDomain.js66
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnFile.js91
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnInitiator.js65
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnMethod.js33
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnPriority.js36
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnProtocol.js43
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnRemoteIP.js43
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnResponseHeader.js59
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnScheme.js38
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnSetCookies.js61
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnStatus.js39
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnTime.js91
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnTransferredSize.js99
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnType.js45
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnUrl.js89
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListColumnWaterfall.js209
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListContent.js524
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListEmptyNotice.js107
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListHeader.js731
-rw-r--r--devtools/client/netmonitor/src/components/request-list/RequestListItem.js412
-rw-r--r--devtools/client/netmonitor/src/components/request-list/moz.build29
24 files changed, 3059 insertions, 0 deletions
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",
+)