Bug 1403927 - Fix Params panel in HTTPi; r=nchevobbe draft
authorJan Odvarko <odvarko@gmail.com>
Thu, 19 Oct 2017 14:49:05 +0200
changeset 683247 87d364ab98dca9ffb275fa979050e062a00daf65
parent 682167 4efa95ae9141a1973b2cdc026f315691f4e3d7ea
child 683248 de3a5e16f136d42bfc1197c64c12fc50608bb9b0
push id85305
push userjodvarko@mozilla.com
push dateThu, 19 Oct 2017 12:54:21 +0000
reviewersnchevobbe
bugs1403927
milestone58.0a1
Bug 1403927 - Fix Params panel in HTTPi; r=nchevobbe MozReview-Commit-ID: 8KIWYs5sDoG
devtools/client/netmonitor/src/components/monitor-panel.js
devtools/client/netmonitor/src/components/params-panel.js
devtools/client/netmonitor/src/components/tabbox-panel.js
devtools/client/netmonitor/src/reducers/requests.js
devtools/client/netmonitor/src/utils/request-utils.js
devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
devtools/client/webconsole/new-console-output/reducers/messages.js
--- a/devtools/client/netmonitor/src/components/monitor-panel.js
+++ b/devtools/client/netmonitor/src/components/monitor-panel.js
@@ -9,17 +9,17 @@ 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 { getFormDataSections } = require("../utils/request-utils");
+const { updateFormDataSections } = require("../utils/request-utils");
 const { getSelectedRequest } = require("../selectors/index");
 
 // Components
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 const NetworkDetailsPanel = createFactory(require("./network-details-panel"));
 const RequestList = createFactory(require("./request-list"));
 const Toolbar = createFactory(require("./toolbar"));
 const { div } = DOM;
@@ -49,42 +49,17 @@ const MonitorPanel = createClass({
     };
   },
 
   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,
-        this.props.connector.getLongString,
-      ).then((newFormDataSections) => {
-        updateRequest(
-          request.id,
-          { formDataSections: newFormDataSections },
-          true,
-        );
-      });
-    }
+    updateFormDataSections(nextProps);
   },
 
   componentWillUnmount() {
     MediaQueryList.removeListener(this.onLayoutChange);
 
     let { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
 
     if (this.state.isVerticalSpliter && clientWidth) {
@@ -103,40 +78,41 @@ const MonitorPanel = createClass({
     });
   },
 
   render() {
     let {
       connector,
       isEmpty,
       networkDetailsOpen,
+      openLink,
       sourceMapService,
-      openLink
     } = this.props;
 
     let initialWidth = Services.prefs.getIntPref(
         "devtools.netmonitor.panes-network-details-width");
     let initialHeight = Services.prefs.getIntPref(
         "devtools.netmonitor.panes-network-details-height");
+
     return (
       div({ className: "monitor-panel" },
         Toolbar(),
         SplitBox({
           className: "devtools-responsive-container",
           initialWidth: `${initialWidth}px`,
           initialHeight: `${initialHeight}px`,
           minSize: "50px",
           maxSize: "80%",
           splitterSize: "1px",
           startPanel: RequestList({ isEmpty, connector }),
           endPanel: networkDetailsOpen && NetworkDetailsPanel({
             ref: "endPanel",
             connector,
+            openLink,
             sourceMapService,
-            openLink,
           }),
           endPanelCollapsed: !networkDetailsOpen,
           endPanelControl: true,
           vert: this.state.isVerticalSpliter,
         }),
       )
     );
   }
--- a/devtools/client/netmonitor/src/components/params-panel.js
+++ b/devtools/client/netmonitor/src/components/params-panel.js
@@ -1,22 +1,26 @@
 /* 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 { L10N } = require("../utils/l10n");
 const { getUrlQuery, parseQueryString, parseFormData } = require("../utils/request-utils");
 const { sortObjectKeys } = require("../utils/sort-utils");
+const { updateFormDataSections } = require("../utils/request-utils");
+const Actions = require("../actions/index");
 
 // Components
 const PropertiesView = createFactory(require("./properties-view"));
 
 const { div } = DOM;
 
 const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
 const PARAMS_EMPTY_TEXT = L10N.getStr("paramsEmptyText");
@@ -26,93 +30,106 @@ const PARAMS_POST_PAYLOAD = L10N.getStr(
 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({
-  openLink,
-  request,
-}) {
-  let {
-    formDataSections,
-    mimeType,
-    requestPostData,
-    url,
-  } = request;
-  let postData = requestPostData ? requestPostData.postData.text : null;
-  let query = getUrlQuery(url);
+const ParamsPanel = createClass({
+  displayName: "ParamsPanel",
+
+  propTypes: {
+    connector: PropTypes.object.isRequired,
+    openLink: PropTypes.func,
+    request: PropTypes.object.isRequired,
+    updateRequest: PropTypes.func.isRequired,
+  },
+
+  componentDidMount() {
+    updateFormDataSections(this.props);
+  },
+
+  componentWillReceiveProps(nextProps) {
+    updateFormDataSections(nextProps);
+  },
+
+  render() {
+    let {
+      openLink,
+      request
+    } = this.props;
+    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;
 
-  if (!formDataSections && !postData && !query) {
-    return div({ className: "empty-notice" },
-      PARAMS_EMPTY_TEXT
+    // 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(parseFormData(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] = sortObjectKeys(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,
+          openLink,
+        })
+      )
     );
   }
-
-  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(parseFormData(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] = sortObjectKeys(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,
-        openLink,
-      })
-    )
-  );
-}
-
-ParamsPanel.displayName = "ParamsPanel";
-
-ParamsPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-  openLink: PropTypes.func,
-};
+});
 
 /**
  * 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
@@ -128,9 +145,13 @@ function getProperties(arr) {
       map[obj.name].push(obj.value);
     } else {
       map[obj.name] = obj.value;
     }
     return map;
   }, {}));
 }
 
-module.exports = ParamsPanel;
+module.exports = connect(null,
+  (dispatch) => ({
+    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
+  }),
+)(ParamsPanel);
--- a/devtools/client/netmonitor/src/components/tabbox-panel.js
+++ b/devtools/client/netmonitor/src/components/tabbox-panel.js
@@ -67,17 +67,17 @@ function TabboxPanel({
         title: COOKIES_TITLE,
       },
         CookiesPanel({ request, openLink }),
       ),
       TabPanel({
         id: PANELS.PARAMS,
         title: PARAMS_TITLE,
       },
-        ParamsPanel({ request, openLink }),
+        ParamsPanel({ connector, openLink, request }),
       ),
       TabPanel({
         id: PANELS.RESPONSE,
         title: RESPONSE_TITLE,
       },
         ResponsePanel({ request, openLink }),
       ),
       TabPanel({
@@ -86,17 +86,17 @@ function TabboxPanel({
       },
         TimingsPanel({ request }),
       ),
       request.cause && request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
       TabPanel({
         id: PANELS.STACK_TRACE,
         title: STACK_TRACE_TITLE,
       },
-        StackTracePanel({ request, sourceMapService, openLink, connector }),
+        StackTracePanel({ connector, openLink, request, sourceMapService }),
       ),
       request.securityState && request.securityState !== "insecure" &&
       TabPanel({
         id: PANELS.SECURITY,
         title: SECURITY_TITLE,
       },
         SecurityPanel({ request, openLink }),
       ),
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -1,27 +1,29 @@
 /* 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 I = require("devtools/client/shared/vendor/immutable");
-const { getUrlDetails } = require("../utils/request-utils");
+const {
+  getUrlDetails,
+  processNetworkUpdates,
+} = require("../utils/request-utils");
 const {
   ADD_REQUEST,
   CLEAR_REQUESTS,
   CLONE_SELECTED_REQUEST,
   OPEN_NETWORK_DETAILS,
   REMOVE_SELECTED_CUSTOM_REQUEST,
   SELECT_REQUEST,
   SEND_CUSTOM_REQUEST,
   TOGGLE_RECORDING,
   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,
@@ -185,40 +187,18 @@ function requestsReducer(state = new Req
       let { requests, lastEndedMillis } = state;
 
       let updatedRequest = requests.get(action.id);
       if (!updatedRequest) {
         return state;
       }
 
       updatedRequest = updatedRequest.withMutations(request => {
-        for (let [key, value] of Object.entries(action.data)) {
-          if (!UPDATE_PROPS.includes(key)) {
-            continue;
-          }
-
-          request[key] = value;
-
-          switch (key) {
-            case "url":
-              // Compute the additional URL details
-              request.urlDetails = getUrlDetails(value);
-              break;
-            case "totalTime":
-              request.endedMillis = request.startedMillis + value;
-              lastEndedMillis = Math.max(lastEndedMillis, request.endedMillis);
-              break;
-            case "requestPostData":
-              request.requestHeadersFromUploadStream = {
-                headers: [],
-                headersSize: 0,
-              };
-              break;
-          }
-        }
+        let values = processNetworkUpdates(action.data);
+        request = Object.assign(request, values);
       });
 
       return state.withMutations(st => {
         st.requests = requests.set(updatedRequest.id, updatedRequest);
         st.lastEndedMillis = lastEndedMillis;
       });
     }
 
--- a/devtools/client/netmonitor/src/utils/request-utils.js
+++ b/devtools/client/netmonitor/src/utils/request-utils.js
@@ -1,16 +1,20 @@
 /* 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 mozilla/reject-some-requires */
 
 "use strict";
 
+const {
+  UPDATE_PROPS,
+} = require("devtools/client/netmonitor/src/constants");
+
 const CONTENT_MIME_TYPE_ABBREVIATIONS = {
   "ecmascript": "js",
   "javascript": "js",
   "x-javascript": "js"
 };
 
 /**
  * Extracts any urlencoded form data sections (e.g. "?foo=bar&baz=42") from a
@@ -384,16 +388,79 @@ function getResponseHeader(item, header)
   for (let responseHeader of responseHeaders.headers) {
     if (responseHeader.name.toLowerCase() == header) {
       return responseHeader.value;
     }
   }
   return null;
 }
 
+/**
+ * Extracts any urlencoded form data sections from a POST request.
+ */
+function updateFormDataSections(props) {
+  let {
+    connector,
+    request = {},
+    updateRequest,
+  } = props;
+  let {
+    formDataSections,
+    requestHeaders,
+    requestHeadersFromUploadStream,
+    requestPostData,
+  } = request;
+
+  if (!formDataSections && requestHeaders &&
+      requestHeadersFromUploadStream && requestPostData) {
+    getFormDataSections(
+      requestHeaders,
+      requestHeadersFromUploadStream,
+      requestPostData,
+      connector.getLongString,
+    ).then((newFormDataSections) => {
+      updateRequest(
+        request.id,
+        { formDataSections: newFormDataSections },
+        true,
+      );
+    });
+  }
+}
+
+/**
+ * This helper function is used for additional processing of
+ * incoming network update packets. It's used by Network and
+ * Console panel reducers.
+ */
+function processNetworkUpdates(request) {
+  let result = {};
+  for (let [key, value] of Object.entries(request)) {
+    if (UPDATE_PROPS.includes(key)) {
+      result[key] = value;
+
+      switch (key) {
+        case "securityInfo":
+          result.securityState = value.state;
+          break;
+        case "totalTime":
+          result.totalTime = request.totalTime;
+          break;
+        case "requestPostData":
+          result.requestHeadersFromUploadStream = {
+            headers: [],
+            headersSize: 0,
+          };
+          break;
+      }
+    }
+  }
+  return result;
+}
+
 module.exports = {
   decodeUnicodeBase64,
   getFormDataSections,
   fetchHeaders,
   formDataURI,
   writeHeaderText,
   decodeUnicodeUrl,
   getAbbreviatedMimeType,
@@ -406,11 +473,13 @@ module.exports = {
   getUrlBaseNameWithQuery,
   getUrlDetails,
   getUrlHost,
   getUrlHostName,
   getUrlQuery,
   getUrlScheme,
   parseQueryString,
   parseFormData,
+  updateFormDataSections,
+  processNetworkUpdates,
   propertiesEqual,
   ipToLong,
 };
--- a/devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/NetworkEventMessage.js
@@ -103,17 +103,19 @@ function NetworkEventMessage({
   // API consumed by Net monitor UI components. Most of the method
   // are not needed in context of the Console panel (atm) and thus
   // let's just provide empty implementation.
   // Individual methods might be implemented step by step as needed.
   let connector = {
     viewSourceInDebugger: (url, line) => {
       serviceContainer.onViewSourceInDebugger({url, line});
     },
-    getLongString: () => {},
+    getLongString: (grip) => {
+      return serviceContainer.getLongString(grip);
+    },
     getTabTarget: () => {},
     getNetworkRequest: () => {},
     sendHTTPRequest: () => {},
     setPreferences: () => {},
     triggerActivity: () => {},
   };
 
   // Only render the attachment if the network-event is
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -69,32 +69,37 @@ NewConsoleOutputWrapper.prototype = {
       let selection = this.document.defaultView.getSelection();
       if (selection && !selection.isCollapsed) {
         return;
       }
 
       this.jsterm.focus();
     });
 
+    let { hud } = this.jsterm;
+
     const serviceContainer = {
       attachRefToHud,
       emitNewMessage: (node, messageId, timeStamp) => {
         this.jsterm.hud.emit("new-messages", new Set([{
           node,
           messageId,
           timeStamp,
         }]));
       },
-      hudProxy: this.jsterm.hud.proxy,
+      hudProxy: hud.proxy,
       openLink: url => {
-        this.jsterm.hud.owner.openLink(url);
+        hud.owner.openLink(url);
       },
       createElement: nodename => {
         return this.document.createElement(nodename);
       },
+      getLongString: (grip) => {
+        return hud.proxy.webConsoleClient.getString(grip);
+      },
     };
 
     // Set `openContextMenu` this way so, `serviceContainer` variable
     // is available in the current scope and we can pass it into
     // `createContextMenu` method.
     serviceContainer.openContextMenu = (e, message) => {
       let { screenX, screenY, target } = e;
 
@@ -228,18 +233,25 @@ NewConsoleOutputWrapper.prototype = {
   dispatchTimestampsToggle: function (enabled) {
     store.dispatch(actions.timestampsToggle(enabled));
   },
 
   dispatchMessageUpdate: function (message, res) {
     // network-message-updated will emit when all the update message arrives.
     // Since we can't ensure the order of the network update, we check
     // that networkInfo.updates has all we need.
+    // Note that 'requestPostData' is sent only for POST requests, so we need
+    // to count with that.
     const NUMBER_OF_NETWORK_UPDATE = 8;
-    if (res.networkInfo.updates.length === NUMBER_OF_NETWORK_UPDATE) {
+    let expectedLength = NUMBER_OF_NETWORK_UPDATE;
+    if (res.networkInfo.updates.indexOf("requestPostData") != -1) {
+      expectedLength++;
+    }
+
+    if (res.networkInfo.updates.length === expectedLength) {
       this.batchedMessageUpdates({ res, message });
     }
   },
 
   dispatchRequestUpdate: function (id, data) {
     this.batchedRequestUpdates({ id, data });
   },
 
--- a/devtools/client/webconsole/new-console-output/reducers/messages.js
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -18,19 +18,23 @@ const {
   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
+  UPDATE_REQUEST,
 } = require("devtools/client/netmonitor/src/constants");
 
+const {
+  processNetworkUpdates,
+} = require("devtools/client/netmonitor/src/utils/request-utils");
+
 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.
@@ -269,44 +273,24 @@ function messages(state = new MessageSta
     case constants.NETWORK_MESSAGE_UPDATE:
       return state.set(
         "networkMessagesUpdateById",
         Object.assign({}, networkMessagesUpdateById, {
           [action.message.id]: action.message
         })
       );
 
+    case UPDATE_REQUEST:
     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 values = processNetworkUpdates(action.data);
       newState = state.set(
         "networkMessagesUpdateById",
         Object.assign({}, networkMessagesUpdateById, {
           [action.id]: Object.assign({}, request, values)
         })
       );
 
       return newState;