diff --git a/devtools/client/netmonitor/src/components/moz.build b/devtools/client/netmonitor/src/components/moz.build index 358b3ef6551d..719cbd0280db 100644 --- a/devtools/client/netmonitor/src/components/moz.build +++ b/devtools/client/netmonitor/src/components/moz.build @@ -12,6 +12,17 @@ DevToolsModules( 'network-details-panel.js', 'params-panel.js', 'properties-view.js', + 'request-list-column-cause.js', + 'request-list-column-content-size.js', + 'request-list-column-domain.js', + 'request-list-column-file.js', + 'request-list-column-method.js', + 'request-list-column-protocol.js', + 'request-list-column-remote-ip.js', + 'request-list-column-status.js', + 'request-list-column-transferred-size.js', + 'request-list-column-type.js', + 'request-list-column-waterfall.js', 'request-list-content.js', 'request-list-empty-notice.js', 'request-list-header.js', diff --git a/devtools/client/netmonitor/src/components/request-list-column-cause.js b/devtools/client/netmonitor/src/components/request-list-column-cause.js new file mode 100644 index 000000000000..06ccf406be8e --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-cause.js @@ -0,0 +1,62 @@ +/* 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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); + +const { div, span } = DOM; + +const RequestListColumnCause = createClass({ + displayName: "RequestListColumnCause", + + propTypes: { + item: PropTypes.object.isRequired, + onCauseBadgeClick: PropTypes.func.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return this.props.item.cause !== nextProps.item.cause; + }, + + render() { + const { + item, + onCauseBadgeClick, + } = this.props; + + const { cause } = item; + + let causeType = ""; + let causeUri = undefined; + let causeHasStack = false; + + if (cause) { + // Legacy server might send a numeric value. Display it as "unknown" + causeType = typeof cause.type === "string" ? cause.type : "unknown"; + causeUri = cause.loadingDocumentUri; + causeHasStack = cause.stacktrace && cause.stacktrace.length > 0; + } + + return ( + div({ + className: "requests-list-subitem requests-list-cause", + title: causeUri, + }, + span({ + className: "requests-list-cause-stack", + hidden: !causeHasStack, + onClick: onCauseBadgeClick, + }, "JS"), + span({ className: "subitem-label" }, causeType), + ) + ); + } +}); + +module.exports = RequestListColumnCause; diff --git a/devtools/client/netmonitor/src/components/request-list-column-content-size.js b/devtools/client/netmonitor/src/components/request-list-column-content-size.js new file mode 100644 index 000000000000..b5da0525b1c3 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-content-size.js @@ -0,0 +1,46 @@ +/* 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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); +const { getFormattedSize } = require("../utils/format-utils"); + +const { div, span } = DOM; + +const RequestListColumnContentSize = createClass({ + displayName: "RequestListColumnContentSize", + + propTypes: { + item: PropTypes.object.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return this.props.item.contentSize !== nextProps.item.contentSize; + }, + + render() { + const { contentSize } = this.props.item; + + let text; + if (typeof contentSize == "number") { + text = getFormattedSize(contentSize); + } + + return ( + div({ + className: "requests-list-subitem subitem-label requests-list-size", + title: text, + }, + span({ className: "subitem-label" }, text), + ) + ); + } +}); + +module.exports = RequestListColumnContentSize; diff --git a/devtools/client/netmonitor/src/components/request-list-column-domain.js b/devtools/client/netmonitor/src/components/request-list-column-domain.js new file mode 100644 index 000000000000..e2b6cbe18657 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-domain.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); +const { L10N } = require("../utils/l10n"); +const { propertiesEqual } = require("../utils/request-utils"); + +const { div, span } = DOM; + +const UPDATED_DOMAIN_PROPS = [ + "urlDetails", + "remoteAddress", + "securityState", +]; + +const RequestListColumnDomain = createClass({ + displayName: "RequestListColumnDomain", + + propTypes: { + item: PropTypes.object.isRequired, + onSecurityIconClick: PropTypes.func.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item); + }, + + render() { + const { item, onSecurityIconClick } = this.props; + const { urlDetails, remoteAddress, securityState } = item; + + let iconClassList = ["requests-security-state-icon"]; + let iconTitle; + if (urlDetails.isLocal) { + iconClassList.push("security-state-local"); + iconTitle = L10N.getStr("netmonitor.security.state.secure"); + } else if (securityState) { + iconClassList.push(`security-state-${securityState}`); + iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`); + } + + let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : ""); + + return ( + div({ className: "requests-list-subitem requests-list-security-and-domain" }, + div({ + className: iconClassList.join(" "), + title: iconTitle, + onClick: onSecurityIconClick, + }), + span({ className: "subitem-label requests-list-domain", title }, urlDetails.host), + ) + ); + } +}); + +module.exports = RequestListColumnDomain; diff --git a/devtools/client/netmonitor/src/components/request-list-column-file.js b/devtools/client/netmonitor/src/components/request-list-column-file.js new file mode 100644 index 000000000000..c97ebf897419 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-file.js @@ -0,0 +1,54 @@ +/* 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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); +const { propertiesEqual } = require("../utils/request-utils"); + +const { div, img } = DOM; + +const UPDATED_FILE_PROPS = [ + "urlDetails", + "responseContentDataUri", +]; + +const RequestListColumnFile = createClass({ + displayName: "RequestListColumnFile", + + propTypes: { + item: PropTypes.object.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item); + }, + + render() { + const { urlDetails, responseContentDataUri } = this.props.item; + + return ( + div({ className: "requests-list-subitem requests-list-icon-and-file" }, + img({ + className: "requests-list-icon", + src: responseContentDataUri, + hidden: !responseContentDataUri, + "data-type": responseContentDataUri ? "thumbnail" : undefined, + }), + div({ + className: "subitem-label requests-list-file", + title: urlDetails.unicodeUrl, + }, + urlDetails.baseNameWithQuery, + ), + ) + ); + } +}); + +module.exports = RequestListColumnFile; diff --git a/devtools/client/netmonitor/src/components/request-list-column-method.js b/devtools/client/netmonitor/src/components/request-list-column-method.js new file mode 100644 index 000000000000..8fa55db0035f --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-method.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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); + +const { div, span } = DOM; + +const RequestListColumnMethod = createClass({ + displayName: "RequestListColumnMethod", + + propTypes: { + item: PropTypes.object.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return this.props.item.method !== nextProps.item.method; + }, + + render() { + const { method } = this.props.item; + return ( + div({ className: "requests-list-subitem requests-list-method-box" }, + span({ className: "subitem-label requests-list-method" }, method) + ) + ); + } +}); + +module.exports = RequestListColumnMethod; diff --git a/devtools/client/netmonitor/src/components/request-list-column-protocol.js b/devtools/client/netmonitor/src/components/request-list-column-protocol.js new file mode 100644 index 000000000000..4c1f5280f365 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-protocol.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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); + +const { div, span } = DOM; + +const RequestListColumnProtocol = createClass({ + displayName: "RequestListColumnProtocol", + + propTypes: { + item: PropTypes.object.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return this.props.item.httpVersion !== nextProps.item.httpVersion; + }, + + render() { + const { httpVersion } = this.props.item; + return ( + div({ className: "requests-list-subitem requests-list-protocol" }, + span({ className: "subitem-label", title: httpVersion }, httpVersion), + ) + ); + } +}); + +module.exports = RequestListColumnProtocol; diff --git a/devtools/client/netmonitor/src/components/request-list-column-remote-ip.js b/devtools/client/netmonitor/src/components/request-list-column-remote-ip.js new file mode 100644 index 000000000000..13df90757f0e --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-remote-ip.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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); + +const { div, span } = DOM; + +const RequestListColumnRemoteIP = createClass({ + displayName: "RequestListColumnRemoteIP", + + propTypes: { + item: PropTypes.object.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return this.props.item.remoteAddress !== nextProps.item.remoteAddress; + }, + + render() { + const { remoteAddress, remotePort } = this.props.item; + let remoteSummary = remoteAddress ? `${remoteAddress}:${remotePort}` : ""; + + return ( + div({ className: "requests-list-subitem requests-list-remoteip" }, + span({ className: "subitem-label", title: remoteSummary }, remoteSummary), + ) + ); + } +}); + +module.exports = RequestListColumnRemoteIP; diff --git a/devtools/client/netmonitor/src/components/request-list-column-status.js b/devtools/client/netmonitor/src/components/request-list-column-status.js new file mode 100644 index 000000000000..0376b9c28f9b --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-status.js @@ -0,0 +1,68 @@ +/* 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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); +const { propertiesEqual } = require("../utils/request-utils"); + +const { div, span } = DOM; + +const UPDATED_STATUS_PROPS = [ + "fromCache", + "fromServiceWorker", + "status", + "statusText", +]; + +const RequestListColumnStatus = createClass({ + displayName: "RequestListColumnStatus", + + propTypes: { + item: PropTypes.object.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item); + }, + + render() { + const { status, statusText, fromCache, fromServiceWorker } = this.props.item; + + let code, title; + + if (status) { + if (fromCache) { + code = "cached"; + } else if (fromServiceWorker) { + code = "service worker"; + } else { + code = status; + } + + if (statusText) { + title = `${status} ${statusText}`; + if (fromCache) { + title += " (cached)"; + } + if (fromServiceWorker) { + title += " (service worker)"; + } + } + } + + return ( + div({ className: "requests-list-subitem requests-list-status", title }, + div({ className: "requests-list-status-icon", "data-code": code }), + span({ className: "subitem-label requests-list-status-code" }, status) + ) + ); + } +}); + +module.exports = RequestListColumnStatus; diff --git a/devtools/client/netmonitor/src/components/request-list-column-transferred-size.js b/devtools/client/netmonitor/src/components/request-list-column-transferred-size.js new file mode 100644 index 000000000000..ea766197b076 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-transferred-size.js @@ -0,0 +1,63 @@ +/* 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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); +const { getFormattedSize } = require("../utils/format-utils"); +const { L10N } = require("../utils/l10n"); +const { propertiesEqual } = require("../utils/request-utils"); + +const { div, span } = DOM; + +const UPDATED_TRANSFERRED_PROPS = [ + "transferredSize", + "fromCache", + "fromServiceWorker", +]; + +const RequestListColumnTransferredSize = createClass({ + displayName: "RequestListColumnTransferredSize", + + propTypes: { + item: PropTypes.object.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item); + }, + + render() { + const { transferredSize, fromCache, fromServiceWorker, status } = this.props.item; + + let text; + let className = "subitem-label"; + if (fromCache || status === "304") { + text = L10N.getStr("networkMenu.sizeCached"); + className += " theme-comment"; + } else if (fromServiceWorker) { + text = L10N.getStr("networkMenu.sizeServiceWorker"); + className += " theme-comment"; + } else if (typeof transferredSize == "number") { + text = getFormattedSize(transferredSize); + } else if (transferredSize === null) { + text = L10N.getStr("networkMenu.sizeUnavailable"); + } + + return ( + div({ + className: "requests-list-subitem requests-list-transferred", + title: text, + }, + span({ className }, text), + ) + ); + } +}); + +module.exports = RequestListColumnTransferredSize; diff --git a/devtools/client/netmonitor/src/components/request-list-column-type.js b/devtools/client/netmonitor/src/components/request-list-column-type.js new file mode 100644 index 000000000000..553e255bb114 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-type.js @@ -0,0 +1,52 @@ +/* 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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); +const { getAbbreviatedMimeType } = require("../utils/request-utils"); + +const { div, span } = DOM; + +const CONTENT_MIME_TYPE_ABBREVIATIONS = { + "ecmascript": "js", + "javascript": "js", + "x-javascript": "js" +}; + +const RequestListColumnType = createClass({ + displayName: "RequestListColumnType", + + propTypes: { + 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); + abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType; + } + + return ( + div({ + className: "requests-list-subitem requests-list-type", + title: mimeType, + }, + span({ className: "subitem-label" }, abbrevType), + ) + ); + } +}); + +module.exports = RequestListColumnType; diff --git a/devtools/client/netmonitor/src/components/request-list-column-waterfall.js b/devtools/client/netmonitor/src/components/request-list-column-waterfall.js new file mode 100644 index 000000000000..fa5149d641c4 --- /dev/null +++ b/devtools/client/netmonitor/src/components/request-list-column-waterfall.js @@ -0,0 +1,95 @@ +/* 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 { + createClass, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); +const { L10N } = require("../utils/l10n"); +const { propertiesEqual } = require("../utils/request-utils"); + +const { div } = DOM; + +const UPDATED_WATERFALL_PROPS = [ + "eventTimings", + "totalTime", + "fromCache", + "fromServiceWorker", +]; + +const RequestListColumnWaterfall = createClass({ + displayName: "RequestListColumnWaterfall", + + propTypes: { + firstRequestStartedMillis: PropTypes.number.isRequired, + item: PropTypes.object.isRequired, + }, + + shouldComponentUpdate(nextProps) { + return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis || + !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item); + }, + + render() { + const { item, firstRequestStartedMillis } = this.props; + + return ( + div({ className: "requests-list-subitem requests-list-waterfall" }, + div({ + className: "requests-list-timings", + style: { + paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`, + }, + }, + timingBoxes(item), + ) + ) + ); + } +}); + +// List of properties of the timing info we want to create boxes for +const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"]; + +function timingBoxes(item) { + const { eventTimings, totalTime, fromCache, fromServiceWorker } = item; + let boxes = []; + + if (fromCache || fromServiceWorker) { + return boxes; + } + + if (eventTimings) { + // Add a set of boxes representing timing information. + for (let key of TIMING_KEYS) { + let width = eventTimings.timings[key]; + + // Don't render anything if it surely won't be visible. + // One millisecond == one unscaled pixel. + if (width > 0) { + boxes.push(div({ + key, + className: "requests-list-timings-box " + key, + style: { width } + })); + } + } + } + + if (typeof totalTime === "number") { + let text = L10N.getFormatStr("networkMenu.totalMS", totalTime); + boxes.push(div({ + key: "total", + className: "requests-list-timings-total", + title: text + }, text)); + } + + return boxes; +} + +module.exports = RequestListColumnWaterfall; diff --git a/devtools/client/netmonitor/src/components/request-list-item.js b/devtools/client/netmonitor/src/components/request-list-item.js index 123c06069233..d2b36ac3fb38 100644 --- a/devtools/client/netmonitor/src/components/request-list-item.js +++ b/devtools/client/netmonitor/src/components/request-list-item.js @@ -12,18 +12,22 @@ const { } = require("devtools/client/shared/vendor/react"); const I = require("devtools/client/shared/vendor/immutable"); -const { getFormattedSize } = require("../utils/format-utils"); -const { L10N } = require("../utils/l10n"); -const { getAbbreviatedMimeType } = require("../utils/request-utils"); +const { propertiesEqual } = require("../utils/request-utils"); -const { div, img, span } = DOM; +// Components +const RequestListColumnCause = createFactory(require("./request-list-column-cause")); +const RequestListColumnContentSize = createFactory(require("./request-list-column-content-size")); +const RequestListColumnDomain = createFactory(require("./request-list-column-domain")); +const RequestListColumnFile = createFactory(require("./request-list-column-file")); +const RequestListColumnMethod = createFactory(require("./request-list-column-method")); +const RequestListColumnProtocol = createFactory(require("./request-list-column-protocol")); +const RequestListColumnRemoteIP = createFactory(require("./request-list-column-remote-ip")); +const RequestListColumnStatus = createFactory(require("./request-list-column-status")); +const RequestListColumnTransferredSize = createFactory(require("./request-list-column-transferred-size")); +const RequestListColumnType = createFactory(require("./request-list-column-type")); +const RequestListColumnWaterfall = createFactory(require("./request-list-column-waterfall")); -/** - * Compare two objects on a subset of their properties - */ -function propertiesEqual(props, item1, item2) { - return item1 === item2 || props.every(p => item1[p] === item2[p]); -} +const { div } = DOM; /** * Used by shouldComponentUpdate: compare two items, and compare only properties @@ -131,458 +135,21 @@ const RequestListItem = createClass({ onContextMenu, onMouseDown, }, - columns.get("status") && StatusColumn({ item }), - columns.get("method") && MethodColumn({ item }), - columns.get("file") && FileColumn({ item }), - columns.get("protocol") && ProtocolColumn({ item }), - columns.get("domain") && DomainColumn({ item, onSecurityIconClick }), - columns.get("remoteip") && RemoteIPColumn({ item }), - columns.get("cause") && CauseColumn({ item, onCauseBadgeClick }), - columns.get("type") && TypeColumn({ item }), - columns.get("transferred") && TransferredSizeColumn({ item }), - columns.get("contentSize") && ContentSizeColumn({ item }), - columns.get("waterfall") && WaterfallColumn({ item, firstRequestStartedMillis }), + columns.get("status") && RequestListColumnStatus({ item }), + columns.get("method") && RequestListColumnMethod({ item }), + columns.get("file") && RequestListColumnFile({ item }), + columns.get("protocol") && RequestListColumnProtocol({ item }), + columns.get("domain") && RequestListColumnDomain({ item, onSecurityIconClick }), + columns.get("remoteip") && RequestListColumnRemoteIP({ item }), + columns.get("cause") && RequestListColumnCause({ item, onCauseBadgeClick }), + columns.get("type") && RequestListColumnType({ item }), + columns.get("transferred") && RequestListColumnTransferredSize({ item }), + columns.get("contentSize") && RequestListColumnContentSize({ item }), + columns.get("waterfall") && + RequestListColumnWaterfall({ item, firstRequestStartedMillis }), ) ); } }); -const UPDATED_STATUS_PROPS = [ - "status", - "statusText", - "fromCache", - "fromServiceWorker", -]; - -const StatusColumn = createFactory(createClass({ - displayName: "StatusColumn", - - propTypes: { - item: PropTypes.object.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item); - }, - - render() { - const { status, statusText, fromCache, fromServiceWorker } = this.props.item; - - let code, title; - - if (status) { - if (fromCache) { - code = "cached"; - } else if (fromServiceWorker) { - code = "service worker"; - } else { - code = status; - } - - if (statusText) { - title = `${status} ${statusText}`; - if (fromCache) { - title += " (cached)"; - } - if (fromServiceWorker) { - title += " (service worker)"; - } - } - } - - return ( - div({ className: "requests-list-subitem requests-list-status", title }, - div({ className: "requests-list-status-icon", "data-code": code }), - span({ className: "subitem-label requests-list-status-code" }, status) - ) - ); - } -})); - -const MethodColumn = createFactory(createClass({ - displayName: "MethodColumn", - - propTypes: { - item: PropTypes.object.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return this.props.item.method !== nextProps.item.method; - }, - - render() { - const { method } = this.props.item; - return ( - div({ className: "requests-list-subitem requests-list-method-box" }, - span({ className: "subitem-label requests-list-method" }, method) - ) - ); - } -})); - -const UPDATED_FILE_PROPS = [ - "urlDetails", - "responseContentDataUri", -]; - -const FileColumn = createFactory(createClass({ - displayName: "FileColumn", - - propTypes: { - item: PropTypes.object.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item); - }, - - render() { - const { urlDetails, responseContentDataUri } = this.props.item; - - return ( - div({ className: "requests-list-subitem requests-list-icon-and-file" }, - img({ - className: "requests-list-icon", - src: responseContentDataUri, - hidden: !responseContentDataUri, - "data-type": responseContentDataUri ? "thumbnail" : undefined, - }), - div({ - className: "subitem-label requests-list-file", - title: urlDetails.unicodeUrl, - }, - urlDetails.baseNameWithQuery, - ), - ) - ); - } -})); - -const ProtocolColumn = createFactory(createClass({ - displayName: "Protocol", - - propTypes: { - item: PropTypes.object.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return this.props.item.httpVersion !== nextProps.item.httpVersion; - }, - - render() { - const { httpVersion } = this.props.item; - return ( - div({ className: "requests-list-subitem requests-list-protocol" }, - span({ className: "subitem-label", title: httpVersion }, httpVersion), - ) - ); - } -})); - -const UPDATED_DOMAIN_PROPS = [ - "urlDetails", - "remoteAddress", - "securityState", -]; - -const DomainColumn = createFactory(createClass({ - displayName: "DomainColumn", - - propTypes: { - item: PropTypes.object.isRequired, - onSecurityIconClick: PropTypes.func.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item); - }, - - render() { - const { item, onSecurityIconClick } = this.props; - const { urlDetails, remoteAddress, securityState } = item; - - let iconClassList = ["requests-security-state-icon"]; - let iconTitle; - if (urlDetails.isLocal) { - iconClassList.push("security-state-local"); - iconTitle = L10N.getStr("netmonitor.security.state.secure"); - } else if (securityState) { - iconClassList.push(`security-state-${securityState}`); - iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`); - } - - let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : ""); - - return ( - div({ className: "requests-list-subitem requests-list-security-and-domain" }, - div({ - className: iconClassList.join(" "), - title: iconTitle, - onClick: onSecurityIconClick, - }), - span({ className: "subitem-label requests-list-domain", title }, urlDetails.host), - ) - ); - } -})); - -const RemoteIPColumn = createFactory(createClass({ - displayName: "RemoteIP", - - propTypes: { - item: PropTypes.object.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return this.props.item.remoteAddress !== nextProps.item.remoteAddress; - }, - - render() { - const { remoteAddress, remotePort } = this.props.item; - let remoteSummary = remoteAddress ? `${remoteAddress}:${remotePort}` : ""; - - return ( - div({ className: "requests-list-subitem requests-list-remoteip" }, - span({ className: "subitem-label", title: remoteSummary }, remoteSummary), - ) - ); - } -})); - -const CauseColumn = createFactory(createClass({ - displayName: "CauseColumn", - - propTypes: { - item: PropTypes.object.isRequired, - onCauseBadgeClick: PropTypes.func.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return this.props.item.cause !== nextProps.item.cause; - }, - - render() { - const { - item, - onCauseBadgeClick, - } = this.props; - - const { cause } = item; - - let causeType = ""; - let causeUri = undefined; - let causeHasStack = false; - - if (cause) { - // Legacy server might send a numeric value. Display it as "unknown" - causeType = typeof cause.type === "string" ? cause.type : "unknown"; - causeUri = cause.loadingDocumentUri; - causeHasStack = cause.stacktrace && cause.stacktrace.length > 0; - } - - return ( - div({ - className: "requests-list-subitem requests-list-cause", - title: causeUri, - }, - span({ - className: "requests-list-cause-stack", - hidden: !causeHasStack, - onClick: onCauseBadgeClick, - }, "JS"), - span({ className: "subitem-label" }, causeType), - ) - ); - } -})); - -const CONTENT_MIME_TYPE_ABBREVIATIONS = { - "ecmascript": "js", - "javascript": "js", - "x-javascript": "js" -}; - -const TypeColumn = createFactory(createClass({ - displayName: "TypeColumn", - - propTypes: { - 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); - abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType; - } - - return ( - div({ - className: "requests-list-subitem requests-list-type", - title: mimeType, - }, - span({ className: "subitem-label" }, abbrevType), - ) - ); - } -})); - -const UPDATED_TRANSFERRED_PROPS = [ - "transferredSize", - "fromCache", - "fromServiceWorker", -]; - -const TransferredSizeColumn = createFactory(createClass({ - displayName: "TransferredSizeColumn", - - propTypes: { - item: PropTypes.object.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item); - }, - - render() { - const { transferredSize, fromCache, fromServiceWorker, status } = this.props.item; - - let text; - let className = "subitem-label"; - if (fromCache || status === "304") { - text = L10N.getStr("networkMenu.sizeCached"); - className += " theme-comment"; - } else if (fromServiceWorker) { - text = L10N.getStr("networkMenu.sizeServiceWorker"); - className += " theme-comment"; - } else if (typeof transferredSize == "number") { - text = getFormattedSize(transferredSize); - } else if (transferredSize === null) { - text = L10N.getStr("networkMenu.sizeUnavailable"); - } - - return ( - div({ - className: "requests-list-subitem requests-list-transferred", - title: text, - }, - span({ className }, text), - ) - ); - } -})); - -const ContentSizeColumn = createFactory(createClass({ - displayName: "ContentSizeColumn", - - propTypes: { - item: PropTypes.object.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return this.props.item.contentSize !== nextProps.item.contentSize; - }, - - render() { - const { contentSize } = this.props.item; - - let text; - if (typeof contentSize == "number") { - text = getFormattedSize(contentSize); - } - - return ( - div({ - className: "requests-list-subitem subitem-label requests-list-size", - title: text, - }, - span({ className: "subitem-label" }, text), - ) - ); - } -})); - -const UPDATED_WATERFALL_PROPS = [ - "eventTimings", - "totalTime", - "fromCache", - "fromServiceWorker", -]; - -const WaterfallColumn = createFactory(createClass({ - displayName: "WaterfallColumn", - - propTypes: { - firstRequestStartedMillis: PropTypes.number.isRequired, - item: PropTypes.object.isRequired, - }, - - shouldComponentUpdate(nextProps) { - return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis || - !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item); - }, - - render() { - const { item, firstRequestStartedMillis } = this.props; - - return ( - div({ className: "requests-list-subitem requests-list-waterfall" }, - div({ - className: "requests-list-timings", - style: { - paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`, - }, - }, - timingBoxes(item), - ) - ) - ); - } -})); - -// List of properties of the timing info we want to create boxes for -const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"]; - -function timingBoxes(item) { - const { eventTimings, totalTime, fromCache, fromServiceWorker } = item; - let boxes = []; - - if (fromCache || fromServiceWorker) { - return boxes; - } - - if (eventTimings) { - // Add a set of boxes representing timing information. - for (let key of TIMING_KEYS) { - let width = eventTimings.timings[key]; - - // Don't render anything if it surely won't be visible. - // One millisecond == one unscaled pixel. - if (width > 0) { - boxes.push(div({ - key, - className: "requests-list-timings-box " + key, - style: { width } - })); - } - } - } - - if (typeof totalTime === "number") { - let text = L10N.getFormatStr("networkMenu.totalMS", totalTime); - boxes.push(div({ - key: "total", - className: "requests-list-timings-total", - title: text - }, text)); - } - - return boxes; -} - module.exports = RequestListItem; diff --git a/devtools/client/netmonitor/src/utils/request-utils.js b/devtools/client/netmonitor/src/utils/request-utils.js index 7e3a32dcb8ec..8816203f7bbe 100644 --- a/devtools/client/netmonitor/src/utils/request-utils.js +++ b/devtools/client/netmonitor/src/utils/request-utils.js @@ -274,6 +274,13 @@ function ipToLong(ip) { }, 0); } +/** + * Compare two objects on a subset of their properties + */ +function propertiesEqual(props, item1, item2) { + return item1 === item2 || props.every(p => item1[p] === item2[p]); +} + module.exports = { getFormDataSections, fetchHeaders, @@ -289,5 +296,6 @@ module.exports = { getUrlDetails, parseQueryString, parseFormData, + propertiesEqual, ipToLong, };