--- a/devtools/client/netmonitor/src/components/headers-panel.js
+++ b/devtools/client/netmonitor/src/components/headers-panel.js
@@ -198,21 +198,21 @@ const HeadersPanel = createClass({
size: `${inputWidth}`,
}),
statusCodeDocURL ? MDNLink({
url: statusCodeDocURL,
}) : span({
className: "headers-summary learn-more-link",
}),
button({
- className: "devtools-button",
+ className: "devtools-button edit-and-resend-button",
onClick: cloneSelectedRequest,
}, EDIT_AND_RESEND),
button({
- className: "devtools-button",
+ className: "devtools-button raw-headers-button",
onClick: this.toggleRawHeaders,
}, RAW_HEADERS),
)
);
}
let summaryVersion = httpVersion ?
this.renderSummary(SUMMARY_VERSION, httpVersion) : null;
--- a/devtools/client/netmonitor/src/components/tabbox-panel.js
+++ b/devtools/client/netmonitor/src/components/tabbox-panel.js
@@ -6,17 +6,17 @@
const {
createFactory,
PropTypes,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const Actions = require("../actions/index");
const { L10N } = require("../utils/l10n");
-const { getSelectedRequest } = require("../selectors/index");
+const { PANELS } = require("../constants");
// 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 ResponsePanel = createFactory(require("./response-panel"));
@@ -51,76 +51,74 @@ function TabboxPanel({
Tabbar({
activeTabId,
menuDocument: window.parent.document,
onSelect: selectTab,
renderOnlySelected: true,
showAllTabsMenu: true,
},
TabPanel({
- id: "headers",
+ id: PANELS.HEADERS,
title: HEADERS_TITLE,
},
HeadersPanel({ request, cloneSelectedRequest }),
),
TabPanel({
- id: "cookies",
+ id: PANELS.COOKIES,
title: COOKIES_TITLE,
},
CookiesPanel({ request }),
),
TabPanel({
- id: "params",
+ id: PANELS.PARAMS,
title: PARAMS_TITLE,
},
ParamsPanel({ request }),
),
TabPanel({
- id: "response",
+ id: PANELS.RESPONSE,
title: RESPONSE_TITLE,
},
ResponsePanel({ request }),
),
TabPanel({
- id: "timings",
+ id: PANELS.TIMINGS,
title: TIMINGS_TITLE,
},
TimingsPanel({ request }),
),
request.cause && request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
TabPanel({
- id: "stack-trace",
+ id: PANELS.STACK_TRACE,
title: STACK_TRACE_TITLE,
},
StackTracePanel({ request, sourceMapService }),
),
request.securityState && request.securityState !== "insecure" &&
TabPanel({
- id: "security",
+ id: PANELS.SECURITY,
title: SECURITY_TITLE,
},
SecurityPanel({ request }),
),
)
);
}
TabboxPanel.displayName = "TabboxPanel";
TabboxPanel.propTypes = {
activeTabId: PropTypes.string,
- cloneSelectedRequest: PropTypes.func.isRequired,
+ cloneSelectedRequest: PropTypes.func,
request: PropTypes.object,
selectTab: PropTypes.func.isRequired,
// Service to enable the source map feature.
sourceMapService: PropTypes.object,
};
module.exports = connect(
(state) => ({
- activeTabId: state.ui.detailsPanelSelectedTab,
- request: getSelectedRequest(state),
}),
(dispatch) => ({
cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
}),
)(TabboxPanel);
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -1,77 +1,59 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Services = require("Services");
-const { CurlUtils } = require("devtools/client/shared/curl");
const { TimelineFront } = require("devtools/shared/fronts/timeline");
const { ACTIVITY_TYPE, EVENTS } = require("../constants");
const { getDisplayedRequestById } = require("../selectors/index");
-const { fetchHeaders, formDataURI } = require("../utils/request-utils");
+const FirefoxDataProvider = require("./firefox-data-provider");
class FirefoxConnector {
constructor() {
- // Internal properties
- this.payloadQueue = [];
-
// Public methods
this.connect = this.connect.bind(this);
this.disconnect = this.disconnect.bind(this);
this.willNavigate = this.willNavigate.bind(this);
this.displayCachedEvents = this.displayCachedEvents.bind(this);
this.onDocLoadingMarker = this.onDocLoadingMarker.bind(this);
- this.addRequest = this.addRequest.bind(this);
- this.updateRequest = this.updateRequest.bind(this);
- this.fetchImage = this.fetchImage.bind(this);
- this.fetchRequestHeaders = this.fetchRequestHeaders.bind(this);
- this.fetchResponseHeaders = this.fetchResponseHeaders.bind(this);
- this.fetchPostData = this.fetchPostData.bind(this);
- this.fetchResponseCookies = this.fetchResponseCookies.bind(this);
- this.fetchRequestCookies = this.fetchRequestCookies.bind(this);
- this.getPayloadFromQueue = this.getPayloadFromQueue.bind(this);
- this.isQueuePayloadReady = this.isQueuePayloadReady.bind(this);
- this.pushPayloadToQueue = this.pushPayloadToQueue.bind(this);
this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
this.setPreferences = this.setPreferences.bind(this);
this.triggerActivity = this.triggerActivity.bind(this);
this.inspectRequest = this.inspectRequest.bind(this);
- this.getLongString = this.getLongString.bind(this);
- this.getNetworkRequest = this.getNetworkRequest.bind(this);
this.getTabTarget = this.getTabTarget.bind(this);
this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
- // Event handlers
- this.onNetworkEvent = this.onNetworkEvent.bind(this);
- this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
- this.onRequestHeaders = this.onRequestHeaders.bind(this);
- this.onRequestCookies = this.onRequestCookies.bind(this);
- this.onRequestPostData = this.onRequestPostData.bind(this);
- this.onSecurityInfo = this.onSecurityInfo.bind(this);
- this.onResponseHeaders = this.onResponseHeaders.bind(this);
- this.onResponseCookies = this.onResponseCookies.bind(this);
- this.onResponseContent = this.onResponseContent.bind(this);
- this.onEventTimings = this.onEventTimings.bind(this);
+ // Internals
+ this.getLongString = this.getLongString.bind(this);
+ this.getNetworkRequest = this.getNetworkRequest.bind(this);
}
async connect(connection, actions, getState) {
this.actions = actions;
this.getState = getState;
this.tabTarget = connection.tabConnection.tabTarget;
this.toolbox = connection.toolbox;
this.webConsoleClient = this.tabTarget.activeConsole;
+ this.dataProvider = new FirefoxDataProvider({
+ webConsoleClient: this.webConsoleClient,
+ actions: this.actions,
+ });
+
this.tabTarget.on("will-navigate", this.willNavigate);
this.tabTarget.on("close", this.disconnect);
- this.webConsoleClient.on("networkEvent", this.onNetworkEvent);
- this.webConsoleClient.on("networkEventUpdate", this.onNetworkEventUpdate);
+ this.webConsoleClient.on("networkEvent",
+ this.dataProvider.onNetworkEvent);
+ this.webConsoleClient.on("networkEventUpdate",
+ this.dataProvider.onNetworkEventUpdate);
// Don't start up waiting for timeline markers if the server isn't
// recent enough to emit the markers we're interested in.
if (this.tabTarget.getTrait("documentLoadingMarkers")) {
this.timelineFront = new TimelineFront(this.tabTarget.client, this.tabTarget.form);
this.timelineFront.on("doc-loading", this.onDocLoadingMarker);
await this.timelineFront.start({ withDocLoadingEvents: true });
}
@@ -91,16 +73,17 @@ class FirefoxConnector {
this.tabTarget.off("will-navigate");
this.tabTarget.off("close");
this.tabTarget = null;
this.webConsoleClient.off("networkEvent");
this.webConsoleClient.off("networkEventUpdate");
this.webConsoleClient = null;
this.timelineFront = null;
+ this.dataProvider = null;
}
willNavigate() {
if (!Services.prefs.getBoolPref("devtools.webconsole.persistlog")) {
this.actions.batchReset();
this.actions.clearRequests();
} else {
// If the log is persistent, just clear all accumulated timing markers.
@@ -109,20 +92,20 @@ class FirefoxConnector {
}
/**
* Display any network events already in the cache.
*/
displayCachedEvents() {
for (let networkInfo of this.webConsoleClient.getNetworkEvents()) {
// First add the request to the timeline.
- this.onNetworkEvent("networkEvent", networkInfo);
+ this.dataProvider.onNetworkEvent("networkEvent", networkInfo);
// Then replay any updates already received.
for (let updateType of networkInfo.updates) {
- this.onNetworkEventUpdate("networkEventUpdate", {
+ this.dataProvider.onNetworkEventUpdate("networkEventUpdate", {
packet: { updateType },
networkInfo,
});
}
}
}
/**
@@ -131,232 +114,16 @@ class FirefoxConnector {
* @param {object} marker
*/
onDocLoadingMarker(marker) {
window.emit(EVENTS.TIMELINE_EVENT, marker);
this.actions.addTimingMarker(marker);
}
/**
- * Add a new network request to application state.
- *
- * @param {string} id request id
- * @param {object} data data payload will be added to application state
- */
- addRequest(id, data) {
- let {
- method,
- url,
- isXHR,
- cause,
- startedDateTime,
- fromCache,
- fromServiceWorker,
- } = data;
-
- this.actions.addRequest(
- id,
- {
- // Convert the received date/time string to a unix timestamp.
- startedMillis: Date.parse(startedDateTime),
- method,
- url,
- isXHR,
- cause,
- fromCache,
- fromServiceWorker,
- },
- true,
- )
- .then(() => window.emit(EVENTS.REQUEST_ADDED, id));
- }
-
- /**
- * Update a network request if it already exists in application state.
- *
- * @param {string} id request id
- * @param {object} data data payload will be updated to application state
- */
- async updateRequest(id, data) {
- let {
- mimeType,
- responseContent,
- responseCookies,
- responseHeaders,
- requestCookies,
- requestHeaders,
- requestPostData,
- } = data;
-
- // fetch request detail contents in parallel
- let [
- imageObj,
- requestHeadersObj,
- responseHeadersObj,
- postDataObj,
- requestCookiesObj,
- responseCookiesObj,
- ] = await Promise.all([
- this.fetchImage(mimeType, responseContent),
- this.fetchRequestHeaders(requestHeaders),
- this.fetchResponseHeaders(responseHeaders),
- this.fetchPostData(requestPostData),
- this.fetchRequestCookies(requestCookies),
- this.fetchResponseCookies(responseCookies),
- ]);
-
- let payload = Object.assign({}, data,
- imageObj, requestHeadersObj, responseHeadersObj,
- postDataObj, requestCookiesObj, responseCookiesObj);
-
- this.pushPayloadToQueue(id, payload);
-
- if (this.isQueuePayloadReady(id)) {
- await this.actions.updateRequest(id, this.getPayloadFromQueue(id).payload, true);
- }
- }
-
- async fetchImage(mimeType, responseContent) {
- let payload = {};
- if (mimeType && responseContent && responseContent.content) {
- let { encoding, text } = responseContent.content;
- let response = await this.getLongString(text);
-
- if (mimeType.includes("image/")) {
- payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
- }
-
- responseContent.content.text = response;
- payload.responseContent = responseContent;
- }
- return payload;
- }
-
- async fetchRequestHeaders(requestHeaders) {
- let payload = {};
- if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
- let headers = await fetchHeaders(requestHeaders, this.getLongString);
- if (headers) {
- payload.requestHeaders = headers;
- }
- }
- return payload;
- }
-
- async fetchResponseHeaders(responseHeaders) {
- let payload = {};
- if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
- let headers = await fetchHeaders(responseHeaders, this.getLongString);
- if (headers) {
- payload.responseHeaders = headers;
- }
- }
- return payload;
- }
-
- async fetchPostData(requestPostData) {
- let payload = {};
- if (requestPostData && requestPostData.postData) {
- let { text } = requestPostData.postData;
- let postData = await this.getLongString(text);
- const headers = CurlUtils.getHeadersFromMultipartText(postData);
- const headersSize = headers.reduce((acc, { name, value }) => {
- return acc + name.length + value.length + 2;
- }, 0);
- requestPostData.postData.text = postData;
- payload.requestPostData = Object.assign({}, requestPostData);
- payload.requestHeadersFromUploadStream = { headers, headersSize };
- }
- return payload;
- }
-
- async fetchResponseCookies(responseCookies) {
- let payload = {};
- if (responseCookies) {
- let resCookies = [];
- // response store cookies in responseCookies or responseCookies.cookies
- let cookies = responseCookies.cookies ?
- responseCookies.cookies : responseCookies;
- // make sure cookies is iterable
- if (typeof cookies[Symbol.iterator] === "function") {
- for (let cookie of cookies) {
- resCookies.push(Object.assign({}, cookie, {
- value: await this.getLongString(cookie.value),
- }));
- }
- if (resCookies.length) {
- payload.responseCookies = resCookies;
- }
- }
- }
- return payload;
- }
-
- async fetchRequestCookies(requestCookies) {
- let payload = {};
- if (requestCookies) {
- let reqCookies = [];
- // request store cookies in requestCookies or requestCookies.cookies
- let cookies = requestCookies.cookies ?
- requestCookies.cookies : requestCookies;
- // make sure cookies is iterable
- if (typeof cookies[Symbol.iterator] === "function") {
- for (let cookie of cookies) {
- reqCookies.push(Object.assign({}, cookie, {
- value: await this.getLongString(cookie.value),
- }));
- }
- if (reqCookies.length) {
- payload.requestCookies = reqCookies;
- }
- }
- }
- return payload;
- }
-
- /**
- * Access a payload item from payload queue.
- *
- * @param {string} id request id
- * @return {boolean} return a queued payload item from queue.
- */
- getPayloadFromQueue(id) {
- return this.payloadQueue.find((item) => item.id === id);
- }
-
- /**
- * Packet order of "networkUpdateEvent" is predictable, as a result we can wait for
- * the last one "eventTimings" packet arrives to check payload is ready.
- *
- * @param {string} id request id
- * @return {boolean} return whether a specific networkEvent has been updated completely.
- */
- isQueuePayloadReady(id) {
- let queuedPayload = this.getPayloadFromQueue(id);
- return queuedPayload && queuedPayload.payload.eventTimings;
- }
-
- /**
- * Push a request payload into a queue if request doesn't exist. Otherwise update the
- * request itself.
- *
- * @param {string} id request id
- * @param {object} payload request data payload
- */
- pushPayloadToQueue(id, payload) {
- let queuedPayload = this.getPayloadFromQueue(id);
- if (!queuedPayload) {
- this.payloadQueue.push({ id, payload });
- } else {
- // Merge upcoming networkEventUpdate payload into existing one
- queuedPayload.payload = Object.assign({}, queuedPayload.payload, payload);
- }
- }
-
- /**
* Send a HTTP request data payload
*
* @param {object} data data payload would like to sent to backend
* @param {function} callback callback will be invoked after the request finished
*/
sendHTTPRequest(data, callback) {
this.webConsoleClient.sendHTTPRequest(data, callback);
}
@@ -485,32 +252,32 @@ class FirefoxConnector {
/**
* Fetches the network information packet from actor server
*
* @param {string} id request id
* @return {object} networkInfo data packet
*/
getNetworkRequest(id) {
- return this.webConsoleClient.getNetworkRequest(id);
+ return this.dataProvider.getNetworkRequest(id);
}
/**
* Fetches the full text of a LongString.
*
* @param {object|string} stringGrip
* The long string grip containing the corresponding actor.
* If you pass in a plain string (by accident or because you're lazy),
* then a promise of the same string is simply returned.
* @return {object}
* A promise that is resolved when the full string contents
* are available, or rejected if something goes wrong.
*/
getLongString(stringGrip) {
- return this.webConsoleClient.getString(stringGrip);
+ return this.dataProvider.getLongString(stringGrip);
}
/**
* Getter that access tab target instance.
* @return {object} browser tab target instance
*/
getTabTarget() {
return this.tabTarget;
@@ -521,218 +288,11 @@ class FirefoxConnector {
* @param {string} sourceURL source url
* @param {number} sourceLine source line number
*/
viewSourceInDebugger(sourceURL, sourceLine) {
if (this.toolbox) {
this.toolbox.viewSourceInDebugger(sourceURL, sourceLine);
}
}
-
- /**
- * The "networkEvent" message type handler.
- *
- * @param {string} type message type
- * @param {object} networkInfo network request information
- */
- onNetworkEvent(type, networkInfo) {
- let {
- actor,
- cause,
- fromCache,
- fromServiceWorker,
- isXHR,
- request: {
- method,
- url,
- },
- startedDateTime,
- } = networkInfo;
-
- this.addRequest(actor, {
- cause,
- fromCache,
- fromServiceWorker,
- isXHR,
- method,
- startedDateTime,
- url,
- });
-
- window.emit(EVENTS.NETWORK_EVENT, actor);
- }
-
- /**
- * The "networkEventUpdate" message type handler.
- *
- * @param {string} type message type
- * @param {object} packet the message received from the server.
- * @param {object} networkInfo the network request information.
- */
- onNetworkEventUpdate(type, { packet, networkInfo }) {
- let { actor } = networkInfo;
-
- switch (packet.updateType) {
- case "requestHeaders":
- this.webConsoleClient.getRequestHeaders(actor, this.onRequestHeaders);
- window.emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
- break;
- case "requestCookies":
- this.webConsoleClient.getRequestCookies(actor, this.onRequestCookies);
- window.emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
- break;
- case "requestPostData":
- this.webConsoleClient.getRequestPostData(actor, this.onRequestPostData);
- window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
- break;
- case "securityInfo":
- this.updateRequest(actor, {
- securityState: networkInfo.securityInfo,
- }).then(() => {
- this.webConsoleClient.getSecurityInfo(actor, this.onSecurityInfo);
- window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
- });
- break;
- case "responseHeaders":
- this.webConsoleClient.getResponseHeaders(actor, this.onResponseHeaders);
- window.emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
- break;
- case "responseCookies":
- this.webConsoleClient.getResponseCookies(actor, this.onResponseCookies);
- window.emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
- break;
- case "responseStart":
- this.updateRequest(actor, {
- httpVersion: networkInfo.response.httpVersion,
- remoteAddress: networkInfo.response.remoteAddress,
- remotePort: networkInfo.response.remotePort,
- status: networkInfo.response.status,
- statusText: networkInfo.response.statusText,
- headersSize: networkInfo.response.headersSize
- }).then(() => {
- window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
- });
- break;
- case "responseContent":
- this.webConsoleClient.getResponseContent(actor,
- this.onResponseContent.bind(this, {
- contentSize: networkInfo.response.bodySize,
- transferredSize: networkInfo.response.transferredSize,
- mimeType: networkInfo.response.content.mimeType
- }));
- window.emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
- break;
- case "eventTimings":
- this.updateRequest(actor, { totalTime: networkInfo.totalTime })
- .then(() => {
- this.webConsoleClient.getEventTimings(actor, this.onEventTimings);
- window.emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
- });
- break;
- }
- }
-
- /**
- * Handles additional information received for a "requestHeaders" packet.
- *
- * @param {object} response the message received from the server.
- */
- onRequestHeaders(response) {
- this.updateRequest(response.from, {
- requestHeaders: response
- }).then(() => {
- window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
- });
- }
-
- /**
- * Handles additional information received for a "requestCookies" packet.
- *
- * @param {object} response the message received from the server.
- */
- onRequestCookies(response) {
- this.updateRequest(response.from, {
- requestCookies: response
- }).then(() => {
- window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
- });
- }
-
- /**
- * Handles additional information received for a "requestPostData" packet.
- *
- * @param {object} response the message received from the server.
- */
- onRequestPostData(response) {
- this.updateRequest(response.from, {
- requestPostData: response
- }).then(() => {
- window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
- });
- }
-
- /**
- * Handles additional information received for a "securityInfo" packet.
- *
- * @param {object} response the message received from the server.
- */
- onSecurityInfo(response) {
- this.updateRequest(response.from, {
- securityInfo: response.securityInfo
- }).then(() => {
- window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
- });
- }
-
- /**
- * Handles additional information received for a "responseHeaders" packet.
- *
- * @param {object} response the message received from the server.
- */
- onResponseHeaders(response) {
- this.updateRequest(response.from, {
- responseHeaders: response
- }).then(() => {
- window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
- });
- }
-
- /**
- * Handles additional information received for a "responseCookies" packet.
- *
- * @param {object} response the message received from the server.
- */
- onResponseCookies(response) {
- this.updateRequest(response.from, {
- responseCookies: response
- }).then(() => {
- window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
- });
- }
-
- /**
- * Handles additional information received for a "responseContent" packet.
- *
- * @param {object} data the message received from the server event.
- * @param {object} response the message received from the server.
- */
- onResponseContent(data, response) {
- let payload = Object.assign({ responseContent: response }, data);
- this.updateRequest(response.from, payload).then(() => {
- window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
- });
- }
-
- /**
- * Handles additional information received for a "eventTimings" packet.
- *
- * @param {object} response the message received from the server.
- */
- onEventTimings(response) {
- this.updateRequest(response.from, {
- eventTimings: response
- }).then(() => {
- window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
- });
- }
}
module.exports = new FirefoxConnector();
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -0,0 +1,525 @@
+/* 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 block-scoped-var */
+
+"use strict";
+
+const { EVENTS } = require("../constants");
+const { CurlUtils } = require("devtools/client/shared/curl");
+const { fetchHeaders, formDataURI } = require("../utils/request-utils");
+
+/**
+ * This object is responsible for fetching additional HTTP
+ * data from the backend.
+ */
+class FirefoxDataProvider {
+ constructor({webConsoleClient, actions}) {
+ // Options
+ this.webConsoleClient = webConsoleClient;
+ this.actions = actions;
+
+ // Internal properties
+ this.payloadQueue = [];
+
+ // Public methods
+ this.addRequest = this.addRequest.bind(this);
+ this.updateRequest = this.updateRequest.bind(this);
+
+ // Internals
+ this.fetchImage = this.fetchImage.bind(this);
+ this.fetchRequestHeaders = this.fetchRequestHeaders.bind(this);
+ this.fetchResponseHeaders = this.fetchResponseHeaders.bind(this);
+ this.fetchPostData = this.fetchPostData.bind(this);
+ this.fetchResponseCookies = this.fetchResponseCookies.bind(this);
+ this.fetchRequestCookies = this.fetchRequestCookies.bind(this);
+ this.getPayloadFromQueue = this.getPayloadFromQueue.bind(this);
+ this.isQueuePayloadReady = this.isQueuePayloadReady.bind(this);
+ this.pushPayloadToQueue = this.pushPayloadToQueue.bind(this);
+ this.getLongString = this.getLongString.bind(this);
+ this.getNetworkRequest = this.getNetworkRequest.bind(this);
+
+ // Event handlers
+ this.onNetworkEvent = this.onNetworkEvent.bind(this);
+ this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
+ this.onRequestHeaders = this.onRequestHeaders.bind(this);
+ this.onRequestCookies = this.onRequestCookies.bind(this);
+ this.onRequestPostData = this.onRequestPostData.bind(this);
+ this.onSecurityInfo = this.onSecurityInfo.bind(this);
+ this.onResponseHeaders = this.onResponseHeaders.bind(this);
+ this.onResponseCookies = this.onResponseCookies.bind(this);
+ this.onResponseContent = this.onResponseContent.bind(this);
+ this.onEventTimings = this.onEventTimings.bind(this);
+ }
+
+ /**
+ * Add a new network request to application state.
+ *
+ * @param {string} id request id
+ * @param {object} data data payload will be added to application state
+ */
+ async addRequest(id, data) {
+ let {
+ method,
+ url,
+ isXHR,
+ cause,
+ startedDateTime,
+ fromCache,
+ fromServiceWorker,
+ } = data;
+
+ if (this.actions.addRequest) {
+ await this.actions.addRequest(id, {
+ // Convert the received date/time string to a unix timestamp.
+ startedMillis: Date.parse(startedDateTime),
+ method,
+ url,
+ isXHR,
+ cause,
+ fromCache,
+ fromServiceWorker},
+ true,
+ );
+ }
+
+ emit(EVENTS.REQUEST_ADDED, id);
+ }
+
+ /**
+ * Update a network request if it already exists in application state.
+ *
+ * @param {string} id request id
+ * @param {object} data data payload will be updated to application state
+ */
+ async updateRequest(id, data) {
+ let {
+ mimeType,
+ responseContent,
+ responseCookies,
+ responseHeaders,
+ requestCookies,
+ requestHeaders,
+ requestPostData,
+ } = data;
+
+ // fetch request detail contents in parallel
+ let [
+ imageObj,
+ requestHeadersObj,
+ responseHeadersObj,
+ postDataObj,
+ requestCookiesObj,
+ responseCookiesObj,
+ ] = await Promise.all([
+ this.fetchImage(mimeType, responseContent),
+ this.fetchRequestHeaders(requestHeaders),
+ this.fetchResponseHeaders(responseHeaders),
+ this.fetchPostData(requestPostData),
+ this.fetchRequestCookies(requestCookies),
+ this.fetchResponseCookies(responseCookies),
+ ]);
+
+ let payload = Object.assign({},
+ data,
+ imageObj,
+ requestHeadersObj,
+ responseHeadersObj,
+ postDataObj,
+ requestCookiesObj,
+ responseCookiesObj
+ );
+
+ this.pushPayloadToQueue(id, payload);
+
+ if (this.actions.updateRequest && this.isQueuePayloadReady(id)) {
+ await this.actions.updateRequest(id, this.getPayloadFromQueue(id).payload, true);
+ }
+ }
+
+ async fetchImage(mimeType, responseContent) {
+ let payload = {};
+ if (mimeType && responseContent && responseContent.content) {
+ let { encoding, text } = responseContent.content;
+ let response = await this.getLongString(text);
+
+ if (mimeType.includes("image/")) {
+ payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
+ }
+
+ responseContent.content.text = response;
+ payload.responseContent = responseContent;
+ }
+ return payload;
+ }
+
+ async fetchRequestHeaders(requestHeaders) {
+ let payload = {};
+ if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
+ let headers = await fetchHeaders(requestHeaders, this.getLongString);
+ if (headers) {
+ payload.requestHeaders = headers;
+ }
+ }
+ return payload;
+ }
+
+ async fetchResponseHeaders(responseHeaders) {
+ let payload = {};
+ if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
+ let headers = await fetchHeaders(responseHeaders, this.getLongString);
+ if (headers) {
+ payload.responseHeaders = headers;
+ }
+ }
+ return payload;
+ }
+
+ async fetchPostData(requestPostData) {
+ let payload = {};
+ if (requestPostData && requestPostData.postData) {
+ let { text } = requestPostData.postData;
+ let postData = await this.getLongString(text);
+ const headers = CurlUtils.getHeadersFromMultipartText(postData);
+
+ // Calculate total header size and don't forget to include
+ // two new-line characters at the end.
+ const headersSize = headers.reduce((acc, { name, value }) => {
+ return acc + name.length + value.length + 2;
+ }, 0);
+
+ requestPostData.postData.text = postData;
+ payload.requestPostData = Object.assign({}, requestPostData);
+ payload.requestHeadersFromUploadStream = { headers, headersSize };
+ }
+ return payload;
+ }
+
+ async fetchResponseCookies(responseCookies) {
+ let payload = {};
+ if (responseCookies) {
+ let resCookies = [];
+ // response store cookies in responseCookies or responseCookies.cookies
+ let cookies = responseCookies.cookies ?
+ responseCookies.cookies : responseCookies;
+ // make sure cookies is iterable
+ if (typeof cookies[Symbol.iterator] === "function") {
+ for (let cookie of cookies) {
+ resCookies.push(Object.assign({}, cookie, {
+ value: await this.getLongString(cookie.value),
+ }));
+ }
+ if (resCookies.length) {
+ payload.responseCookies = resCookies;
+ }
+ }
+ }
+ return payload;
+ }
+
+ async fetchRequestCookies(requestCookies) {
+ let payload = {};
+ if (requestCookies) {
+ let reqCookies = [];
+ // request store cookies in requestCookies or requestCookies.cookies
+ let cookies = requestCookies.cookies ?
+ requestCookies.cookies : requestCookies;
+ // make sure cookies is iterable
+ if (typeof cookies[Symbol.iterator] === "function") {
+ for (let cookie of cookies) {
+ reqCookies.push(Object.assign({}, cookie, {
+ value: await this.getLongString(cookie.value),
+ }));
+ }
+ if (reqCookies.length) {
+ payload.requestCookies = reqCookies;
+ }
+ }
+ }
+ return payload;
+ }
+
+ /**
+ * Access a payload item from payload queue.
+ *
+ * @param {string} id request id
+ * @return {boolean} return a queued payload item from queue.
+ */
+ getPayloadFromQueue(id) {
+ return this.payloadQueue.find((item) => item.id === id);
+ }
+
+ /**
+ * Return true if payload is ready (all data fetched from the backend)
+ *
+ * @param {string} id request id
+ * @return {boolean} return whether a specific networkEvent has been updated completely.
+ */
+ isQueuePayloadReady(id) {
+ let queuedPayload = this.getPayloadFromQueue(id);
+
+ // TODO we should find a better solution since it might happen
+ // that eventTimings is not the last update.
+ return queuedPayload && queuedPayload.payload.eventTimings;
+ }
+
+ /**
+ * Push a request payload into a queue if request doesn't exist. Otherwise update the
+ * request itself.
+ *
+ * @param {string} id request id
+ * @param {object} payload request data payload
+ */
+ pushPayloadToQueue(id, payload) {
+ let queuedPayload = this.getPayloadFromQueue(id);
+ if (!queuedPayload) {
+ this.payloadQueue.push({ id, payload });
+ } else {
+ // Merge upcoming networkEventUpdate payload into existing one
+ queuedPayload.payload = Object.assign({}, queuedPayload.payload, payload);
+ }
+ }
+
+ /**
+ * Fetches the network information packet from actor server
+ *
+ * @param {string} id request id
+ * @return {object} networkInfo data packet
+ */
+ getNetworkRequest(id) {
+ return this.webConsoleClient.getNetworkRequest(id);
+ }
+
+ /**
+ * Fetches the full text of a LongString.
+ *
+ * @param {object|string} stringGrip
+ * The long string grip containing the corresponding actor.
+ * If you pass in a plain string (by accident or because you're lazy),
+ * then a promise of the same string is simply returned.
+ * @return {object}
+ * A promise that is resolved when the full string contents
+ * are available, or rejected if something goes wrong.
+ */
+ getLongString(stringGrip) {
+ return this.webConsoleClient.getString(stringGrip);
+ }
+
+ /**
+ * The "networkEvent" message type handler.
+ *
+ * @param {string} type message type
+ * @param {object} networkInfo network request information
+ */
+ onNetworkEvent(type, networkInfo) {
+ let {
+ actor,
+ cause,
+ fromCache,
+ fromServiceWorker,
+ isXHR,
+ request: {
+ method,
+ url,
+ },
+ startedDateTime,
+ } = networkInfo;
+
+ this.addRequest(actor, {
+ cause,
+ fromCache,
+ fromServiceWorker,
+ isXHR,
+ method,
+ startedDateTime,
+ url,
+ });
+
+ emit(EVENTS.NETWORK_EVENT, actor);
+ }
+
+ /**
+ * The "networkEventUpdate" message type handler.
+ *
+ * @param {string} type message type
+ * @param {object} packet the message received from the server.
+ * @param {object} networkInfo the network request information.
+ */
+ onNetworkEventUpdate(type, { packet, networkInfo }) {
+ let { actor } = networkInfo;
+
+ switch (packet.updateType) {
+ case "requestHeaders":
+ this.webConsoleClient.getRequestHeaders(actor, this.onRequestHeaders);
+ emit(EVENTS.UPDATING_REQUEST_HEADERS, actor);
+ break;
+ case "requestCookies":
+ this.webConsoleClient.getRequestCookies(actor, this.onRequestCookies);
+ emit(EVENTS.UPDATING_REQUEST_COOKIES, actor);
+ break;
+ case "requestPostData":
+ this.webConsoleClient.getRequestPostData(actor, this.onRequestPostData);
+ emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
+ break;
+ case "securityInfo":
+ this.updateRequest(actor, {
+ securityState: networkInfo.securityInfo,
+ }).then(() => {
+ this.webConsoleClient.getSecurityInfo(actor, this.onSecurityInfo);
+ emit(EVENTS.UPDATING_SECURITY_INFO, actor);
+ });
+ break;
+ case "responseHeaders":
+ this.webConsoleClient.getResponseHeaders(actor, this.onResponseHeaders);
+ emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
+ break;
+ case "responseCookies":
+ this.webConsoleClient.getResponseCookies(actor, this.onResponseCookies);
+ emit(EVENTS.UPDATING_RESPONSE_COOKIES, actor);
+ break;
+ case "responseStart":
+ this.updateRequest(actor, {
+ httpVersion: networkInfo.response.httpVersion,
+ remoteAddress: networkInfo.response.remoteAddress,
+ remotePort: networkInfo.response.remotePort,
+ status: networkInfo.response.status,
+ statusText: networkInfo.response.statusText,
+ headersSize: networkInfo.response.headersSize
+ }).then(() => {
+ emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
+ });
+ break;
+ case "responseContent":
+ this.webConsoleClient.getResponseContent(actor,
+ this.onResponseContent.bind(this, {
+ contentSize: networkInfo.response.bodySize,
+ transferredSize: networkInfo.response.transferredSize,
+ mimeType: networkInfo.response.content.mimeType
+ }));
+ emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
+ break;
+ case "eventTimings":
+ this.updateRequest(actor, { totalTime: networkInfo.totalTime })
+ .then(() => {
+ this.webConsoleClient.getEventTimings(actor, this.onEventTimings);
+ emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
+ });
+ break;
+ }
+ }
+
+ /**
+ * Handles additional information received for a "requestHeaders" packet.
+ *
+ * @param {object} response the message received from the server.
+ */
+ onRequestHeaders(response) {
+ this.updateRequest(response.from, {
+ requestHeaders: response
+ }).then(() => {
+ emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
+ });
+ }
+
+ /**
+ * Handles additional information received for a "requestCookies" packet.
+ *
+ * @param {object} response the message received from the server.
+ */
+ onRequestCookies(response) {
+ this.updateRequest(response.from, {
+ requestCookies: response
+ }).then(() => {
+ emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
+ });
+ }
+
+ /**
+ * Handles additional information received for a "requestPostData" packet.
+ *
+ * @param {object} response the message received from the server.
+ */
+ onRequestPostData(response) {
+ this.updateRequest(response.from, {
+ requestPostData: response
+ }).then(() => {
+ emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
+ });
+ }
+
+ /**
+ * Handles additional information received for a "securityInfo" packet.
+ *
+ * @param {object} response the message received from the server.
+ */
+ onSecurityInfo(response) {
+ this.updateRequest(response.from, {
+ securityInfo: response.securityInfo
+ }).then(() => {
+ emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
+ });
+ }
+
+ /**
+ * Handles additional information received for a "responseHeaders" packet.
+ *
+ * @param {object} response the message received from the server.
+ */
+ onResponseHeaders(response) {
+ this.updateRequest(response.from, {
+ responseHeaders: response
+ }).then(() => {
+ emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
+ });
+ }
+
+ /**
+ * Handles additional information received for a "responseCookies" packet.
+ *
+ * @param {object} response the message received from the server.
+ */
+ onResponseCookies(response) {
+ this.updateRequest(response.from, {
+ responseCookies: response
+ }).then(() => {
+ emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
+ });
+ }
+
+ /**
+ * Handles additional information received for a "responseContent" packet.
+ *
+ * @param {object} data the message received from the server event.
+ * @param {object} response the message received from the server.
+ */
+ onResponseContent(data, response) {
+ let payload = Object.assign({ responseContent: response }, data);
+ this.updateRequest(response.from, payload).then(() => {
+ emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
+ });
+ }
+
+ /**
+ * Handles additional information received for a "eventTimings" packet.
+ *
+ * @param {object} response the message received from the server.
+ */
+ onEventTimings(response) {
+ this.updateRequest(response.from, {
+ eventTimings: response
+ }).then(() => {
+ emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
+ });
+ }
+}
+
+/**
+ * Guard 'emit' to avoid exception in non-window environment.
+ */
+function emit(type, data) {
+ if (typeof window != "undefined") {
+ window.emit(type, data);
+ }
+}
+
+module.exports = FirefoxDataProvider;
--- a/devtools/client/netmonitor/src/connector/moz.build
+++ b/devtools/client/netmonitor/src/connector/moz.build
@@ -1,8 +1,9 @@
# 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(
'firefox-connector.js',
+ 'firefox-data-provider.js',
'index.js',
)
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -89,16 +89,54 @@ const EVENTS = {
STARTED_RECEIVING_RESPONSE: "NetMonitor:NetworkEventUpdating:ResponseStart",
UPDATING_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdating:ResponseContent",
RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent",
// Fired once the connection is established
CONNECTED: "connected",
};
+const UPDATE_PROPS = [
+ "method",
+ "url",
+ "remotePort",
+ "remoteAddress",
+ "status",
+ "statusText",
+ "httpVersion",
+ "securityState",
+ "securityInfo",
+ "mimeType",
+ "contentSize",
+ "transferredSize",
+ "totalTime",
+ "eventTimings",
+ "headersSize",
+ "customQueryValue",
+ "requestHeaders",
+ "requestHeadersFromUploadStream",
+ "requestCookies",
+ "requestPostData",
+ "responseHeaders",
+ "responseCookies",
+ "responseContent",
+ "responseContentDataUri",
+ "formDataSections",
+];
+
+const PANELS = {
+ COOKIES: "cookies",
+ HEADERS: "headers",
+ PARAMS: "params",
+ RESPONSE: "response",
+ SECURITY: "security",
+ STACK_TRACE: "stack-trace",
+ TIMINGS: "timings",
+};
+
const RESPONSE_HEADERS = [
"Cache-Control",
"Connection",
"Content-Encoding",
"Content-Length",
"ETag",
"Keep-Alive",
"Last-Modified",
@@ -239,17 +277,19 @@ const REQUESTS_WATERFALL = {
// Reserve extra space for rendering waterfall time label
LABEL_WIDTH: 50, // px
};
const general = {
ACTIVITY_TYPE,
EVENTS,
FILTER_SEARCH_DELAY: 200,
+ UPDATE_PROPS,
HEADERS,
RESPONSE_HEADERS,
FILTER_FLAGS,
SOURCE_EDITOR_SYNTAX_HIGHLIGHT_MAX_SIZE: 51200, // 50 KB in bytes
REQUESTS_WATERFALL,
+ PANELS,
};
// flatten constants
module.exports = Object.assign({}, general, actionTypes);
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -10,16 +10,17 @@ const {
ADD_REQUEST,
CLEAR_REQUESTS,
CLONE_SELECTED_REQUEST,
OPEN_NETWORK_DETAILS,
REMOVE_SELECTED_CUSTOM_REQUEST,
SELECT_REQUEST,
SEND_CUSTOM_REQUEST,
UPDATE_REQUEST,
+ UPDATE_PROPS,
} = require("../constants");
const Request = I.Record({
id: null,
// Set to true in case of a request that's being edited as part of "edit and resend"
isCustom: false,
// Request properties - at the beginning, they are unknown and are gradually filled in
startedMillis: undefined,
@@ -63,44 +64,16 @@ const Requests = I.Record({
// Selection state
selectedId: null,
preselectedId: null,
// Auxiliary fields to hold requests stats
firstStartedMillis: +Infinity,
lastEndedMillis: -Infinity,
});
-const UPDATE_PROPS = [
- "method",
- "url",
- "remotePort",
- "remoteAddress",
- "status",
- "statusText",
- "httpVersion",
- "securityState",
- "securityInfo",
- "mimeType",
- "contentSize",
- "transferredSize",
- "totalTime",
- "eventTimings",
- "headersSize",
- "customQueryValue",
- "requestHeaders",
- "requestHeadersFromUploadStream",
- "requestCookies",
- "requestPostData",
- "responseHeaders",
- "responseCookies",
- "responseContent",
- "responseContentDataUri",
- "formDataSections",
-];
-
/**
* Remove the currently selected custom request.
*/
function closeCustomRequest(state) {
let { requests, selectedId } = state;
if (!selectedId) {
return state;
--- a/devtools/client/netmonitor/src/reducers/ui.js
+++ b/devtools/client/netmonitor/src/reducers/ui.js
@@ -14,16 +14,17 @@ const {
REMOVE_SELECTED_CUSTOM_REQUEST,
RESET_COLUMNS,
RESPONSE_HEADERS,
SELECT_DETAILS_PANEL_TAB,
SEND_CUSTOM_REQUEST,
SELECT_REQUEST,
TOGGLE_COLUMN,
WATERFALL_RESIZE,
+ PANELS,
} = require("../constants");
const cols = {
status: true,
method: true,
file: true,
protocol: false,
scheme: false,
@@ -46,17 +47,17 @@ const Columns = I.Record(
Object.assign(
cols,
RESPONSE_HEADERS.reduce((acc, header) => Object.assign(acc, { [header]: false }), {})
)
);
const UI = I.Record({
columns: new Columns(),
- detailsPanelSelectedTab: "headers",
+ detailsPanelSelectedTab: PANELS.HEADERS,
networkDetailsOpen: false,
browserCacheDisabled: Services.prefs.getBoolPref("devtools.cache.disabled"),
statisticsOpen: false,
waterfallWidth: null,
});
function resetColumns(state) {
return state.set("columns", new Columns());
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -844,24 +844,53 @@ a.learn-more-link.webconsole-learn-more-
.message.warn > .icon::before {
background-position: -24px -36px;
}
.message.info > .icon::before {
background-position: -36px -36px;
}
+/* Network Messages */
+
.message.network .method {
margin-inline-end: 5px;
}
+.network.message .network-info {
+ display: none;
+ margin-top: 8px;
+ border: solid 1px var(--theme-splitter-color);
+}
+
+.network.message.open .network-info {
+ display: block;
+}
+
+.network.message .network-info .panels {
+ max-height: 250px;
+ min-height: 100px;
+}
+
+/* Hide 'Edit And Resend' button since the feature isn't
+ supported in the Console panel. */
+.network.message #headers-panel .edit-and-resend-button {
+ display: none;
+}
+
+.network.message #response-panel .treeTable {
+ overflow-y: hidden;
+}
+
.network .message-flex-body > .message-body {
display: flex;
}
+/* Output Wrapper */
+
.webconsole-output-wrapper .message .indent {
display: inline-block;
border-inline-end: solid 1px var(--theme-splitter-color);
}
.webconsole-output-wrapper .message .indent[data-indent="0"] {
border-inline-end: none;
}
--- a/devtools/client/webconsole/new-console-output/actions/messages.js
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -10,16 +10,17 @@ const {
prepareMessage
} = require("devtools/client/webconsole/new-console-output/utils/messages");
const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
const { batchActions } = require("devtools/client/shared/redux/middleware/debounce");
const {
MESSAGE_ADD,
NETWORK_MESSAGE_UPDATE,
+ NETWORK_UPDATE_REQUEST,
MESSAGES_CLEAR,
MESSAGE_OPEN,
MESSAGE_CLOSE,
MESSAGE_TYPE,
MESSAGE_TABLE_RECEIVE,
MESSAGE_OBJECT_PROPERTIES_RECEIVE,
MESSAGE_OBJECT_ENTRIES_RECEIVE,
} = require("../constants");
@@ -89,26 +90,35 @@ function messageTableDataGet(id, client,
function messageTableDataReceive(id, data) {
return {
type: MESSAGE_TABLE_RECEIVE,
id,
data
};
}
-function networkMessageUpdate(packet, idGenerator = null) {
+function networkMessageUpdate(packet, idGenerator = null, response) {
if (idGenerator == null) {
idGenerator = defaultIdGenerator;
}
let message = prepareMessage(packet, idGenerator);
return {
type: NETWORK_MESSAGE_UPDATE,
message,
+ response,
+ };
+}
+
+function networkUpdateRequest(id, data) {
+ return {
+ type: NETWORK_UPDATE_REQUEST,
+ id,
+ data,
};
}
/**
* This action is used to load the properties of a grip passed as an argument,
* for a given message. The action then dispatch the messageObjectPropertiesReceive
* action with the loaded properties.
* This action is mainly called by the ObjectInspector component when the user expands
@@ -174,16 +184,17 @@ function messageObjectEntriesReceive(id,
module.exports = {
messageAdd,
messagesClear,
messageOpen,
messageClose,
messageTableDataGet,
networkMessageUpdate,
+ networkUpdateRequest,
messageObjectPropertiesLoad,
messageObjectEntriesLoad,
// for test purpose only.
messageTableDataReceive,
messageObjectPropertiesReceive,
messageObjectEntriesReceive,
};
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -62,17 +62,17 @@ const ConsoleOutput = createClass({
if (!outputNode || !outputNode.lastChild) {
return;
}
const lastChild = outputNode.lastChild;
const visibleMessagesDelta =
nextProps.visibleMessages.length - this.props.visibleMessages.length;
const messagesDelta =
- nextProps.messages.length - this.props.messages.length;
+ nextProps.messages.size - this.props.messages.size;
// We need to scroll to the bottom if:
// - the number of messages displayed changed
// and we are already scrolled to the bottom
// - the number of messages in the store changed
// and the new message is an evaluation result.
this.shouldScrollBottom =
(messagesDelta > 0 && nextProps.messages.last().type === MESSAGE_TYPE.RESULT) ||
--- a/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
@@ -9,35 +9,53 @@
// React & Redux
const {
createFactory,
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const TabboxPanel = createFactory(require("devtools/client/netmonitor/src/components/tabbox-panel"));
+const { PANELS } = require("devtools/client/netmonitor/src/constants");
NetworkEventMessage.displayName = "NetworkEventMessage";
NetworkEventMessage.propTypes = {
message: PropTypes.object.isRequired,
serviceContainer: PropTypes.shape({
openNetworkPanel: PropTypes.func.isRequired,
}),
timestampsVisible: PropTypes.bool.isRequired,
networkMessageUpdate: PropTypes.object.isRequired,
};
+/**
+ * This component is responsible for rendering network messages
+ * in the Console panel.
+ *
+ * Network logs are expandable and the user can inspect it inline
+ * within the Console panel (no need to switch to the Network panel).
+ *
+ * HTTP details are rendered using `TabboxPanel` component used to
+ * render contents of the side bar in the Network panel.
+ *
+ * All HTTP details data are fetched from the backend on-demand
+ * when the user is expanding network log for the first time.
+ */
function NetworkEventMessage({
message = {},
serviceContainer,
timestampsVisible,
networkMessageUpdate = {},
+ dispatch,
+ open,
}) {
const {
+ id,
actor,
indent,
source,
type,
level,
request,
isXHR,
timeStamp,
@@ -72,21 +90,38 @@ function NetworkEventMessage({
const url = dom.a({ className: "url", title: request.url, onClick: openNetworkMonitor },
request.url.replace(/\?.+/, ""));
const statusBody = statusInfo
? dom.a({ className: "status", onClick: openNetworkMonitor }, statusInfo)
: null;
const messageBody = [method, xhr, url, statusBody];
+ // Only render the attachment if the network-event is
+ // actually opened (performance optimization).
+ const attachment = open && dom.div({className: "network-info devtools-monospace"},
+ TabboxPanel({
+ activeTabId: PANELS.HEADERS,
+ request: networkMessageUpdate,
+ sourceMapService: serviceContainer.sourceMapService,
+ cloneSelectedRequest: () => {},
+ selectTab: (tabId) => {},
+ })
+ );
+
return Message({
+ dispatch,
+ messageId: id,
source,
type,
level,
indent,
+ collapsible: true,
+ open,
+ attachment,
topLevelClasses,
timeStamp,
messageBody,
serviceContainer,
request,
timestampsVisible,
});
}
--- a/devtools/client/webconsole/new-console-output/constants.js
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -7,16 +7,17 @@
const actionTypes = {
BATCH_ACTIONS: "BATCH_ACTIONS",
MESSAGE_ADD: "MESSAGE_ADD",
MESSAGES_CLEAR: "MESSAGES_CLEAR",
MESSAGE_OPEN: "MESSAGE_OPEN",
MESSAGE_CLOSE: "MESSAGE_CLOSE",
NETWORK_MESSAGE_UPDATE: "NETWORK_MESSAGE_UPDATE",
+ NETWORK_UPDATE_REQUEST: "NETWORK_UPDATE_REQUEST",
MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
MESSAGE_OBJECT_PROPERTIES_RECEIVE: "MESSAGE_OBJECT_PROPERTIES_RECEIVE",
MESSAGE_OBJECT_ENTRIES_RECEIVE: "MESSAGE_OBJECT_ENTRIES_RECEIVE",
REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
FILTER_TOGGLE: "FILTER_TOGGLE",
FILTER_TEXT_SET: "FILTER_TEXT_SET",
FILTERS_CLEAR: "FILTERS_CLEAR",
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -47,16 +47,21 @@ NewConsoleOutputWrapper.prototype = {
}
// Do not focus if a link was clicked
let target = event.originalTarget || event.target;
if (target.closest("a")) {
return;
}
+ // Do not focus if an input field was clicked
+ if (target.closest("input")) {
+ return;
+ }
+
// Do not focus if something other than the output region was clicked
if (!target.closest(".webconsole-output")) {
return;
}
// Do not focus if something is selected
let selection = this.document.defaultView.getSelection();
if (selection && !selection.isCollapsed) {
@@ -214,16 +219,20 @@ NewConsoleOutputWrapper.prototype = {
// that networkInfo.updates has all we need.
const NUMBER_OF_NETWORK_UPDATE = 8;
if (res.networkInfo.updates.length === NUMBER_OF_NETWORK_UPDATE) {
batchedMessageAdd(actions.networkMessageUpdate(message));
this.jsterm.hud.emit("network-message-updated", res);
}
},
+ dispatchRequestUpdate: function (id, data) {
+ batchedMessageAdd(actions.networkUpdateRequest(id, data));
+ },
+
// Should be used for test purpose only.
getStore: function () {
return store;
}
};
function batchedMessageAdd(action) {
queuedActions.push(action);
--- a/devtools/client/webconsole/new-console-output/reducers/messages.js
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -17,16 +17,20 @@ const {
DEFAULT_FILTERS,
FILTERS,
MESSAGE_TYPE,
MESSAGE_SOURCE,
} = constants;
const { getGripPreviewItems } = require("devtools/client/shared/components/reps/reps");
const { getSourceNames } = require("devtools/client/shared/source-utils");
+const {
+ UPDATE_PROPS
+} = require("devtools/client/netmonitor/src/constants");
+
const MessageState = Immutable.Record({
// List of all the messages added to the console.
messagesById: Immutable.OrderedMap(),
// Array of the visible messages.
visibleMessages: [],
// Object for the filtered messages.
filteredMessagesCount: getDefaultFiltersCounter(),
// List of the message ids which are opened.
@@ -161,18 +165,20 @@ function messages(state = new MessageSta
return res;
}, [])
});
case constants.MESSAGE_OPEN:
return state.withMutations(function (record) {
record.set("messagesUiById", messagesUiById.push(action.id));
+ let currMessage = messagesById.get(action.id);
+
// If the message is a group
- if (isGroupType(messagesById.get(action.id).type)) {
+ if (isGroupType(currMessage.type)) {
// We want to make its children visible
const messagesToShow = [...messagesById].reduce((res, [id, message]) => {
if (
!visibleMessages.includes(message.id)
&& getParentGroups(message.groupId, groupsById).includes(action.id)
&& getMessageVisibility(
message,
record,
@@ -190,16 +196,31 @@ function messages(state = new MessageSta
// We can then insert the messages ids right after the one of the group.
const insertIndex = visibleMessages.indexOf(action.id) + 1;
record.set("visibleMessages", [
...visibleMessages.slice(0, insertIndex),
...messagesToShow,
...visibleMessages.slice(insertIndex),
]);
}
+
+ // If the current message is a network event, mark it as opened-once,
+ // so HTTP details are not fetched again the next time the user
+ // opens the log.
+ if (currMessage.source == "network") {
+ record.set("messagesById",
+ messagesById.set(
+ action.id, Object.assign({},
+ currMessage, {
+ openedOnce: true
+ }
+ )
+ )
+ );
+ }
});
case constants.MESSAGE_CLOSE:
return state.withMutations(function (record) {
let messageId = action.id;
let index = record.messagesUiById.indexOf(messageId);
record.deleteIn(["messagesUiById", index]);
@@ -245,16 +266,54 @@ function messages(state = new MessageSta
case constants.NETWORK_MESSAGE_UPDATE:
return state.set(
"networkMessagesUpdateById",
Object.assign({}, networkMessagesUpdateById, {
[action.message.id]: action.message
})
);
+ case constants.NETWORK_UPDATE_REQUEST: {
+ let request = networkMessagesUpdateById[action.id];
+ if (!request) {
+ return state;
+ }
+
+ let values = {};
+ for (let [key, value] of Object.entries(action.data)) {
+ if (UPDATE_PROPS.includes(key)) {
+ values[key] = value;
+
+ switch (key) {
+ case "securityInfo":
+ values.securityState = value.state;
+ break;
+ case "totalTime":
+ values.totalTime = request.totalTime;
+ break;
+ case "requestPostData":
+ values.requestHeadersFromUploadStream = {
+ headers: [],
+ headersSize: 0,
+ };
+ break;
+ }
+ }
+ }
+
+ let newState = state.set(
+ "networkMessagesUpdateById",
+ Object.assign({}, networkMessagesUpdateById, {
+ [action.id]: Object.assign({}, request, values)
+ })
+ );
+
+ return newState;
+ }
+
case constants.REMOVED_ACTORS_CLEAR:
return state.set("removedActors", []);
case constants.FILTER_TOGGLE:
case constants.FILTER_TEXT_SET:
case constants.FILTERS_CLEAR:
case constants.DEFAULT_FILTERS_RESET:
return state.withMutations(function (record) {
--- a/devtools/client/webconsole/new-console-output/store.js
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -12,23 +12,34 @@ const {
createStore
} = require("devtools/client/shared/vendor/redux");
const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
const {
BATCH_ACTIONS
} = require("devtools/client/shared/redux/middleware/debounce");
const {
MESSAGE_ADD,
+ MESSAGE_OPEN,
MESSAGES_CLEAR,
REMOVED_ACTORS_CLEAR,
+ NETWORK_MESSAGE_UPDATE,
PREFS,
} = require("devtools/client/webconsole/new-console-output/constants");
const { reducers } = require("./reducers/index");
const Services = require("Services");
+const {
+ getMessage,
+ getAllMessagesUiById,
+} = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const DataProvider = require("devtools/client/netmonitor/src/connector/firefox-data-provider");
+/**
+ * Create and configure store for the Console panel. This is the place
+ * where various enhancers and middleware can be registered.
+ */
function configureStore(hud, options = {}) {
const logLimit = options.logLimit
|| Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1);
const initialState = {
prefs: new PrefState({ logLimit }),
filters: new FilterState({
error: Services.prefs.getBoolPref(PREFS.FILTER.ERROR),
@@ -43,17 +54,22 @@ function configureStore(hud, options = {
ui: new UiState({
filterBarVisible: Services.prefs.getBoolPref(PREFS.UI.FILTER_BAR),
})
};
return createStore(
createRootReducer(),
initialState,
- compose(applyMiddleware(thunk), enableActorReleaser(hud), enableBatching())
+ compose(
+ applyMiddleware(thunk),
+ enableActorReleaser(hud),
+ enableBatching(),
+ enableNetProvider(hud)
+ )
);
}
function createRootReducer() {
return function rootReducer(state, action) {
// We want to compute the new state for all properties except "messages".
const newState = [...Object.entries(reducers)].reduce((res, [key, reducer]) => {
if (key !== "messages") {
@@ -121,16 +137,79 @@ function enableActorReleaser(hud) {
return state;
}
return next(releaseActorsEnhancer, initialState, enhancer);
};
}
/**
+ * This enhancer is responsible for fetching HTTP details data
+ * collected by the backend. The fetch happens on-demand
+ * when the user expands network log in order to inspect it.
+ *
+ * This way we don't slow down the Console logging by fetching.
+ * unnecessary data over RDP.
+ */
+function enableNetProvider(hud) {
+ let dataProvider;
+ return next => (reducer, initialState, enhancer) => {
+ function netProviderEnhancer(state, action) {
+ let proxy = hud ? hud.proxy : null;
+ if (!proxy) {
+ return reducer(state, action);
+ }
+
+ let actions = {
+ updateRequest: (id, data, batch) => {
+ proxy.dispatchRequestUpdate(id, data);
+ }
+ };
+
+ // Data provider implements async logic for fetching
+ // data from the backend. It's created the first
+ // time it's needed.
+ if (!dataProvider) {
+ dataProvider = new DataProvider({
+ actions,
+ webConsoleClient: proxy.webConsoleClient
+ });
+ }
+
+ let type = action.type;
+
+ // If network message has been opened, fetch all
+ // HTTP details from the backend.
+ if (type == MESSAGE_OPEN) {
+ let message = getMessage(state, action.id);
+ if (!message.openedOnce && message.source == "network") {
+ message.updates.forEach(updateType => {
+ dataProvider.onNetworkEventUpdate(null, {
+ packet: { updateType: updateType },
+ networkInfo: message,
+ });
+ });
+ }
+ }
+
+ // Process all incoming HTTP details packets.
+ if (type == NETWORK_MESSAGE_UPDATE) {
+ let open = getAllMessagesUiById(state).includes(action.id);
+ if (open) {
+ dataProvider.onNetworkEventUpdate(null, action.response);
+ }
+ }
+
+ return reducer(state, action);
+ }
+
+ return next(netProviderEnhancer, initialState, enhancer);
+ };
+}
+/**
* Helper function for releasing backend actors.
*/
function releaseActors(removedActors, proxy) {
if (!proxy) {
return;
}
removedActors.forEach(actor => proxy.releaseActor(actor));
--- a/devtools/client/webconsole/new-console-output/types.js
+++ b/devtools/client/webconsole/new-console-output/types.js
@@ -55,10 +55,15 @@ exports.NetworkEventMessage = function (
request: null,
response: null,
source: MESSAGE_SOURCE.NETWORK,
type: MESSAGE_TYPE.LOG,
groupId: null,
timeStamp: null,
totalTime: null,
indent: 0,
+ updates: null,
+ openedOnce: false,
+ securityState: null,
+ securityInfo: null,
+ requestHeadersFromUploadStream: null,
}, props);
};
--- a/devtools/client/webconsole/new-console-output/utils/messages.js
+++ b/devtools/client/webconsole/new-console-output/utils/messages.js
@@ -2,16 +2,17 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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 l10n = require("devtools/client/webconsole/webconsole-l10n");
+const { getUrlDetails } = require("devtools/client/netmonitor/src/utils/request-utils");
const {
MESSAGE_SOURCE,
MESSAGE_TYPE,
MESSAGE_LEVEL,
} = require("../constants");
const {
ConsoleMessage,
@@ -227,16 +228,21 @@ function transformNetworkEventPacket(pac
return new NetworkEventMessage({
actor: networkEvent.actor,
isXHR: networkEvent.isXHR,
request: networkEvent.request,
response: networkEvent.response,
timeStamp: networkEvent.timeStamp,
totalTime: networkEvent.totalTime,
+ url: networkEvent.request.url,
+ urlDetails: getUrlDetails(networkEvent.request.url),
+ method: networkEvent.request.method,
+ updates: networkEvent.updates,
+ cause: networkEvent.cause,
});
}
function transformEvaluationResultPacket(packet) {
let {
exceptionMessage: messageText,
exceptionDocURL,
frame,
--- a/devtools/client/webconsole/panel.js
+++ b/devtools/client/webconsole/panel.js
@@ -83,17 +83,17 @@ WebConsolePanel.prototype = {
this.hud = webConsole;
this._isReady = true;
this.emit("ready");
return this;
}, (reason) => {
let msg = "WebConsolePanel open failed. " +
reason.error + ": " + reason.message;
dump(msg + "\n");
- console.error(msg);
+ console.error(msg, reason);
});
},
get target() {
return this._toolbox.target;
},
_isReady: false,
--- a/devtools/client/webconsole/webconsole-connection-proxy.js
+++ b/devtools/client/webconsole/webconsole-connection-proxy.js
@@ -238,16 +238,20 @@ WebConsoleConnectionProxy.prototype = {
/**
* Dispatch a message event on the new frontend and emit an event for tests.
*/
dispatchMessageUpdate: function (networkInfo, response) {
this.webConsoleFrame.newConsoleOutput.dispatchMessageUpdate(networkInfo, response);
},
+ dispatchRequestUpdate: function (id, data) {
+ this.webConsoleFrame.newConsoleOutput.dispatchRequestUpdate(id, data);
+ },
+
/**
* The "cachedMessages" response handler.
*
* @private
* @param object response
* The JSON response object received from the server.
*/
_onCachedMessages: function (response) {
--- a/devtools/client/webconsole/webconsole.xhtml
+++ b/devtools/client/webconsole/webconsole.xhtml
@@ -6,16 +6,20 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" dir="">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
<link rel="stylesheet" href="resource://devtools/client/themes/light-theme.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/webconsole.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/components-frame.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/reps/reps.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabs.css"/>
+ <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabbar.css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/netmonitor/src/assets/styles/netmonitor.css"/>
+
<script src="chrome://devtools/content/shared/theme-switching.js"></script>
<script type="application/javascript"
src="resource://devtools/client/webconsole/new-console-output/main.js"></script>
</head>
<body class="theme-sidebar" role="application">
<div id="app-wrapper" class="theme-body">
<div id="output-container" role="document" aria-live="polite"/>
<div id="jsterm-wrapper">