summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/request-details/RequestPanel.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/netmonitor/src/components/request-details/RequestPanel.js301
1 files changed, 301 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/request-details/RequestPanel.js b/devtools/client/netmonitor/src/components/request-details/RequestPanel.js
new file mode 100644
index 0000000000..5306161d2e
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/request-details/RequestPanel.js
@@ -0,0 +1,301 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
+const {
+ L10N,
+} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const {
+ fetchNetworkUpdatePacket,
+ parseFormData,
+ parseJSON,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+const {
+ sortObjectKeys,
+} = require("resource://devtools/client/netmonitor/src/utils/sort-utils.js");
+const {
+ FILTER_SEARCH_DELAY,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+const {
+ updateFormDataSections,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
+
+// Components
+const PropertiesView = createFactory(
+ require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js")
+);
+const SearchBox = createFactory(
+ require("resource://devtools/client/shared/components/SearchBox.js")
+);
+
+loader.lazyGetter(this, "SourcePreview", function () {
+ return createFactory(
+ require("resource://devtools/client/netmonitor/src/components/previews/SourcePreview.js")
+ );
+});
+
+const { div, input, label, span, h2 } = dom;
+
+const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
+const REQUEST_EMPTY_TEXT = L10N.getStr("paramsNoPayloadText");
+const REQUEST_FILTER_TEXT = L10N.getStr("paramsFilterText");
+const REQUEST_FORM_DATA = L10N.getStr("paramsFormData");
+const REQUEST_POST_PAYLOAD = L10N.getStr("paramsPostPayload");
+const RAW_REQUEST_PAYLOAD = L10N.getStr("netmonitor.request.raw");
+const REQUEST_TRUNCATED = L10N.getStr("requestTruncated");
+
+/**
+ * Params panel component
+ * Displays the GET parameters and POST data of a request
+ */
+class RequestPanel extends Component {
+ static get propTypes() {
+ return {
+ connector: PropTypes.object.isRequired,
+ openLink: PropTypes.func,
+ request: PropTypes.object.isRequired,
+ updateRequest: PropTypes.func.isRequired,
+ targetSearchResult: PropTypes.object,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ filterText: "",
+ rawRequestPayloadDisplayed: !!props.targetSearchResult,
+ };
+
+ this.toggleRawRequestPayload = this.toggleRawRequestPayload.bind(this);
+ this.renderRawRequestPayloadBtn =
+ this.renderRawRequestPayloadBtn.bind(this);
+ }
+
+ componentDidMount() {
+ const { request, connector } = this.props;
+ fetchNetworkUpdatePacket(connector.requestData, request, [
+ "requestPostData",
+ ]);
+ updateFormDataSections(this.props);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { request, connector } = nextProps;
+ fetchNetworkUpdatePacket(connector.requestData, request, [
+ "requestPostData",
+ ]);
+ updateFormDataSections(nextProps);
+
+ if (nextProps.targetSearchResult !== null) {
+ this.setState({
+ rawRequestPayloadDisplayed: !!nextProps.targetSearchResult,
+ });
+ }
+ }
+
+ /**
+ * Update only if:
+ * 1) The rendered object has changed
+ * 2) The filter text has changed
+ * 2) The display got toggled between formatted and raw data
+ * 3) The user selected another search result target.
+ */
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.request !== nextProps.request ||
+ this.state.filterText !== nextState.filterText ||
+ this.state.rawRequestPayloadDisplayed !==
+ nextState.rawRequestPayloadDisplayed ||
+ this.props.targetSearchResult !== nextProps.targetSearchResult
+ );
+ }
+
+ /**
+ * 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)
+ *
+ * This function is not sorting result properties since it can
+ * results in unexpected order of params. See bug 1469533
+ *
+ * @param {Object[]} arr - key-value pair array or form params
+ * @returns {Object} Rep compatible object
+ */
+ getProperties(arr) {
+ return arr.reduce((map, obj) => {
+ const value = map[obj.name];
+ if (value || value === "") {
+ if (typeof value !== "object") {
+ map[obj.name] = [value];
+ }
+ map[obj.name].push(obj.value);
+ } else {
+ map[obj.name] = obj.value;
+ }
+ return map;
+ }, {});
+ }
+
+ toggleRawRequestPayload() {
+ this.setState({
+ rawRequestPayloadDisplayed: !this.state.rawRequestPayloadDisplayed,
+ });
+ }
+
+ renderRawRequestPayloadBtn(key, checked, onChange) {
+ return [
+ label(
+ {
+ key: `${key}RawRequestPayloadBtn`,
+ className: "raw-data-toggle",
+ htmlFor: `raw-${key}-checkbox`,
+ onClick: event => {
+ // stop the header click event
+ event.stopPropagation();
+ },
+ },
+ span({ className: "raw-data-toggle-label" }, RAW_REQUEST_PAYLOAD),
+ span(
+ { className: "raw-data-toggle-input" },
+ input({
+ id: `raw-${key}-checkbox`,
+ checked,
+ className: "devtools-checkbox-toggle",
+ onChange,
+ type: "checkbox",
+ })
+ )
+ ),
+ ];
+ }
+
+ renderRequestPayload(component, componentProps) {
+ return component(componentProps);
+ }
+
+ render() {
+ const { request, targetSearchResult } = this.props;
+ const { filterText, rawRequestPayloadDisplayed } = this.state;
+ const { formDataSections, mimeType, requestPostData } = request;
+ const postData = requestPostData ? requestPostData.postData?.text : null;
+
+ if ((!formDataSections || formDataSections.length === 0) && !postData) {
+ return div({ className: "empty-notice" }, REQUEST_EMPTY_TEXT);
+ }
+
+ let component;
+ let componentProps;
+ let requestPayloadLabel = REQUEST_POST_PAYLOAD;
+ let hasFormattedDisplay = false;
+
+ let error;
+
+ // Form Data section
+ if (formDataSections && formDataSections.length) {
+ const sections = formDataSections.filter(str => /\S/.test(str)).join("&");
+ component = PropertiesView;
+ componentProps = {
+ object: this.getProperties(parseFormData(sections)),
+ filterText,
+ targetSearchResult,
+ defaultSelectFirstNode: false,
+ };
+ requestPayloadLabel = REQUEST_FORM_DATA;
+ hasFormattedDisplay = true;
+ }
+
+ // Request payload section
+ const limit = Services.prefs.getIntPref(
+ "devtools.netmonitor.requestBodyLimit"
+ );
+
+ // Check if the request post data has been truncated from the backend,
+ // in which case no parse should be attempted.
+ if (postData && limit <= postData.length) {
+ error = REQUEST_TRUNCATED;
+ }
+ if (formDataSections && formDataSections.length === 0 && postData) {
+ if (!error) {
+ const jsonParsedPostData = parseJSON(postData);
+ const { json, strippedChars } = jsonParsedPostData;
+ // If XSSI characters were present in the request just display the raw
+ // data because a request should never have XSSI escape characters
+ if (strippedChars) {
+ hasFormattedDisplay = false;
+ } else if (json) {
+ component = PropertiesView;
+ componentProps = {
+ object: sortObjectKeys(json),
+ filterText,
+ targetSearchResult,
+ defaultSelectFirstNode: false,
+ };
+ requestPayloadLabel = JSON_SCOPE_NAME;
+ hasFormattedDisplay = true;
+ }
+ }
+ }
+
+ if (
+ (!hasFormattedDisplay || this.state.rawRequestPayloadDisplayed) &&
+ postData
+ ) {
+ component = SourcePreview;
+ componentProps = {
+ text: postData,
+ mode: mimeType?.replace(/;.+/, ""),
+ targetSearchResult,
+ };
+ requestPayloadLabel = REQUEST_POST_PAYLOAD;
+ }
+
+ return div(
+ { className: "panel-container" },
+ error && div({ className: "request-error-header", title: error }, error),
+ div(
+ { className: "devtools-toolbar devtools-input-toolbar" },
+ SearchBox({
+ delay: FILTER_SEARCH_DELAY,
+ type: "filter",
+ onChange: text => this.setState({ filterText: text }),
+ placeholder: REQUEST_FILTER_TEXT,
+ })
+ ),
+ h2({ className: "data-header", role: "heading" }, [
+ span(
+ {
+ key: "data-label",
+ className: "data-label",
+ },
+ requestPayloadLabel
+ ),
+ hasFormattedDisplay &&
+ this.renderRawRequestPayloadBtn(
+ "request",
+ rawRequestPayloadDisplayed,
+ this.toggleRawRequestPayload
+ ),
+ ]),
+ this.renderRequestPayload(component, componentProps)
+ );
+ }
+}
+
+module.exports = connect(null, dispatch => ({
+ updateRequest: (id, data, batch) =>
+ dispatch(Actions.updateRequest(id, data, batch)),
+}))(RequestPanel);