summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/CustomRequestPanel.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/netmonitor/src/components/CustomRequestPanel.js')
-rw-r--r--devtools/client/netmonitor/src/components/CustomRequestPanel.js371
1 files changed, 371 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/CustomRequestPanel.js b/devtools/client/netmonitor/src/components/CustomRequestPanel.js
new file mode 100644
index 0000000000..66f63b7d8d
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/CustomRequestPanel.js
@@ -0,0 +1,371 @@
+/* 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,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.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,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
+const {
+ getSelectedRequest,
+} = require("resource://devtools/client/netmonitor/src/selectors/index.js");
+const {
+ getUrlQuery,
+ parseQueryString,
+ writeHeaderText,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+
+const { button, div, input, label, textarea } = dom;
+
+const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
+const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
+const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
+const CUSTOM_NEW_REQUEST_METHOD_LABEL = L10N.getStr(
+ "netmonitor.custom.newRequestMethodLabel"
+);
+const CUSTOM_NEW_REQUEST_URL_LABEL = L10N.getStr(
+ "netmonitor.custom.newRequestUrlLabel"
+);
+const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
+const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
+const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
+
+/*
+ * Custom request panel component
+ * A network request editor which simply provide edit and resend interface
+ * for network development.
+ */
+class CustomRequestPanel extends Component {
+ static get propTypes() {
+ return {
+ connector: PropTypes.object.isRequired,
+ removeSelectedCustomRequest: PropTypes.func.isRequired,
+ request: PropTypes.object.isRequired,
+ sendCustomRequest: PropTypes.func.isRequired,
+ updateRequest: PropTypes.func.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ const { request, connector } = this.props;
+ this.initialRequestMethod = request.method;
+ fetchNetworkUpdatePacket(connector.requestData, request, [
+ "requestHeaders",
+ "responseHeaders",
+ "requestPostData",
+ ]);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { request, connector } = nextProps;
+ fetchNetworkUpdatePacket(connector.requestData, request, [
+ "requestHeaders",
+ "responseHeaders",
+ "requestPostData",
+ ]);
+ }
+
+ /**
+ * Parse a text representation of a name[divider]value list with
+ * the given name regex and divider character.
+ *
+ * @param {string} text - Text of list
+ * @return {array} array of headers info {name, value}
+ */
+ parseRequestText(text, namereg, divider) {
+ const regex = new RegExp(`(${namereg})\\${divider}\\s*(\\S.*)`);
+ const pairs = [];
+
+ for (const line of text.split("\n")) {
+ const matches = regex.exec(line);
+ if (matches) {
+ const [, name, value] = matches;
+ pairs.push({ name, value });
+ }
+ }
+ return pairs;
+ }
+
+ /**
+ * Update Custom Request Fields
+ *
+ * @param {Object} evt click event
+ * @param {Object} request current request
+ * @param {updateRequest} updateRequest action
+ */
+ updateCustomRequestFields(evt, request, updateRequest) {
+ const val = evt.target.value;
+ let data;
+
+ switch (evt.target.id) {
+ case "custom-headers-value":
+ data = {
+ requestHeaders: {
+ customHeadersValue: val || "",
+ // Parse text representation of multiple HTTP headers
+ headers: this.parseRequestText(val, "\\S+?", ":"),
+ },
+ };
+ break;
+ case "custom-method-value":
+ // If val is empty when leaving the "method" field, set the method to
+ // its original value
+ data =
+ evt.type === "blur" && val === ""
+ ? { method: this.initialRequestMethod }
+ : { method: val.trim() };
+ break;
+ case "custom-postdata-value":
+ // Update "content-length" header value to reflect change
+ // in post data field.
+ const { requestHeaders } = request;
+ const newHeaders = requestHeaders.headers.map(header => {
+ if (header.name.toLowerCase() == "content-length") {
+ return {
+ name: header.name,
+ value: val.length,
+ };
+ }
+ return header;
+ });
+
+ data = {
+ requestPostData: {
+ postData: { text: val },
+ },
+ requestHeaders: {
+ headers: newHeaders,
+ },
+ };
+ break;
+ case "custom-query-value":
+ let customQueryValue = val || "";
+ // Parse readable text list of a query string
+ const queryArray = customQueryValue
+ ? this.parseRequestText(customQueryValue, ".+?", "=")
+ : [];
+ // Write out a list of query params into a query string
+ const queryString = queryArray
+ .map(({ name, value }) => name + "=" + value)
+ .join("&");
+ const url = queryString
+ ? [request.url.split("?")[0], queryString].join("?")
+ : request.url.split("?")[0];
+ // Remove temp customQueryValue while query string is parsable
+ if (
+ customQueryValue === "" ||
+ queryArray.length === customQueryValue.split("\n").length
+ ) {
+ customQueryValue = null;
+ }
+ data = {
+ customQueryValue,
+ url,
+ };
+ break;
+ case "custom-url-value":
+ data = {
+ customQueryValue: null,
+ url: val,
+ };
+ break;
+ default:
+ break;
+ }
+ if (data) {
+ // All updateRequest batch mode should be disabled to make UI editing in sync
+ updateRequest(request.id, data, false);
+ }
+ }
+
+ render() {
+ const {
+ removeSelectedCustomRequest,
+ request = {},
+ sendCustomRequest,
+ updateRequest,
+ } = this.props;
+ const { method, customQueryValue, requestHeaders, requestPostData, url } =
+ request;
+
+ let headers = "";
+ if (requestHeaders) {
+ headers = requestHeaders.customHeadersValue
+ ? requestHeaders.customHeadersValue
+ : writeHeaderText(requestHeaders.headers).trim();
+ }
+ const queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
+ let params = customQueryValue;
+ if (!params) {
+ params = queryArray
+ ? queryArray.map(({ name, value }) => name + "=" + value).join("\n")
+ : "";
+ }
+ const postData = requestPostData?.postData.text
+ ? requestPostData.postData.text
+ : "";
+
+ return div(
+ { className: "custom-request-panel" },
+ div(
+ { className: "custom-request-label custom-header" },
+ CUSTOM_NEW_REQUEST
+ ),
+ div(
+ { className: "custom-request-panel-content" },
+ div(
+ { className: "tabpanel-summary-container custom-request" },
+ div(
+ { className: "custom-request-button-container" },
+ button(
+ {
+ className: "devtools-button",
+ id: "custom-request-close-button",
+ onClick: removeSelectedCustomRequest,
+ },
+ CUSTOM_CANCEL
+ ),
+ button(
+ {
+ className: "devtools-button",
+ id: "custom-request-send-button",
+ onClick: sendCustomRequest,
+ },
+ CUSTOM_SEND
+ )
+ )
+ ),
+ div(
+ {
+ className: "tabpanel-summary-container custom-method-and-url",
+ id: "custom-method-and-url",
+ },
+ label(
+ {
+ className: "custom-method-value-label custom-request-label",
+ htmlFor: "custom-method-value",
+ },
+ CUSTOM_NEW_REQUEST_METHOD_LABEL
+ ),
+ input({
+ className: "custom-method-value",
+ id: "custom-method-value",
+ onChange: evt =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ onBlur: evt =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ value: method,
+ }),
+ label(
+ {
+ className: "custom-url-value-label custom-request-label",
+ htmlFor: "custom-url-value",
+ },
+ CUSTOM_NEW_REQUEST_URL_LABEL
+ ),
+ input({
+ className: "custom-url-value",
+ id: "custom-url-value",
+ onChange: evt =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ value: url || "http://",
+ })
+ ),
+ // Hide query field when there is no params
+ params
+ ? div(
+ {
+ className: "tabpanel-summary-container custom-section",
+ id: "custom-query",
+ },
+ label(
+ {
+ className: "custom-request-label",
+ htmlFor: "custom-query-value",
+ },
+ CUSTOM_QUERY
+ ),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-query-value",
+ onChange: evt =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ rows: 4,
+ value: params,
+ wrap: "off",
+ })
+ )
+ : null,
+ div(
+ {
+ id: "custom-headers",
+ className: "tabpanel-summary-container custom-section",
+ },
+ label(
+ {
+ className: "custom-request-label",
+ htmlFor: "custom-headers-value",
+ },
+ CUSTOM_HEADERS
+ ),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-headers-value",
+ onChange: evt =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ rows: 8,
+ value: headers,
+ wrap: "off",
+ })
+ ),
+ div(
+ {
+ id: "custom-postdata",
+ className: "tabpanel-summary-container custom-section",
+ },
+ label(
+ {
+ className: "custom-request-label",
+ htmlFor: "custom-postdata-value",
+ },
+ CUSTOM_POSTDATA
+ ),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-postdata-value",
+ onChange: evt =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ rows: 6,
+ value: postData,
+ wrap: "off",
+ })
+ )
+ )
+ );
+ }
+}
+
+module.exports = connect(
+ state => ({ request: getSelectedRequest(state) }),
+ (dispatch, props) => ({
+ removeSelectedCustomRequest: () =>
+ dispatch(Actions.removeSelectedCustomRequest()),
+ sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
+ updateRequest: (id, data, batch) =>
+ dispatch(Actions.updateRequest(id, data, batch)),
+ })
+)(CustomRequestPanel);