new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/App.js
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+// Components
+const MonitorPanel = createFactory(require("./MonitorPanel"));
+const StatisticsPanel = createFactory(require("./StatisticsPanel"));
+
+const { div } = DOM;
+
+/*
+ * App component
+ * The top level component for representing main panel
+ */
+function App({ statisticsOpen }) {
+ return (
+ div({ className: "network-monitor" },
+ !statisticsOpen ? MonitorPanel() : StatisticsPanel()
+ )
+ );
+}
+
+App.displayName = "App";
+
+App.propTypes = {
+ statisticsOpen: PropTypes.bool.isRequired,
+};
+
+module.exports = connect(
+ (state) => ({ statisticsOpen: state.ui.statisticsOpen }),
+)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/CookiesPanel.js
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+
+// Component
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const { div } = DOM;
+
+const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText");
+const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText");
+const REQUEST_COOKIES = L10N.getStr("requestCookies");
+const RESPONSE_COOKIES = L10N.getStr("responseCookies");
+const SECTION_NAMES = [
+ RESPONSE_COOKIES,
+ REQUEST_COOKIES,
+];
+
+/*
+ * Cookies panel component
+ * This tab lists full details of any cookies sent with the request or response
+ */
+function CookiesPanel({
+ request,
+}) {
+ let {
+ requestCookies = { cookies: [] },
+ responseCookies = { cookies: [] },
+ } = request;
+
+ requestCookies = requestCookies.cookies || requestCookies;
+ responseCookies = responseCookies.cookies || responseCookies;
+
+ if (!requestCookies.length && !responseCookies.length) {
+ return div({ className: "empty-notice" },
+ COOKIES_EMPTY_TEXT
+ );
+ }
+
+ let object = {};
+
+ if (responseCookies.length) {
+ object[RESPONSE_COOKIES] = getProperties(responseCookies);
+ }
+
+ if (requestCookies.length) {
+ object[REQUEST_COOKIES] = getProperties(requestCookies);
+ }
+
+ return (
+ div({ className: "panel-container" },
+ PropertiesView({
+ object,
+ filterPlaceHolder: COOKIES_FILTER_TEXT,
+ sectionNames: SECTION_NAMES,
+ })
+ )
+ );
+}
+
+CookiesPanel.displayName = "CookiesPanel";
+
+CookiesPanel.propTypes = {
+ request: PropTypes.object.isRequired,
+};
+
+/**
+ * Mapping array to dict for TreeView usage.
+ * Since TreeView only support Object(dict) format.
+ *
+ * @param {Object[]} arr - key-value pair array like cookies or params
+ * @returns {Object}
+ */
+function getProperties(arr) {
+ return arr.reduce((map, obj) => {
+ // Generally cookies object contains only name and value properties and can
+ // be rendered as name: value pair.
+ // When there are more properties in cookies object such as extra or path,
+ // We will pass the object to display these extra information
+ if (Object.keys(obj).length > 2) {
+ map[obj.name] = Object.assign({}, obj);
+ delete map[obj.name].name;
+ } else {
+ map[obj.name] = obj.value;
+ }
+ return map;
+ }, {});
+}
+
+module.exports = CookiesPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/CustomRequestPanel.js
@@ -0,0 +1,257 @@
+/* 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 { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../utils/l10n");
+const Actions = require("../actions/index");
+const { getSelectedRequest } = require("../selectors/index");
+const {
+ getUrlQuery,
+ parseQueryString,
+ writeHeaderText,
+} = require("../utils/request-utils");
+
+const {
+ button,
+ div,
+ input,
+ textarea,
+} = DOM;
+
+const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
+const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
+const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
+const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
+const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
+const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
+
+function CustomRequestPanel({
+ removeSelectedCustomRequest,
+ request = {},
+ sendCustomRequest,
+ updateRequest,
+}) {
+ let {
+ method,
+ customQueryValue,
+ requestHeaders,
+ requestPostData,
+ url,
+ } = request;
+
+ let headers = "";
+ if (requestHeaders) {
+ headers = requestHeaders.customHeadersValue ?
+ requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
+ }
+ let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
+ let params = customQueryValue;
+ if (!params) {
+ params = queryArray ?
+ queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
+ }
+ let postData = requestPostData && requestPostData.postData.text ?
+ requestPostData.postData.text : "";
+
+ return (
+ div({ className: "custom-request-panel" },
+ div({ className: "tabpanel-summary-container custom-request" },
+ div({ className: "custom-request-label custom-header" },
+ CUSTOM_NEW_REQUEST
+ ),
+ button({
+ className: "devtools-button",
+ id: "custom-request-send-button",
+ onClick: sendCustomRequest,
+ },
+ CUSTOM_SEND
+ ),
+ button({
+ className: "devtools-button",
+ id: "custom-request-close-button",
+ onClick: removeSelectedCustomRequest,
+ },
+ CUSTOM_CANCEL
+ ),
+ ),
+ div({
+ className: "tabpanel-summary-container custom-method-and-url",
+ id: "custom-method-and-url",
+ },
+ input({
+ className: "custom-method-value",
+ id: "custom-method-value",
+ onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+ value: method || "GET",
+ }),
+ input({
+ className: "custom-url-value",
+ id: "custom-url-value",
+ onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+ value: url || "http://",
+ }),
+ ),
+ // Hide query field when there is no params
+ params ? div({
+ className: "tabpanel-summary-container custom-section",
+ id: "custom-query",
+ },
+ div({ className: "custom-request-label" }, CUSTOM_QUERY),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-query-value",
+ onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+ rows: 4,
+ value: params,
+ wrap: "off",
+ })
+ ) : null,
+ div({
+ id: "custom-headers",
+ className: "tabpanel-summary-container custom-section",
+ },
+ div({ className: "custom-request-label" }, CUSTOM_HEADERS),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-headers-value",
+ onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+ rows: 8,
+ value: headers,
+ wrap: "off",
+ })
+ ),
+ div({
+ id: "custom-postdata",
+ className: "tabpanel-summary-container custom-section",
+ },
+ div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-postdata-value",
+ onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+ rows: 6,
+ value: postData,
+ wrap: "off",
+ })
+ ),
+ )
+ );
+}
+
+CustomRequestPanel.displayName = "CustomRequestPanel";
+
+CustomRequestPanel.propTypes = {
+ removeSelectedCustomRequest: PropTypes.func.isRequired,
+ request: PropTypes.object,
+ sendCustomRequest: PropTypes.func.isRequired,
+ updateRequest: PropTypes.func.isRequired,
+};
+
+/**
+ * Parse a text representation of a name[divider]value list with
+ * the given name regex and divider character.
+ *
+ * @param {string} text - Text of list
+ * @return {array} array of headers info {name, value}
+ */
+function parseRequestText(text, namereg, divider) {
+ let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
+ let pairs = [];
+
+ for (let line of text.split("\n")) {
+ let matches = regex.exec(line);
+ if (matches) {
+ let [, name, value] = matches;
+ pairs.push({ name, value });
+ }
+ }
+ return pairs;
+}
+
+/**
+ * Update Custom Request Fields
+ *
+ * @param {Object} evt click event
+ * @param {Object} request current request
+ * @param {updateRequest} updateRequest action
+ */
+function updateCustomRequestFields(evt, request, updateRequest) {
+ const val = evt.target.value;
+ let data;
+ switch (evt.target.id) {
+ case "custom-headers-value":
+ let customHeadersValue = val || "";
+ // Parse text representation of multiple HTTP headers
+ let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
+ // Remove temp customHeadersValue while query string is parsable
+ if (customHeadersValue === "" ||
+ headersArray.length === customHeadersValue.split("\n").length) {
+ customHeadersValue = null;
+ }
+ data = {
+ requestHeaders: {
+ customHeadersValue,
+ headers: headersArray,
+ },
+ };
+ break;
+ case "custom-method-value":
+ data = { method: val.trim() };
+ break;
+ case "custom-postdata-value":
+ data = {
+ requestPostData: {
+ postData: { text: val },
+ }
+ };
+ break;
+ case "custom-query-value":
+ let customQueryValue = val || "";
+ // Parse readable text list of a query string
+ let queryArray = customQueryValue ?
+ parseRequestText(customQueryValue, ".+?", "=") : [];
+ // Write out a list of query params into a query string
+ let queryString = queryArray.map(
+ ({ name, value }) => name + "=" + value).join("&");
+ let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
+ request.url.split("?")[0];
+ // Remove temp customQueryValue while query string is parsable
+ if (customQueryValue === "" ||
+ queryArray.length === customQueryValue.split("\n").length) {
+ customQueryValue = null;
+ }
+ data = {
+ customQueryValue,
+ url,
+ };
+ break;
+ case "custom-url-value":
+ data = {
+ customQueryValue: null,
+ url: val
+ };
+ break;
+ default:
+ break;
+ }
+ if (data) {
+ // All updateRequest batch mode should be disabled to make UI editing in sync
+ updateRequest(request.id, data, false);
+ }
+}
+
+module.exports = connect(
+ (state) => ({ request: getSelectedRequest(state) }),
+ (dispatch) => ({
+ removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
+ sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
+ updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
+ })
+)(CustomRequestPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/Editor.js
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-disable react/prop-types */
+
+"use strict";
+
+const {
+ createClass,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const SourceEditor = require("devtools/client/sourceeditor/editor");
+
+const { div } = DOM;
+const SYNTAX_HIGHLIGHT_MAX_SIZE = 102400;
+
+/**
+ * CodeMirror editor as a React component
+ */
+const Editor = createClass({
+ displayName: "Editor",
+
+ propTypes: {
+ // Source editor syntax hightligh mode, which is a mime type defined in CodeMirror
+ mode: PropTypes.string,
+ // Source editor is displayed if set to true
+ open: PropTypes.bool,
+ // Source editor content
+ text: PropTypes.string,
+ },
+
+ getDefaultProps() {
+ return {
+ mode: null,
+ open: true,
+ text: "",
+ };
+ },
+
+ componentDidMount() {
+ const { mode, text } = this.props;
+
+ this.editor = new SourceEditor({
+ lineNumbers: true,
+ mode: text.length < SYNTAX_HIGHLIGHT_MAX_SIZE ? mode : null,
+ readOnly: true,
+ value: text,
+ });
+
+ this.deferEditor = this.editor.appendTo(this.refs.editorElement);
+ },
+
+ componentDidUpdate(prevProps) {
+ const { mode, open, text } = this.props;
+
+ if (!open) {
+ return;
+ }
+
+ if (prevProps.mode !== mode && text.length < SYNTAX_HIGHLIGHT_MAX_SIZE) {
+ this.deferEditor.then(() => {
+ this.editor.setMode(mode);
+ });
+ }
+
+ if (prevProps.text !== text) {
+ this.deferEditor.then(() => {
+ // FIXME: Workaround for browser_net_accessibility test to
+ // make sure editor node exists while setting editor text.
+ // deferEditor workaround should be removed in bug 1308442
+ if (this.refs.editorElement) {
+ this.editor.setText(text);
+ }
+ });
+ }
+ },
+
+ componentWillUnmount() {
+ this.deferEditor.then(() => {
+ this.editor.destroy();
+ this.editor = null;
+ });
+ this.deferEditor = null;
+ },
+
+ render() {
+ const { open } = this.props;
+
+ return (
+ div({ className: "editor-container devtools-monospace" },
+ div({
+ ref: "editorElement",
+ className: "editor-mount devtools-monospace",
+ // Using visibility instead of display property to avoid breaking
+ // CodeMirror indentation
+ style: { visibility: open ? "visible" : "hidden" },
+ }),
+ )
+ );
+ }
+});
+
+module.exports = Editor;
+
+/* eslint-enable react/prop-types */
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/HeadersPanel.js
@@ -0,0 +1,260 @@
+/* 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 { L10N } = require("../utils/l10n");
+const {
+ getHeadersURL,
+ getHTTPStatusCodeURL,
+} = require("../utils/mdn-utils");
+const { writeHeaderText } = require("../utils/request-utils");
+
+// Components
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const MDNLink = createFactory(require("./MDNLink"));
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const Rep = createFactory(REPS.Rep);
+const { button, div, input, textarea } = DOM;
+
+const EDIT_AND_RESEND = L10N.getStr("netmonitor.summary.editAndResend");
+const RAW_HEADERS = L10N.getStr("netmonitor.summary.rawHeaders");
+const RAW_HEADERS_REQUEST = L10N.getStr("netmonitor.summary.rawHeaders.requestHeaders");
+const RAW_HEADERS_RESPONSE = L10N.getStr("netmonitor.summary.rawHeaders.responseHeaders");
+const HEADERS_EMPTY_TEXT = L10N.getStr("headersEmptyText");
+const HEADERS_FILTER_TEXT = L10N.getStr("headersFilterText");
+const REQUEST_HEADERS = L10N.getStr("requestHeaders");
+const REQUEST_HEADERS_FROM_UPLOAD = L10N.getStr("requestHeadersFromUpload");
+const RESPONSE_HEADERS = L10N.getStr("responseHeaders");
+const SUMMARY_ADDRESS = L10N.getStr("netmonitor.summary.address");
+const SUMMARY_METHOD = L10N.getStr("netmonitor.summary.method");
+const SUMMARY_URL = L10N.getStr("netmonitor.summary.url");
+const SUMMARY_STATUS = L10N.getStr("netmonitor.summary.status");
+const SUMMARY_VERSION = L10N.getStr("netmonitor.summary.version");
+
+/*
+ * Headers panel component
+ * Lists basic information about the request
+ */
+const HeadersPanel = createClass({
+ displayName: "HeadersPanel",
+
+ propTypes: {
+ cloneSelectedRequest: PropTypes.func.isRequired,
+ request: PropTypes.object.isRequired,
+ renderValue: PropTypes.func
+ },
+
+ getInitialState() {
+ return {
+ rawHeadersOpened: false,
+ };
+ },
+
+ getProperties(headers, title) {
+ if (headers && headers.headers.length) {
+ return {
+ [`${title} (${getFormattedSize(headers.headersSize, 3)})`]:
+ headers.headers.reduce((acc, { name, value }) =>
+ name ? Object.assign(acc, { [name]: value }) : acc
+ , {})
+ };
+ }
+
+ return null;
+ },
+
+ toggleRawHeaders() {
+ this.setState({
+ rawHeadersOpened: !this.state.rawHeadersOpened,
+ });
+ },
+
+ renderSummary(label, value) {
+ return (
+ div({ className: "tabpanel-summary-container headers-summary" },
+ div({
+ className: "tabpanel-summary-label headers-summary-label",
+ }, label),
+ input({
+ className: "tabpanel-summary-value textbox-input devtools-monospace",
+ readOnly: true,
+ value,
+ }),
+ )
+ );
+ },
+
+ renderValue(props) {
+ const member = props.member;
+ const value = props.value;
+
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ let headerDocURL = getHeadersURL(member.name);
+
+ return (
+ div({ className: "treeValueCellDivider" },
+ Rep(Object.assign(props, {
+ // FIXME: A workaround for the issue in StringRep
+ // Force StringRep to crop the text everytime
+ member: Object.assign({}, member, { open: false }),
+ mode: MODE.TINY,
+ cropLimit: 60,
+ })),
+ headerDocURL ? MDNLink({
+ url: headerDocURL,
+ }) : null
+ )
+ );
+ },
+
+ render() {
+ const {
+ cloneSelectedRequest,
+ request: {
+ fromCache,
+ fromServiceWorker,
+ httpVersion,
+ method,
+ remoteAddress,
+ remotePort,
+ requestHeaders,
+ requestHeadersFromUploadStream: uploadHeaders,
+ responseHeaders,
+ status,
+ statusText,
+ urlDetails,
+ },
+ } = this.props;
+
+ if ((!requestHeaders || !requestHeaders.headers.length) &&
+ (!uploadHeaders || !uploadHeaders.headers.length) &&
+ (!responseHeaders || !responseHeaders.headers.length)) {
+ return div({ className: "empty-notice" },
+ HEADERS_EMPTY_TEXT
+ );
+ }
+
+ let object = Object.assign({},
+ this.getProperties(responseHeaders, RESPONSE_HEADERS),
+ this.getProperties(requestHeaders, REQUEST_HEADERS),
+ this.getProperties(uploadHeaders, REQUEST_HEADERS_FROM_UPLOAD),
+ );
+
+ let summaryUrl = urlDetails.unicodeUrl ?
+ this.renderSummary(SUMMARY_URL, urlDetails.unicodeUrl) : null;
+
+ let summaryMethod = method ?
+ this.renderSummary(SUMMARY_METHOD, method) : null;
+
+ let summaryAddress = remoteAddress ?
+ this.renderSummary(SUMMARY_ADDRESS,
+ remotePort ? `${remoteAddress}:${remotePort}` : remoteAddress) : null;
+
+ let summaryStatus;
+
+ if (status) {
+ let code;
+ if (fromCache) {
+ code = "cached";
+ } else if (fromServiceWorker) {
+ code = "service worker";
+ } else {
+ code = status;
+ }
+
+ let statusCodeDocURL = getHTTPStatusCodeURL(code);
+ let inputWidth = status.length + statusText.length + 1;
+
+ summaryStatus = (
+ div({ className: "tabpanel-summary-container headers-summary" },
+ div({
+ className: "tabpanel-summary-label headers-summary-label",
+ }, SUMMARY_STATUS),
+ div({
+ className: "requests-list-status-icon",
+ "data-code": code,
+ }),
+ input({
+ className: "tabpanel-summary-value textbox-input devtools-monospace"
+ + " status-text",
+ readOnly: true,
+ value: `${status} ${statusText}`,
+ size: `${inputWidth}`,
+ }),
+ statusCodeDocURL ? MDNLink({
+ url: statusCodeDocURL,
+ }) : null,
+ window.NetMonitorController.supportsCustomRequest && button({
+ className: "devtools-button",
+ onClick: cloneSelectedRequest,
+ }, EDIT_AND_RESEND),
+ button({
+ className: "devtools-button",
+ onClick: this.toggleRawHeaders,
+ }, RAW_HEADERS),
+ )
+ );
+ }
+
+ let summaryVersion = httpVersion ?
+ this.renderSummary(SUMMARY_VERSION, httpVersion) : null;
+
+ let summaryRawHeaders;
+ if (this.state.rawHeadersOpened) {
+ summaryRawHeaders = (
+ div({ className: "tabpanel-summary-container headers-summary" },
+ div({ className: "raw-headers-container" },
+ div({ className: "raw-headers" },
+ div({ className: "tabpanel-summary-label" }, RAW_HEADERS_REQUEST),
+ textarea({
+ value: writeHeaderText(requestHeaders.headers),
+ readOnly: true,
+ }),
+ ),
+ div({ className: "raw-headers" },
+ div({ className: "tabpanel-summary-label" }, RAW_HEADERS_RESPONSE),
+ textarea({
+ value: writeHeaderText(responseHeaders.headers),
+ readOnly: true,
+ }),
+ ),
+ )
+ )
+ );
+ }
+
+ return (
+ div({ className: "panel-container" },
+ div({ className: "headers-overview" },
+ summaryUrl,
+ summaryMethod,
+ summaryAddress,
+ summaryStatus,
+ summaryVersion,
+ summaryRawHeaders,
+ ),
+ PropertiesView({
+ object,
+ filterPlaceHolder: HEADERS_FILTER_TEXT,
+ sectionNames: Object.keys(object),
+ renderValue: this.renderValue,
+ }),
+ )
+ );
+ }
+});
+
+module.exports = HeadersPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/MDNLink.js
@@ -0,0 +1,47 @@
+/* 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 Services = require("Services");
+const {
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { L10N } = require("../utils/l10n");
+
+const { a } = DOM;
+
+const LEARN_MORE = L10N.getStr("netmonitor.headers.learnMore");
+
+function MDNLink({ url }) {
+ return (
+ a({
+ className: "learn-more-link",
+ title: url,
+ onClick: (e) => onLearnMoreClick(e, url),
+ }, `[${LEARN_MORE}]`)
+ );
+}
+
+MDNLink.displayName = "MDNLink";
+
+MDNLink.propTypes = {
+ url: PropTypes.string.isRequired,
+};
+
+function onLearnMoreClick(e, url) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (e.button === 1) {
+ win.openUILinkIn(url, "tabshifted");
+ } else {
+ win.openUILinkIn(url, "tab");
+ }
+}
+
+module.exports = MDNLink;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/MonitorPanel.js
@@ -0,0 +1,135 @@
+/* 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 { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+const Actions = require("../actions/index");
+const { getLongString } = require("../utils/client");
+const { Prefs } = require("../utils/prefs");
+const { getFormDataSections } = require("../utils/request-utils");
+const { getSelectedRequest } = require("../selectors/index");
+
+// Components
+const SplitBox = createFactory(require("devtools/client/shared/components/splitter/split-box"));
+const NetworkDetailsPanel = createFactory(require("./NetworkDetailsPanel"));
+const RequestList = createFactory(require("./RequestList"));
+const Toolbar = createFactory(require("./Toolbar"));
+
+const { div } = DOM;
+const MediaQueryList = window.matchMedia("(min-width: 700px)");
+
+/*
+ * Monitor panel component
+ * The main panel for displaying various network request information
+ */
+const MonitorPanel = createClass({
+ displayName: "MonitorPanel",
+
+ propTypes: {
+ isEmpty: PropTypes.bool.isRequired,
+ networkDetailsOpen: PropTypes.bool.isRequired,
+ openNetworkDetails: PropTypes.func.isRequired,
+ request: PropTypes.object,
+ updateRequest: PropTypes.func.isRequired,
+ },
+
+ getInitialState() {
+ return {
+ isVerticalSpliter: MediaQueryList.matches,
+ };
+ },
+
+ componentDidMount() {
+ MediaQueryList.addListener(this.onLayoutChange);
+ },
+
+ componentWillReceiveProps(nextProps) {
+ let {
+ request = {},
+ updateRequest,
+ } = nextProps;
+ let {
+ formDataSections,
+ requestHeaders,
+ requestHeadersFromUploadStream,
+ requestPostData,
+ } = request;
+
+ if (!formDataSections && requestHeaders &&
+ requestHeadersFromUploadStream && requestPostData) {
+ getFormDataSections(
+ requestHeaders,
+ requestHeadersFromUploadStream,
+ requestPostData,
+ getLongString,
+ ).then((newFormDataSections) => {
+ updateRequest(
+ request.id,
+ { formDataSections: newFormDataSections },
+ true,
+ );
+ });
+ }
+ },
+
+ componentWillUnmount() {
+ MediaQueryList.removeListener(this.onLayoutChange);
+
+ let { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
+
+ if (this.state.isVerticalSpliter && clientWidth) {
+ Prefs.networkDetailsWidth = clientWidth;
+ }
+ if (!this.state.isVerticalSpliter && clientHeight) {
+ Prefs.networkDetailsHeight = clientHeight;
+ }
+ },
+
+ onLayoutChange() {
+ this.setState({
+ isVerticalSpliter: MediaQueryList.matches,
+ });
+ },
+
+ render() {
+ let { isEmpty, networkDetailsOpen } = this.props;
+ return (
+ div({ className: "monitor-panel" },
+ Toolbar(),
+ SplitBox({
+ className: "devtools-responsive-container",
+ initialWidth: `${Prefs.networkDetailsWidth}px`,
+ initialHeight: `${Prefs.networkDetailsHeight}px`,
+ minSize: "50px",
+ maxSize: "80%",
+ splitterSize: "1px",
+ startPanel: RequestList({ isEmpty }),
+ endPanel: networkDetailsOpen && NetworkDetailsPanel({ ref: "endPanel" }),
+ endPanelControl: true,
+ vert: this.state.isVerticalSpliter,
+ }),
+ )
+ );
+ }
+});
+
+module.exports = connect(
+ (state) => ({
+ isEmpty: state.requests.requests.isEmpty(),
+ networkDetailsOpen: state.ui.networkDetailsOpen,
+ request: getSelectedRequest(state),
+ }),
+ (dispatch) => ({
+ openNetworkDetails: (open) => dispatch(Actions.openNetworkDetails(open)),
+ updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
+ }),
+)(MonitorPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/NetworkDetailsPanel.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const Actions = require("../actions/index");
+const { getSelectedRequest } = require("../selectors/index");
+
+// Components
+const CustomRequestPanel = createFactory(require("./CustomRequestPanel"));
+const TabboxPanel = createFactory(require("./TabboxPanel"));
+
+const { div } = DOM;
+
+/*
+ * Network details panel component
+ */
+function NetworkDetailsPanel({
+ activeTabId,
+ cloneSelectedRequest,
+ request,
+ selectTab,
+}) {
+ if (!request) {
+ return null;
+ }
+
+ return (
+ div({ className: "network-details-panel" },
+ !request.isCustom ?
+ TabboxPanel({
+ activeTabId,
+ request,
+ selectTab,
+ }) :
+ CustomRequestPanel({
+ cloneSelectedRequest,
+ request,
+ })
+ )
+ );
+}
+
+NetworkDetailsPanel.displayName = "NetworkDetailsPanel";
+
+NetworkDetailsPanel.propTypes = {
+ activeTabId: PropTypes.string,
+ cloneSelectedRequest: PropTypes.func.isRequired,
+ open: PropTypes.bool,
+ request: PropTypes.object,
+ selectTab: PropTypes.func.isRequired,
+};
+
+module.exports = connect(
+ (state) => ({
+ activeTabId: state.ui.detailsPanelSelectedTab,
+ request: getSelectedRequest(state),
+ }),
+ (dispatch) => ({
+ cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+ selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
+ }),
+)(NetworkDetailsPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/ParamsPanel.js
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { getUrlQuery, parseQueryString } = require("../utils/request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const { div } = DOM;
+
+const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
+const PARAMS_EMPTY_TEXT = L10N.getStr("paramsEmptyText");
+const PARAMS_FILTER_TEXT = L10N.getStr("paramsFilterText");
+const PARAMS_FORM_DATA = L10N.getStr("paramsFormData");
+const PARAMS_POST_PAYLOAD = L10N.getStr("paramsPostPayload");
+const PARAMS_QUERY_STRING = L10N.getStr("paramsQueryString");
+const SECTION_NAMES = [
+ JSON_SCOPE_NAME,
+ PARAMS_FORM_DATA,
+ PARAMS_POST_PAYLOAD,
+ PARAMS_QUERY_STRING,
+];
+
+/*
+ * Params panel component
+ * Displays the GET parameters and POST data of a request
+ */
+function ParamsPanel({ request }) {
+ let {
+ formDataSections,
+ mimeType,
+ requestPostData,
+ url,
+ } = request;
+ let postData = requestPostData ? requestPostData.postData.text : null;
+ let query = getUrlQuery(url);
+
+ if (!formDataSections && !postData && !query) {
+ return div({ className: "empty-notice" },
+ PARAMS_EMPTY_TEXT
+ );
+ }
+
+ let object = {};
+ let json;
+
+ // Query String section
+ if (query) {
+ object[PARAMS_QUERY_STRING] = getProperties(parseQueryString(query));
+ }
+
+ // Form Data section
+ if (formDataSections && formDataSections.length > 0) {
+ let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
+ object[PARAMS_FORM_DATA] = getProperties(parseQueryString(sections));
+ }
+
+ // Request payload section
+ if (formDataSections && formDataSections.length === 0 && postData) {
+ try {
+ json = JSON.parse(postData);
+ } catch (error) {
+ // Continue regardless of parsing error
+ }
+
+ if (json) {
+ object[JSON_SCOPE_NAME] = json;
+ } else {
+ object[PARAMS_POST_PAYLOAD] = {
+ EDITOR_CONFIG: {
+ text: postData,
+ mode: mimeType.replace(/;.+/, ""),
+ },
+ };
+ }
+ } else {
+ postData = "";
+ }
+
+ return (
+ div({ className: "panel-container" },
+ PropertiesView({
+ object,
+ filterPlaceHolder: PARAMS_FILTER_TEXT,
+ sectionNames: SECTION_NAMES,
+ })
+ )
+ );
+}
+
+ParamsPanel.displayName = "ParamsPanel";
+
+ParamsPanel.propTypes = {
+ request: PropTypes.object.isRequired,
+};
+
+/**
+ * Mapping array to dict for TreeView usage.
+ * Since TreeView only support Object(dict) format.
+ * This function also deal with duplicate key case
+ * (for multiple selection and query params with same keys)
+ *
+ * @param {Object[]} arr - key-value pair array like query or form params
+ * @returns {Object} Rep compatible object
+ */
+function getProperties(arr) {
+ return arr.reduce((map, obj) => {
+ let value = map[obj.name];
+ if (value) {
+ if (typeof value !== "object") {
+ map[obj.name] = [value];
+ }
+ map[obj.name].push(obj.value);
+ } else {
+ map[obj.name] = obj.value;
+ }
+ return map;
+ }, {});
+}
+
+module.exports = ParamsPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/PreviewPanel.js
@@ -0,0 +1,35 @@
+/* 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, iframe } = DOM;
+
+/*
+ * Preview panel component
+ * Display HTML content within a sandbox enabled iframe
+ */
+function PreviewPanel({ request }) {
+ const htmlBody = request.responseContent ?
+ request.responseContent.content.text : "";
+
+ return (
+ div({ className: "panel-container" },
+ iframe({
+ sandbox: "",
+ srcDoc: typeof htmlBody === "string" ? htmlBody : "",
+ })
+ )
+ );
+}
+
+PreviewPanel.displayName = "PreviewPanel";
+
+PreviewPanel.propTypes = {
+ request: PropTypes.object.isRequired,
+};
+
+module.exports = PreviewPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/PropertiesView.js
@@ -0,0 +1,218 @@
+/* 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,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const Rep = createFactory(REPS.Rep);
+
+const { FILTER_SEARCH_DELAY } = require("../constants");
+
+// Components
+const SearchBox = createFactory(require("devtools/client/shared/components/search-box"));
+const TreeView = createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const TreeRow = createFactory(require("devtools/client/shared/components/tree/tree-row"));
+const Editor = createFactory(require("./Editor"));
+
+const { div, tr, td } = DOM;
+const AUTO_EXPAND_MAX_LEVEL = 7;
+const AUTO_EXPAND_MAX_NODES = 50;
+const EDITOR_CONFIG_ID = "EDITOR_CONFIG";
+
+/*
+ * Properties View component
+ * A scrollable tree view component which provides some useful features for
+ * representing object properties.
+ *
+ * Search filter - Set enableFilter to enable / disable SearchBox feature.
+ * Tree view - Default enabled.
+ * Source editor - Enable by specifying object level 1 property name to EDITOR_CONFIG_ID.
+ * Rep - Default enabled.
+ */
+const PropertiesView = createClass({
+ displayName: "PropertiesView",
+
+ propTypes: {
+ object: PropTypes.object,
+ enableInput: PropTypes.bool,
+ expandableStrings: PropTypes.bool,
+ filterPlaceHolder: PropTypes.string,
+ sectionNames: PropTypes.array,
+ },
+
+ getDefaultProps() {
+ return {
+ enableInput: true,
+ enableFilter: true,
+ expandableStrings: false,
+ filterPlaceHolder: "",
+ sectionNames: [],
+ };
+ },
+
+ getInitialState() {
+ return {
+ filterText: "",
+ };
+ },
+
+ getRowClass(object, sectionNames) {
+ return sectionNames.includes(object.name) ? "tree-section" : "";
+ },
+
+ onFilter(object, whiteList) {
+ let { name, value } = object;
+ let filterText = this.state.filterText;
+
+ if (!filterText || whiteList.includes(name)) {
+ return true;
+ }
+
+ let jsonString = JSON.stringify({ [name]: value }).toLowerCase();
+ return jsonString.includes(filterText.toLowerCase());
+ },
+
+ renderRowWithEditor(props) {
+ const { level, name, value, path } = props.member;
+
+ // Display source editor when specifying to EDITOR_CONFIG_ID along with config
+ if (level === 1 && name === EDITOR_CONFIG_ID) {
+ return (
+ tr({ className: "editor-row-container" },
+ td({ colSpan: 2 },
+ Editor(value)
+ )
+ )
+ );
+ }
+
+ // Skip for editor config
+ if (level >= 1 && path.includes(EDITOR_CONFIG_ID)) {
+ return null;
+ }
+
+ return TreeRow(props);
+ },
+
+ renderValueWithRep(props) {
+ const { member } = props;
+
+ // Hide strings with following conditions
+ // 1. this row is a togglable section and content is object ('cause it shouldn't hide
+ // when string or number)
+ // 2. the `value` object has a `value` property, only happened in Cookies panel
+ // Put 2 here to not dup this method
+ if (member.level === 0 && member.type === "object" ||
+ (typeof member.value === "object" && member.value && member.value.value)) {
+ return null;
+ }
+
+ return Rep(Object.assign(props, {
+ // FIXME: A workaround for the issue in StringRep
+ // Force StringRep to crop the text everytime
+ member: Object.assign({}, member, { open: false }),
+ mode: MODE.TINY,
+ cropLimit: 60,
+ }));
+ },
+
+ shouldRenderSearchBox(object) {
+ return this.props.enableFilter && object && Object.keys(object)
+ .filter((section) => !object[section][EDITOR_CONFIG_ID]).length > 0;
+ },
+
+ updateFilterText(filterText) {
+ this.setState({
+ filterText,
+ });
+ },
+
+ getExpandedNodes: function (object, path = "", level = 0) {
+ if (typeof object != "object") {
+ return null;
+ }
+
+ if (level > AUTO_EXPAND_MAX_LEVEL) {
+ return null;
+ }
+
+ let expandedNodes = new Set();
+ for (let prop in object) {
+ if (expandedNodes.size > AUTO_EXPAND_MAX_NODES) {
+ // If we reached the limit of expandable nodes, bail out to avoid performance
+ // issues.
+ break;
+ }
+
+ let nodePath = path + "/" + prop;
+ expandedNodes.add(nodePath);
+
+ let nodes = this.getExpandedNodes(object[prop], nodePath, level + 1);
+ if (nodes) {
+ let newSize = expandedNodes.size + nodes.size;
+ if (newSize < AUTO_EXPAND_MAX_NODES) {
+ // Avoid having a subtree half expanded.
+ expandedNodes = new Set([...expandedNodes, ...nodes]);
+ }
+ }
+ }
+ return expandedNodes;
+ },
+
+ render() {
+ const {
+ decorator,
+ enableInput,
+ expandableStrings,
+ filterPlaceHolder,
+ object,
+ renderRow,
+ renderValue,
+ sectionNames,
+ } = this.props;
+
+ return (
+ div({ className: "properties-view" },
+ this.shouldRenderSearchBox(object) &&
+ div({ className: "searchbox-section" },
+ SearchBox({
+ delay: FILTER_SEARCH_DELAY,
+ type: "filter",
+ onChange: this.updateFilterText,
+ placeholder: filterPlaceHolder,
+ }),
+ ),
+ div({ className: "tree-container" },
+ TreeView({
+ object,
+ columns: [{
+ id: "value",
+ width: "100%",
+ }],
+ decorator: decorator || {
+ getRowClass: (rowObject) => this.getRowClass(rowObject, sectionNames),
+ },
+ enableInput,
+ expandableStrings,
+ expandedNodes: this.getExpandedNodes(object),
+ onFilter: (props) => this.onFilter(props, sectionNames),
+ renderRow: renderRow || this.renderRowWithEditor,
+ renderValue: renderValue || this.renderValueWithRep,
+ }),
+ ),
+ )
+ );
+ }
+});
+
+module.exports = PropertiesView;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/RequestList.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+// Components
+const RequestListContent = createFactory(require("./RequestListContent"));
+const RequestListEmptyNotice = createFactory(require("./RequestListEmptyNotice"));
+const RequestListHeader = createFactory(require("./RequestListHeader"));
+
+const { div } = DOM;
+
+/**
+ * Request panel component
+ */
+function RequestList({ isEmpty }) {
+ return (
+ div({ className: "request-list-container" },
+ RequestListHeader(),
+ isEmpty ? RequestListEmptyNotice() : RequestListContent(),
+ )
+ );
+}
+
+RequestList.displayName = "RequestList";
+
+RequestList.propTypes = {
+ isEmpty: PropTypes.bool.isRequired,
+};
+
+module.exports = RequestList;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/RequestListContent.js
@@ -0,0 +1,280 @@
+/* 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("./RequestListItem"));
+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);
rename from devtools/client/netmonitor/components/request-list-empty.js
rename to devtools/client/netmonitor/components/RequestListEmptyNotice.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/RequestListHeader.js
@@ -0,0 +1,203 @@
+/* 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 { connect } = require("devtools/client/shared/vendor/react-redux");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const Actions = require("../actions/index");
+const { getWaterfallScale } = require("../selectors/index");
+const { getFormattedTime } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const WaterfallBackground = require("../waterfall-background");
+
+const { div, button } = DOM;
+
+const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
+const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
+
+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);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/RequestListItem.js
@@ -0,0 +1,528 @@
+/* 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 { L10N } = require("../utils/l10n");
+const { getAbbreviatedMimeType } = require("../utils/request-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/ResponsePanel.js
@@ -0,0 +1,185 @@
+/* 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 { formDataURI, getUrlBaseName } = require("../utils/request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const { div, img } = DOM;
+const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
+const JSON_FILTER_TEXT = L10N.getStr("jsonFilterText");
+const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name");
+const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions");
+const RESPONSE_IMG_MIMETYPE = L10N.getStr("netmonitor.response.mime");
+const RESPONSE_PAYLOAD = L10N.getStr("responsePayload");
+
+/*
+ * Response panel component
+ * Displays the GET parameters and POST data of a request
+ */
+const ResponsePanel = createClass({
+ displayName: "ResponsePanel",
+
+ propTypes: {
+ request: PropTypes.object.isRequired,
+ },
+
+ getInitialState() {
+ return {
+ imageDimensions: {
+ width: 0,
+ height: 0,
+ },
+ };
+ },
+
+ updateImageDimemsions({ target }) {
+ this.setState({
+ imageDimensions: {
+ width: target.naturalWidth,
+ height: target.naturalHeight,
+ },
+ });
+ },
+
+ // Handle json, which we tentatively identify by checking the MIME type
+ // for "json" after any word boundary. This works for the standard
+ // "application/json", and also for custom types like "x-bigcorp-json".
+ // Additionally, we also directly parse the response text content to
+ // verify whether it's json or not, to handle responses incorrectly
+ // labeled as text/plain instead.
+ isJSON(mimeType, response) {
+ let json, error;
+ try {
+ json = JSON.parse(response);
+ } catch (err) {
+ try {
+ json = JSON.parse(atob(response));
+ } catch (err64) {
+ error = err;
+ }
+ }
+
+ if (/\bjson/.test(mimeType) || json) {
+ // Extract the actual json substring in case this might be a "JSONP".
+ // This regex basically parses a function call and captures the
+ // function name and arguments in two separate groups.
+ let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
+ let [, jsonpCallback, jsonp] = response.match(jsonpRegex) || [];
+ let result = {};
+
+ // Make sure this is a valid JSON object first. If so, nicely display
+ // the parsing results in a tree view.
+ if (jsonpCallback && jsonp) {
+ error = null;
+ try {
+ json = JSON.parse(jsonp);
+ } catch (err) {
+ error = err;
+ }
+ }
+
+ // Valid JSON
+ if (json) {
+ result.json = json;
+ }
+ // Valid JSONP
+ if (jsonpCallback) {
+ result.jsonpCallback = jsonpCallback;
+ }
+ // Malformed JSON
+ if (error) {
+ result.error = "" + error;
+ }
+
+ return result;
+ }
+
+ return null;
+ },
+
+ render() {
+ let { responseContent, url } = this.props.request;
+
+ if (!responseContent || typeof responseContent.content.text !== "string") {
+ return null;
+ }
+
+ let { encoding, mimeType, text } = responseContent.content;
+
+ if (mimeType.includes("image/")) {
+ let { width, height } = this.state.imageDimensions;
+
+ return (
+ div({ className: "panel-container response-image-box devtools-monospace" },
+ img({
+ className: "response-image",
+ src: formDataURI(mimeType, encoding, text),
+ onLoad: this.updateImageDimemsions,
+ }),
+ div({ className: "response-summary" },
+ div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_NAME),
+ div({ className: "tabpanel-summary-value" }, getUrlBaseName(url)),
+ ),
+ div({ className: "response-summary" },
+ div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_DIMENSIONS),
+ div({ className: "tabpanel-summary-value" }, `${width} × ${height}`),
+ ),
+ div({ className: "response-summary" },
+ div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_MIMETYPE),
+ div({ className: "tabpanel-summary-value" }, mimeType),
+ ),
+ )
+ );
+ }
+
+ // Display Properties View
+ let { json, jsonpCallback, error } = this.isJSON(mimeType, text) || {};
+ let object = {};
+ let sectionName;
+
+ if (json) {
+ if (jsonpCallback) {
+ sectionName = L10N.getFormatStr("jsonpScopeName", jsonpCallback);
+ } else {
+ sectionName = JSON_SCOPE_NAME;
+ }
+ object[sectionName] = json;
+ } else {
+ sectionName = RESPONSE_PAYLOAD;
+
+ object[sectionName] = {
+ EDITOR_CONFIG: {
+ text,
+ mode: mimeType.replace(/;.+/, ""),
+ },
+ };
+ }
+
+ return (
+ div({ className: "panel-container" },
+ error && div({ className: "response-error-header", title: error },
+ error
+ ),
+ PropertiesView({
+ object,
+ filterPlaceHolder: JSON_FILTER_TEXT,
+ sectionNames: [sectionName],
+ }),
+ )
+ );
+ }
+});
+
+module.exports = ResponsePanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/SecurityPanel.js
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ DOM,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { getUrlHost } = require("../utils/request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const { div, input, span } = DOM;
+
+/*
+ * Security panel component
+ * If the site is being served over HTTPS, you get an extra tab labeled "Security".
+ * This contains details about the secure connection used including the protocol,
+ * the cipher suite, and certificate details
+ */
+function SecurityPanel({ request }) {
+ const { securityInfo, url } = request;
+
+ if (!securityInfo || !url) {
+ return null;
+ }
+
+ const notAvailable = L10N.getStr("netmonitor.security.notAvailable");
+ let object;
+
+ if (securityInfo.state === "secure" || securityInfo.state === "weak") {
+ const { subject, issuer, validity, fingerprint } = securityInfo.cert;
+ const enabledLabel = L10N.getStr("netmonitor.security.enabled");
+ const disabledLabel = L10N.getStr("netmonitor.security.disabled");
+
+ object = {
+ [L10N.getStr("netmonitor.security.connection")]: {
+ [L10N.getStr("netmonitor.security.protocolVersion")]:
+ securityInfo.protocolVersion || notAvailable,
+ [L10N.getStr("netmonitor.security.cipherSuite")]:
+ securityInfo.cipherSuite || notAvailable,
+ },
+ [L10N.getFormatStr("netmonitor.security.hostHeader", getUrlHost(url))]: {
+ [L10N.getStr("netmonitor.security.hsts")]:
+ securityInfo.hsts ? enabledLabel : disabledLabel,
+ [L10N.getStr("netmonitor.security.hpkp")]:
+ securityInfo.hpkp ? enabledLabel : disabledLabel,
+ },
+ [L10N.getStr("netmonitor.security.certificate")]: {
+ [L10N.getStr("certmgr.subjectinfo.label")]: {
+ [L10N.getStr("certmgr.certdetail.cn")]:
+ subject.commonName || notAvailable,
+ [L10N.getStr("certmgr.certdetail.o")]:
+ subject.organization || notAvailable,
+ [L10N.getStr("certmgr.certdetail.ou")]:
+ subject.organizationUnit || notAvailable,
+ },
+ [L10N.getStr("certmgr.issuerinfo.label")]: {
+ [L10N.getStr("certmgr.certdetail.cn")]:
+ issuer.commonName || notAvailable,
+ [L10N.getStr("certmgr.certdetail.o")]:
+ issuer.organization || notAvailable,
+ [L10N.getStr("certmgr.certdetail.ou")]:
+ issuer.organizationUnit || notAvailable,
+ },
+ [L10N.getStr("certmgr.periodofvalidity.label")]: {
+ [L10N.getStr("certmgr.begins")]:
+ validity.start || notAvailable,
+ [L10N.getStr("certmgr.expires")]:
+ validity.end || notAvailable,
+ },
+ [L10N.getStr("certmgr.fingerprints.label")]: {
+ [L10N.getStr("certmgr.certdetail.sha256fingerprint")]:
+ fingerprint.sha256 || notAvailable,
+ [L10N.getStr("certmgr.certdetail.sha1fingerprint")]:
+ fingerprint.sha1 || notAvailable,
+ },
+ },
+ };
+ } else {
+ object = {
+ [L10N.getStr("netmonitor.security.error")]:
+ new DOMParser().parseFromString(securityInfo.errorMessage, "text/html")
+ .body.textContent || notAvailable
+ };
+ }
+
+ return div({ className: "panel-container security-panel" },
+ PropertiesView({
+ object,
+ renderValue: (props) => renderValue(props, securityInfo.weaknessReasons),
+ enableFilter: false,
+ expandedNodes: getExpandedNodes(object),
+ })
+ );
+}
+
+SecurityPanel.displayName = "SecurityPanel";
+
+SecurityPanel.propTypes = {
+ request: PropTypes.object.isRequired,
+};
+
+function renderValue(props, weaknessReasons = []) {
+ const { member, value } = props;
+
+ // Hide object summary
+ if (typeof member.value === "object") {
+ return null;
+ }
+
+ return span({ className: "security-info-value" },
+ member.name === L10N.getStr("netmonitor.security.error") ?
+ // Display multiline text for security error
+ value
+ :
+ // Display one line selectable text for security details
+ input({
+ className: "textbox-input",
+ readOnly: "true",
+ value,
+ })
+ ,
+ weaknessReasons.indexOf("cipher") !== -1 &&
+ member.name === L10N.getStr("netmonitor.security.cipherSuite") ?
+ // Display an extra warning icon after the cipher suite
+ div({
+ id: "security-warning-cipher",
+ className: "security-warning-icon",
+ title: L10N.getStr("netmonitor.security.warning.cipher"),
+ })
+ :
+ null
+ );
+}
+
+function getExpandedNodes(object, path = "", level = 0) {
+ if (typeof object !== "object") {
+ return null;
+ }
+
+ let expandedNodes = new Set();
+ for (let prop in object) {
+ let nodePath = path + "/" + prop;
+ expandedNodes.add(nodePath);
+
+ let nodes = getExpandedNodes(object[prop], nodePath, level + 1);
+ if (nodes) {
+ expandedNodes = new Set([...expandedNodes, ...nodes]);
+ }
+ }
+ return expandedNodes;
+}
+
+module.exports = SecurityPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/StatisticsPanel.js
@@ -0,0 +1,276 @@
+/* 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 { Chart } = require("devtools/client/shared/widgets/Chart");
+const { PluralForm } = require("devtools/shared/plural-form");
+const Actions = require("../actions/index");
+const { Filters } = require("../utils/filter-predicates");
+const {
+ getSizeWithDecimals,
+ getTimeWithDecimals
+} = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+
+const { button, div } = DOM;
+const MediaQueryList = window.matchMedia("(min-width: 700px)");
+
+const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
+const BACK_BUTTON = L10N.getStr("netmonitor.backButton");
+const CHARTS_CACHE_ENABLED = L10N.getStr("charts.cacheEnabled");
+const CHARTS_CACHE_DISABLED = L10N.getStr("charts.cacheDisabled");
+
+/*
+ * Statistics panel component
+ * Performance analysis tool which shows you how long the browser takes to
+ * download the different parts of your site.
+ */
+const StatisticsPanel = createClass({
+ displayName: "StatisticsPanel",
+
+ propTypes: {
+ closeStatistics: PropTypes.func.isRequired,
+ enableRequestFilterTypeOnly: PropTypes.func.isRequired,
+ requests: PropTypes.object,
+ },
+
+ getInitialState() {
+ return {
+ isVerticalSpliter: MediaQueryList.matches,
+ };
+ },
+
+ componentDidUpdate(prevProps) {
+ MediaQueryList.addListener(this.onLayoutChange);
+
+ const { requests } = this.props;
+ let ready = requests && !requests.isEmpty() && requests.every((req) =>
+ req.contentSize !== undefined && req.mimeType && req.responseHeaders &&
+ req.status !== undefined && req.totalTime !== undefined
+ );
+
+ this.createChart({
+ id: "primedCacheChart",
+ title: CHARTS_CACHE_ENABLED,
+ data: ready ? this.sanitizeChartDataSource(requests, false) : null,
+ });
+
+ this.createChart({
+ id: "emptyCacheChart",
+ title: CHARTS_CACHE_DISABLED,
+ data: ready ? this.sanitizeChartDataSource(requests, true) : null,
+ });
+ },
+
+ componentWillUnmount() {
+ MediaQueryList.removeListener(this.onLayoutChange);
+ },
+
+ createChart({ id, title, data }) {
+ // Create a new chart.
+ let chart = Chart.PieTable(document, {
+ diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
+ title,
+ header: {
+ cached: "",
+ count: "",
+ label: L10N.getStr("charts.type"),
+ size: L10N.getStr("charts.size"),
+ transferredSize: L10N.getStr("charts.transferred"),
+ time: L10N.getStr("charts.time"),
+ },
+ data,
+ strings: {
+ size: (value) =>
+ L10N.getFormatStr("charts.sizeKB", getSizeWithDecimals(value / 1024)),
+ transferredSize: (value) =>
+ L10N.getFormatStr("charts.transferredSizeKB",
+ getSizeWithDecimals(value / 1024)),
+ time: (value) =>
+ L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
+ },
+ totals: {
+ cached: (total) => L10N.getFormatStr("charts.totalCached", total),
+ count: (total) => L10N.getFormatStr("charts.totalCount", total),
+ size: (total) =>
+ L10N.getFormatStr("charts.totalSize", getSizeWithDecimals(total / 1024)),
+ transferredSize: total =>
+ L10N.getFormatStr("charts.totalTransferredSize",
+ getSizeWithDecimals(total / 1024)),
+ time: (total) => {
+ let seconds = total / 1000;
+ let string = getTimeWithDecimals(seconds);
+ return PluralForm.get(seconds,
+ L10N.getStr("charts.totalSeconds")).replace("#1", string);
+ },
+ },
+ sorted: true,
+ });
+
+ chart.on("click", (_, { label }) => {
+ // Reset FilterButtons and enable one filter exclusively
+ this.props.closeStatistics();
+ this.props.enableRequestFilterTypeOnly(label);
+ });
+
+ let container = this.refs[id];
+
+ // Nuke all existing charts of the specified type.
+ while (container.hasChildNodes()) {
+ container.firstChild.remove();
+ }
+
+ container.appendChild(chart.node);
+ },
+
+ sanitizeChartDataSource(requests, emptyCache) {
+ const data = [
+ "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
+ ].map((type) => ({
+ cached: 0,
+ count: 0,
+ label: type,
+ size: 0,
+ transferredSize: 0,
+ time: 0,
+ }));
+
+ for (let request of requests) {
+ let type;
+
+ if (Filters.html(request)) {
+ // "html"
+ type = 0;
+ } else if (Filters.css(request)) {
+ // "css"
+ type = 1;
+ } else if (Filters.js(request)) {
+ // "js"
+ type = 2;
+ } else if (Filters.fonts(request)) {
+ // "fonts"
+ type = 4;
+ } else if (Filters.images(request)) {
+ // "images"
+ type = 5;
+ } else if (Filters.media(request)) {
+ // "media"
+ type = 6;
+ } else if (Filters.flash(request)) {
+ // "flash"
+ type = 7;
+ } else if (Filters.ws(request)) {
+ // "ws"
+ type = 8;
+ } else if (Filters.xhr(request)) {
+ // Verify XHR last, to categorize other mime types in their own blobs.
+ // "xhr"
+ type = 3;
+ } else {
+ // "other"
+ type = 9;
+ }
+
+ if (emptyCache || !this.responseIsFresh(request)) {
+ data[type].time += request.totalTime || 0;
+ data[type].size += request.contentSize || 0;
+ data[type].transferredSize += request.transferredSize || 0;
+ } else {
+ data[type].cached++;
+ }
+ data[type].count++;
+ }
+
+ return data.filter(e => e.count > 0);
+ },
+
+ /**
+ * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
+ * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
+ *
+ * @param object
+ * An object containing the { responseHeaders, status } properties.
+ * @return boolean
+ * True if the response is fresh and loaded from cache.
+ */
+ responseIsFresh({ responseHeaders, status }) {
+ // Check for a "304 Not Modified" status and response headers availability.
+ if (status != 304 || !responseHeaders) {
+ return false;
+ }
+
+ let list = responseHeaders.headers;
+ let cacheControl = list.find(e => e.name.toLowerCase() === "cache-control");
+ let expires = list.find(e => e.name.toLowerCase() === "expires");
+
+ // Check the "Cache-Control" header for a maximum age value.
+ if (cacheControl) {
+ let maxAgeMatch =
+ cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
+ cacheControl.value.match(/max-age\s*=\s*(\d+)/);
+
+ if (maxAgeMatch && maxAgeMatch.pop() > 0) {
+ return true;
+ }
+ }
+
+ // Check the "Expires" header for a valid date.
+ if (expires && Date.parse(expires.value)) {
+ return true;
+ }
+
+ return false;
+ },
+
+ onLayoutChange() {
+ this.setState({
+ isVerticalSpliter: MediaQueryList.matches,
+ });
+ },
+
+ render() {
+ const { closeStatistics } = this.props;
+ let splitterClassName = ["splitter"];
+
+ if (this.state.isVerticalSpliter) {
+ splitterClassName.push("devtools-side-splitter");
+ } else {
+ splitterClassName.push("devtools-horizontal-splitter");
+ }
+
+ return (
+ div({ className: "statistics-panel" },
+ button({
+ className: "back-button devtools-button",
+ "data-text-only": "true",
+ title: BACK_BUTTON,
+ onClick: closeStatistics,
+ }, BACK_BUTTON),
+ div({ className: "charts-container" },
+ div({ ref: "primedCacheChart", className: "charts primed-cache-chart" }),
+ div({ className: splitterClassName.join(" ") }),
+ div({ ref: "emptyCacheChart", className: "charts empty-cache-chart" }),
+ ),
+ )
+ );
+ }
+});
+
+module.exports = connect(
+ (state) => ({
+ requests: state.requests.requests.valueSeq(),
+ }),
+ (dispatch) => ({
+ closeStatistics: () => dispatch(Actions.openStatistics(false)),
+ enableRequestFilterTypeOnly: (label) =>
+ dispatch(Actions.enableRequestFilterTypeOnly(label)),
+ })
+)(StatisticsPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/TabboxPanel.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const Actions = require("../actions/index");
+const { Filters } = require("../utils/filter-predicates");
+const { L10N } = require("../utils/l10n");
+const { getSelectedRequest } = require("../selectors/index");
+
+// Components
+const Tabbar = createFactory(require("devtools/client/shared/components/tabs/tabbar"));
+const TabPanel = createFactory(require("devtools/client/shared/components/tabs/tabs").TabPanel);
+const CookiesPanel = createFactory(require("./CookiesPanel"));
+const HeadersPanel = createFactory(require("./HeadersPanel"));
+const ParamsPanel = createFactory(require("./ParamsPanel"));
+const PreviewPanel = createFactory(require("./PreviewPanel"));
+const ResponsePanel = createFactory(require("./ResponsePanel"));
+const SecurityPanel = createFactory(require("./SecurityPanel"));
+const TimingsPanel = createFactory(require("./TimingsPanel"));
+
+const HEADERS_TITLE = L10N.getStr("netmonitor.tab.headers");
+const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
+const PARAMS_TITLE = L10N.getStr("netmonitor.tab.params");
+const RESPONSE_TITLE = L10N.getStr("netmonitor.tab.response");
+const TIMINGS_TITLE = L10N.getStr("netmonitor.tab.timings");
+const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
+const PREVIEW_TITLE = L10N.getStr("netmonitor.tab.preview");
+
+/*
+ * Tabbox panel component
+ * Display the network request details
+ */
+function TabboxPanel({
+ activeTabId,
+ cloneSelectedRequest,
+ request,
+ selectTab,
+}) {
+ if (!request) {
+ return null;
+ }
+
+ return (
+ Tabbar({
+ activeTabId,
+ onSelect: selectTab,
+ renderOnlySelected: true,
+ showAllTabsMenu: true,
+ },
+ TabPanel({
+ id: "headers",
+ title: HEADERS_TITLE,
+ },
+ HeadersPanel({ request, cloneSelectedRequest }),
+ ),
+ TabPanel({
+ id: "cookies",
+ title: COOKIES_TITLE,
+ },
+ CookiesPanel({ request }),
+ ),
+ TabPanel({
+ id: "params",
+ title: PARAMS_TITLE,
+ },
+ ParamsPanel({ request }),
+ ),
+ TabPanel({
+ id: "response",
+ title: RESPONSE_TITLE,
+ },
+ ResponsePanel({ request }),
+ ),
+ TabPanel({
+ id: "timings",
+ title: TIMINGS_TITLE,
+ },
+ TimingsPanel({ request }),
+ ),
+ request.securityState && request.securityState !== "insecure" &&
+ TabPanel({
+ id: "security",
+ title: SECURITY_TITLE,
+ },
+ SecurityPanel({ request }),
+ ),
+ Filters.html(request) &&
+ TabPanel({
+ id: "preview",
+ title: PREVIEW_TITLE,
+ },
+ PreviewPanel({ request }),
+ ),
+ )
+ );
+}
+
+TabboxPanel.displayName = "TabboxPanel";
+
+TabboxPanel.propTypes = {
+ activeTabId: PropTypes.string,
+ cloneSelectedRequest: PropTypes.func.isRequired,
+ request: PropTypes.object,
+ selectTab: PropTypes.func.isRequired,
+};
+
+module.exports = connect(
+ (state) => ({
+ activeTabId: state.ui.detailsPanelSelectedTab,
+ request: getSelectedRequest(state),
+ }),
+ (dispatch) => ({
+ cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+ selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
+ }),
+)(TabboxPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/TimingsPanel.js
@@ -0,0 +1,70 @@
+/* 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 { L10N } = require("../utils/l10n");
+
+const { div, span } = DOM;
+const types = ["blocked", "dns", "connect", "send", "wait", "receive"];
+const TIMINGS_END_PADDING = "80px";
+
+/*
+ * Timings panel component
+ * Display timeline bars that shows the total wait time for various stages
+ */
+function TimingsPanel({ request }) {
+ if (!request.eventTimings) {
+ return null;
+ }
+
+ const { timings, totalTime } = request.eventTimings;
+ const timelines = types.map((type, idx) => {
+ // Determine the relative offset for each timings box. For example, the
+ // offset of third timings box will be 0 + blocked offset + dns offset
+ const offset = types
+ .slice(0, idx)
+ .reduce((acc, cur) => (acc + timings[cur] || 0), 0);
+ const offsetScale = offset / totalTime || 0;
+ const timelineScale = timings[type] / totalTime || 0;
+
+ return div({
+ key: type,
+ id: `timings-summary-${type}`,
+ className: "tabpanel-summary-container timings-container",
+ },
+ span({ className: "tabpanel-summary-label timings-label" },
+ L10N.getStr(`netmonitor.timings.${type}`)
+ ),
+ div({ className: "requests-list-timings-container" },
+ span({
+ className: "requests-list-timings-offset",
+ style: {
+ width: `calc(${offsetScale} * (100% - ${TIMINGS_END_PADDING})`,
+ },
+ }),
+ span({
+ className: `requests-list-timings-box ${type}`,
+ style: {
+ width: `calc(${timelineScale} * (100% - ${TIMINGS_END_PADDING}))`,
+ },
+ }),
+ span({ className: "requests-list-timings-total" },
+ L10N.getFormatStr("networkMenu.totalMS", timings[type])
+ )
+ ),
+ );
+ });
+
+ return div({ className: "panel-container" }, timelines);
+}
+
+TimingsPanel.displayName = "TimingsPanel";
+
+TimingsPanel.propTypes = {
+ request: PropTypes.object.isRequired,
+};
+
+module.exports = TimingsPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/monitor-panel.js
+++ /dev/null
@@ -1,135 +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 { connect } = require("devtools/client/shared/vendor/react-redux");
-const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
-const Actions = require("../actions/index");
-const { getLongString } = require("../utils/client");
-const { Prefs } = require("../utils/prefs");
-const { getFormDataSections } = require("../utils/request-utils");
-const { getSelectedRequest } = require("../selectors/index");
-
-// Components
-const SplitBox = createFactory(require("devtools/client/shared/components/splitter/split-box"));
-const NetworkDetailsPanel = createFactory(require("../shared/components/network-details-panel"));
-const RequestList = createFactory(require("./request-list"));
-const Toolbar = createFactory(require("./toolbar"));
-
-const { div } = DOM;
-const MediaQueryList = window.matchMedia("(min-width: 700px)");
-
-/*
- * Monitor panel component
- * The main panel for displaying various network request information
- */
-const MonitorPanel = createClass({
- displayName: "MonitorPanel",
-
- propTypes: {
- isEmpty: PropTypes.bool.isRequired,
- networkDetailsOpen: PropTypes.bool.isRequired,
- openNetworkDetails: PropTypes.func.isRequired,
- request: PropTypes.object,
- updateRequest: PropTypes.func.isRequired,
- },
-
- getInitialState() {
- return {
- isVerticalSpliter: MediaQueryList.matches,
- };
- },
-
- componentDidMount() {
- MediaQueryList.addListener(this.onLayoutChange);
- },
-
- componentWillReceiveProps(nextProps) {
- let {
- request = {},
- updateRequest,
- } = nextProps;
- let {
- formDataSections,
- requestHeaders,
- requestHeadersFromUploadStream,
- requestPostData,
- } = request;
-
- if (!formDataSections && requestHeaders &&
- requestHeadersFromUploadStream && requestPostData) {
- getFormDataSections(
- requestHeaders,
- requestHeadersFromUploadStream,
- requestPostData,
- getLongString,
- ).then((newFormDataSections) => {
- updateRequest(
- request.id,
- { formDataSections: newFormDataSections },
- true,
- );
- });
- }
- },
-
- componentWillUnmount() {
- MediaQueryList.removeListener(this.onLayoutChange);
-
- let { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
-
- if (this.state.isVerticalSpliter && clientWidth) {
- Prefs.networkDetailsWidth = clientWidth;
- }
- if (!this.state.isVerticalSpliter && clientHeight) {
- Prefs.networkDetailsHeight = clientHeight;
- }
- },
-
- onLayoutChange() {
- this.setState({
- isVerticalSpliter: MediaQueryList.matches,
- });
- },
-
- render() {
- let { isEmpty, networkDetailsOpen } = this.props;
- return (
- div({ className: "monitor-panel" },
- Toolbar(),
- SplitBox({
- className: "devtools-responsive-container",
- initialWidth: `${Prefs.networkDetailsWidth}px`,
- initialHeight: `${Prefs.networkDetailsHeight}px`,
- minSize: "50px",
- maxSize: "80%",
- splitterSize: "1px",
- startPanel: RequestList({ isEmpty }),
- endPanel: networkDetailsOpen && NetworkDetailsPanel({ ref: "endPanel" }),
- endPanelControl: true,
- vert: this.state.isVerticalSpliter,
- }),
- )
- );
- }
-});
-
-module.exports = connect(
- (state) => ({
- isEmpty: state.requests.requests.isEmpty(),
- networkDetailsOpen: state.ui.networkDetailsOpen,
- request: getSelectedRequest(state),
- }),
- (dispatch) => ({
- openNetworkDetails: (open) => dispatch(Actions.openNetworkDetails(open)),
- updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
- }),
-)(MonitorPanel);
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -1,15 +1,28 @@
# 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-empty.js',
- 'request-list-header.js',
- 'request-list-item.js',
- 'request-list.js',
- 'statistics-panel.js',
- 'toolbar.js',
+ 'App.js',
+ 'CookiesPanel.js',
+ 'CustomRequestPanel.js',
+ 'Editor.js',
+ 'HeadersPanel.js',
+ 'MDNLink.js',
+ 'MonitorPanel.js',
+ 'NetworkDetailsPanel.js',
+ 'ParamsPanel.js',
+ 'PreviewPanel.js',
+ 'PropertiesView.js',
+ 'RequestList.js',
+ 'RequestListContent.js',
+ 'RequestListEmptyNotice.js',
+ 'RequestListHeader.js',
+ 'RequestListItem.js',
+ 'ResponsePanel.js',
+ 'SecurityPanel.js',
+ 'StatisticsPanel.js',
+ 'TabboxPanel.js',
+ 'TimingsPanel.js',
+ 'Toolbar.js',
)
deleted file mode 100644
--- a/devtools/client/netmonitor/components/network-monitor.js
+++ /dev/null
@@ -1,39 +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 {
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-
-// Components
-const MonitorPanel = createFactory(require("./monitor-panel"));
-const StatisticsPanel = createFactory(require("./statistics-panel"));
-
-const { div } = DOM;
-
-/*
- * Network monitor component
- */
-function NetworkMonitor({ statisticsOpen }) {
- return (
- div({ className: "network-monitor" },
- !statisticsOpen ? MonitorPanel() : StatisticsPanel()
- )
- );
-}
-
-NetworkMonitor.displayName = "NetworkMonitor";
-
-NetworkMonitor.propTypes = {
- statisticsOpen: PropTypes.bool.isRequired,
-};
-
-module.exports = connect(
- (state) => ({ statisticsOpen: state.ui.statisticsOpen }),
-)(NetworkMonitor);
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);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-header.js
+++ /dev/null
@@ -1,198 +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");
-
-const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
-const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
-
-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;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list.js
+++ /dev/null
@@ -1,38 +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 {
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-
-// Components
-const RequestListContent = createFactory(require("./request-list-content"));
-const RequestListEmptyNotice = createFactory(require("./request-list-empty"));
-const RequestListHeader = createFactory(require("./request-list-header"));
-
-const { div } = DOM;
-
-/**
- * Request panel component
- */
-function RequestList({ isEmpty }) {
- return (
- div({ className: "request-list-container" },
- RequestListHeader(),
- isEmpty ? RequestListEmptyNotice() : RequestListContent(),
- )
- );
-}
-
-RequestList.displayName = "RequestList";
-
-RequestList.propTypes = {
- isEmpty: PropTypes.bool.isRequired,
-};
-
-module.exports = RequestList;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/statistics-panel.js
+++ /dev/null
@@ -1,276 +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,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { Chart } = require("devtools/client/shared/widgets/Chart");
-const { PluralForm } = require("devtools/shared/plural-form");
-const Actions = require("../actions/index");
-const { Filters } = require("../utils/filter-predicates");
-const { L10N } = require("../utils/l10n");
-const {
- getSizeWithDecimals,
- getTimeWithDecimals
-} = require("../utils/format-utils");
-
-const { button, div } = DOM;
-const MediaQueryList = window.matchMedia("(min-width: 700px)");
-
-const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
-const BACK_BUTTON = L10N.getStr("netmonitor.backButton");
-const CHARTS_CACHE_ENABLED = L10N.getStr("charts.cacheEnabled");
-const CHARTS_CACHE_DISABLED = L10N.getStr("charts.cacheDisabled");
-
-/*
- * Statistics panel component
- * Performance analysis tool which shows you how long the browser takes to
- * download the different parts of your site.
- */
-const StatisticsPanel = createClass({
- displayName: "StatisticsPanel",
-
- propTypes: {
- closeStatistics: PropTypes.func.isRequired,
- enableRequestFilterTypeOnly: PropTypes.func.isRequired,
- requests: PropTypes.object,
- },
-
- getInitialState() {
- return {
- isVerticalSpliter: MediaQueryList.matches,
- };
- },
-
- componentDidUpdate(prevProps) {
- MediaQueryList.addListener(this.onLayoutChange);
-
- const { requests } = this.props;
- let ready = requests && !requests.isEmpty() && requests.every((req) =>
- req.contentSize !== undefined && req.mimeType && req.responseHeaders &&
- req.status !== undefined && req.totalTime !== undefined
- );
-
- this.createChart({
- id: "primedCacheChart",
- title: CHARTS_CACHE_ENABLED,
- data: ready ? this.sanitizeChartDataSource(requests, false) : null,
- });
-
- this.createChart({
- id: "emptyCacheChart",
- title: CHARTS_CACHE_DISABLED,
- data: ready ? this.sanitizeChartDataSource(requests, true) : null,
- });
- },
-
- componentWillUnmount() {
- MediaQueryList.removeListener(this.onLayoutChange);
- },
-
- createChart({ id, title, data }) {
- // Create a new chart.
- let chart = Chart.PieTable(document, {
- diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
- title,
- header: {
- cached: "",
- count: "",
- label: L10N.getStr("charts.type"),
- size: L10N.getStr("charts.size"),
- transferredSize: L10N.getStr("charts.transferred"),
- time: L10N.getStr("charts.time"),
- },
- data,
- strings: {
- size: (value) =>
- L10N.getFormatStr("charts.sizeKB", getSizeWithDecimals(value / 1024)),
- transferredSize: (value) =>
- L10N.getFormatStr("charts.transferredSizeKB",
- getSizeWithDecimals(value / 1024)),
- time: (value) =>
- L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
- },
- totals: {
- cached: (total) => L10N.getFormatStr("charts.totalCached", total),
- count: (total) => L10N.getFormatStr("charts.totalCount", total),
- size: (total) =>
- L10N.getFormatStr("charts.totalSize", getSizeWithDecimals(total / 1024)),
- transferredSize: total =>
- L10N.getFormatStr("charts.totalTransferredSize",
- getSizeWithDecimals(total / 1024)),
- time: (total) => {
- let seconds = total / 1000;
- let string = getTimeWithDecimals(seconds);
- return PluralForm.get(seconds,
- L10N.getStr("charts.totalSeconds")).replace("#1", string);
- },
- },
- sorted: true,
- });
-
- chart.on("click", (_, { label }) => {
- // Reset FilterButtons and enable one filter exclusively
- this.props.closeStatistics();
- this.props.enableRequestFilterTypeOnly(label);
- });
-
- let container = this.refs[id];
-
- // Nuke all existing charts of the specified type.
- while (container.hasChildNodes()) {
- container.firstChild.remove();
- }
-
- container.appendChild(chart.node);
- },
-
- sanitizeChartDataSource(requests, emptyCache) {
- const data = [
- "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
- ].map((type) => ({
- cached: 0,
- count: 0,
- label: type,
- size: 0,
- transferredSize: 0,
- time: 0,
- }));
-
- for (let request of requests) {
- let type;
-
- if (Filters.html(request)) {
- // "html"
- type = 0;
- } else if (Filters.css(request)) {
- // "css"
- type = 1;
- } else if (Filters.js(request)) {
- // "js"
- type = 2;
- } else if (Filters.fonts(request)) {
- // "fonts"
- type = 4;
- } else if (Filters.images(request)) {
- // "images"
- type = 5;
- } else if (Filters.media(request)) {
- // "media"
- type = 6;
- } else if (Filters.flash(request)) {
- // "flash"
- type = 7;
- } else if (Filters.ws(request)) {
- // "ws"
- type = 8;
- } else if (Filters.xhr(request)) {
- // Verify XHR last, to categorize other mime types in their own blobs.
- // "xhr"
- type = 3;
- } else {
- // "other"
- type = 9;
- }
-
- if (emptyCache || !this.responseIsFresh(request)) {
- data[type].time += request.totalTime || 0;
- data[type].size += request.contentSize || 0;
- data[type].transferredSize += request.transferredSize || 0;
- } else {
- data[type].cached++;
- }
- data[type].count++;
- }
-
- return data.filter(e => e.count > 0);
- },
-
- /**
- * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
- * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
- *
- * @param object
- * An object containing the { responseHeaders, status } properties.
- * @return boolean
- * True if the response is fresh and loaded from cache.
- */
- responseIsFresh({ responseHeaders, status }) {
- // Check for a "304 Not Modified" status and response headers availability.
- if (status != 304 || !responseHeaders) {
- return false;
- }
-
- let list = responseHeaders.headers;
- let cacheControl = list.find(e => e.name.toLowerCase() === "cache-control");
- let expires = list.find(e => e.name.toLowerCase() === "expires");
-
- // Check the "Cache-Control" header for a maximum age value.
- if (cacheControl) {
- let maxAgeMatch =
- cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
- cacheControl.value.match(/max-age\s*=\s*(\d+)/);
-
- if (maxAgeMatch && maxAgeMatch.pop() > 0) {
- return true;
- }
- }
-
- // Check the "Expires" header for a valid date.
- if (expires && Date.parse(expires.value)) {
- return true;
- }
-
- return false;
- },
-
- onLayoutChange() {
- this.setState({
- isVerticalSpliter: MediaQueryList.matches,
- });
- },
-
- render() {
- const { closeStatistics } = this.props;
- let splitterClassName = ["splitter"];
-
- if (this.state.isVerticalSpliter) {
- splitterClassName.push("devtools-side-splitter");
- } else {
- splitterClassName.push("devtools-horizontal-splitter");
- }
-
- return (
- div({ className: "statistics-panel" },
- button({
- className: "back-button devtools-button",
- "data-text-only": "true",
- title: BACK_BUTTON,
- onClick: closeStatistics,
- }, BACK_BUTTON),
- div({ className: "charts-container" },
- div({ ref: "primedCacheChart", className: "charts primed-cache-chart" }),
- div({ className: splitterClassName.join(" ") }),
- div({ ref: "emptyCacheChart", className: "charts empty-cache-chart" }),
- ),
- )
- );
- }
-});
-
-module.exports = connect(
- (state) => ({
- requests: state.requests.requests.valueSeq(),
- }),
- (dispatch) => ({
- closeStatistics: () => dispatch(Actions.openStatistics(false)),
- enableRequestFilterTypeOnly: (label) =>
- dispatch(Actions.enableRequestFilterTypeOnly(label)),
- })
-)(StatisticsPanel);
--- a/devtools/client/netmonitor/components/toolbar.js
+++ b/devtools/client/netmonitor/components/toolbar.js
@@ -8,27 +8,27 @@ const {
createClass,
createFactory,
DOM,
PropTypes,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { PluralForm } = require("devtools/shared/plural-form");
const Actions = require("../actions/index");
-const { L10N } = require("../utils/l10n");
+const { FILTER_SEARCH_DELAY } = require("../constants");
const {
getDisplayedRequestsSummary,
getRequestFilterTypes,
isNetworkDetailsToggleButtonDisabled,
} = require("../selectors/index");
const {
getFormattedSize,
getFormattedTime
} = require("../utils/format-utils");
-const { FILTER_SEARCH_DELAY } = require("../constants");
+const { L10N } = require("../utils/l10n");
// Components
const SearchBox = createFactory(require("devtools/client/shared/components/search-box"));
const { button, div, span } = DOM;
const COLLPASE_DETAILS_PANE = L10N.getStr("collapseDetailsPane");
const EXPAND_DETAILS_PANE = L10N.getStr("expandDetailsPane");
--- a/devtools/client/netmonitor/moz.build
+++ b/devtools/client/netmonitor/moz.build
@@ -4,17 +4,16 @@
DIRS += [
'actions',
'components',
'har',
'middleware',
'reducers',
'selectors',
- 'shared',
'utils',
]
DevToolsModules(
'constants.js',
'netmonitor-controller.js',
'panel.js',
'request-list-context-menu.js',
--- a/devtools/client/netmonitor/netmonitor.js
+++ b/devtools/client/netmonitor/netmonitor.js
@@ -20,25 +20,25 @@ var Netmonitor = {
const { createFactory } = require("devtools/client/shared/vendor/react");
const { render } = require("devtools/client/shared/vendor/react-dom");
const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
const { configureStore } = require("./store");
const store = window.gStore = configureStore();
const { NetMonitorController } = require("./netmonitor-controller");
this.NetMonitorController = NetMonitorController;
- // Components
- const NetworkMonitor = createFactory(require("./components/network-monitor"));
-
// Inject EventEmitter into netmonitor window.
EventEmitter.decorate(window);
+ // Components
+ const App = createFactory(require("./components/App"));
+
this.root = document.querySelector(".root");
- render(Provider({ store }, NetworkMonitor()), this.root);
+ render(Provider({ store }, App()), this.root);
return NetMonitorController.startupNetMonitor({
client: {
getTabTarget: () => toolbox.target,
},
toolbox,
});
},
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/cookies-panel.js
+++ /dev/null
@@ -1,99 +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 {
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-
-// Component
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div } = DOM;
-
-const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText");
-const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText");
-const REQUEST_COOKIES = L10N.getStr("requestCookies");
-const RESPONSE_COOKIES = L10N.getStr("responseCookies");
-const SECTION_NAMES = [
- RESPONSE_COOKIES,
- REQUEST_COOKIES,
-];
-
-/*
- * Cookies panel component
- * This tab lists full details of any cookies sent with the request or response
- */
-function CookiesPanel({
- request,
-}) {
- let {
- requestCookies = { cookies: [] },
- responseCookies = { cookies: [] },
- } = request;
-
- requestCookies = requestCookies.cookies || requestCookies;
- responseCookies = responseCookies.cookies || responseCookies;
-
- if (!requestCookies.length && !responseCookies.length) {
- return div({ className: "empty-notice" },
- COOKIES_EMPTY_TEXT
- );
- }
-
- let object = {};
-
- if (responseCookies.length) {
- object[RESPONSE_COOKIES] = getProperties(responseCookies);
- }
-
- if (requestCookies.length) {
- object[REQUEST_COOKIES] = getProperties(requestCookies);
- }
-
- return (
- div({ className: "panel-container" },
- PropertiesView({
- object,
- filterPlaceHolder: COOKIES_FILTER_TEXT,
- sectionNames: SECTION_NAMES,
- })
- )
- );
-}
-
-CookiesPanel.displayName = "CookiesPanel";
-
-CookiesPanel.propTypes = {
- request: PropTypes.object.isRequired,
-};
-
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- *
- * @param {Object[]} arr - key-value pair array like cookies or params
- * @returns {Object}
- */
-function getProperties(arr) {
- return arr.reduce((map, obj) => {
- // Generally cookies object contains only name and value properties and can
- // be rendered as name: value pair.
- // When there are more properties in cookies object such as extra or path,
- // We will pass the object to display these extra information
- if (Object.keys(obj).length > 2) {
- map[obj.name] = Object.assign({}, obj);
- delete map[obj.name].name;
- } else {
- map[obj.name] = obj.value;
- }
- return map;
- }, {});
-}
-
-module.exports = CookiesPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/custom-request-panel.js
+++ /dev/null
@@ -1,257 +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 {
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { L10N } = require("../../utils/l10n");
-const Actions = require("../../actions/index");
-const { getSelectedRequest } = require("../../selectors/index");
-const {
- getUrlQuery,
- parseQueryString,
- writeHeaderText,
-} = require("../../utils/request-utils");
-
-const {
- button,
- div,
- input,
- textarea,
-} = DOM;
-
-const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
-const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
-const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
-const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
-const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
-const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
-
-function CustomRequestPanel({
- removeSelectedCustomRequest,
- request = {},
- sendCustomRequest,
- updateRequest,
-}) {
- let {
- method,
- customQueryValue,
- requestHeaders,
- requestPostData,
- url,
- } = request;
-
- let headers = "";
- if (requestHeaders) {
- headers = requestHeaders.customHeadersValue ?
- requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
- }
- let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
- let params = customQueryValue;
- if (!params) {
- params = queryArray ?
- queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
- }
- let postData = requestPostData && requestPostData.postData.text ?
- requestPostData.postData.text : "";
-
- return (
- div({ className: "custom-request-panel" },
- div({ className: "tabpanel-summary-container custom-request" },
- div({ className: "custom-request-label custom-header" },
- CUSTOM_NEW_REQUEST
- ),
- button({
- className: "devtools-button",
- id: "custom-request-send-button",
- onClick: sendCustomRequest,
- },
- CUSTOM_SEND
- ),
- button({
- className: "devtools-button",
- id: "custom-request-close-button",
- onClick: removeSelectedCustomRequest,
- },
- CUSTOM_CANCEL
- ),
- ),
- div({
- className: "tabpanel-summary-container custom-method-and-url",
- id: "custom-method-and-url",
- },
- input({
- className: "custom-method-value",
- id: "custom-method-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- value: method || "GET",
- }),
- input({
- className: "custom-url-value",
- id: "custom-url-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- value: url || "http://",
- }),
- ),
- // Hide query field when there is no params
- params ? div({
- className: "tabpanel-summary-container custom-section",
- id: "custom-query",
- },
- div({ className: "custom-request-label" }, CUSTOM_QUERY),
- textarea({
- className: "tabpanel-summary-input",
- id: "custom-query-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- rows: 4,
- value: params,
- wrap: "off",
- })
- ) : null,
- div({
- id: "custom-headers",
- className: "tabpanel-summary-container custom-section",
- },
- div({ className: "custom-request-label" }, CUSTOM_HEADERS),
- textarea({
- className: "tabpanel-summary-input",
- id: "custom-headers-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- rows: 8,
- value: headers,
- wrap: "off",
- })
- ),
- div({
- id: "custom-postdata",
- className: "tabpanel-summary-container custom-section",
- },
- div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
- textarea({
- className: "tabpanel-summary-input",
- id: "custom-postdata-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- rows: 6,
- value: postData,
- wrap: "off",
- })
- ),
- )
- );
-}
-
-CustomRequestPanel.displayName = "CustomRequestPanel";
-
-CustomRequestPanel.propTypes = {
- removeSelectedCustomRequest: PropTypes.func.isRequired,
- request: PropTypes.object,
- sendCustomRequest: PropTypes.func.isRequired,
- updateRequest: PropTypes.func.isRequired,
-};
-
-/**
- * Parse a text representation of a name[divider]value list with
- * the given name regex and divider character.
- *
- * @param {string} text - Text of list
- * @return {array} array of headers info {name, value}
- */
-function parseRequestText(text, namereg, divider) {
- let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
- let pairs = [];
-
- for (let line of text.split("\n")) {
- let matches = regex.exec(line);
- if (matches) {
- let [, name, value] = matches;
- pairs.push({ name, value });
- }
- }
- return pairs;
-}
-
-/**
- * Update Custom Request Fields
- *
- * @param {Object} evt click event
- * @param {Object} request current request
- * @param {updateRequest} updateRequest action
- */
-function updateCustomRequestFields(evt, request, updateRequest) {
- const val = evt.target.value;
- let data;
- switch (evt.target.id) {
- case "custom-headers-value":
- let customHeadersValue = val || "";
- // Parse text representation of multiple HTTP headers
- let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
- // Remove temp customHeadersValue while query string is parsable
- if (customHeadersValue === "" ||
- headersArray.length === customHeadersValue.split("\n").length) {
- customHeadersValue = null;
- }
- data = {
- requestHeaders: {
- customHeadersValue,
- headers: headersArray,
- },
- };
- break;
- case "custom-method-value":
- data = { method: val.trim() };
- break;
- case "custom-postdata-value":
- data = {
- requestPostData: {
- postData: { text: val },
- }
- };
- break;
- case "custom-query-value":
- let customQueryValue = val || "";
- // Parse readable text list of a query string
- let queryArray = customQueryValue ?
- parseRequestText(customQueryValue, ".+?", "=") : [];
- // Write out a list of query params into a query string
- let queryString = queryArray.map(
- ({ name, value }) => name + "=" + value).join("&");
- let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
- request.url.split("?")[0];
- // Remove temp customQueryValue while query string is parsable
- if (customQueryValue === "" ||
- queryArray.length === customQueryValue.split("\n").length) {
- customQueryValue = null;
- }
- data = {
- customQueryValue,
- url,
- };
- break;
- case "custom-url-value":
- data = {
- customQueryValue: null,
- url: val
- };
- break;
- default:
- break;
- }
- if (data) {
- // All updateRequest batch mode should be disabled to make UI editing in sync
- updateRequest(request.id, data, false);
- }
-}
-
-module.exports = connect(
- (state) => ({ request: getSelectedRequest(state) }),
- (dispatch) => ({
- removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
- sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
- updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
- })
-)(CustomRequestPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/editor.js
+++ /dev/null
@@ -1,103 +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/. */
-
-/* eslint-disable react/prop-types */
-
-"use strict";
-
-const { createClass, DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-const SourceEditor = require("devtools/client/sourceeditor/editor");
-
-const { div } = DOM;
-const SYNTAX_HIGHLIGHT_MAX_SIZE = 102400;
-
-/**
- * CodeMirror editor as a React component
- */
-const Editor = createClass({
- displayName: "Editor",
-
- propTypes: {
- // Source editor syntax hightligh mode, which is a mime type defined in CodeMirror
- mode: PropTypes.string,
- // Source editor is displayed if set to true
- open: PropTypes.bool,
- // Source editor content
- text: PropTypes.string,
- },
-
- getDefaultProps() {
- return {
- mode: null,
- open: true,
- text: "",
- };
- },
-
- componentDidMount() {
- const { mode, text } = this.props;
-
- this.editor = new SourceEditor({
- lineNumbers: true,
- mode: text.length < SYNTAX_HIGHLIGHT_MAX_SIZE ? mode : null,
- readOnly: true,
- value: text,
- });
-
- this.deferEditor = this.editor.appendTo(this.refs.editorElement);
- },
-
- componentDidUpdate(prevProps) {
- const { mode, open, text } = this.props;
-
- if (!open) {
- return;
- }
-
- if (prevProps.mode !== mode && text.length < SYNTAX_HIGHLIGHT_MAX_SIZE) {
- this.deferEditor.then(() => {
- this.editor.setMode(mode);
- });
- }
-
- if (prevProps.text !== text) {
- this.deferEditor.then(() => {
- // FIXME: Workaround for browser_net_accessibility test to
- // make sure editor node exists while setting editor text.
- // deferEditor workaround should be removed in bug 1308442
- if (this.refs.editorElement) {
- this.editor.setText(text);
- }
- });
- }
- },
-
- componentWillUnmount() {
- this.deferEditor.then(() => {
- this.editor.destroy();
- this.editor = null;
- });
- this.deferEditor = null;
- },
-
- render() {
- const { open } = this.props;
-
- return (
- div({ className: "editor-container devtools-monospace" },
- div({
- ref: "editorElement",
- className: "editor-mount devtools-monospace",
- // Using visibility instead of display property to avoid breaking
- // CodeMirror indentation
- style: { visibility: open ? "visible" : "hidden" },
- }),
- )
- );
- }
-});
-
-module.exports = Editor;
-
-/* eslint-enable react/prop-types */
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/headers-panel.js
+++ /dev/null
@@ -1,260 +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 { writeHeaderText } = require("../../utils/request-utils");
-const {
- getHeadersURL,
- getHTTPStatusCodeURL,
-} = require("../../utils/mdn-utils");
-const { getFormattedSize } = require("../../utils/format-utils");
-const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
-const Rep = createFactory(REPS.Rep);
-
-// Components
-const MDNLink = createFactory(require("./mdn-link"));
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { button, div, input, textarea } = DOM;
-
-const EDIT_AND_RESEND = L10N.getStr("netmonitor.summary.editAndResend");
-const RAW_HEADERS = L10N.getStr("netmonitor.summary.rawHeaders");
-const RAW_HEADERS_REQUEST = L10N.getStr("netmonitor.summary.rawHeaders.requestHeaders");
-const RAW_HEADERS_RESPONSE = L10N.getStr("netmonitor.summary.rawHeaders.responseHeaders");
-const HEADERS_EMPTY_TEXT = L10N.getStr("headersEmptyText");
-const HEADERS_FILTER_TEXT = L10N.getStr("headersFilterText");
-const REQUEST_HEADERS = L10N.getStr("requestHeaders");
-const REQUEST_HEADERS_FROM_UPLOAD = L10N.getStr("requestHeadersFromUpload");
-const RESPONSE_HEADERS = L10N.getStr("responseHeaders");
-const SUMMARY_ADDRESS = L10N.getStr("netmonitor.summary.address");
-const SUMMARY_METHOD = L10N.getStr("netmonitor.summary.method");
-const SUMMARY_URL = L10N.getStr("netmonitor.summary.url");
-const SUMMARY_STATUS = L10N.getStr("netmonitor.summary.status");
-const SUMMARY_VERSION = L10N.getStr("netmonitor.summary.version");
-
-/*
- * Headers panel component
- * Lists basic information about the request
- */
-const HeadersPanel = createClass({
- displayName: "HeadersPanel",
-
- propTypes: {
- cloneSelectedRequest: PropTypes.func.isRequired,
- request: PropTypes.object.isRequired,
- renderValue: PropTypes.func
- },
-
- getInitialState() {
- return {
- rawHeadersOpened: false,
- };
- },
-
- getProperties(headers, title) {
- if (headers && headers.headers.length) {
- return {
- [`${title} (${getFormattedSize(headers.headersSize, 3)})`]:
- headers.headers.reduce((acc, { name, value }) =>
- name ? Object.assign(acc, { [name]: value }) : acc
- , {})
- };
- }
-
- return null;
- },
-
- toggleRawHeaders() {
- this.setState({
- rawHeadersOpened: !this.state.rawHeadersOpened,
- });
- },
-
- renderSummary(label, value) {
- return (
- div({ className: "tabpanel-summary-container headers-summary" },
- div({
- className: "tabpanel-summary-label headers-summary-label",
- }, label),
- input({
- className: "tabpanel-summary-value textbox-input devtools-monospace",
- readOnly: true,
- value,
- }),
- )
- );
- },
-
- renderValue(props) {
- const member = props.member;
- const value = props.value;
-
- if (typeof value !== "string") {
- return null;
- }
-
- let headerDocURL = getHeadersURL(member.name);
-
- return (
- div({ className: "treeValueCellDivider" },
- Rep(Object.assign(props, {
- // FIXME: A workaround for the issue in StringRep
- // Force StringRep to crop the text everytime
- member: Object.assign({}, member, { open: false }),
- mode: MODE.TINY,
- cropLimit: 60,
- })),
- headerDocURL ? MDNLink({
- url: headerDocURL,
- }) : null
- )
- );
- },
-
- render() {
- const {
- cloneSelectedRequest,
- request: {
- fromCache,
- fromServiceWorker,
- httpVersion,
- method,
- remoteAddress,
- remotePort,
- requestHeaders,
- requestHeadersFromUploadStream: uploadHeaders,
- responseHeaders,
- status,
- statusText,
- urlDetails,
- },
- } = this.props;
-
- if ((!requestHeaders || !requestHeaders.headers.length) &&
- (!uploadHeaders || !uploadHeaders.headers.length) &&
- (!responseHeaders || !responseHeaders.headers.length)) {
- return div({ className: "empty-notice" },
- HEADERS_EMPTY_TEXT
- );
- }
-
- let object = Object.assign({},
- this.getProperties(responseHeaders, RESPONSE_HEADERS),
- this.getProperties(requestHeaders, REQUEST_HEADERS),
- this.getProperties(uploadHeaders, REQUEST_HEADERS_FROM_UPLOAD),
- );
-
- let summaryUrl = urlDetails.unicodeUrl ?
- this.renderSummary(SUMMARY_URL, urlDetails.unicodeUrl) : null;
-
- let summaryMethod = method ?
- this.renderSummary(SUMMARY_METHOD, method) : null;
-
- let summaryAddress = remoteAddress ?
- this.renderSummary(SUMMARY_ADDRESS,
- remotePort ? `${remoteAddress}:${remotePort}` : remoteAddress) : null;
-
- let summaryStatus;
-
- if (status) {
- let code;
- if (fromCache) {
- code = "cached";
- } else if (fromServiceWorker) {
- code = "service worker";
- } else {
- code = status;
- }
-
- let statusCodeDocURL = getHTTPStatusCodeURL(code);
- let inputWidth = status.length + statusText.length + 1;
-
- summaryStatus = (
- div({ className: "tabpanel-summary-container headers-summary" },
- div({
- className: "tabpanel-summary-label headers-summary-label",
- }, SUMMARY_STATUS),
- div({
- className: "requests-list-status-icon",
- "data-code": code,
- }),
- input({
- className: "tabpanel-summary-value textbox-input devtools-monospace"
- + " status-text",
- readOnly: true,
- value: `${status} ${statusText}`,
- size: `${inputWidth}`,
- }),
- statusCodeDocURL ? MDNLink({
- url: statusCodeDocURL,
- }) : null,
- window.NetMonitorController.supportsCustomRequest && button({
- className: "devtools-button",
- onClick: cloneSelectedRequest,
- }, EDIT_AND_RESEND),
- button({
- className: "devtools-button",
- onClick: this.toggleRawHeaders,
- }, RAW_HEADERS),
- )
- );
- }
-
- let summaryVersion = httpVersion ?
- this.renderSummary(SUMMARY_VERSION, httpVersion) : null;
-
- let summaryRawHeaders;
- if (this.state.rawHeadersOpened) {
- summaryRawHeaders = (
- div({ className: "tabpanel-summary-container headers-summary" },
- div({ className: "raw-headers-container" },
- div({ className: "raw-headers" },
- div({ className: "tabpanel-summary-label" }, RAW_HEADERS_REQUEST),
- textarea({
- value: writeHeaderText(requestHeaders.headers),
- readOnly: true,
- }),
- ),
- div({ className: "raw-headers" },
- div({ className: "tabpanel-summary-label" }, RAW_HEADERS_RESPONSE),
- textarea({
- value: writeHeaderText(responseHeaders.headers),
- readOnly: true,
- }),
- ),
- )
- )
- );
- }
-
- return (
- div({ className: "panel-container" },
- div({ className: "headers-overview" },
- summaryUrl,
- summaryMethod,
- summaryAddress,
- summaryStatus,
- summaryVersion,
- summaryRawHeaders,
- ),
- PropertiesView({
- object,
- filterPlaceHolder: HEADERS_FILTER_TEXT,
- sectionNames: Object.keys(object),
- renderValue: this.renderValue,
- }),
- )
- );
- }
-});
-
-module.exports = HeadersPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/mdn-link.js
+++ /dev/null
@@ -1,47 +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 Services = require("Services");
-const {
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { gDevTools } = require("devtools/client/framework/devtools");
-const { L10N } = require("../../utils/l10n");
-
-const { a } = DOM;
-
-const LEARN_MORE = L10N.getStr("netmonitor.headers.learnMore");
-
-function MDNLink({ url }) {
- return (
- a({
- className: "learn-more-link",
- title: url,
- onClick: (e) => onLearnMoreClick(e, url),
- }, `[${LEARN_MORE}]`)
- );
-}
-
-MDNLink.displayName = "MDNLink";
-
-MDNLink.propTypes = {
- url: PropTypes.string.isRequired,
-};
-
-function onLearnMoreClick(e, url) {
- e.stopPropagation();
- e.preventDefault();
-
- let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
- if (e.button === 1) {
- win.openUILinkIn(url, "tabshifted");
- } else {
- win.openUILinkIn(url, "tab");
- }
-}
-
-module.exports = MDNLink;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/moz.build
+++ /dev/null
@@ -1,19 +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/.
-
-DevToolsModules(
- 'cookies-panel.js',
- 'custom-request-panel.js',
- 'editor.js',
- 'headers-panel.js',
- 'mdn-link.js',
- 'network-details-panel.js',
- 'params-panel.js',
- 'preview-panel.js',
- 'properties-view.js',
- 'response-panel.js',
- 'security-panel.js',
- 'tabbox-panel.js',
- 'timings-panel.js',
-)
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/network-details-panel.js
+++ /dev/null
@@ -1,70 +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 {
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const Actions = require("../../actions/index");
-const { getSelectedRequest } = require("../../selectors/index");
-
-// Components
-const CustomRequestPanel = createFactory(require("./custom-request-panel"));
-const TabboxPanel = createFactory(require("./tabbox-panel"));
-
-const { div } = DOM;
-
-/*
- * Network details panel component
- */
-function NetworkDetailsPanel({
- activeTabId,
- cloneSelectedRequest,
- request,
- selectTab,
-}) {
- if (!request) {
- return null;
- }
-
- return (
- div({ className: "network-details-panel" },
- !request.isCustom ?
- TabboxPanel({
- activeTabId,
- request,
- selectTab,
- }) :
- CustomRequestPanel({
- cloneSelectedRequest,
- request,
- })
- )
- );
-}
-
-NetworkDetailsPanel.displayName = "NetworkDetailsPanel";
-
-NetworkDetailsPanel.propTypes = {
- activeTabId: PropTypes.string,
- cloneSelectedRequest: PropTypes.func.isRequired,
- open: PropTypes.bool,
- request: PropTypes.object,
- selectTab: PropTypes.func.isRequired,
-};
-
-module.exports = connect(
- (state) => ({
- activeTabId: state.ui.detailsPanelSelectedTab,
- request: getSelectedRequest(state),
- }),
- (dispatch) => ({
- cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
- selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
- }),
-)(NetworkDetailsPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/params-panel.js
+++ /dev/null
@@ -1,132 +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 {
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-const { getUrlQuery, parseQueryString } = require("../../utils/request-utils");
-
-// Components
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div } = DOM;
-
-const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
-const PARAMS_EMPTY_TEXT = L10N.getStr("paramsEmptyText");
-const PARAMS_FILTER_TEXT = L10N.getStr("paramsFilterText");
-const PARAMS_FORM_DATA = L10N.getStr("paramsFormData");
-const PARAMS_POST_PAYLOAD = L10N.getStr("paramsPostPayload");
-const PARAMS_QUERY_STRING = L10N.getStr("paramsQueryString");
-const SECTION_NAMES = [
- JSON_SCOPE_NAME,
- PARAMS_FORM_DATA,
- PARAMS_POST_PAYLOAD,
- PARAMS_QUERY_STRING,
-];
-
-/*
- * Params panel component
- * Displays the GET parameters and POST data of a request
- */
-function ParamsPanel({
- request,
-}) {
- let {
- formDataSections,
- mimeType,
- requestPostData,
- url,
- } = request;
- let postData = requestPostData ? requestPostData.postData.text : null;
- let query = getUrlQuery(url);
-
- if (!formDataSections && !postData && !query) {
- return div({ className: "empty-notice" },
- PARAMS_EMPTY_TEXT
- );
- }
-
- let object = {};
- let json;
-
- // Query String section
- if (query) {
- object[PARAMS_QUERY_STRING] = getProperties(parseQueryString(query));
- }
-
- // Form Data section
- if (formDataSections && formDataSections.length > 0) {
- let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
- object[PARAMS_FORM_DATA] = getProperties(parseQueryString(sections));
- }
-
- // Request payload section
- if (formDataSections && formDataSections.length === 0 && postData) {
- try {
- json = JSON.parse(postData);
- } catch (error) {
- // Continue regardless of parsing error
- }
-
- if (json) {
- object[JSON_SCOPE_NAME] = json;
- } else {
- object[PARAMS_POST_PAYLOAD] = {
- EDITOR_CONFIG: {
- text: postData,
- mode: mimeType.replace(/;.+/, ""),
- },
- };
- }
- } else {
- postData = "";
- }
-
- return (
- div({ className: "panel-container" },
- PropertiesView({
- object,
- filterPlaceHolder: PARAMS_FILTER_TEXT,
- sectionNames: SECTION_NAMES,
- })
- )
- );
-}
-
-ParamsPanel.displayName = "ParamsPanel";
-
-ParamsPanel.propTypes = {
- request: PropTypes.object.isRequired,
-};
-
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- * This function also deal with duplicate key case
- * (for multiple selection and query params with same keys)
- *
- * @param {Object[]} arr - key-value pair array like query or form params
- * @returns {Object} Rep compatible object
- */
-function getProperties(arr) {
- return arr.reduce((map, obj) => {
- let value = map[obj.name];
- if (value) {
- if (typeof value !== "object") {
- map[obj.name] = [value];
- }
- map[obj.name].push(obj.value);
- } else {
- map[obj.name] = obj.value;
- }
- return map;
- }, {});
-}
-
-module.exports = ParamsPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/preview-panel.js
+++ /dev/null
@@ -1,37 +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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-
-const { div, iframe } = DOM;
-
-/*
- * Preview panel component
- * Display HTML content within a sandbox enabled iframe
- */
-function PreviewPanel({
- request,
-}) {
- const htmlBody = request.responseContent ?
- request.responseContent.content.text : "";
-
- return (
- div({ className: "panel-container" },
- iframe({
- sandbox: "",
- srcDoc: typeof htmlBody === "string" ? htmlBody : "",
- })
- )
- );
-}
-
-PreviewPanel.displayName = "PreviewPanel";
-
-PreviewPanel.propTypes = {
- request: PropTypes.object.isRequired,
-};
-
-module.exports = PreviewPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/properties-view.js
+++ /dev/null
@@ -1,218 +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/. */
-
-/* eslint-disable react/prop-types */
-
-"use strict";
-
-const {
- createClass,
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-
-const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
-const Rep = createFactory(REPS.Rep);
-
-const { FILTER_SEARCH_DELAY } = require("../../constants");
-
-// Components
-const Editor = createFactory(require("devtools/client/netmonitor/shared/components/editor"));
-const SearchBox = createFactory(require("devtools/client/shared/components/search-box"));
-const TreeView = createFactory(require("devtools/client/shared/components/tree/tree-view"));
-const TreeRow = createFactory(require("devtools/client/shared/components/tree/tree-row"));
-
-const { div, tr, td } = DOM;
-const AUTO_EXPAND_MAX_LEVEL = 7;
-const AUTO_EXPAND_MAX_NODES = 50;
-const EDITOR_CONFIG_ID = "EDITOR_CONFIG";
-
-/*
- * Properties View component
- * A scrollable tree view component which provides some useful features for
- * representing object properties.
- *
- * Search filter - Set enableFilter to enable / disable SearchBox feature.
- * Tree view - Default enabled.
- * Source editor - Enable by specifying object level 1 property name to EDITOR_CONFIG_ID.
- * Rep - Default enabled.
- */
-const PropertiesView = createClass({
- displayName: "PropertiesView",
-
- propTypes: {
- object: PropTypes.object,
- enableInput: PropTypes.bool,
- expandableStrings: PropTypes.bool,
- filterPlaceHolder: PropTypes.string,
- sectionNames: PropTypes.array,
- },
-
- getDefaultProps() {
- return {
- enableInput: true,
- enableFilter: true,
- expandableStrings: false,
- filterPlaceHolder: "",
- sectionNames: [],
- };
- },
-
- getInitialState() {
- return {
- filterText: "",
- };
- },
-
- getRowClass(object, sectionNames) {
- return sectionNames.includes(object.name) ? "tree-section" : "";
- },
-
- onFilter(object, whiteList) {
- let { name, value } = object;
- let filterText = this.state.filterText;
-
- if (!filterText || whiteList.includes(name)) {
- return true;
- }
-
- let jsonString = JSON.stringify({ [name]: value }).toLowerCase();
- return jsonString.includes(filterText.toLowerCase());
- },
-
- renderRowWithEditor(props) {
- const { level, name, value, path } = props.member;
-
- // Display source editor when specifying to EDITOR_CONFIG_ID along with config
- if (level === 1 && name === EDITOR_CONFIG_ID) {
- return (
- tr({ className: "editor-row-container" },
- td({ colSpan: 2 },
- Editor(value)
- )
- )
- );
- }
-
- // Skip for editor config
- if (level >= 1 && path.includes(EDITOR_CONFIG_ID)) {
- return null;
- }
-
- return TreeRow(props);
- },
-
- renderValueWithRep(props) {
- const { member } = props;
-
- // Hide strings with following conditions
- // 1. this row is a togglable section and content is object ('cause it shouldn't hide
- // when string or number)
- // 2. the `value` object has a `value` property, only happened in Cookies panel
- // Put 2 here to not dup this method
- if (member.level === 0 && member.type === "object" ||
- (typeof member.value === "object" && member.value && member.value.value)) {
- return null;
- }
-
- return Rep(Object.assign(props, {
- // FIXME: A workaround for the issue in StringRep
- // Force StringRep to crop the text everytime
- member: Object.assign({}, member, { open: false }),
- mode: MODE.TINY,
- cropLimit: 60,
- }));
- },
-
- shouldRenderSearchBox(object) {
- return this.props.enableFilter && object && Object.keys(object)
- .filter((section) => !object[section][EDITOR_CONFIG_ID]).length > 0;
- },
-
- updateFilterText(filterText) {
- this.setState({
- filterText,
- });
- },
-
- getExpandedNodes: function (object, path = "", level = 0) {
- if (typeof object != "object") {
- return null;
- }
-
- if (level > AUTO_EXPAND_MAX_LEVEL) {
- return null;
- }
-
- let expandedNodes = new Set();
- for (let prop in object) {
- if (expandedNodes.size > AUTO_EXPAND_MAX_NODES) {
- // If we reached the limit of expandable nodes, bail out to avoid performance
- // issues.
- break;
- }
-
- let nodePath = path + "/" + prop;
- expandedNodes.add(nodePath);
-
- let nodes = this.getExpandedNodes(object[prop], nodePath, level + 1);
- if (nodes) {
- let newSize = expandedNodes.size + nodes.size;
- if (newSize < AUTO_EXPAND_MAX_NODES) {
- // Avoid having a subtree half expanded.
- expandedNodes = new Set([...expandedNodes, ...nodes]);
- }
- }
- }
- return expandedNodes;
- },
-
- render() {
- const {
- decorator,
- enableInput,
- expandableStrings,
- filterPlaceHolder,
- object,
- renderRow,
- renderValue,
- sectionNames,
- } = this.props;
-
- return (
- div({ className: "properties-view" },
- this.shouldRenderSearchBox(object) &&
- div({ className: "searchbox-section" },
- SearchBox({
- delay: FILTER_SEARCH_DELAY,
- type: "filter",
- onChange: this.updateFilterText,
- placeholder: filterPlaceHolder,
- }),
- ),
- div({ className: "tree-container" },
- TreeView({
- object,
- columns: [{
- id: "value",
- width: "100%",
- }],
- decorator: decorator || {
- getRowClass: (rowObject) => this.getRowClass(rowObject, sectionNames),
- },
- enableInput,
- expandableStrings,
- expandedNodes: this.getExpandedNodes(object),
- onFilter: (props) => this.onFilter(props, sectionNames),
- renderRow: renderRow || this.renderRowWithEditor,
- renderValue: renderValue || this.renderValueWithRep,
- }),
- ),
- )
- );
- }
-});
-
-module.exports = PropertiesView;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/response-panel.js
+++ /dev/null
@@ -1,185 +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 { formDataURI, getUrlBaseName } = require("../../utils/request-utils");
-
-// Components
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div, img } = DOM;
-const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
-const JSON_FILTER_TEXT = L10N.getStr("jsonFilterText");
-const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name");
-const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions");
-const RESPONSE_IMG_MIMETYPE = L10N.getStr("netmonitor.response.mime");
-const RESPONSE_PAYLOAD = L10N.getStr("responsePayload");
-
-/*
- * Response panel component
- * Displays the GET parameters and POST data of a request
- */
-const ResponsePanel = createClass({
- displayName: "ResponsePanel",
-
- propTypes: {
- request: PropTypes.object.isRequired,
- },
-
- getInitialState() {
- return {
- imageDimensions: {
- width: 0,
- height: 0,
- },
- };
- },
-
- updateImageDimemsions({ target }) {
- this.setState({
- imageDimensions: {
- width: target.naturalWidth,
- height: target.naturalHeight,
- },
- });
- },
-
- // Handle json, which we tentatively identify by checking the MIME type
- // for "json" after any word boundary. This works for the standard
- // "application/json", and also for custom types like "x-bigcorp-json".
- // Additionally, we also directly parse the response text content to
- // verify whether it's json or not, to handle responses incorrectly
- // labeled as text/plain instead.
- isJSON(mimeType, response) {
- let json, error;
- try {
- json = JSON.parse(response);
- } catch (err) {
- try {
- json = JSON.parse(atob(response));
- } catch (err64) {
- error = err;
- }
- }
-
- if (/\bjson/.test(mimeType) || json) {
- // Extract the actual json substring in case this might be a "JSONP".
- // This regex basically parses a function call and captures the
- // function name and arguments in two separate groups.
- let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
- let [, jsonpCallback, jsonp] = response.match(jsonpRegex) || [];
- let result = {};
-
- // Make sure this is a valid JSON object first. If so, nicely display
- // the parsing results in a tree view.
- if (jsonpCallback && jsonp) {
- error = null;
- try {
- json = JSON.parse(jsonp);
- } catch (err) {
- error = err;
- }
- }
-
- // Valid JSON
- if (json) {
- result.json = json;
- }
- // Valid JSONP
- if (jsonpCallback) {
- result.jsonpCallback = jsonpCallback;
- }
- // Malformed JSON
- if (error) {
- result.error = "" + error;
- }
-
- return result;
- }
-
- return null;
- },
-
- render() {
- let { responseContent, url } = this.props.request;
-
- if (!responseContent || typeof responseContent.content.text !== "string") {
- return null;
- }
-
- let { encoding, mimeType, text } = responseContent.content;
-
- if (mimeType.includes("image/")) {
- let { width, height } = this.state.imageDimensions;
-
- return (
- div({ className: "panel-container response-image-box devtools-monospace" },
- img({
- className: "response-image",
- src: formDataURI(mimeType, encoding, text),
- onLoad: this.updateImageDimemsions,
- }),
- div({ className: "response-summary" },
- div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_NAME),
- div({ className: "tabpanel-summary-value" }, getUrlBaseName(url)),
- ),
- div({ className: "response-summary" },
- div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_DIMENSIONS),
- div({ className: "tabpanel-summary-value" }, `${width} × ${height}`),
- ),
- div({ className: "response-summary" },
- div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_MIMETYPE),
- div({ className: "tabpanel-summary-value" }, mimeType),
- ),
- )
- );
- }
-
- // Display Properties View
- let { json, jsonpCallback, error } = this.isJSON(mimeType, text) || {};
- let object = {};
- let sectionName;
-
- if (json) {
- if (jsonpCallback) {
- sectionName = L10N.getFormatStr("jsonpScopeName", jsonpCallback);
- } else {
- sectionName = JSON_SCOPE_NAME;
- }
- object[sectionName] = json;
- } else {
- sectionName = RESPONSE_PAYLOAD;
-
- object[sectionName] = {
- EDITOR_CONFIG: {
- text,
- mode: mimeType.replace(/;.+/, ""),
- },
- };
- }
-
- return (
- div({ className: "panel-container" },
- error && div({ className: "response-error-header", title: error },
- error
- ),
- PropertiesView({
- object,
- filterPlaceHolder: JSON_FILTER_TEXT,
- sectionNames: [sectionName],
- }),
- )
- );
- }
-});
-
-module.exports = ResponsePanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/security-panel.js
+++ /dev/null
@@ -1,162 +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 {
- createFactory,
- DOM,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-const { getUrlHost } = require("../../utils/request-utils");
-
-// Components
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div, input, span } = DOM;
-
-/*
- * Security panel component
- * If the site is being served over HTTPS, you get an extra tab labeled "Security".
- * This contains details about the secure connection used including the protocol,
- * the cipher suite, and certificate details
- */
-function SecurityPanel({
- request,
-}) {
- const { securityInfo, url } = request;
-
- if (!securityInfo || !url) {
- return null;
- }
-
- const notAvailable = L10N.getStr("netmonitor.security.notAvailable");
- let object;
-
- if (securityInfo.state === "secure" || securityInfo.state === "weak") {
- const { subject, issuer, validity, fingerprint } = securityInfo.cert;
- const enabledLabel = L10N.getStr("netmonitor.security.enabled");
- const disabledLabel = L10N.getStr("netmonitor.security.disabled");
-
- object = {
- [L10N.getStr("netmonitor.security.connection")]: {
- [L10N.getStr("netmonitor.security.protocolVersion")]:
- securityInfo.protocolVersion || notAvailable,
- [L10N.getStr("netmonitor.security.cipherSuite")]:
- securityInfo.cipherSuite || notAvailable,
- },
- [L10N.getFormatStr("netmonitor.security.hostHeader", getUrlHost(url))]: {
- [L10N.getStr("netmonitor.security.hsts")]:
- securityInfo.hsts ? enabledLabel : disabledLabel,
- [L10N.getStr("netmonitor.security.hpkp")]:
- securityInfo.hpkp ? enabledLabel : disabledLabel,
- },
- [L10N.getStr("netmonitor.security.certificate")]: {
- [L10N.getStr("certmgr.subjectinfo.label")]: {
- [L10N.getStr("certmgr.certdetail.cn")]:
- subject.commonName || notAvailable,
- [L10N.getStr("certmgr.certdetail.o")]:
- subject.organization || notAvailable,
- [L10N.getStr("certmgr.certdetail.ou")]:
- subject.organizationUnit || notAvailable,
- },
- [L10N.getStr("certmgr.issuerinfo.label")]: {
- [L10N.getStr("certmgr.certdetail.cn")]:
- issuer.commonName || notAvailable,
- [L10N.getStr("certmgr.certdetail.o")]:
- issuer.organization || notAvailable,
- [L10N.getStr("certmgr.certdetail.ou")]:
- issuer.organizationUnit || notAvailable,
- },
- [L10N.getStr("certmgr.periodofvalidity.label")]: {
- [L10N.getStr("certmgr.begins")]:
- validity.start || notAvailable,
- [L10N.getStr("certmgr.expires")]:
- validity.end || notAvailable,
- },
- [L10N.getStr("certmgr.fingerprints.label")]: {
- [L10N.getStr("certmgr.certdetail.sha256fingerprint")]:
- fingerprint.sha256 || notAvailable,
- [L10N.getStr("certmgr.certdetail.sha1fingerprint")]:
- fingerprint.sha1 || notAvailable,
- },
- },
- };
- } else {
- object = {
- [L10N.getStr("netmonitor.security.error")]:
- new DOMParser().parseFromString(securityInfo.errorMessage, "text/html")
- .body.textContent || notAvailable
- };
- }
-
- return div({ className: "panel-container security-panel" },
- PropertiesView({
- object,
- renderValue: (props) => renderValue(props, securityInfo.weaknessReasons),
- enableFilter: false,
- expandedNodes: getExpandedNodes(object),
- })
- );
-}
-
-SecurityPanel.displayName = "SecurityPanel";
-
-SecurityPanel.propTypes = {
- request: PropTypes.object.isRequired,
-};
-
-function renderValue(props, weaknessReasons = []) {
- const { member, value } = props;
-
- // Hide object summary
- if (typeof member.value === "object") {
- return null;
- }
-
- return span({ className: "security-info-value" },
- member.name === L10N.getStr("netmonitor.security.error") ?
- // Display multiline text for security error
- value
- :
- // Display one line selectable text for security details
- input({
- className: "textbox-input",
- readOnly: "true",
- value,
- })
- ,
- weaknessReasons.indexOf("cipher") !== -1 &&
- member.name === L10N.getStr("netmonitor.security.cipherSuite") ?
- // Display an extra warning icon after the cipher suite
- div({
- id: "security-warning-cipher",
- className: "security-warning-icon",
- title: L10N.getStr("netmonitor.security.warning.cipher"),
- })
- :
- null
- );
-}
-
-function getExpandedNodes(object, path = "", level = 0) {
- if (typeof object !== "object") {
- return null;
- }
-
- let expandedNodes = new Set();
- for (let prop in object) {
- let nodePath = path + "/" + prop;
- expandedNodes.add(nodePath);
-
- let nodes = getExpandedNodes(object[prop], nodePath, level + 1);
- if (nodes) {
- expandedNodes = new Set([...expandedNodes, ...nodes]);
- }
- }
- return expandedNodes;
-}
-
-module.exports = SecurityPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/tabbox-panel.js
+++ /dev/null
@@ -1,123 +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 {
- createFactory,
- PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const Actions = require("../../actions/index");
-const { Filters } = require("../../utils/filter-predicates");
-const { L10N } = require("../../utils/l10n");
-const { getSelectedRequest } = require("../../selectors/index");
-
-// Components
-const Tabbar = createFactory(require("devtools/client/shared/components/tabs/tabbar"));
-const TabPanel = createFactory(require("devtools/client/shared/components/tabs/tabs").TabPanel);
-const CookiesPanel = createFactory(require("./cookies-panel"));
-const HeadersPanel = createFactory(require("./headers-panel"));
-const ParamsPanel = createFactory(require("./params-panel"));
-const PreviewPanel = createFactory(require("./preview-panel"));
-const ResponsePanel = createFactory(require("./response-panel"));
-const SecurityPanel = createFactory(require("./security-panel"));
-const TimingsPanel = createFactory(require("./timings-panel"));
-
-const HEADERS_TITLE = L10N.getStr("netmonitor.tab.headers");
-const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
-const PARAMS_TITLE = L10N.getStr("netmonitor.tab.params");
-const RESPONSE_TITLE = L10N.getStr("netmonitor.tab.response");
-const TIMINGS_TITLE = L10N.getStr("netmonitor.tab.timings");
-const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
-const PREVIEW_TITLE = L10N.getStr("netmonitor.tab.preview");
-
-/*
- * Tabbox panel component
- * Display the network request details
- */
-function TabboxPanel({
- activeTabId,
- cloneSelectedRequest,
- request,
- selectTab,
-}) {
- if (!request) {
- return null;
- }
-
- return (
- Tabbar({
- activeTabId,
- onSelect: selectTab,
- renderOnlySelected: true,
- showAllTabsMenu: true,
- },
- TabPanel({
- id: "headers",
- title: HEADERS_TITLE,
- },
- HeadersPanel({ request, cloneSelectedRequest }),
- ),
- TabPanel({
- id: "cookies",
- title: COOKIES_TITLE,
- },
- CookiesPanel({ request }),
- ),
- TabPanel({
- id: "params",
- title: PARAMS_TITLE,
- },
- ParamsPanel({ request }),
- ),
- TabPanel({
- id: "response",
- title: RESPONSE_TITLE,
- },
- ResponsePanel({ request }),
- ),
- TabPanel({
- id: "timings",
- title: TIMINGS_TITLE,
- },
- TimingsPanel({ request }),
- ),
- request.securityState && request.securityState !== "insecure" &&
- TabPanel({
- id: "security",
- title: SECURITY_TITLE,
- },
- SecurityPanel({ request }),
- ),
- Filters.html(request) &&
- TabPanel({
- id: "preview",
- title: PREVIEW_TITLE,
- },
- PreviewPanel({ request }),
- ),
- )
- );
-}
-
-TabboxPanel.displayName = "TabboxPanel";
-
-TabboxPanel.propTypes = {
- activeTabId: PropTypes.string,
- cloneSelectedRequest: PropTypes.func.isRequired,
- request: PropTypes.object,
- selectTab: PropTypes.func.isRequired,
-};
-
-module.exports = connect(
- (state) => ({
- activeTabId: state.ui.detailsPanelSelectedTab,
- request: getSelectedRequest(state),
- }),
- (dispatch) => ({
- cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
- selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
- }),
-)(TabboxPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/timings-panel.js
+++ /dev/null
@@ -1,72 +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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-
-const { div, span } = DOM;
-const types = ["blocked", "dns", "connect", "send", "wait", "receive"];
-const TIMINGS_END_PADDING = "80px";
-
-/*
- * Timings panel component
- * Display timeline bars that shows the total wait time for various stages
- */
-function TimingsPanel({
- request,
-}) {
- if (!request.eventTimings) {
- return null;
- }
-
- const { timings, totalTime } = request.eventTimings;
- const timelines = types.map((type, idx) => {
- // Determine the relative offset for each timings box. For example, the
- // offset of third timings box will be 0 + blocked offset + dns offset
- const offset = types
- .slice(0, idx)
- .reduce((acc, cur) => (acc + timings[cur] || 0), 0);
- const offsetScale = offset / totalTime || 0;
- const timelineScale = timings[type] / totalTime || 0;
-
- return div({
- key: type,
- id: `timings-summary-${type}`,
- className: "tabpanel-summary-container timings-container",
- },
- span({ className: "tabpanel-summary-label timings-label" },
- L10N.getStr(`netmonitor.timings.${type}`)
- ),
- div({ className: "requests-list-timings-container" },
- span({
- className: "requests-list-timings-offset",
- style: {
- width: `calc(${offsetScale} * (100% - ${TIMINGS_END_PADDING})`,
- },
- }),
- span({
- className: `requests-list-timings-box ${type}`,
- style: {
- width: `calc(${timelineScale} * (100% - ${TIMINGS_END_PADDING}))`,
- },
- }),
- span({ className: "requests-list-timings-total" },
- L10N.getFormatStr("networkMenu.totalMS", timings[type])
- )
- ),
- );
- });
-
- return div({ className: "panel-container" }, timelines);
-}
-
-TimingsPanel.displayName = "TimingsPanel";
-
-TimingsPanel.propTypes = {
- request: PropTypes.object.isRequired,
-};
-
-module.exports = TimingsPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/moz.build
+++ /dev/null
@@ -1,7 +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/.
-
-DIRS += [
- 'components',
-]