fune/devtools/client/netmonitor/components/request-list-item.js

451 lines
12 KiB
JavaScript

/* 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/. */
/* eslint-disable react/prop-types */
"use strict";
const { createClass, createFactory, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
const { div, span, img } = DOM;
const { L10N } = require("../l10n");
const { getFormattedSize } = require("../utils/format-utils");
const { getAbbreviatedMimeType } = require("../request-utils");
/**
* Compare two objects on a subset of their properties
*/
function propertiesEqual(props, item1, item2) {
return item1 === item2 || props.every(p => item1[p] === item2[p]);
}
/**
* 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",
"responseContentDataUri",
"status",
"statusText",
"fromCache",
"fromServiceWorker",
"method",
"url",
"remoteAddress",
"cause",
"contentSize",
"transferredSize",
"startedMillis",
"totalTime",
];
const UPDATED_REQ_PROPS = [
"index",
"isSelected",
"firstRequestStartedMillis"
];
/**
* Render one row in the request list.
*/
const RequestListItem = createClass({
displayName: "RequestListItem",
propTypes: {
item: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
isSelected: PropTypes.bool.isRequired,
firstRequestStartedMillis: PropTypes.number.isRequired,
onContextMenu: PropTypes.func.isRequired,
onFocusedNodeChange: PropTypes.func,
onFocusedNodeUnmount: PropTypes.func,
onMouseDown: PropTypes.func.isRequired,
onSecurityIconClick: PropTypes.func.isRequired,
},
componentDidMount() {
if (this.props.isSelected) {
this.refs.el.focus();
}
},
shouldComponentUpdate(nextProps) {
return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
!propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
},
componentDidUpdate(prevProps) {
if (!prevProps.isSelected && this.props.isSelected) {
this.refs.el.focus();
if (this.props.onFocusedNodeChange) {
this.props.onFocusedNodeChange();
}
}
},
componentWillUnmount() {
// If this node is being destroyed and has focus, transfer the focus manually
// to the parent tree component. Otherwise, the focus will get lost and keyboard
// navigation in the tree will stop working. This is a workaround for a XUL bug.
// See bugs 1259228 and 1152441 for details.
// DE-XUL: Remove this hack once all usages are only in HTML documents.
if (this.props.isSelected) {
this.refs.el.blur();
if (this.props.onFocusedNodeUnmount) {
this.props.onFocusedNodeUnmount();
}
}
},
render() {
const {
item,
index,
isSelected,
firstRequestStartedMillis,
onContextMenu,
onMouseDown,
onSecurityIconClick
} = this.props;
let classList = [ "request-list-item" ];
if (isSelected) {
classList.push("selected");
}
classList.push(index % 2 ? "odd" : "even");
return div(
{
ref: "el",
className: classList.join(" "),
"data-id": item.id,
tabIndex: 0,
onContextMenu,
onMouseDown,
},
StatusColumn({ item }),
MethodColumn({ item }),
FileColumn({ item }),
DomainColumn({ item, onSecurityIconClick }),
CauseColumn({ item }),
TypeColumn({ item }),
TransferredSizeColumn({ item }),
ContentSizeColumn({ item }),
WaterfallColumn({ item, firstRequestStartedMillis })
);
}
});
const UPDATED_STATUS_PROPS = [
"status",
"statusText",
"fromCache",
"fromServiceWorker",
];
const StatusColumn = createFactory(createClass({
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-menu-subitem requests-menu-status", title },
div({ className: "requests-menu-status-icon", "data-code": code }),
span({ className: "subitem-label requests-menu-status-code" }, status)
);
}
}));
const MethodColumn = createFactory(createClass({
shouldComponentUpdate(nextProps) {
return this.props.item.method !== nextProps.item.method;
},
render() {
const { method } = this.props.item;
return div({ className: "requests-menu-subitem requests-menu-method-box" },
span({ className: "subitem-label requests-menu-method" }, method)
);
}
}));
const UPDATED_FILE_PROPS = [
"urlDetails",
"responseContentDataUri",
];
const FileColumn = createFactory(createClass({
shouldComponentUpdate(nextProps) {
return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
},
render() {
const { urlDetails, responseContentDataUri } = this.props.item;
return div({ className: "requests-menu-subitem requests-menu-icon-and-file" },
img({
className: "requests-menu-icon",
src: responseContentDataUri,
hidden: !responseContentDataUri,
"data-type": responseContentDataUri ? "thumbnail" : undefined
}),
div(
{
className: "subitem-label requests-menu-file",
title: urlDetails.unicodeUrl
},
urlDetails.baseNameWithQuery
)
);
}
}));
const UPDATED_DOMAIN_PROPS = [
"urlDetails",
"remoteAddress",
"securityState",
];
const DomainColumn = createFactory(createClass({
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-menu-subitem requests-menu-security-and-domain" },
div({
className: iconClassList.join(" "),
title: iconTitle,
onClick: onSecurityIconClick,
}),
span({ className: "subitem-label requests-menu-domain", title }, urlDetails.host)
);
}
}));
const CauseColumn = createFactory(createClass({
shouldComponentUpdate(nextProps) {
return this.props.item.cause !== nextProps.item.cause;
},
render() {
const { cause } = this.props.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-menu-subitem requests-menu-cause", title: causeUri },
span({ className: "requests-menu-cause-stack", hidden: !causeHasStack }, "JS"),
span({ className: "subitem-label" }, causeType)
);
}
}));
const CONTENT_MIME_TYPE_ABBREVIATIONS = {
"ecmascript": "js",
"javascript": "js",
"x-javascript": "js"
};
const TypeColumn = createFactory(createClass({
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-menu-subitem requests-menu-type", title: mimeType },
span({ className: "subitem-label" }, abbrevType)
);
}
}));
const UPDATED_TRANSFERRED_PROPS = [
"transferredSize",
"fromCache",
"fromServiceWorker",
];
const TransferredSizeColumn = createFactory(createClass({
shouldComponentUpdate(nextProps) {
return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item);
},
render() {
const { transferredSize, fromCache, fromServiceWorker } = this.props.item;
let text;
let className = "subitem-label";
if (fromCache) {
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-menu-subitem requests-menu-transferred", title: text },
span({ className }, text)
);
}
}));
const ContentSizeColumn = createFactory(createClass({
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-menu-subitem subitem-label requests-menu-size",
title: text
},
span({ className: "subitem-label" }, text)
);
}
}));
const UPDATED_WATERFALL_PROPS = [
"eventTimings",
"totalTime",
"fromCache",
"fromServiceWorker",
];
const WaterfallColumn = createFactory(createClass({
shouldComponentUpdate(nextProps) {
return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis ||
!propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item);
},
render() {
const { item, firstRequestStartedMillis } = this.props;
const startedDeltaMillis = item.startedMillis - firstRequestStartedMillis;
const paddingInlineStart = `${startedDeltaMillis}px`;
return div({ className: "requests-menu-subitem requests-menu-waterfall" },
div(
{ className: "requests-menu-timings", style: { paddingInlineStart } },
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-menu-timings-box " + key,
style: { width }
}));
}
}
}
if (typeof totalTime == "number") {
let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
boxes.push(div({
key: "total",
className: "requests-menu-timings-total",
title: text
}, text));
}
return boxes;
}
module.exports = RequestListItem;
/* eslint-enable react/prop-types */