--- a/devtools/client/netmonitor/index.html
+++ b/devtools/client/netmonitor/index.html
@@ -35,17 +35,17 @@
// Export NetMonitorController to global window
// FIXME: Use module export mechanism instead of this tricky global variables
window.NetMonitorController = NetMonitorController;
window.Netmonitor = {
bootstrap({ toolbox }) {
this.mount = document.querySelector("#mount");
- const App = createFactory(require("./src/components/App"));
+ const App = createFactory(require("./src/components/app"));
render(Provider({ store }, App()), this.mount);
return NetMonitorController.startupNetMonitor({
client: {
getTabTarget: () => toolbox.target,
},
toolbox,
});
},
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/App.js
+++ /dev/null
@@ -1,40 +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("./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);
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/CookiesPanel.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("./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;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/HeadersPanel.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 { 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;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/MonitorPanel.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("./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);
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/NetworkDetailsPanel.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("./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);
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/ParamsPanel.js
+++ /dev/null
@@ -1,130 +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("./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;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/PropertiesView.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 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;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestList.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("./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;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestListContent.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("./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);
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/ResponsePanel.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("./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;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/SecurityPanel.js
+++ /dev/null
@@ -1,160 +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("./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;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/TabboxPanel.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("./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/src/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("./monitor-panel"));
+const StatisticsPanel = createFactory(require("./statistics-panel"));
+
+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/src/components/cookies-panel.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("./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;
rename from devtools/client/netmonitor/src/components/CustomRequestPanel.js
rename to devtools/client/netmonitor/src/components/custom-request-panel.js
rename from devtools/client/netmonitor/src/components/Editor.js
rename to devtools/client/netmonitor/src/components/editor.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/headers-panel.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("./mdn-link"));
+const PropertiesView = createFactory(require("./properties-view"));
+
+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;
rename from devtools/client/netmonitor/src/components/MDNLink.js
rename to devtools/client/netmonitor/src/components/mdn-link.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/monitor-panel.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("./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/src/components/moz.build
+++ b/devtools/client/netmonitor/src/components/moz.build
@@ -1,28 +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(
- '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',
+ 'app.js',
+ 'cookies-panel.js',
+ 'custom-request-panel.js',
+ 'editor.js',
+ 'headers-panel.js',
+ 'mdn-link.js',
+ 'monitor-panel.js',
+ 'network-details-panel.js',
+ 'params-panel.js',
+ 'preview-panel.js',
+ 'properties-view.js',
+ 'request-list-content.js',
+ 'request-list-empty-notice.js',
+ 'request-list-header.js',
+ 'request-list-item.js',
+ 'request-list.js',
+ 'response-panel.js',
+ 'security-panel.js',
+ 'statistics-panel.js',
+ 'tabbox-panel.js',
+ 'timings-panel.js',
+ 'toolbar.js',
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/network-details-panel.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("./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);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/params-panel.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("./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;
rename from devtools/client/netmonitor/src/components/PreviewPanel.js
rename to devtools/client/netmonitor/src/components/preview-panel.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/properties-view.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/src/components/request-list-content.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("./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);
rename from devtools/client/netmonitor/src/components/RequestListEmptyNotice.js
rename to devtools/client/netmonitor/src/components/request-list-empty-notice.js
rename from devtools/client/netmonitor/src/components/RequestListHeader.js
rename to devtools/client/netmonitor/src/components/request-list-header.js
rename from devtools/client/netmonitor/src/components/RequestListItem.js
rename to devtools/client/netmonitor/src/components/request-list-item.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/request-list.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("./request-list-content"));
+const RequestListEmptyNotice = createFactory(require("./request-list-empty-notice"));
+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;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/response-panel.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("./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;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/security-panel.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("./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;
rename from devtools/client/netmonitor/src/components/StatisticsPanel.js
rename to devtools/client/netmonitor/src/components/statistics-panel.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/tabbox-panel.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("./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);
rename from devtools/client/netmonitor/src/components/TimingsPanel.js
rename to devtools/client/netmonitor/src/components/timings-panel.js
rename from devtools/client/netmonitor/src/components/Toolbar.js
rename to devtools/client/netmonitor/src/components/toolbar.js