--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -1,15 +1,23 @@
# 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(
'monitor-panel.js',
'network-monitor.js',
- 'request-list-content.js',
+ 'request-list-column-cause.js',
+ 'request-list-column-domain.js',
+ 'request-list-column-file.js',
+ 'request-list-column-header.js',
+ 'request-list-column-method.js',
+ 'request-list-column-size.js',
+ 'request-list-column-status.js',
+ 'request-list-column-transferred.js',
+ 'request-list-column-type.js',
+ 'request-list-column-waterfall.js',
'request-list-empty.js',
- 'request-list-header.js',
- 'request-list-item.js',
+ 'request-list-row.js',
'request-list.js',
'statistics-panel.js',
'toolbar.js',
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-cause.js
@@ -0,0 +1,81 @@
+/* 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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = ["cause"];
+
+/**
+ * Request list cause column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnCause() {
+ let name = "cause";
+ return (
+ Column({
+ cellRenderer: CauseColumnCell,
+ className: "requests-list-subitem requests-list-cause",
+ dataKey: name,
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name,
+ width: 100,
+ maxWidth: 100,
+ minWidth: 45,
+ })
+ );
+}
+
+RequestListColumnCause.displayName = "RequestListColumnCause";
+
+const CauseColumnCell = createFactory(createClass({
+ displayName: "CauseColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData } = this.props;
+ let { cause } = rowData;
+ let causeType = "";
+ let causeUri;
+ 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({ title: causeUri },
+ div({
+ className: "requests-list-cause-stack",
+ hidden: !causeHasStack,
+ }, "JS"),
+ div({ className: "subitem-label" }, causeType),
+ )
+ );
+ }
+}));
+
+module.exports = RequestListColumnCause;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-domain.js
@@ -0,0 +1,101 @@
+/* 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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = [
+ "remoteAddress",
+ "securityState",
+ "urlDetails",
+];
+
+/**
+ * Request list domain column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnDomain({
+ selectDetailsPanelTab,
+}) {
+ let name = "domain";
+ return (
+ Column({
+ cellRenderer: (props) =>
+ DomainColumnCell(Object.assign(props, { selectDetailsPanelTab })),
+ className: "requests-list-subitem requests-list-domain",
+ dataKey: name,
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name,
+ width: 150,
+ maxWidth: 220,
+ minWidth: 60,
+ })
+ );
+}
+
+RequestListColumnDomain.displayName = "RequestListColumnDomain";
+
+const DomainColumnCell = createFactory(createClass({
+ displayName: "DomainColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ selectDetailsPanelTab: PropTypes.func.isRequired,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData, selectDetailsPanelTab } = this.props;
+ let {
+ urlDetails,
+ remoteAddress,
+ securityState,
+ } = rowData;
+
+ let iconClassList = ["requests-list-domain-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({},
+ div({
+ className: iconClassList.join(" "),
+ title: iconTitle,
+ onClick: () => {
+ if (securityState && securityState !== "insecure") {
+ selectDetailsPanelTab("security");
+ }
+ },
+ }),
+ div({ className: "requests-list-domain-url", title }, urlDetails.host),
+ )
+ );
+ }
+}));
+
+module.exports = RequestListColumnDomain;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-file.js
@@ -0,0 +1,73 @@
+/* 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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = [
+ "responseContentDataUri",
+ "urlDetails",
+];
+
+/**
+ * Request list file column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnFile() {
+ let name = "file";
+ return (
+ Column({
+ cellRenderer: FileColumnCell,
+ className: "requests-list-subitem requests-list-file",
+ dataKey: name,
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name,
+ width: 300,
+ maxWidth: 350,
+ minWidth: 80,
+ })
+ );
+}
+
+RequestListColumnFile.displayName = "RequestListColumnFile";
+
+const FileColumnCell = createFactory(createClass({
+ displayName: "FileColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData } = this.props;
+ let { urlDetails } = rowData;
+
+ return (
+ div({
+ className: "requests-list-url subitem-label",
+ title: urlDetails.unicodeUrl,
+ },
+ urlDetails.baseNameWithQuery,
+ )
+ );
+ }
+}));
+
+module.exports = RequestListColumnFile;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-header.js
@@ -0,0 +1,143 @@
+/* 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 SortDirection = require("devtools/client/shared/vendor/react-virtualized").SortDirection;
+const { getFormattedTime } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const WaterfallBackground = require("../waterfall-background");
+
+const { div } = DOM;
+
+/**
+ * Request list column header component
+ * Describes the header of a table column
+ */
+const RequestListColumnHeader = createClass({
+ displayName: "RequestListColumnHeader",
+
+ propTypes: {
+ columnData: PropTypes.object,
+ dataKey: PropTypes.string.isRequired,
+ label: PropTypes.string,
+ sortBy: PropTypes.string,
+ sortDirection: PropTypes.string,
+ },
+
+ componentDidMount() {
+ if (this.props.dataKey === "waterfall") {
+ // Create the object that takes care of drawing the waterfall canvas background
+ this.background = new WaterfallBackground();
+ this.background.draw(this.props);
+ // Measure its width and update the 'waterfallWidth' property in the store.
+ setTimeout(() => {
+ let { width } = this.refs.header.getBoundingClientRect();
+ this.props.columnData.resizeWaterfall(width);
+ }, 50);
+ }
+ },
+
+ componentDidUpdate() {
+ if (this.props.dataKey === "waterfall") {
+ this.background.draw(this.props);
+ }
+ },
+
+ componentWillUnmount() {
+ if (this.props.dataKey === "waterfall") {
+ this.background.destroy();
+ this.background = null;
+ }
+ },
+
+ renderWaterfallLabel(waterfallWidth, scale, label) {
+ let labels = [];
+ if (waterfallWidth && scale) {
+ // Set millisecond tick labels by 5ms
+ let timingStep = 5;
+ let scaledStep = scale * timingStep;
+
+ // Ignore any divisions < 60px that would end up being too close to each other.
+ while (scaledStep < 60) {
+ scaledStep *= 2;
+ }
+
+ // Insert one label for each division on the current scale.
+ for (let x = 0; x < waterfallWidth; x += scaledStep) {
+ let 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.length > 0 ? labels : "";
+ },
+
+ render() {
+ let {
+ columnData = {},
+ dataKey,
+ label,
+ sortBy,
+ sortDirection,
+ } = this.props;
+ let title;
+ let className = ["requests-list-header-button", `requests-list-${dataKey}`];
+
+ if (sortBy === dataKey) {
+ className.push(sortDirection === SortDirection.ASC ? "ascending" : "descending");
+ title = L10N.getStr(sortDirection === SortDirection.ASC ?
+ "networkMenu.sortedAsc" : "networkMenu.sortedDesc");
+ }
+
+ let { waterfallWidth, scale } = columnData;
+
+ return (
+ div({
+ ref: "header",
+ className: className.join(" "),
+ title,
+ },
+ dataKey === "waterfall"
+ ? this.renderWaterfallLabel(waterfallWidth, scale, label)
+ : div({ className: "button-text" }, L10N.getStr(`netmonitor.toolbar.${label}`)),
+ div({ className: "button-icon" }),
+ )
+ );
+ }
+});
+
+module.exports = RequestListColumnHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-method.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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = ["method"];
+
+/**
+ * Request list method column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnMethod() {
+ let name = "method";
+ return (
+ Column({
+ cellRenderer: MethodColumnCell,
+ className: "requests-list-subitem requests-list-method",
+ dataKey: name,
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name,
+ width: 75,
+ maxWidth: 84,
+ minWidth: 55,
+ })
+ );
+}
+
+RequestListColumnMethod.displayName = "RequestListColumnMethod";
+
+const MethodColumnCell = createFactory(createClass({
+ displayName: "MethodColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData } = this.props;
+
+ return (
+ div({ className: "subitem-label requests-list-method" }, rowData.method)
+ );
+ }
+}));
+
+module.exports = RequestListColumnMethod;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-size.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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { getFormattedSize } = require("../utils/format-utils");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = ["contentSize"];
+
+/**
+ * Request list size column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnsize() {
+ let name = "size";
+ return (
+ Column({
+ cellRenderer: ContentSizeColumnCell,
+ className: "requests-list-subitem requests-list-size",
+ dataKey: name,
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name,
+ width: 72,
+ maxWidth: 72,
+ minWidth: 45,
+ })
+ );
+}
+
+RequestListColumnsize.displayName = "RequestListColumnsize";
+
+const ContentSizeColumnCell = createFactory(createClass({
+ displayName: "ContentSizeColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData } = this.props;
+ let { contentSize } = rowData;
+ let size = typeof contentSize === "number" ? getFormattedSize(contentSize) : null;
+ return size ? div({ title: size }, size) : null;
+ }
+}));
+
+module.exports = RequestListColumnsize;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-status.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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div, span } = DOM;
+const UPDATED_PROPS = [
+ "fromCache",
+ "fromServiceWorker",
+ "status",
+ "statusText",
+];
+
+/**
+ * Request list status column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnStatus() {
+ let name = "status";
+ return (
+ Column({
+ cellRenderer: StatusColumnCell,
+ className: "requests-list-subitem requests-list-status",
+ dataKey: name,
+ disableSort: false,
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name + "3",
+ width: 72,
+ maxWidth: 72,
+ minWidth: 50,
+ })
+ );
+}
+
+RequestListColumnStatus.displayName = "RequestListColumnStatus";
+
+const StatusColumnCell = createFactory(createClass({
+ displayName: "StatusColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData } = this.props;
+ let { status, statusText, fromCache, fromServiceWorker } = rowData;
+ 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({ title },
+ span({ className: "requests-list-status-icon", "data-code": code }),
+ span({ className: "requests-list-status-code subitem-label" }, status),
+ )
+ );
+ }
+}));
+
+module.exports = RequestListColumnStatus;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-transferred.js
@@ -0,0 +1,83 @@
+/* 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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { getFormattedSize } = require("../utils/format-utils");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = [
+ "fromCache",
+ "fromServiceWorker",
+ "transferredSize",
+];
+
+/**
+ * Request list transferred column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnTransferred() {
+ let name = "transferred";
+ return (
+ Column({
+ cellRenderer: TransferredSizeColumnCell,
+ className: "requests-list-subitem requests-list-transferred",
+ dataKey: name,
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name,
+ width: 96,
+ maxWidth: 96,
+ minWidth: 65,
+ })
+ );
+}
+
+RequestListColumnTransferred.displayName = "RequestListColumnTransferred";
+
+const TransferredSizeColumnCell = createFactory(createClass({
+ displayName: "TransferredSizeColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData } = this.props;
+ let { transferredSize, fromCache, fromServiceWorker, status } = rowData;
+
+ let title;
+ let className = "subitem-label";
+ if (fromCache || status === "304") {
+ title = L10N.getStr("networkMenu.sizeCached");
+ className += " theme-comment";
+ } else if (fromServiceWorker) {
+ title = L10N.getStr("networkMenu.sizeServiceWorker");
+ className += " theme-comment";
+ } else if (typeof transferredSize === "number") {
+ title = getFormattedSize(transferredSize);
+ } else if (transferredSize === null) {
+ title = L10N.getStr("networkMenu.sizeUnavailable");
+ }
+
+ return div({ className, title }, title);
+ }
+}));
+
+module.exports = RequestListColumnTransferred;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-type.js
@@ -0,0 +1,78 @@
+/* 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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const {
+ getAbbreviatedMimeType,
+ propertiesEqual,
+} = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+ "ecmascript": "js",
+ "javascript": "js",
+ "x-javascript": "js"
+};
+const UPDATED_PROPS = ["mimeType"];
+
+/**
+ * Request list type column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnType() {
+ let name = "type";
+ return (
+ Column({
+ cellRenderer: TypeColumnCell,
+ className: "requests-list-subitem requests-list-type",
+ dataKey: name,
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name,
+ width: 72,
+ maxWidth: 72,
+ minWidth: 45,
+ })
+ );
+}
+
+RequestListColumnType.displayName = "RequestListColumnType";
+
+const TypeColumnCell = createFactory(createClass({
+ displayName: "TypeColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData } = this.props;
+ let { mimeType } = rowData;
+ let abbrevType;
+
+ if (mimeType) {
+ abbrevType = getAbbreviatedMimeType(mimeType);
+ abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
+ }
+
+ return div({ title: mimeType }, abbrevType);
+ }
+}));
+
+module.exports = RequestListColumnType;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-column-waterfall.js
@@ -0,0 +1,122 @@
+/* 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,
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { propertiesEqual } = require("../utils/request-utils");
+
+// Components
+const Column = createFactory(require("devtools/client/shared/vendor/react-virtualized").Column);
+const RequestListColumnHeader = createFactory(require("./request-list-column-header"));
+
+const { div } = DOM;
+const UPDATED_PROPS = [
+ "eventTimings",
+ "fromCache",
+ "fromServiceWorker",
+ "totalTime",
+];
+
+/**
+ * Request list waterfall column component
+ * Describes the header and cell contents of a table column
+ */
+function RequestListColumnWaterfall(columnData) {
+ let name = "waterfall";
+ let { firstRequestStartedMillis } = columnData;
+ return (
+ Column({
+ cellRenderer: (props) =>
+ WaterfallColumnCell(Object.assign(props, { firstRequestStartedMillis })),
+ className: "requests-list-subitem requests-list-waterfall",
+ columnData,
+ dataKey: name,
+ flexGrow: 1,
+ headerClassName: "requests-list-waterfall",
+ headerRenderer: (props) => RequestListColumnHeader(props),
+ label: name,
+ width: 300,
+ })
+ );
+}
+
+RequestListColumnWaterfall.displayName = "RequestListColumnWaterfall";
+
+const WaterfallColumnCell = createFactory(createClass({
+ displayName: "WaterfallColumnCell",
+
+ propTypes: {
+ rowData: PropTypes.object,
+ firstRequestStartedMillis: PropTypes.number,
+ },
+
+ shouldComponentUpdate(nextProps) {
+ return !propertiesEqual(UPDATED_PROPS, this.props.rowData, nextProps.rowData);
+ },
+
+ render() {
+ let { rowData, firstRequestStartedMillis } = this.props;
+ return (
+ div({
+ className: "requests-list-timings",
+ style: {
+ paddingInlineStart: `${rowData.startedMillis - firstRequestStartedMillis}px`,
+ },
+ },
+ timingBoxes(rowData),
+ )
+ );
+ }
+}));
+
+// 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;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-content.js
+++ /dev/null
@@ -1,280 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { KeyCodes } = require("devtools/client/shared/keycodes");
-const {
- createClass,
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
-const Actions = require("../actions/index");
-const {
- setTooltipImageContent,
- setTooltipStackTraceContent,
-} = require("../request-list-tooltip");
-const {
- getDisplayedRequests,
- getWaterfallScale,
-} = require("../selectors/index");
-
-// Components
-const RequestListItem = createFactory(require("./request-list-item"));
-const RequestListContextMenu = require("../request-list-context-menu");
-
-const { div } = DOM;
-
-// tooltip show/hide delay in ms
-const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
-
-/**
- * Renders the actual contents of the request list.
- */
-const RequestListContent = createClass({
- displayName: "RequestListContent",
-
- propTypes: {
- dispatch: PropTypes.func.isRequired,
- displayedRequests: PropTypes.object.isRequired,
- firstRequestStartedMillis: PropTypes.number.isRequired,
- fromCache: PropTypes.bool.isRequired,
- onItemMouseDown: PropTypes.func.isRequired,
- onSecurityIconClick: PropTypes.func.isRequired,
- onSelectDelta: PropTypes.func.isRequired,
- scale: PropTypes.number,
- selectedRequestId: PropTypes.string,
- },
-
- componentWillMount() {
- const { dispatch } = this.props;
- this.contextMenu = new RequestListContextMenu({
- cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
- openStatistics: (open) => dispatch(Actions.openStatistics(open)),
- });
- this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
- },
-
- componentDidMount() {
- // Set the CSS variables for waterfall scaling
- this.setScalingStyles();
-
- // Install event handler for displaying a tooltip
- this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
- toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
- interactive: true
- });
-
- // Install event handler to hide the tooltip on scroll
- this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
- },
-
- componentWillUpdate(nextProps) {
- // Check if the list is scrolled to bottom before the UI update.
- // The scroll is ever needed only if new rows are added to the list.
- const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
- this.shouldScrollBottom = delta > 0 && this.isScrolledToBottom();
- },
-
- componentDidUpdate(prevProps) {
- // Update the CSS variables for waterfall scaling after props change
- this.setScalingStyles(prevProps);
-
- // Keep the list scrolled to bottom if a new row was added
- if (this.shouldScrollBottom) {
- let node = this.refs.contentEl;
- node.scrollTop = node.scrollHeight;
- }
- },
-
- componentWillUnmount() {
- this.refs.contentEl.removeEventListener("scroll", this.onScroll, true);
-
- // Uninstall the tooltip event handler
- this.tooltip.stopTogglingOnHover();
- },
-
- /**
- * Set the CSS variables for waterfall scaling. If React supported setting CSS
- * variables as part of the "style" property of a DOM element, we would use that.
- *
- * However, React doesn't support this, so we need to use a hack and update the
- * DOM element directly: https://github.com/facebook/react/issues/6411
- */
- setScalingStyles(prevProps) {
- const { scale } = this.props;
- if (prevProps && prevProps.scale === scale) {
- return;
- }
-
- const { style } = this.refs.contentEl;
- style.removeProperty("--timings-scale");
- style.removeProperty("--timings-rev-scale");
- style.setProperty("--timings-scale", scale);
- style.setProperty("--timings-rev-scale", 1 / scale);
- },
-
- isScrolledToBottom() {
- const { contentEl } = this.refs;
- const lastChildEl = contentEl.lastElementChild;
-
- if (!lastChildEl) {
- return false;
- }
-
- let lastChildRect = lastChildEl.getBoundingClientRect();
- let contentRect = contentEl.getBoundingClientRect();
-
- return (lastChildRect.height + lastChildRect.top) <= contentRect.bottom;
- },
-
- /**
- * The predicate used when deciding whether a popup should be shown
- * over a request item or not.
- *
- * @param nsIDOMNode target
- * The element node currently being hovered.
- * @param object tooltip
- * The current tooltip instance.
- * @return {Promise}
- */
- onHover(target, tooltip) {
- let itemEl = target.closest(".request-list-item");
- if (!itemEl) {
- return false;
- }
- let itemId = itemEl.dataset.id;
- if (!itemId) {
- return false;
- }
- let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
- if (!requestItem) {
- return false;
- }
-
- if (requestItem.responseContent && target.closest(".requests-list-icon-and-file")) {
- return setTooltipImageContent(tooltip, itemEl, requestItem);
- } else if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
- return setTooltipStackTraceContent(tooltip, requestItem);
- }
-
- return false;
- },
-
- /**
- * Scroll listener for the requests menu view.
- */
- onScroll() {
- this.tooltip.hide();
- },
-
- /**
- * Handler for keyboard events. For arrow up/down, page up/down, home/end,
- * move the selection up or down.
- */
- onKeyDown(e) {
- let delta;
-
- switch (e.keyCode) {
- case KeyCodes.DOM_VK_UP:
- case KeyCodes.DOM_VK_LEFT:
- delta = -1;
- break;
- case KeyCodes.DOM_VK_DOWN:
- case KeyCodes.DOM_VK_RIGHT:
- delta = +1;
- break;
- case KeyCodes.DOM_VK_PAGE_UP:
- delta = "PAGE_UP";
- break;
- case KeyCodes.DOM_VK_PAGE_DOWN:
- delta = "PAGE_DOWN";
- break;
- case KeyCodes.DOM_VK_HOME:
- delta = -Infinity;
- break;
- case KeyCodes.DOM_VK_END:
- delta = +Infinity;
- break;
- }
-
- if (delta) {
- // Prevent scrolling when pressing navigation keys.
- e.preventDefault();
- e.stopPropagation();
- this.props.onSelectDelta(delta);
- }
- },
-
- onContextMenu(evt) {
- evt.preventDefault();
- this.contextMenu.open(evt);
- },
-
- /**
- * If selection has just changed (by keyboard navigation), don't keep the list
- * scrolled to bottom, but allow scrolling up with the selection.
- */
- onFocusedNodeChange() {
- this.shouldScrollBottom = false;
- },
-
- render() {
- const {
- displayedRequests,
- firstRequestStartedMillis,
- selectedRequestId,
- onItemMouseDown,
- onSecurityIconClick,
- } = this.props;
-
- return (
- div({
- ref: "contentEl",
- className: "requests-list-contents",
- tabIndex: 0,
- onKeyDown: this.onKeyDown,
- },
- displayedRequests.map((item, index) => RequestListItem({
- firstRequestStartedMillis,
- fromCache: item.status === "304" || item.fromCache,
- item,
- index,
- isSelected: item.id === selectedRequestId,
- key: item.id,
- onContextMenu: this.onContextMenu,
- onFocusedNodeChange: this.onFocusedNodeChange,
- onMouseDown: () => onItemMouseDown(item.id),
- onSecurityIconClick: () => onSecurityIconClick(item.securityState),
- }))
- )
- );
- },
-});
-
-module.exports = connect(
- (state) => ({
- displayedRequests: getDisplayedRequests(state),
- firstRequestStartedMillis: state.requests.firstStartedMillis,
- selectedRequestId: state.requests.selectedId,
- scale: getWaterfallScale(state),
- }),
- (dispatch) => ({
- dispatch,
- 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.
- */
- onSecurityIconClick: (securityState) => {
- if (securityState && securityState !== "insecure") {
- dispatch(Actions.selectDetailsPanelTab("security"));
- }
- },
- onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
- }),
-)(RequestListContent);
--- a/devtools/client/netmonitor/components/request-list-empty.js
+++ b/devtools/client/netmonitor/components/request-list-empty.js
@@ -1,70 +1,55 @@
/* 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 { connect } = require("devtools/client/shared/vendor/react-redux");
-const Actions = require("../actions/index");
-const { ACTIVITY_TYPE } = require("../constants");
const { L10N } = require("../utils/l10n");
const { button, div, span } = DOM;
/**
* UI displayed when the request list is empty. Contains instructions on reloading
* the page and on triggering performance analysis of the page.
*/
-const RequestListEmptyNotice = createClass({
- displayName: "RequestListEmptyNotice",
-
- propTypes: {
- onReloadClick: PropTypes.func.isRequired,
- onPerfClick: PropTypes.func.isRequired,
- },
-
- render() {
- return div(
- {
- className: "request-list-empty-notice",
- },
+function RequestListEmptyNotice({
+ onPerfClick,
+ onReloadClick,
+}) {
+ return (
+ div({ className: "request-list-empty-notice" },
div({ className: "notice-reload-message" },
span(null, L10N.getStr("netmonitor.reloadNotice1")),
- button(
- {
- className: "devtools-toolbarbutton requests-list-reload-notice-button",
- "data-standalone": true,
- onClick: this.props.onReloadClick,
- },
- L10N.getStr("netmonitor.reloadNotice2")
- ),
+ button({
+ className: "devtools-button requests-list-reload-notice-button",
+ "data-standalone": true,
+ onClick: onReloadClick,
+ }, L10N.getStr("netmonitor.reloadNotice2")),
span(null, L10N.getStr("netmonitor.reloadNotice3"))
),
div({ className: "notice-perf-message" },
span(null, L10N.getStr("netmonitor.perfNotice1")),
button({
title: L10N.getStr("netmonitor.perfNotice3"),
className: "devtools-button requests-list-perf-notice-button",
"data-standalone": true,
- onClick: this.props.onPerfClick,
+ onClick: onPerfClick,
}),
span(null, L10N.getStr("netmonitor.perfNotice2"))
)
- );
- }
-});
+ )
+ );
+}
+
+RequestListEmptyNotice.displayName = "RequestListEmptyNotice";
-module.exports = connect(
- undefined,
- dispatch => ({
- onPerfClick: () => dispatch(Actions.openStatistics(true)),
- onReloadClick: () =>
- window.NetMonitorController
- .triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT),
- })
-)(RequestListEmptyNotice);
+RequestListEmptyNotice.propTypes = {
+ onPerfClick: PropTypes.func.isRequired,
+ onReloadClick: PropTypes.func.isRequired,
+};
+
+module.exports = RequestListEmptyNotice;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-header.js
+++ /dev/null
@@ -1,200 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
-const { div, button } = DOM;
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
-const { L10N } = require("../utils/l10n");
-const { getWaterfallScale } = require("../selectors/index");
-const Actions = require("../actions/index");
-const WaterfallBackground = require("../waterfall-background");
-const { getFormattedTime } = require("../utils/format-utils");
-
-// ms
-const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
-// px
-const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
-
-const HEADERS = [
- { name: "status", label: "status3" },
- { name: "method" },
- { name: "file", boxName: "icon-and-file" },
- { name: "domain", boxName: "security-and-domain" },
- { name: "cause" },
- { name: "type" },
- { name: "transferred" },
- { name: "size" },
- { name: "waterfall" }
-];
-
-/**
- * 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.
- */
-const RequestListHeader = createClass({
- displayName: "RequestListHeader",
-
- propTypes: {
- sort: PropTypes.object,
- scale: PropTypes.number,
- waterfallWidth: PropTypes.number,
- onHeaderClick: PropTypes.func.isRequired,
- resizeWaterfall: PropTypes.func.isRequired,
- },
-
- componentDidMount() {
- // Create the object that takes care of drawing the waterfall canvas background
- this.background = new WaterfallBackground(document);
- this.background.draw(this.props);
- this.resizeWaterfall();
- window.addEventListener("resize", this.resizeWaterfall);
- },
-
- componentDidUpdate() {
- this.background.draw(this.props);
- },
-
- componentWillUnmount() {
- this.background.destroy();
- this.background = null;
- window.removeEventListener("resize", this.resizeWaterfall);
- },
-
- resizeWaterfall() {
- // Measure its width and update the 'waterfallWidth' property in the store.
- // The 'waterfallWidth' will be further updated on every window resize.
- setNamedTimeout("resize-events", 50, () => {
- const { width } = this.refs.header.getBoundingClientRect();
- this.props.resizeWaterfall(width);
- });
- },
-
- render() {
- const { sort, scale, waterfallWidth, onHeaderClick } = this.props;
-
- return div(
- { className: "devtools-toolbar requests-list-toolbar" },
- div({ className: "toolbar-labels" },
- HEADERS.map(header => {
- const name = header.name;
- const boxName = header.boxName || name;
- const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
-
- 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");
- }
-
- return div(
- {
- id: `requests-list-${boxName}-header-box`,
- className: `requests-list-header requests-list-${boxName}`,
- key: name,
- ref: "header",
- // Used to style the next column.
- "data-active": active,
- },
- button(
- {
- id: `requests-list-${name}-button`,
- className: `requests-list-header-button requests-list-${name}`,
- "data-sorted": sorted,
- title: sortedTitle,
- onClick: () => onHeaderClick(name),
- },
- name == "waterfall" ? WaterfallLabel(waterfallWidth, scale, label)
- : div({ className: "button-text" }, label),
- div({ className: "button-icon" })
- )
- );
- })
- )
- );
- }
-});
-
-/**
- * Build the waterfall header - timing tick marks with the right spacing
- */
-function waterfallDivisionLabels(waterfallWidth, scale) {
- let labels = [];
-
- // Build new millisecond tick labels...
- let 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) {
- let 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;
-}
-
-function WaterfallLabel(waterfallWidth, scale, label) {
- let className = "button-text requests-list-waterfall-label-wrapper";
-
- if (waterfallWidth != null && scale != null) {
- label = waterfallDivisionLabels(waterfallWidth, scale);
- className += " requests-list-waterfall-visible";
- }
-
- return div({ className }, label);
-}
-
-module.exports = connect(
- state => ({
- sort: state.sort,
- scale: getWaterfallScale(state),
- waterfallWidth: state.ui.waterfallWidth,
- firstRequestStartedMillis: state.requests.firstStartedMillis,
- timingMarkers: state.timingMarkers,
- }),
- dispatch => ({
- onHeaderClick: type => dispatch(Actions.sortBy(type)),
- resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)),
- })
-)(RequestListHeader);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-item.js
+++ /dev/null
@@ -1,528 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
- createClass,
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../utils/l10n");
-const { getAbbreviatedMimeType } = require("../utils/request-utils");
-const { getFormattedSize } = require("../utils/format-utils");
-
-const { div, img, span } = DOM;
-
-/**
- * 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,
- fromCache: PropTypes.bool.isRequired,
- onContextMenu: PropTypes.func.isRequired,
- onFocusedNodeChange: 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();
- }
- }
- },
-
- render() {
- const {
- item,
- index,
- isSelected,
- firstRequestStartedMillis,
- fromCache,
- onContextMenu,
- onMouseDown,
- onSecurityIconClick
- } = this.props;
-
- let classList = ["request-list-item"];
- if (isSelected) {
- classList.push("selected");
- }
-
- if (fromCache) {
- classList.push("fromCache");
- }
-
- 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({
- 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 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 CauseColumn = createFactory(createClass({
- displayName: "CauseColumn",
-
- propTypes: {
- item: PropTypes.object.isRequired,
- },
-
- 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-list-subitem requests-list-cause",
- title: causeUri,
- },
- span({
- className: "requests-list-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({
- 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;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/request-list-row.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 {
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { div } = DOM;
+
+/**
+ * Request list row component
+ */
+function RequestListRow({
+ className,
+ columns,
+ index,
+ key,
+ onRowClick,
+ rowData,
+ style,
+}) {
+ return (
+ div({
+ "aria-label": "row",
+ "data-id": rowData.id,
+ role: "row",
+ className: rowData.fromCache ? `${className} fromCache` : className,
+ key,
+ onMouseDown: () => onRowClick({ index }),
+ style,
+ },
+ columns
+ )
+ );
+}
+
+RequestListRow.displayName = "RequestListRow";
+
+RequestListRow.propTypes = {
+ className: PropTypes.string.isRequired,
+ columns: PropTypes.object.isRequired,
+ index: PropTypes.number.isRequired,
+ key: PropTypes.string.isRequired,
+ onRowClick: PropTypes.func.isRequired,
+ onRowContextMenu: PropTypes.func.isRequired,
+ rowData: PropTypes.object.isRequired,
+ style: PropTypes.object.isRequired,
+};
+
+module.exports = RequestListRow;
--- a/devtools/client/netmonitor/components/request-list.js
+++ b/devtools/client/netmonitor/components/request-list.js
@@ -1,38 +1,328 @@
/* 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,
createFactory,
DOM,
PropTypes,
} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const SortDirection = require("devtools/client/shared/vendor/react-virtualized").SortDirection;
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const Actions = require("../actions/index");
+const { ACTIVITY_TYPE } = require("../constants");
+const RequestListContextMenu = require("../request-list-context-menu");
+const { setTooltipStackTraceContent } = require("../request-list-tooltip");
+const {
+ getDisplayedRequests,
+ getSortedRequests,
+ getWaterfallScale,
+} = require("../selectors/index");
// Components
-const RequestListContent = createFactory(require("./request-list-content"));
-const RequestListEmptyNotice = createFactory(require("./request-list-empty"));
-const RequestListHeader = createFactory(require("./request-list-header"));
+const AutoSizer = createFactory(require("devtools/client/shared/vendor/react-virtualized").AutoSizer);
+const Table = createFactory(require("devtools/client/shared/vendor/react-virtualized").Table);
+const RequestListColumnCause = require("./request-list-column-cause");
+const RequestListColumnDomain = require("./request-list-column-domain");
+const RequestListColumnFile = require("./request-list-column-file");
+const RequestListColumnMethod = require("./request-list-column-method");
+const RequestListColumnSize = require("./request-list-column-size");
+const RequestListColumnStatus = require("./request-list-column-status");
+const RequestListColumnTransferred = require("./request-list-column-transferred");
+const RequestListColumnType = require("./request-list-column-type");
+const RequestListColumnWaterfall = require("./request-list-column-waterfall");
+const RequestListEmpty = createFactory(require("./request-list-empty"));
+const RequestListRow = createFactory(require("./request-list-row"));
const { div } = DOM;
+const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
/**
- * Request panel component
+ * Request list component
*/
-function RequestList({ isEmpty }) {
- return (
- div({ className: "request-list-container" },
- RequestListHeader(),
- isEmpty ? RequestListEmptyNotice() : RequestListContent(),
- )
- );
-}
+const RequestList = createClass({
+ displayName: "RequestList",
+
+ propTypes: {
+ cloneSelectedRequest: PropTypes.func.isRequired,
+ displayedRequests: PropTypes.object.isRequired,
+ firstRequestStartedMillis: PropTypes.number.isRequired,
+ isEmpty: PropTypes.bool.isEmpty,
+ openStatistics: PropTypes.func.isRequired,
+ onPerfClick: PropTypes.func.isRequired,
+ onReloadClick: PropTypes.func.isRequired,
+ resizeWaterfall: PropTypes.func.isRequired,
+ scale: PropTypes.func.isRequired,
+ selectedRowIndex: PropTypes.number,
+ selectDelta: PropTypes.func.isRequired,
+ selectDetailsPanelTab: PropTypes.func.isRequired,
+ selectRequestByIndex: PropTypes.func.isRequired,
+ sort: PropTypes.func.isRequired,
+ timingMarkers: PropTypes.object.isRequired,
+ waterfallWidth: PropTypes.func.isRequired,
+ },
+
+ getInitialState() {
+ return {
+ sortBy: "index",
+ sortDirection: SortDirection.ASC,
+ };
+ },
+
+ componentWillMount() {
+ const { cloneSelectedRequest, openStatistics } = this.props;
+ this.contextMenu = new RequestListContextMenu({
+ cloneSelectedRequest,
+ openStatistics,
+ });
+ this.tooltip = new HTMLTooltip(document, { type: "arrow" });
+ },
+
+ componentDidMount() {
+ // Set the CSS variables for waterfall scaling
+ this.setScalingStyles();
+
+ // Install event handler for displaying a tooltip
+ this.tooltip.startTogglingOnHover(this.refs.list, this.onHover, {
+ toggleDelay: 500,
+ interactive: true
+ });
+
+ this.shouldScrollBottom = true;
+ this.shouldScrollToSelectedIndex = false;
+ },
+
+ componentWillUpdate(nextProps) {
+ let { selectedRowIndex } = nextProps;
+ if (selectedRowIndex !== -1 && selectedRowIndex !== this.props.selectedRowIndex) {
+ this.shouldScrollToSelectedIndex = true;
+ }
+ },
+
+ componentDidUpdate(prevProps) {
+ this.setScalingStyles(prevProps);
+ },
+
+ componentWillUnmount() {
+ this.tooltip.stopTogglingOnHover();
+ },
+
+ getRowClassName({ index }) {
+ if (index < 0) {
+ return "requests-list-toolbar devtools-toolbar";
+ }
+ let className = ["request-list-item"];
+ if (this.props.selectedRowIndex === index) {
+ className.push("selected");
+ }
+ className.push(index % 2 === 0 ? "even" : "odd");
+ return className.join(" ");
+ },
+
+ noRowsRenderer() {
+ let { isEmpty, onPerfClick, onReloadClick } = this.props;
+ return isEmpty ? RequestListEmpty({ onPerfClick, onReloadClick }) : null;
+ },
+
+ onContextMenu(evt) {
+ evt.preventDefault();
+ this.contextMenu.open(evt);
+ },
+
+ onHover(target, tooltip) {
+ let itemEl = target.closest(".request-list-item");
+ if (!itemEl) {
+ return false;
+ }
+
+ let itemId = itemEl.dataset.id;
+ if (!itemId) {
+ return false;
+ }
+
+ let requestItem = this.props.displayedRequests.find(r => r.id === itemId);
+ if (!requestItem) {
+ return false;
+ }
+
+ if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
+ return setTooltipStackTraceContent(tooltip, requestItem);
+ }
+
+ return false;
+ },
+
+ onKeyDown(evt) {
+ let { displayedRequests } = this.props;
+ let delta;
+
+ switch (evt.key) {
+ case "ArrowUp":
+ case "ArrowLeft":
+ delta = -1;
+ break;
+ case "ArrowDown":
+ case "ArrowRight":
+ delta = +1;
+ break;
+ case "PageUp":
+ delta = -Math.ceil(displayedRequests.size / PAGE_SIZE_ITEM_COUNT_RATIO);
+ break;
+ case "PageDown":
+ delta = Math.ceil(displayedRequests.size / PAGE_SIZE_ITEM_COUNT_RATIO);
+ break;
+ case "Home":
+ delta = -Infinity;
+ break;
+ case "End":
+ delta = +Infinity;
+ break;
+ }
-RequestList.displayName = "RequestList";
+ if (delta) {
+ // Prevent scrolling when pressing navigation keys.
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.props.selectDelta(delta);
+ }
+ },
+
+ onRowClick({ index }) {
+ this.props.selectRequestByIndex(index);
+ },
+
+ onScroll({ clientHeight, scrollHeight, scrollTop }) {
+ this.shouldScrollToSelectedIndex = false;
+ this.shouldScrollBottom = clientHeight + scrollTop >= scrollHeight;
+ this.tooltip.hide();
+ },
+
+ /**
+ * Set the CSS variables for waterfall scaling. If React supported setting CSS
+ * variables as part of the "style" property of a DOM element, we would use that.
+ *
+ * However, React doesn't support this, so we need to use a hack and update the
+ * DOM element directly: https://github.com/facebook/react/issues/6411
+ */
+ setScalingStyles(prevProps) {
+ const { scale } = this.props;
+ if (prevProps && prevProps.scale === scale) {
+ return;
+ }
+
+ const { style } = this.refs.list;
+ style.removeProperty("--timings-scale");
+ style.removeProperty("--timings-rev-scale");
+ style.setProperty("--timings-scale", scale);
+ style.setProperty("--timings-rev-scale", 1 / scale);
+ },
+
+ sort({ sortBy, sortDirection }) {
+ this.props.sort(sortBy);
+ this.setState({ sortBy, sortDirection });
+ },
+
+ render() {
+ let {
+ displayedRequests,
+ firstRequestStartedMillis,
+ resizeWaterfall,
+ scale,
+ selectedRowIndex,
+ selectDetailsPanelTab,
+ timingMarkers,
+ waterfallWidth,
+ } = this.props;
+
+ let {
+ sortBy,
+ sortDirection,
+ } = this.state;
+
+ let scrollToIndex = -1;
+
+ if (this.shouldScrollToSelectedIndex) {
+ scrollToIndex = selectedRowIndex;
+ } else if (this.shouldScrollBottom) {
+ scrollToIndex = displayedRequests.size - 1;
+ }
-RequestList.propTypes = {
- isEmpty: PropTypes.bool.isRequired,
-};
+ return (
+ div({
+ className: "requests-list-container",
+ onContextMenu: this.onContextMenu,
+ onKeyDown: this.onKeyDown,
+ ref: "list",
+ },
+ AutoSizer({},
+ ({ width, height }) => (
+ Table({
+ headerHeight: 24,
+ noRowsRenderer: this.noRowsRenderer,
+ onRowClick: this.onRowClick,
+ onScroll: this.onScroll,
+ overscanRowCount: 100,
+ rowClassName: this.getRowClassName,
+ rowCount: displayedRequests.size,
+ rowGetter: ({ index }) => displayedRequests.get(index),
+ rowHeight: 22,
+ rowRenderer: RequestListRow,
+ scrollToIndex,
+ sort: this.sort,
+ sortBy,
+ sortDirection,
+ width,
+ height,
+ },
+ RequestListColumnStatus(),
+ RequestListColumnMethod(),
+ RequestListColumnFile(),
+ RequestListColumnDomain({ selectDetailsPanelTab }),
+ RequestListColumnCause(),
+ RequestListColumnType(),
+ RequestListColumnTransferred(),
+ RequestListColumnSize(),
+ RequestListColumnWaterfall({
+ firstRequestStartedMillis,
+ scale,
+ resizeWaterfall,
+ timingMarkers,
+ waterfallWidth,
+ }),
+ )
+ )
+ )
+ )
+ );
+ }
+});
-module.exports = RequestList;
+module.exports = connect(
+ (state) => ({
+ displayedRequests: getDisplayedRequests(state),
+ firstRequestStartedMillis: state.requests.firstStartedMillis,
+ isEmpty: state.requests.requests.isEmpty(),
+ scale: getWaterfallScale(state),
+ selectedRowIndex: getSortedRequests(state)
+ .findIndex(r => r.id === state.requests.selectedId),
+ timingMarkers: state.timingMarkers,
+ waterfallWidth: state.ui.waterfallWidth,
+ }),
+ (dispatch) => ({
+ cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+ onReloadClick: () =>
+ window.NetMonitorController
+ .triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT),
+ onPerfClick: () => dispatch(Actions.openStatistics(true)),
+ openStatistics: (open) => dispatch(Actions.openStatistics(open)),
+ resizeWaterfall: (width) => dispatch(Actions.resizeWaterfall(width)),
+ sort: (type) => dispatch(Actions.sortBy(type)),
+ selectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
+ selectDetailsPanelTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
+ selectRequestByIndex: (index) => dispatch(Actions.selectRequestByIndex(index)),
+ }),
+)(RequestList);
--- a/devtools/client/netmonitor/reducers/ui.js
+++ b/devtools/client/netmonitor/reducers/ui.js
@@ -6,18 +6,18 @@
const I = require("devtools/client/shared/vendor/immutable");
const {
CLEAR_REQUESTS,
OPEN_NETWORK_DETAILS,
OPEN_STATISTICS,
REMOVE_SELECTED_CUSTOM_REQUEST,
SELECT_DETAILS_PANEL_TAB,
+ SELECT_REQUEST,
SEND_CUSTOM_REQUEST,
- SELECT_REQUEST,
WATERFALL_RESIZE,
} = require("../constants");
const UI = I.Record({
detailsPanelSelectedTab: "headers",
networkDetailsOpen: false,
statisticsOpen: false,
waterfallWidth: null,
--- a/devtools/client/netmonitor/request-list-tooltip.js
+++ b/devtools/client/netmonitor/request-list-tooltip.js
@@ -1,45 +1,21 @@
/* 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 {
- setImageTooltip,
- getImageDimensions,
-} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
const { WEBCONSOLE_L10N } = require("./utils/l10n");
-const { formDataURI } = require("./utils/request-utils");
// px
-const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
-// px
const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
const HTML_NS = "http://www.w3.org/1999/xhtml";
-async function setTooltipImageContent(tooltip, itemEl, requestItem) {
- let { mimeType, text, encoding } = requestItem.responseContent.content;
-
- if (!mimeType || !mimeType.includes("image/")) {
- return false;
- }
-
- let string = await window.gNetwork.getString(text);
- let src = formDataURI(mimeType, encoding, string);
- let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
- let { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
- let options = { maxDim, naturalWidth, naturalHeight };
- setImageTooltip(tooltip, tooltip.doc, src, options);
-
- return itemEl.querySelector(".requests-list-icon");
-}
-
async function setTooltipStackTraceContent(tooltip, requestItem) {
let {stacktrace} = requestItem.cause;
if (!stacktrace || stacktrace.length == 0) {
return false;
}
let doc = tooltip.doc;
@@ -96,11 +72,10 @@ async function setTooltipStackTraceConte
}
tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
return true;
}
module.exports = {
- setTooltipImageContent,
setTooltipStackTraceContent,
};
--- a/devtools/client/netmonitor/utils/request-utils.js
+++ b/devtools/client/netmonitor/utils/request-utils.js
@@ -217,23 +217,31 @@ function parseQueryString(query) {
let param = e.split("=");
return {
name: param[0] ? decodeUnicodeUrl(param[0]) : "",
value: param[1] ? decodeUnicodeUrl(param[1]) : "",
};
});
}
+/**
+ * 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,
formDataURI,
writeHeaderText,
decodeUnicodeUrl,
getAbbreviatedMimeType,
getUrlBaseName,
getUrlQuery,
getUrlBaseNameWithQuery,
getUrlHostName,
getUrlHost,
getUrlDetails,
parseQueryString,
+ propertiesEqual,
};
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -6,43 +6,16 @@
@import "resource://devtools/client/shared/components/tree/tree-view.css";
@import "resource://devtools/client/shared/components/tabs/tabs.css";
@import "resource://devtools/client/shared/components/tabs/tabbar.css";
* {
box-sizing: border-box;
}
-.toolbar-labels {
- overflow: hidden;
- display: flex;
- flex: auto;
-}
-
-.devtools-toolbar-container {
- display: flex;
- justify-content: space-between;
-}
-
-.devtools-toolbar-group {
- display: flex;
- flex: 0 0 auto;
- flex-wrap: nowrap;
- align-items: center;
-}
-
-#response-content-image-box {
- overflow: auto;
-}
-
-.cropped-textbox .textbox-input {
- /* workaround for textbox not supporting the @crop attribute */
- text-overflow: ellipsis;
-}
-
:root.theme-dark {
--table-splitter-color: rgba(255,255,255,0.15);
--table-zebra-background: rgba(255,255,255,0.05);
--timing-blocked-color: rgba(235, 83, 104, 0.8);
--timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
--timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
--timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
@@ -78,246 +51,290 @@
--timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
--timing-wait-color: rgba(94, 136, 176, 0.8); /* blue grey */
--timing-receive-color: rgba(112, 191, 83, 0.8); /* green */
--sort-ascending-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
--sort-descending-image: url(chrome://devtools/skin/images/firebug/arrow-down.svg);
}
-.request-list-container {
+.cropped-textbox .textbox-input {
+ /* workaround for textbox not supporting the @crop attribute */
+ text-overflow: ellipsis;
+}
+
+/* Toolbar */
+
+.devtools-toolbar-container {
display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
+ justify-content: space-between;
}
+.devtools-toolbar-group {
+ display: flex;
+ flex: 0 0 auto;
+ flex-wrap: nowrap;
+ align-items: center;
+}
+
+.theme-firebug .devtools-toolbar {
+ line-height: initial;
+}
+
+/* Empty requests list */
+
.request-list-empty-notice {
margin: 0;
padding: 12px;
font-size: 120%;
}
-.notice-perf-message {
- margin-top: 2px;
-}
-
.requests-list-perf-notice-button {
min-width: 30px;
- min-height: 26px;
margin: 0 5px;
vertical-align: middle;
}
.requests-list-perf-notice-button::before {
background-image: url(images/profiler-stopwatch.svg);
}
.requests-list-reload-notice-button {
font-size: inherit;
min-height: 26px;
margin: 0 5px;
}
-/* Network requests table */
+/* Requests list toolbar */
.requests-list-toolbar {
display: flex;
padding: 0;
}
.requests-list-filter-buttons {
display: flex;
flex-wrap: nowrap;
}
.theme-firebug .requests-list-toolbar {
height: 19px !important;
}
-.requests-list-contents {
- display: flex;
- flex-direction: column;
- overflow-x: hidden;
- overflow-y: auto;
- --timings-scale: 1;
- --timings-rev-scale: 1;
-}
-
-.requests-list-subitem {
- display: flex;
- flex: none;
- box-sizing: border-box;
- align-items: center;
- padding: 3px;
- cursor: default;
-}
-
-.subitem-label {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.requests-list-header {
- display: flex;
- flex: none;
-}
-
-.requests-list-header-button {
- display: flex;
- align-items: center;
- flex: auto;
- -moz-appearance: none;
+.requests-list-toolbar .requests-list-header-button {
background-color: transparent;
border-image: linear-gradient(transparent 15%,
var(--theme-splitter-color) 15%,
var(--theme-splitter-color) 85%,
transparent 85%) 1 1;
border-style: solid;
border-width: 0;
border-inline-start-width: 1px;
- min-width: 1px;
- min-height: 24px;
- margin: 0;
- padding-top: 2px;
- padding-bottom: 2px;
- padding-inline-start: 16px;
- padding-inline-end: 0;
text-align: center;
- color: inherit;
- font-weight: inherit !important;
+ height: 100%;
}
-.requests-list-header-button::-moz-focus-inner {
- border: 0;
- padding: 0;
+/* Requests list */
+
+.requests-list-container {
+ flex: 1 1 auto;
+ overflow: hidden;
}
-.requests-list-header:first-child .requests-list-header-button {
+/* Requests list headers */
+
+.requests-list-header-button.requests-list-status {
border-width: 0;
}
.requests-list-header-button:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.requests-list-header-button > .button-text {
- flex: auto;
+ display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ cursor: default;
+ max-width: 100%;
}
.requests-list-header-button > .button-icon {
- flex: none;
- height: 4px;
- margin-inline-start: 3px;
- margin-inline-end: 6px;
- width: 7px;
-}
-
-.requests-list-header-button[data-sorted=ascending] > .button-icon {
- background-image: var(--sort-ascending-image);
-}
-
-.requests-list-header-button[data-sorted=descending] > .button-icon {
- background-image: var(--sort-descending-image);
+ display: inline-block;
+ height: 100%;
+ width: 0px;
}
-.requests-list-waterfall-label-wrapper {
- display: flex;
+.requests-list-header-button.ascending > .button-icon {
+ background: var(--sort-ascending-image) no-repeat center;
+ width: 7px;
+ margin-inline-start: 3px;
+ margin-inline-end: 6px;
}
-.requests-list-header-button[data-sorted],
-.requests-list-header-button[data-sorted]:hover {
- background-color: var(--theme-selection-background);
- color: var(--theme-selection-color);
+.requests-list-header-button.descending > .button-icon {
+ background: var(--sort-descending-image) no-repeat center;
+ width: 7px;
+ margin-inline-start: 3px;
+ margin-inline-end: 6px;
}
-.requests-list-header-button[data-sorted],
-.requests-list-header[data-active] + .requests-list-header .requests-list-header-button {
- border-image: linear-gradient(var(--theme-splitter-color), var(--theme-splitter-color)) 1 1;
-}
-
-/* Firebug theme support for Network panel header */
-
.theme-firebug .requests-list-header {
padding: 0 !important;
font-weight: bold;
background: linear-gradient(rgba(255, 255, 255, 0.05),
rgba(0, 0, 0, 0.05)),
#C8D2DC;
}
-.theme-firebug .requests-list-header-button {
- min-height: 17px;
-}
-
-.theme-firebug .requests-list-header-button > .button-icon {
- height: 7px;
-}
-
-.theme-firebug .requests-list-header-button[data-sorted] {
- background-color: #AAC3DC;
-}
-
-:root[platform="linux"].theme-firebug .requests-list-header-button[data-sorted] {
- background-color: #FAC8AF !important;
- color: inherit !important;
-}
-
.theme-firebug .requests-list-header:hover:active {
background-image: linear-gradient(rgba(0, 0, 0, 0.1),
transparent);
}
+/* Request list row and cell */
-/* Network requests table: specific column dimensions */
+.request-list-item {
+ display: flex;
+}
+
+.request-list-item.selected {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
-.requests-list-status {
- max-width: 6em;
- text-align: center;
- width: 10vw;
+.request-list-item.fromCache > .requests-list-subitem:not(.requests-list-waterfall) {
+ opacity: 0.6;
+}
+
+.request-list-item:not(.selected).odd {
+ background-color: var(--table-zebra-background);
+}
+
+.request-list-item:not(.selected):hover {
+ background-color: var(--theme-selection-background-semitransparent);
+}
+
+.theme-firebug .request-list-item:not(.selected):hover {
+ background: #EFEFEF;
}
-.requests-list-method,
-.requests-list-method-box {
- max-width: 7em;
+.requests-list-subitem {
text-align: center;
- width: 10vw;
+ padding: 0 3px;
+ cursor: default;
+ margin: auto;
+ white-space: nowrap;
+}
+
+.theme-firebug .requests-list-subitem {
+ padding: 1px;
+}
+
+.theme-firebug .requests-list-status.requests-list-subitem {
+ font-weight: bold;
+}
+
+.subitem-label {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+/* Status column */
+
+.requests-list-status-icon {
+ display: inline-block;
+ height: 10px;
+ width: 10px;
+ margin-inline-start: 5px;
+ margin-inline-end: 5px;
+ border-radius: 10px;
+}
+
+.requests-list-status-icon:not([data-code]) {
+ background-color: var(--theme-content-color2);
}
-.requests-list-icon-and-file {
- width: 22vw;
+.requests-list-status-icon[data-code="cached"] {
+ border: 2px solid var(--theme-content-color2);
+ background-color: transparent;
+}
+
+.requests-list-status-icon[data-code^="1"] {
+ background-color: var(--theme-highlight-blue);
+}
+
+.requests-list-status-icon[data-code^="2"] {
+ background-color: var(--theme-highlight-green);
}
-.requests-list-icon {
- background: transparent;
- width: 15px;
- height: 15px;
- margin-inline-end: 4px;
+.requests-list-status-icon[data-code^="3"] {
+ background-color: transparent;
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 10px solid var(--theme-highlight-lightorange);
+ border-radius: 0;
+}
+
+.requests-list-status-icon[data-code^="4"] {
+ background-color: var(--theme-highlight-red);
+ border-radius: 0;
}
-.requests-list-icon {
- outline: 1px solid var(--table-splitter-color);
+.requests-list-status-icon[data-code^="5"] {
+ background-color: var(--theme-highlight-pink);
+ border-radius: 0;
+ transform: rotate(45deg);
+}
+
+.requests-list-status-code {
+ display: inline-block;
+ min-width: 20px;
+ vertical-align: sub;
+}
+
+/* Method column */
+
+.theme-firebug .requests-list-method.requests-list-subitem {
+ color: rgb(128, 128, 128);
}
-.requests-list-security-and-domain {
- width: 14vw;
+/* File column */
+
+.requests-list-file {
+ text-align: left;
}
-.requests-security-state-icon {
- flex: none;
+/* Domain column */
+
+.requests-list-domain {
+ text-align: left;
+}
+
+.requests-list-domain-url {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.requests-list-domain-icon {
+ display: inline-block;
width: 16px;
height: 16px;
margin-inline-end: 4px;
+ background-repeat: no-repeat;
+ vertical-align: middle;
}
-.request-list-item.selected .requests-security-state-icon {
+.request-list-item.selected .requests-list-domain-icon {
filter: brightness(1.3);
}
.security-state-insecure {
background-image: url(chrome://devtools/skin/images/security-state-insecure.svg);
}
.security-state-secure {
@@ -331,131 +348,63 @@
.security-state-broken {
background-image: url(chrome://devtools/skin/images/security-state-broken.svg);
}
.security-state-local {
background-image: url(chrome://devtools/skin/images/globe.svg);
}
-.requests-list-type,
-.requests-list-size {
- max-width: 6em;
- width: 8vw;
- justify-content: center;
-}
-
-.requests-list-transferred {
- max-width: 8em;
- width: 8vw;
- justify-content: center;
-}
+/* Cause column */
.requests-list-cause {
- max-width: 8em;
- width: 8vw;
+ text-align: left;
}
.requests-list-cause-stack {
+ display: inline-block;
background-color: var(--theme-body-color-alt);
color: var(--theme-body-background);
font-size: 8px;
font-weight: bold;
- line-height: 10px;
border-radius: 3px;
padding: 0 2px;
- margin: 0;
margin-inline-end: 3px;
- -moz-user-select: none;
-}
-
-.request-list-item.selected .requests-list-transferred.theme-comment {
- color: var(--theme-selection-color);
}
-/* Network requests table: status codes */
-
-.requests-list-status-code {
- margin-inline-start: 3px !important;
- width: 3em;
- margin-inline-end: -3em !important;
-}
+/* Waterfall column */
-.requests-list-status-icon {
- background: #fff;
- height: 10px;
- width: 10px;
- margin-inline-start: 5px;
- margin-inline-end: 5px;
- border-radius: 10px;
- transition: box-shadow 0.5s ease-in-out;
- box-sizing: border-box;
-}
-
-.request-list-item.selected .requests-list-status-icon {
- filter: brightness(1.3);
-}
-
-.requests-list-status-icon:not([data-code]) {
- background-color: var(--theme-content-color2);
+.requests-list-waterfall {
+ display: flex;
+ width: 100%;
+ overflow: hidden;
}
-.requests-list-status-icon[data-code="cached"] {
- border: 2px solid var(--theme-content-color2);
- background-color: transparent;
-}
-
-.requests-list-status-icon[data-code^="1"] {
- background-color: var(--theme-highlight-blue);
-}
-
-.requests-list-status-icon[data-code^="2"] {
- background-color: var(--theme-highlight-green);
+.requests-list-timings-box {
+ display: inline-block;
+ height: 9px;
}
-/* 3xx are triangles */
-.requests-list-status-icon[data-code^="3"] {
- background-color: transparent;
- width: 0;
- height: 0;
- border-left: 5px solid transparent;
- border-right: 5px solid transparent;
- border-bottom: 10px solid var(--theme-highlight-lightorange);
- border-radius: 0;
+.theme-firebug .requests-list-timings-box {
+ background-image: linear-gradient(rgba(255, 255, 255, 0.3), rgba(0, 0, 0, 0.2));
+ height: 16px;
}
-/* 4xx and 5xx are squares - error codes */
-.requests-list-status-icon[data-code^="4"] {
- background-color: var(--theme-highlight-red);
- border-radius: 0; /* squares */
-}
-
-.requests-list-status-icon[data-code^="5"] {
- background-color: var(--theme-highlight-pink);
- border-radius: 0;
- transform: rotate(45deg);
-}
-
-/* Network requests table: waterfall header */
-
-.requests-list-waterfall {
- flex: auto;
- padding-inline-start: 0;
-}
-
-.requests-list-waterfall-label-wrapper:not(.requests-list-waterfall-visible) {
- padding-inline-start: 16px;
+.requests-list-timings {
+ display: flex;
+ flex: none;
+ align-items: center;
+ transform: scaleX(var(--timings-scale));
}
.requests-list-timings-division {
padding-top: 2px;
padding-inline-start: 4px;
font-size: 75%;
pointer-events: none;
- box-sizing: border-box;
text-align: start;
/* Allow the timing label to shrink if the container gets too narrow.
* The container width then is not limited by the content. */
flex: initial;
}
.requests-list-timings-division:not(:first-child) {
border-inline-start: 1px dashed;
@@ -477,39 +426,16 @@
border-inline-start-color: #585959 !important;
}
.requests-list-timings-division[data-division-scale=second],
.requests-list-timings-division[data-division-scale=minute] {
font-weight: 600;
}
-/* Network requests table: waterfall items */
-
-.requests-list-subitem.requests-list-waterfall {
- padding-inline-start: 0;
- padding-inline-end: 4px;
- /* Background created on a <canvas> in js. */
- /* @see devtools/client/netmonitor/netmonitor-view.js */
- background-image: -moz-element(#waterfall-background);
- background-repeat: repeat-y;
- background-position: left center;
-}
-
-.requests-list-subitem.requests-list-waterfall:-moz-locale-dir(rtl) {
- background-position: right center;
-}
-
-.requests-list-timings {
- display: flex;
- flex: none;
- align-items: center;
- transform: scaleX(var(--timings-scale));
-}
-
.requests-list-timings:-moz-locale-dir(ltr) {
transform-origin: left center;
}
.requests-list-timings:-moz-locale-dir(rtl) {
transform-origin: right center;
}
@@ -526,26 +452,16 @@
padding-inline-start: 4px;
font-size: 85%;
font-weight: 600;
white-space: nowrap;
/* This node should not be scaled - apply a reversed transformation */
transform: scaleX(var(--timings-rev-scale));
}
-.requests-list-timings-box {
- display: inline-block;
- height: 9px;
-}
-
-.theme-firebug .requests-list-timings-box {
- background-image: linear-gradient(rgba(255, 255, 255, 0.3), rgba(0, 0, 0, 0.2));
- height: 16px;
-}
-
.requests-list-timings-box.blocked {
background-color: var(--timing-blocked-color);
}
.requests-list-timings-box.dns {
background-color: var(--timing-dns-color);
}
@@ -560,98 +476,33 @@
.requests-list-timings-box.wait {
background-color: var(--timing-wait-color);
}
.requests-list-timings-box.receive {
background-color: var(--timing-receive-color);
}
-/* SideMenuWidget */
-#network-table .request-list-empty-notice,
-#network-table .request-list-container {
- background-color: var(--theme-body-background);
-}
-
-.request-list-item {
- display: flex;
- border-top-color: transparent;
- border-bottom-color: transparent;
- padding: 0;
-}
-
-.request-list-item.selected {
- background-color: var(--theme-selection-background);
- color: var(--theme-selection-color);
-}
-
-.request-list-item:not(.selected).odd {
- background-color: var(--table-zebra-background);
-}
-
-.request-list-item:not(.selected):hover {
- background-color: var(--theme-selection-background-semitransparent);
-}
-
-.request-list-item.fromCache > .requests-list-subitem:not(.requests-list-waterfall) {
- opacity: 0.6;
-}
-
-.theme-firebug .request-list-item:not(.selected):hover {
- background: #EFEFEF;
-}
-
-.theme-firebug .requests-list-subitem {
- padding: 1px;
-}
-
-/* HTTP Status Column */
-.theme-firebug .requests-list-subitem.requests-list-status {
- font-weight: bold;
-}
-
-/* Method Column */
-
-.theme-firebug .requests-list-subitem.requests-list-method-box {
- color: rgb(128, 128, 128);
-}
-
-.request-list-item.selected .requests-list-method {
- color: var(--theme-selection-color);
-}
-
-/* Size Column */
-.theme-firebug .requests-list-subitem.requests-list-size {
- justify-content: end;
- padding-inline-end: 4px;
-}
-
/* Network details panel */
.network-details-panel-toggle[disabled] {
display: none;
}
.network-details-panel-toggle:-moz-locale-dir(ltr)::before,
.network-details-panel-toggle.pane-collapsed:-moz-locale-dir(rtl)::before {
background-image: var(--theme-pane-collapse-image);
}
.network-details-panel-toggle.pane-collapsed:-moz-locale-dir(ltr)::before,
.network-details-panel-toggle:-moz-locale-dir(rtl)::before {
background-image: var(--theme-pane-expand-image);
}
-/* Network request details tabpanels */
-
-.theme-firebug .variables-view-scope:focus > .title {
- color: var(--theme-body-color);
-}
-
-/* Summary tabpanel */
+/* Summary panel */
.tabpanel-summary-container {
padding: 1px;
}
.tabpanel-summary-label {
display: inline-block;
padding-inline-start: 4px;
@@ -663,17 +514,17 @@
color: inherit;
padding-inline-start: 3px;
}
.theme-dark .tabpanel-summary-value {
color: var(--theme-selection-color);
}
-/* Headers tabpanel */
+/* Headers panel */
.headers-overview {
background: var(--theme-toolbar-background);
}
.headers-summary .status-text {
width: auto!important;
}
@@ -685,17 +536,17 @@
white-space: nowrap;
flex-grow: 1;
}
.headers-summary .learn-more-link:hover {
text-decoration: underline;
}
-/* Response tabpanel */
+/* Response panel */
.response-error-header {
margin: 0;
padding: 3px 8px;
background-color: var(--theme-highlight-red);
color: var(--theme-selection-color);
}
@@ -711,17 +562,17 @@
.response-image {
background: #fff;
border: 1px dashed GrayText;
margin-bottom: 10px;
max-width: 300px;
max-height: 100px;
}
-/* Timings tabpanel */
+/* Timings panel */
.timings-container {
display: flex;
}
.timings-label {
width: 10em;
}
@@ -741,17 +592,17 @@
min-width: 1px;
transition: width 0.2s ease-out;
}
.theme-firebug .requests-list-timings-total {
color: var(--theme-body-color);
}
-/* Security tabpanel */
+/* Security panel */
/* Overwrite tree-view cell colon `:` for security panel and tree section */
.security-panel .treeTable .treeLabelCell::after,
.treeTable .tree-section .treeLabelCell::after {
content: "";
}
/* Layout additional warning icon in tree value cell */
@@ -769,17 +620,17 @@
}
@media (min-resolution: 1.1dppx) {
.security-warning-icon {
background-image: url(images/alerticon-warning@2x.png);
}
}
-/* Custom request view */
+/* Custom request panel */
.custom-request-panel {
height: 100%;
overflow: auto;
padding: 0 4px;
background-color: var(--theme-sidebar-background);
}
@@ -818,17 +669,17 @@
width: 4.5em;
}
.custom-url-value {
flex-grow: 1;
margin-inline-start: 6px;
}
-/* Performance analysis buttons */
+/* Statistics summary button */
.requests-list-network-summary-button {
display: flex;
flex-wrap: nowrap;
align-items: center;
background: none;
box-shadow: none;
border-color: transparent;
@@ -851,17 +702,17 @@
margin-inline-start: 0.5em;
}
.requests-list-network-summary-button:hover > .summary-info-icon,
.requests-list-network-summary-button:hover > .summary-info-text {
opacity: 1;
}
-/* Performance analysis view */
+/* Statistics panel */
.statistics-panel {
display: flex;
height: 100vh;
}
.statistics-panel .devtools-toolbarbutton.back-button {
min-width: 4em;
@@ -1023,103 +874,48 @@
.theme-firebug .chart-colored-blob[name=flash] {
fill: rgba(84, 235, 159, 0.8); /* cyan */
background: rgba(84, 235, 159, 0.8);
}
/* Responsive sidebar */
@media (max-width: 700px) {
- #toolbar-spacer,
.network-details-panel-toggle,
- .requests-list-network-summary-button > .summary-info-text {
- display: none;
- }
-
- .requests-list-toolbar {
- height: 22px;
- }
-
- .requests-list-header-button {
- min-height: 22px;
- padding-left: 8px;
- }
-
- .requests-list-status {
- max-width: none;
- width: 10vw;
- }
-
- .requests-list-status-code {
- width: auto;
- }
-
- .requests-list-method,
- .requests-list-method-box {
- max-width: none;
- width: 12vw;
- }
-
- .requests-list-icon-and-file {
- width: 22vw;
- }
-
- .requests-list-security-and-domain {
- width: 16vw;
- }
-
- .requests-list-cause,
- .requests-list-type,
- .requests-list-transferred,
- .requests-list-size {
- max-width: none;
- width: 10vw;
- }
-
+ .requests-list-network-summary-button > .summary-info-text,
.requests-list-waterfall {
display: none;
}
.statistics-panel .charts-container {
flex-direction: column;
/* Minus 4em for statistics back button width */
width: calc(100% - 4em);
}
.statistics-panel .splitter {
width: 100%;
height: 1px;
}
+
+ :root[platform="linux"] .requests-list-header-button {
+ font-size: 85%;
+ }
}
/* Platform overrides (copied in from the old platform specific files) */
:root[platform="win"] .requests-list-header-button > .button-box {
padding: 0;
}
:root[platform="win"] .requests-list-timings-division {
padding-top: 1px;
font-size: 90%;
}
-:root[platform="linux"] #headers-summary-resend {
- padding: 4px;
-}
-
-:root[platform="linux"] #toggle-raw-headers {
- padding: 4px;
-}
-
-/* Responsive sidebar */
-@media (max-width: 700px) {
- :root[platform="linux"] .requests-list-header-button {
- font-size: 85%;
- }
-}
-
.textbox-input {
text-overflow: ellipsis;
border: none;
background: none;
color: inherit;
width: 100%;
}
@@ -1214,17 +1010,17 @@
}
.tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel,
.tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel:hover {
color: var(--theme-body-color-alt);
}
.tree-container .treeTable .treeValueCell {
- /* FIXME: Make value cell can be reduced to shorter width */
+ /* Make value cell can be reduced to shorter width */
max-width: 0;
padding-inline-end: 5px;
}
.headers-summary input:not([type="button"]) {
width: 100%;
background: none;
border: none;
@@ -1267,17 +1063,16 @@
padding: 0 4px;
}
.headers-summary .raw-headers textarea {
width: 100%;
height: 50vh;
font: message-box;
resize: none;
- box-sizing: border-box;
}
.headers-summary .raw-headers .tabpanel-summary-label {
padding: 0 0 4px 0;
}
.empty-notice {
color: var(--theme-body-color-alt);