summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/netmonitor/src/widgets')
-rw-r--r--devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js137
-rw-r--r--devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js113
-rw-r--r--devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js78
-rw-r--r--devtools/client/netmonitor/src/widgets/RequestListContextMenu.js793
-rw-r--r--devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js105
-rw-r--r--devtools/client/netmonitor/src/widgets/WaterfallBackground.js163
-rw-r--r--devtools/client/netmonitor/src/widgets/moz.build12
7 files changed, 1401 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js b/devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js
new file mode 100644
index 0000000000..6f159ded73
--- /dev/null
+++ b/devtools/client/netmonitor/src/widgets/HeadersPanelContextMenu.js
@@ -0,0 +1,137 @@
+/* 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("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const {
+ contextMenuFormatters,
+} = require("resource://devtools/client/netmonitor/src/utils/context-menu-utils.js");
+
+loader.lazyRequireGetter(
+ this,
+ "copyString",
+ "resource://devtools/shared/platform/clipboard.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "showMenu",
+ "resource://devtools/client/shared/components/menu/utils.js",
+ true
+);
+
+class HeadersPanelContextMenu {
+ constructor(props = {}) {
+ this.props = props;
+ this.copyAll = this.copyAll.bind(this);
+ this.copyValue = this.copyValue.bind(this);
+ }
+
+ /**
+ * Handle the context menu opening.
+ * @param {Object} event open event
+ * @param {Object} selection object representing the current selection
+ */
+ open(event = {}, selection) {
+ const { target } = event;
+ const menuItems = [
+ {
+ id: "headers-panel-context-menu-copyvalue",
+ label: L10N.getStr("netmonitor.context.copyValue"),
+ accesskey: L10N.getStr("netmonitor.context.copyValue.accesskey"),
+ click: () => {
+ const { name, value } = getSummaryContent(
+ target.closest(".tabpanel-summary-container")
+ );
+ this.copyValue(
+ { name, value, object: null, hasChildren: false },
+ selection
+ );
+ },
+ },
+ {
+ id: "headers-panel-context-menu-copyall",
+ label: L10N.getStr("netmonitor.context.copyAll"),
+ accesskey: L10N.getStr("netmonitor.context.copyAll.accesskey"),
+ click: () => {
+ const root = target.closest(".summary");
+ const object = {};
+ if (root) {
+ const { children } = root;
+ for (let i = 0; i < children.length; i++) {
+ const content = getSummaryContent(children[i]);
+ object[content.name] = content.value;
+ }
+ }
+ return this.copyAll(object, selection);
+ },
+ },
+ ];
+
+ showMenu(menuItems, {
+ screenX: event.screenX,
+ screenY: event.screenY,
+ });
+ }
+
+ /**
+ * Copies all.
+ * @param {Object} object the whole tree data
+ * @param {Object} selection object representing the current selection
+ */
+ copyAll(object, selection) {
+ let buffer = "";
+ if (selection.toString() !== "") {
+ buffer = selection.toString();
+ } else {
+ const { customFormatters } = this.props;
+ buffer = contextMenuFormatters.baseCopyAllFormatter(object);
+ if (customFormatters?.copyAllFormatter) {
+ buffer = customFormatters.copyAllFormatter(
+ object,
+ contextMenuFormatters.baseCopyAllFormatter
+ );
+ }
+ }
+ try {
+ copyString(buffer);
+ } catch (error) {}
+ }
+
+ /**
+ * Copies the value of a single item.
+ * @param {Object} object data object for specific node
+ * @param {Object} selection object representing the current selection
+ */
+ copyValue(object, selection) {
+ let buffer = "";
+ if (selection.toString() !== "") {
+ buffer = selection.toString();
+ } else {
+ const { customFormatters } = this.props;
+ buffer = contextMenuFormatters.baseCopyFormatter(object);
+ if (customFormatters?.copyFormatter) {
+ buffer = customFormatters.copyFormatter(
+ object,
+ contextMenuFormatters.baseCopyFormatter
+ );
+ }
+ }
+ try {
+ copyString(buffer);
+ } catch (error) {}
+ }
+}
+
+function getSummaryContent(el) {
+ return {
+ name: el.querySelector(".tabpanel-summary-label").textContent,
+ value: el.querySelector(".tabpanel-summary-value").textContent,
+ };
+}
+
+module.exports = HeadersPanelContextMenu;
diff --git a/devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js b/devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js
new file mode 100644
index 0000000000..8d823444c2
--- /dev/null
+++ b/devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js
@@ -0,0 +1,113 @@
+/* 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("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const {
+ contextMenuFormatters,
+} = require("resource://devtools/client/netmonitor/src/utils/context-menu-utils.js");
+
+loader.lazyRequireGetter(
+ this,
+ "copyString",
+ "resource://devtools/shared/platform/clipboard.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "showMenu",
+ "resource://devtools/client/shared/components/menu/utils.js",
+ true
+);
+
+class PropertiesViewContextMenu {
+ constructor(props = {}) {
+ this.props = props;
+ this.copyAll = this.copyAll.bind(this);
+ this.copyValue = this.copyValue.bind(this);
+ }
+
+ /**
+ * Handle the context menu opening.
+ * @param {Object} event open event
+ * @param {Object} selection object representing the current selection
+ * @param {Object} data object containing information
+ * @param {Object} data.member member of the right-clicked row
+ * @param {Object} data.object the whole tree data
+ */
+ open(event = {}, selection, { member, object }) {
+ const menuItems = [
+ {
+ id: "properties-view-context-menu-copyvalue",
+ label: L10N.getStr("netmonitor.context.copyValue"),
+ accesskey: L10N.getStr("netmonitor.context.copyValue.accesskey"),
+ click: () => this.copyValue(member, selection),
+ },
+ {
+ id: "properties-view-context-menu-copyall",
+ label: L10N.getStr("netmonitor.context.copyAll"),
+ accesskey: L10N.getStr("netmonitor.context.copyAll.accesskey"),
+ click: () => this.copyAll(object, selection),
+ },
+ ];
+
+ showMenu(menuItems, {
+ screenX: event.screenX,
+ screenY: event.screenY,
+ });
+ }
+
+ /**
+ * Copies all.
+ * @param {Object} object the whole tree data
+ * @param {Object} selection object representing the current selection
+ */
+ copyAll(object, selection) {
+ let buffer = "";
+ if (selection.toString() !== "") {
+ buffer = selection.toString();
+ } else {
+ const { customFormatters } = this.props;
+ buffer = contextMenuFormatters.baseCopyAllFormatter(object);
+ if (customFormatters?.copyAllFormatter) {
+ buffer = customFormatters.copyAllFormatter(
+ object,
+ contextMenuFormatters.baseCopyAllFormatter
+ );
+ }
+ }
+ try {
+ copyString(buffer);
+ } catch (error) {}
+ }
+
+ /**
+ * Copies the value of a single item.
+ * @param {Object} member member of the right-clicked row
+ * @param {Object} selection object representing the current selection
+ */
+ copyValue(member, selection) {
+ let buffer = "";
+ if (selection.toString() !== "") {
+ buffer = selection.toString();
+ } else {
+ const { customFormatters } = this.props;
+ buffer = contextMenuFormatters.baseCopyFormatter(member);
+ if (customFormatters?.copyFormatter) {
+ buffer = customFormatters.copyFormatter(
+ member,
+ contextMenuFormatters.baseCopyFormatter
+ );
+ }
+ }
+ try {
+ copyString(buffer);
+ } catch (error) {}
+ }
+}
+
+module.exports = PropertiesViewContextMenu;
diff --git a/devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js b/devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js
new file mode 100644
index 0000000000..f6a10b0ca3
--- /dev/null
+++ b/devtools/client/netmonitor/src/widgets/RequestBlockingContextMenu.js
@@ -0,0 +1,78 @@
+/* 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("resource://devtools/client/netmonitor/src/utils/l10n.js");
+
+loader.lazyRequireGetter(
+ this,
+ "showMenu",
+ "resource://devtools/client/shared/components/menu/utils.js",
+ true
+);
+
+class RequestBlockingContextMenu {
+ constructor(props) {
+ this.props = props;
+ }
+
+ createMenu(contextMenuOptions) {
+ const {
+ removeAllBlockedUrls,
+ disableAllBlockedUrls,
+ enableAllBlockedUrls,
+ } = this.props;
+
+ const { disableDisableAllBlockedUrls, disableEnableAllBlockedUrls } =
+ contextMenuOptions;
+
+ const menu = [
+ {
+ id: "request-blocking-enable-all",
+ label: L10N.getStr(
+ "netmonitor.requestBlockingMenu.enableAllBlockedUrls"
+ ),
+ accesskey: "",
+ disabled: disableEnableAllBlockedUrls,
+ visible: true,
+ click: () => enableAllBlockedUrls(),
+ },
+ {
+ id: "request-blocking-disable-all",
+ label: L10N.getStr(
+ "netmonitor.requestBlockingMenu.disableAllBlockedUrls"
+ ),
+ accesskey: "",
+ disabled: disableDisableAllBlockedUrls,
+ visible: true,
+ click: () => disableAllBlockedUrls(),
+ },
+ {
+ id: "request-blocking-remove-all",
+ label: L10N.getStr(
+ "netmonitor.requestBlockingMenu.removeAllBlockedUrls"
+ ),
+ accesskey: "",
+ visible: true,
+ click: () => removeAllBlockedUrls(),
+ },
+ ];
+
+ return menu;
+ }
+
+ open(event, contextMenuOptions) {
+ const menu = this.createMenu(contextMenuOptions);
+
+ showMenu(menu, {
+ screenX: event.screenX,
+ screenY: event.screenY,
+ });
+ }
+}
+
+module.exports = RequestBlockingContextMenu;
diff --git a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
new file mode 100644
index 0000000000..1702cdbc64
--- /dev/null
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -0,0 +1,793 @@
+/* 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("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const {
+ formDataURI,
+ getUrlQuery,
+ getUrlBaseName,
+ parseQueryString,
+ getRequestHeadersRawText,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+const {
+ hasMatchingBlockingRequestPattern,
+} = require("resource://devtools/client/netmonitor/src/utils/request-blocking.js");
+
+loader.lazyRequireGetter(
+ this,
+ "Curl",
+ "resource://devtools/client/shared/curl.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "saveAs",
+ "resource://devtools/shared/DevToolsUtils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "PowerShell",
+ "resource://devtools/client/netmonitor/src/utils/powershell.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "copyString",
+ "resource://devtools/shared/platform/clipboard.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "showMenu",
+ "resource://devtools/client/shared/components/menu/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "HarMenuUtils",
+ "resource://devtools/client/netmonitor/src/har/har-menu-utils.js",
+ true
+);
+
+const { OS } = Services.appinfo;
+
+class RequestListContextMenu {
+ constructor(props) {
+ this.props = props;
+ }
+
+ createCopySubMenu(clickedRequest, requests) {
+ const { connector } = this.props;
+
+ const {
+ id,
+ formDataSections,
+ method,
+ mimeType,
+ httpVersion,
+ requestHeaders,
+ requestHeadersAvailable,
+ requestPostData,
+ requestPostDataAvailable,
+ responseHeaders,
+ responseHeadersAvailable,
+ responseContent,
+ responseContentAvailable,
+ url,
+ } = clickedRequest;
+
+ const copySubMenu = [];
+
+ copySubMenu.push({
+ id: "request-list-context-copy-url",
+ label: L10N.getStr("netmonitor.context.copyUrl"),
+ accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
+ visible: !!clickedRequest,
+ click: () => this.copyUrl(url),
+ });
+
+ copySubMenu.push({
+ id: "request-list-context-copy-url-params",
+ label: L10N.getStr("netmonitor.context.copyUrlParams"),
+ accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
+ visible: !!(clickedRequest && getUrlQuery(url)),
+ click: () => this.copyUrlParams(url),
+ });
+
+ copySubMenu.push({
+ id: "request-list-context-copy-post-data",
+ label: L10N.getFormatStr("netmonitor.context.copyRequestData", method),
+ accesskey: L10N.getStr("netmonitor.context.copyRequestData.accesskey"),
+ // Menu item will be visible even if data hasn't arrived, so we need to check
+ // *Available property and then fetch data lazily once user triggers the action.
+ visible: !!(
+ clickedRequest &&
+ (requestPostDataAvailable || requestPostData)
+ ),
+ click: () => this.copyPostData(id, formDataSections, requestPostData),
+ });
+
+ if (OS === "WINNT") {
+ copySubMenu.push({
+ id: "request-list-context-copy-as-curl-win",
+ label: L10N.getFormatStr(
+ "netmonitor.context.copyAsCurl.win",
+ L10N.getStr("netmonitor.context.copyAsCurl")
+ ),
+ accesskey: L10N.getStr("netmonitor.context.copyAsCurl.win.accesskey"),
+ // Menu item will be visible even if data hasn't arrived, so we need to check
+ // *Available property and then fetch data lazily once user triggers the action.
+ visible: !!clickedRequest,
+ click: () =>
+ this.copyAsCurl(
+ id,
+ url,
+ method,
+ httpVersion,
+ requestHeaders,
+ requestPostData,
+ responseHeaders,
+ "WINNT"
+ ),
+ });
+
+ copySubMenu.push({
+ id: "request-list-context-copy-as-curl-posix",
+ label: L10N.getFormatStr(
+ "netmonitor.context.copyAsCurl.posix",
+ L10N.getStr("netmonitor.context.copyAsCurl")
+ ),
+ accesskey: L10N.getStr("netmonitor.context.copyAsCurl.posix.accesskey"),
+ // Menu item will be visible even if data hasn't arrived, so we need to check
+ // *Available property and then fetch data lazily once user triggers the action.
+ visible: !!clickedRequest,
+ click: () =>
+ this.copyAsCurl(
+ id,
+ url,
+ method,
+ httpVersion,
+ requestHeaders,
+ requestPostData,
+ responseHeaders,
+ "Linux"
+ ),
+ });
+ } else {
+ copySubMenu.push({
+ id: "request-list-context-copy-as-curl",
+ label: L10N.getStr("netmonitor.context.copyAsCurl"),
+ accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
+ // Menu item will be visible even if data hasn't arrived, so we need to check
+ // *Available property and then fetch data lazily once user triggers the action.
+ visible: !!clickedRequest,
+ click: () =>
+ this.copyAsCurl(
+ id,
+ url,
+ method,
+ httpVersion,
+ requestHeaders,
+ requestPostData,
+ responseHeaders
+ ),
+ });
+ }
+
+ copySubMenu.push({
+ id: "request-list-context-copy-as-powershell",
+ label: L10N.getStr("netmonitor.context.copyAsPowerShell"),
+ accesskey: L10N.getStr("netmonitor.context.copyAsPowerShell.accesskey"),
+ // Menu item will be visible even if data hasn't arrived, so we need to check
+ // *Available property and then fetch data lazily once user triggers the action.
+ visible: !!clickedRequest,
+ click: () => this.copyAsPowerShell(clickedRequest),
+ });
+
+ copySubMenu.push({
+ id: "request-list-context-copy-as-fetch",
+ label: L10N.getStr("netmonitor.context.copyAsFetch"),
+ accesskey: L10N.getStr("netmonitor.context.copyAsFetch.accesskey"),
+ visible: !!clickedRequest,
+ click: () =>
+ this.copyAsFetch(id, url, method, requestHeaders, requestPostData),
+ });
+
+ copySubMenu.push({
+ type: "separator",
+ visible: copySubMenu.slice(0, 4).some(subMenu => subMenu.visible),
+ });
+
+ copySubMenu.push({
+ id: "request-list-context-copy-request-headers",
+ label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
+ accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
+ // Menu item will be visible even if data hasn't arrived, so we need to check
+ // *Available property and then fetch data lazily once user triggers the action.
+ visible: !!(
+ clickedRequest &&
+ (requestHeadersAvailable || requestHeaders)
+ ),
+ click: () => this.copyRequestHeaders(id, clickedRequest),
+ });
+
+ copySubMenu.push({
+ id: "response-list-context-copy-response-headers",
+ label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
+ accesskey: L10N.getStr(
+ "netmonitor.context.copyResponseHeaders.accesskey"
+ ),
+ // Menu item will be visible even if data hasn't arrived, so we need to check
+ // *Available property and then fetch data lazily once user triggers the action.
+ visible: !!(
+ clickedRequest &&
+ (responseHeadersAvailable || responseHeaders)
+ ),
+ click: () => this.copyResponseHeaders(id, responseHeaders),
+ });
+
+ copySubMenu.push({
+ id: "request-list-context-copy-response",
+ label: L10N.getStr("netmonitor.context.copyResponse"),
+ accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
+ // Menu item will be visible even if data hasn't arrived, so we need to check
+ // *Available property and then fetch data lazily once user triggers the action.
+ visible: !!(
+ clickedRequest &&
+ (responseContentAvailable || responseContent)
+ ),
+ click: () => this.copyResponse(id, responseContent),
+ });
+
+ copySubMenu.push({
+ id: "request-list-context-copy-image-as-data-uri",
+ label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
+ accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
+ visible: !!(
+ clickedRequest &&
+ (responseContentAvailable || responseContent) &&
+ mimeType &&
+ mimeType.includes("image/")
+ ),
+ click: () => this.copyImageAsDataUri(id, mimeType, responseContent),
+ });
+
+ copySubMenu.push({
+ type: "separator",
+ visible: copySubMenu.slice(5, 9).some(subMenu => subMenu.visible),
+ });
+
+ copySubMenu.push({
+ id: "request-list-context-copy-all-as-har",
+ label: L10N.getStr("netmonitor.context.copyAllAsHar"),
+ accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
+ visible: !!requests.length,
+ click: () => HarMenuUtils.copyAllAsHar(requests, connector),
+ });
+
+ return copySubMenu;
+ }
+
+ createMenu(clickedRequest, requests, blockedUrls) {
+ const {
+ connector,
+ cloneRequest,
+ openDetailsPanelTab,
+ openHTTPCustomRequestTab,
+ closeHTTPCustomRequestTab,
+ sendCustomRequest,
+ sendHTTPCustomRequest,
+ openStatistics,
+ openRequestInTab,
+ openRequestBlockingAndAddUrl,
+ openRequestBlockingAndDisableUrls,
+ removeBlockedUrl,
+ } = this.props;
+
+ const {
+ id,
+ isCustom,
+ method,
+ mimeType,
+ requestHeaders,
+ requestPostData,
+ responseContent,
+ responseContentAvailable,
+ url,
+ } = clickedRequest;
+
+ const copySubMenu = this.createCopySubMenu(clickedRequest, requests);
+ const newEditAndResendPref = Services.prefs.getBoolPref(
+ "devtools.netmonitor.features.newEditAndResend"
+ );
+
+ return [
+ {
+ label: L10N.getStr("netmonitor.context.copyValue"),
+ accesskey: L10N.getStr("netmonitor.context.copyValue.accesskey"),
+ visible: !!clickedRequest,
+ submenu: copySubMenu,
+ },
+ {
+ id: "request-list-context-save-all-as-har",
+ label: L10N.getStr("netmonitor.context.saveAllAsHar"),
+ accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
+ visible: !!requests.length,
+ click: () => HarMenuUtils.saveAllAsHar(requests, connector),
+ },
+ {
+ id: "request-list-context-save-image-as",
+ label: L10N.getStr("netmonitor.context.saveImageAs"),
+ accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
+ visible: !!(
+ clickedRequest &&
+ (responseContentAvailable || responseContent) &&
+ mimeType &&
+ mimeType.includes("image/")
+ ),
+ click: () => this.saveImageAs(id, url, responseContent),
+ },
+ {
+ type: "separator",
+ visible: copySubMenu.slice(10, 14).some(subMenu => subMenu.visible),
+ },
+ {
+ id: "request-list-context-resend-only",
+ label: L10N.getStr("netmonitor.context.resend.label"),
+ accesskey: L10N.getStr("netmonitor.context.resend.accesskey"),
+ visible: !!(clickedRequest && !isCustom),
+ click: () => {
+ if (!newEditAndResendPref) {
+ cloneRequest(id);
+ sendCustomRequest();
+ } else {
+ sendHTTPCustomRequest(clickedRequest);
+ }
+ },
+ },
+
+ {
+ id: "request-list-context-edit-resend",
+ label: L10N.getStr("netmonitor.context.editAndResend"),
+ accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
+ visible: !!(clickedRequest && !isCustom),
+ click: () => {
+ this.fetchRequestHeaders(id).then(() => {
+ if (!newEditAndResendPref) {
+ cloneRequest(id);
+ openDetailsPanelTab();
+ } else {
+ closeHTTPCustomRequestTab();
+ openHTTPCustomRequestTab();
+ }
+ });
+ },
+ },
+ {
+ id: "request-list-context-block-url",
+ label: L10N.getStr("netmonitor.context.blockURL"),
+ visible: !hasMatchingBlockingRequestPattern(
+ blockedUrls,
+ clickedRequest.url
+ ),
+ click: () => {
+ openRequestBlockingAndAddUrl(clickedRequest.url);
+ },
+ },
+ {
+ id: "request-list-context-unblock-url",
+ label: L10N.getStr("netmonitor.context.unblockURL"),
+ visible: hasMatchingBlockingRequestPattern(
+ blockedUrls,
+ clickedRequest.url
+ ),
+ click: () => {
+ if (
+ blockedUrls.find(blockedUrl => blockedUrl === clickedRequest.url)
+ ) {
+ removeBlockedUrl(clickedRequest.url);
+ } else {
+ openRequestBlockingAndDisableUrls(clickedRequest.url);
+ }
+ },
+ },
+ {
+ type: "separator",
+ visible: copySubMenu.slice(15, 16).some(subMenu => subMenu.visible),
+ },
+ {
+ id: "request-list-context-newtab",
+ label: L10N.getStr("netmonitor.context.newTab"),
+ accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
+ visible: !!clickedRequest,
+ click: () => openRequestInTab(id, url, requestHeaders, requestPostData),
+ },
+ {
+ id: "request-list-context-open-in-debugger",
+ label: L10N.getStr("netmonitor.context.openInDebugger"),
+ accesskey: L10N.getStr("netmonitor.context.openInDebugger.accesskey"),
+ visible: !!(
+ clickedRequest &&
+ mimeType &&
+ mimeType.includes("javascript")
+ ),
+ click: () => this.openInDebugger(url),
+ },
+ {
+ id: "request-list-context-open-in-style-editor",
+ label: L10N.getStr("netmonitor.context.openInStyleEditor"),
+ accesskey: L10N.getStr(
+ "netmonitor.context.openInStyleEditor.accesskey"
+ ),
+ visible: !!(
+ clickedRequest &&
+ Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
+ mimeType &&
+ mimeType.includes("css")
+ ),
+ click: () => this.openInStyleEditor(url),
+ },
+ {
+ id: "request-list-context-perf",
+ label: L10N.getStr("netmonitor.context.perfTools"),
+ accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
+ visible: !!requests.length,
+ click: () => openStatistics(true),
+ },
+ {
+ type: "separator",
+ },
+ {
+ id: "request-list-context-use-as-fetch",
+ label: L10N.getStr("netmonitor.context.useAsFetch"),
+ accesskey: L10N.getStr("netmonitor.context.useAsFetch.accesskey"),
+ visible: !!clickedRequest,
+ click: () =>
+ this.useAsFetch(id, url, method, requestHeaders, requestPostData),
+ },
+ ];
+ }
+
+ open(event, clickedRequest, requests, blockedUrls) {
+ const menu = this.createMenu(clickedRequest, requests, blockedUrls);
+
+ showMenu(menu, {
+ screenX: event.screenX,
+ screenY: event.screenY,
+ });
+ }
+
+ /**
+ * Opens selected item in the debugger
+ */
+ openInDebugger(url) {
+ const toolbox = this.props.connector.getToolbox();
+ toolbox.viewGeneratedSourceInDebugger(url);
+ }
+
+ /**
+ * Opens selected item in the style editor
+ */
+ openInStyleEditor(url) {
+ const toolbox = this.props.connector.getToolbox();
+ toolbox.viewGeneratedSourceInStyleEditor(url);
+ }
+
+ /**
+ * Copy the request url from the currently selected item.
+ */
+ copyUrl(url) {
+ copyString(url);
+ }
+
+ /**
+ * Copy the request url query string parameters from the currently
+ * selected item.
+ */
+ copyUrlParams(url) {
+ const params = getUrlQuery(url).split("&");
+ copyString(params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"));
+ }
+
+ /**
+ * Copy the request form data parameters (or raw payload) from
+ * the currently selected item.
+ */
+ async copyPostData(id, formDataSections, requestPostData) {
+ let params = [];
+ // Try to extract any form data parameters if formDataSections is already
+ // available, which is only true if RequestPanel has ever been mounted before.
+ if (formDataSections) {
+ formDataSections.forEach(section => {
+ const paramsArray = parseQueryString(section);
+ if (paramsArray) {
+ params = [...params, ...paramsArray];
+ }
+ });
+ }
+
+ let string = params
+ .map(param => param.name + (param.value ? "=" + param.value : ""))
+ .join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
+
+ // Fall back to raw payload.
+ if (!string) {
+ requestPostData =
+ requestPostData ||
+ (await this.props.connector.requestData(id, "requestPostData"));
+
+ string = requestPostData.postData.text;
+ if (Services.appinfo.OS !== "WINNT") {
+ string = string.replace(/\r/g, "");
+ }
+ }
+ copyString(string);
+ }
+
+ /**
+ * Copy a cURL command from the currently selected item.
+ */
+ async copyAsCurl(
+ id,
+ url,
+ method,
+ httpVersion,
+ requestHeaders,
+ requestPostData,
+ responseHeaders,
+ platform
+ ) {
+ requestHeaders =
+ requestHeaders ||
+ (await this.props.connector.requestData(id, "requestHeaders"));
+
+ requestPostData =
+ requestPostData ||
+ (await this.props.connector.requestData(id, "requestPostData"));
+
+ responseHeaders =
+ responseHeaders ||
+ (await this.props.connector.requestData(id, "responseHeaders"));
+
+ // Create a sanitized object for the Curl command generator.
+ const data = {
+ url,
+ method,
+ headers: requestHeaders.headers,
+ responseHeaders: responseHeaders.headers,
+ httpVersion,
+ postDataText: requestPostData ? requestPostData.postData.text : "",
+ };
+ copyString(Curl.generateCommand(data, platform));
+ }
+
+ async copyAsPowerShell(request) {
+ let { id, url, method, requestHeaders, requestPostData, requestCookies } =
+ request;
+
+ requestHeaders =
+ requestHeaders ||
+ (await this.props.connector.requestData(id, "requestHeaders"));
+
+ requestPostData =
+ requestPostData ||
+ (await this.props.connector.requestData(id, "requestPostData"));
+
+ requestCookies =
+ requestCookies ||
+ (await this.props.connector.requestData(id, "requestCookies"));
+
+ return copyString(
+ PowerShell.generateCommand(
+ url,
+ method,
+ requestHeaders.headers,
+ requestPostData.postData,
+ requestCookies.cookies || requestCookies
+ )
+ );
+ }
+
+ /**
+ * Generate fetch string
+ */
+ async generateFetchString(id, url, method, requestHeaders, requestPostData) {
+ requestHeaders =
+ requestHeaders ||
+ (await this.props.connector.requestData(id, "requestHeaders"));
+
+ requestPostData =
+ requestPostData ||
+ (await this.props.connector.requestData(id, "requestPostData"));
+
+ // https://fetch.spec.whatwg.org/#forbidden-header-name
+ const forbiddenHeaders = {
+ "accept-charset": 1,
+ "accept-encoding": 1,
+ "access-control-request-headers": 1,
+ "access-control-request-method": 1,
+ connection: 1,
+ "content-length": 1,
+ cookie: 1,
+ cookie2: 1,
+ date: 1,
+ dnt: 1,
+ expect: 1,
+ host: 1,
+ "keep-alive": 1,
+ origin: 1,
+ referer: 1,
+ te: 1,
+ trailer: 1,
+ "transfer-encoding": 1,
+ upgrade: 1,
+ via: 1,
+ };
+ const credentialHeaders = { cookie: 1, authorization: 1 };
+
+ const headers = {};
+ for (const { name, value } of requestHeaders.headers) {
+ if (!forbiddenHeaders[name.toLowerCase()]) {
+ headers[name] = value;
+ }
+ }
+
+ const referrerHeader = requestHeaders.headers.find(
+ ({ name }) => name.toLowerCase() === "referer"
+ );
+
+ const referrerPolicy = requestHeaders.headers.find(
+ ({ name }) => name.toLowerCase() === "referrer-policy"
+ );
+
+ const referrer = referrerHeader ? referrerHeader.value : undefined;
+ const credentials = requestHeaders.headers.some(
+ ({ name }) => credentialHeaders[name.toLowerCase()]
+ )
+ ? "include"
+ : "omit";
+
+ const fetchOptions = {
+ credentials,
+ headers,
+ referrer,
+ referrerPolicy,
+ body: requestPostData.postData.text,
+ method,
+ mode: "cors",
+ };
+
+ const options = JSON.stringify(fetchOptions, null, 4);
+ const fetchString = `await fetch("${url}", ${options});`;
+ return fetchString;
+ }
+
+ /**
+ * Copy the currently selected item as fetch request.
+ */
+ async copyAsFetch(id, url, method, requestHeaders, requestPostData) {
+ const fetchString = await this.generateFetchString(
+ id,
+ url,
+ method,
+ requestHeaders,
+ requestPostData
+ );
+ copyString(fetchString);
+ }
+
+ /**
+ * Open split console and fill it with fetch command for selected item
+ */
+ async useAsFetch(id, url, method, requestHeaders, requestPostData) {
+ const fetchString = await this.generateFetchString(
+ id,
+ url,
+ method,
+ requestHeaders,
+ requestPostData
+ );
+ const toolbox = this.props.connector.getToolbox();
+ await toolbox.openSplitConsole();
+ const { hud } = await toolbox.getPanel("webconsole");
+ hud.setInputValue(fetchString);
+ }
+
+ /**
+ * Copy the raw request headers from the currently selected item.
+ */
+ async copyRequestHeaders(
+ id,
+ { method, httpVersion, requestHeaders, urlDetails }
+ ) {
+ requestHeaders =
+ requestHeaders ||
+ (await this.props.connector.requestData(id, "requestHeaders"));
+
+ let rawHeaders = getRequestHeadersRawText(
+ method,
+ httpVersion,
+ requestHeaders,
+ urlDetails
+ );
+
+ if (Services.appinfo.OS !== "WINNT") {
+ rawHeaders = rawHeaders.replace(/\r/g, "");
+ }
+ copyString(rawHeaders);
+ }
+
+ /**
+ * Copy the raw response headers from the currently selected item.
+ */
+ async copyResponseHeaders(id, responseHeaders) {
+ responseHeaders =
+ responseHeaders ||
+ (await this.props.connector.requestData(id, "responseHeaders"));
+
+ let rawHeaders = responseHeaders.rawHeaders.trim();
+
+ if (Services.appinfo.OS !== "WINNT") {
+ rawHeaders = rawHeaders.replace(/\r/g, "");
+ }
+ copyString(rawHeaders);
+ }
+
+ /**
+ * Copy image as data uri.
+ */
+ async copyImageAsDataUri(id, mimeType, responseContent) {
+ responseContent =
+ responseContent ||
+ (await this.props.connector.requestData(id, "responseContent"));
+
+ const { encoding, text } = responseContent.content;
+ copyString(formDataURI(mimeType, encoding, text));
+ }
+
+ /**
+ * Save image as.
+ */
+ async saveImageAs(id, url, responseContent) {
+ responseContent =
+ responseContent ||
+ (await this.props.connector.requestData(id, "responseContent"));
+
+ const { encoding, text } = responseContent.content;
+ const fileName = getUrlBaseName(url);
+ let data;
+ if (encoding === "base64") {
+ const decoded = atob(text);
+ data = new Uint8Array(decoded.length);
+ for (let i = 0; i < decoded.length; ++i) {
+ data[i] = decoded.charCodeAt(i);
+ }
+ } else {
+ data = new TextEncoder().encode(text);
+ }
+ saveAs(window, data, fileName);
+ }
+
+ /**
+ * Copy response data as a string.
+ */
+ async copyResponse(id, responseContent) {
+ responseContent =
+ responseContent ||
+ (await this.props.connector.requestData(id, "responseContent"));
+
+ copyString(responseContent.content.text);
+ }
+
+ async fetchRequestHeaders(id) {
+ await this.props.connector.requestData(id, "requestHeaders");
+ }
+}
+
+module.exports = RequestListContextMenu;
diff --git a/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
new file mode 100644
index 0000000000..02ee502790
--- /dev/null
+++ b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
@@ -0,0 +1,105 @@
+/* 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 {
+ showMenu,
+} = require("resource://devtools/client/shared/components/menu/utils.js");
+const {
+ HEADERS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+const {
+ L10N,
+} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const {
+ getVisibleColumns,
+} = require("resource://devtools/client/netmonitor/src/selectors/index.js");
+
+const stringMap = HEADERS.filter(header =>
+ header.hasOwnProperty("label")
+).reduce((acc, { name, label }) => Object.assign(acc, { [name]: label }), {});
+
+const subMenuMap = HEADERS.filter(header =>
+ header.hasOwnProperty("subMenu")
+).reduce(
+ (acc, { name, subMenu }) => Object.assign(acc, { [name]: subMenu }),
+ {}
+);
+
+const nonLocalizedHeaders = HEADERS.filter(header =>
+ header.hasOwnProperty("noLocalization")
+).map(header => header.name);
+
+class RequestListHeaderContextMenu {
+ constructor(props) {
+ this.props = props;
+ }
+
+ /**
+ * Handle the context menu opening.
+ */
+ open(event = {}, columns) {
+ const menu = [];
+ const subMenu = { timings: [], responseHeaders: [] };
+ const onlyOneColumn = getVisibleColumns(columns).length === 1;
+
+ for (const column in columns) {
+ const shown = columns[column];
+ const label = nonLocalizedHeaders.includes(column)
+ ? stringMap[column] || column
+ : L10N.getStr(`netmonitor.toolbar.${stringMap[column] || column}`);
+ const entry = {
+ id: `request-list-header-${column}-toggle`,
+ label,
+ type: "checkbox",
+ checked: shown,
+ click: () => this.props.toggleColumn(column),
+ // We don't want to allow hiding the last visible column
+ disabled: onlyOneColumn && shown,
+ };
+ subMenuMap.hasOwnProperty(column)
+ ? subMenu[subMenuMap[column]].push(entry)
+ : menu.push(entry);
+ }
+
+ menu.push({ type: "separator" });
+ menu.push({
+ label: L10N.getStr("netmonitor.toolbar.timings"),
+ submenu: subMenu.timings,
+ });
+ menu.push({
+ label: L10N.getStr("netmonitor.toolbar.responseHeaders"),
+ submenu: subMenu.responseHeaders,
+ });
+
+ menu.push({ type: "separator" });
+ menu.push({
+ id: "request-list-header-reset-columns",
+ label: L10N.getStr("netmonitor.toolbar.resetColumns"),
+ click: () => this.props.resetColumns(),
+ });
+
+ menu.push({
+ id: "request-list-header-reset-sorting",
+ label: L10N.getStr("netmonitor.toolbar.resetSorting"),
+ click: () => this.props.resetSorting(),
+ });
+
+ const columnName = event.target.getAttribute("data-name");
+
+ menu.push({
+ id: "request-list-header-resize-column-to-fit-content",
+ label: L10N.getStr("netmonitor.toolbar.resizeColumnToFitContent"),
+ click: () => this.props.resizeColumnToFitContent(columnName),
+ });
+
+ showMenu(menu, {
+ screenX: event.screenX,
+ screenY: event.screenY,
+ });
+ }
+}
+
+module.exports = RequestListHeaderContextMenu;
diff --git a/devtools/client/netmonitor/src/widgets/WaterfallBackground.js b/devtools/client/netmonitor/src/widgets/WaterfallBackground.js
new file mode 100644
index 0000000000..e2be7f5715
--- /dev/null
+++ b/devtools/client/netmonitor/src/widgets/WaterfallBackground.js
@@ -0,0 +1,163 @@
+/* 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 { getColor } = require("resource://devtools/client/shared/theme.js");
+const { colorUtils } = require("resource://devtools/shared/css/color.js");
+const {
+ REQUESTS_WATERFALL,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const STATE_KEYS = [
+ "firstRequestStartedMs",
+ "scale",
+ "timingMarkers",
+ "waterfallWidth",
+];
+
+/**
+ * Creates the background displayed on each waterfall view in this container.
+ */
+class WaterfallBackground {
+ constructor() {
+ this.canvas = document.createElementNS(HTML_NS, "canvas");
+ this.ctx = this.canvas.getContext("2d");
+ this.prevState = {};
+ }
+
+ /**
+ * Changes the element being used as the CSS background for a background
+ * with a given background element ID.
+ *
+ * The funtion wrap the Firefox only API. Waterfall Will not draw the
+ * vertical line when running on non-firefox browser.
+ * Could be fixed by Bug 1308695
+ */
+ setImageElement(imageElementId, imageElement) {
+ if (document.mozSetImageElement) {
+ document.mozSetImageElement(imageElementId, imageElement);
+ }
+ }
+
+ draw(state) {
+ // Do a shallow compare of the previous and the new state
+ const shouldUpdate = STATE_KEYS.some(
+ key => this.prevState[key] !== state[key]
+ );
+ if (!shouldUpdate) {
+ return;
+ }
+
+ this.prevState = state;
+
+ if (state.waterfallWidth === null || state.scale === null) {
+ this.setImageElement("waterfall-background", null);
+ return;
+ }
+
+ // Nuke the context.
+ const canvasWidth = (this.canvas.width = Math.max(
+ state.waterfallWidth - REQUESTS_WATERFALL.LABEL_WIDTH,
+ 1
+ ));
+ // Awww yeah, 1px, repeats on Y axis.
+ const canvasHeight = (this.canvas.height = 1);
+
+ // Start over.
+ const imageData = this.ctx.createImageData(canvasWidth, canvasHeight);
+ const pixelArray = imageData.data;
+
+ const buf = new ArrayBuffer(pixelArray.length);
+ const view8bit = new Uint8ClampedArray(buf);
+ const view32bit = new Uint32Array(buf);
+
+ // Build new millisecond tick lines...
+ let timingStep = REQUESTS_WATERFALL.BACKGROUND_TICKS_MULTIPLE;
+ let optimalTickIntervalFound = false;
+ let scaledStep;
+
+ while (!optimalTickIntervalFound) {
+ // Ignore any divisions that would end up being too close to each other.
+ scaledStep = state.scale * timingStep;
+ if (scaledStep < REQUESTS_WATERFALL.BACKGROUND_TICKS_SPACING_MIN) {
+ timingStep <<= 1;
+ continue;
+ }
+ optimalTickIntervalFound = true;
+ }
+
+ const isRTL = document.dir === "rtl";
+ const [r, g, b] = REQUESTS_WATERFALL.BACKGROUND_TICKS_COLOR_RGB;
+ let alphaComponent = REQUESTS_WATERFALL.BACKGROUND_TICKS_OPACITY_MIN;
+
+ function drawPixelAt(offset, color) {
+ const position = (isRTL ? canvasWidth - offset : offset - 1) | 0;
+ const [rc, gc, bc, ac] = color;
+ view32bit[position] = (ac << 24) | (bc << 16) | (gc << 8) | rc;
+ }
+
+ // Insert one pixel for each division on each scale.
+ for (let i = 1; i <= REQUESTS_WATERFALL.BACKGROUND_TICKS_SCALES; i++) {
+ const increment = scaledStep * Math.pow(2, i);
+ for (let x = 0; x < canvasWidth; x += increment) {
+ drawPixelAt(x, [r, g, b, alphaComponent]);
+ }
+ alphaComponent += REQUESTS_WATERFALL.BACKGROUND_TICKS_OPACITY_ADD;
+ }
+
+ function drawTimestamp(timestamp, color) {
+ if (timestamp === -1) {
+ return;
+ }
+
+ const delta = Math.floor(
+ (timestamp - state.firstRequestStartedMs) * state.scale
+ );
+ drawPixelAt(delta, color);
+ }
+
+ const { DOMCONTENTLOADED_TICKS_COLOR, LOAD_TICKS_COLOR } =
+ REQUESTS_WATERFALL;
+ drawTimestamp(
+ state.timingMarkers.firstDocumentDOMContentLoadedTimestamp,
+ this.getThemeColorAsRgba(DOMCONTENTLOADED_TICKS_COLOR, state.theme)
+ );
+
+ drawTimestamp(
+ state.timingMarkers.firstDocumentLoadTimestamp,
+ this.getThemeColorAsRgba(LOAD_TICKS_COLOR, state.theme)
+ );
+
+ // Flush the image data and cache the waterfall background.
+ pixelArray.set(view8bit);
+ this.ctx.putImageData(imageData, 0, 0);
+
+ this.setImageElement("waterfall-background", this.canvas);
+ }
+
+ /**
+ * Retrieve a color defined for the provided theme as a rgba array. The alpha channel is
+ * forced to the waterfall constant TICKS_COLOR_OPACITY.
+ *
+ * @param {String} colorName
+ * The name of the theme color
+ * @param {String} theme
+ * The name of the theme
+ * @return {Array} RGBA array for the color.
+ */
+ getThemeColorAsRgba(colorName, theme) {
+ const colorStr = getColor(colorName, theme);
+ const color = new colorUtils.CssColor(colorStr);
+ const { r, g, b } = color.getRGBATuple();
+ return [r, g, b, REQUESTS_WATERFALL.TICKS_COLOR_OPACITY];
+ }
+
+ destroy() {
+ this.setImageElement("waterfall-background", null);
+ }
+}
+
+module.exports = WaterfallBackground;
diff --git a/devtools/client/netmonitor/src/widgets/moz.build b/devtools/client/netmonitor/src/widgets/moz.build
new file mode 100644
index 0000000000..106deca935
--- /dev/null
+++ b/devtools/client/netmonitor/src/widgets/moz.build
@@ -0,0 +1,12 @@
+# 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(
+ "HeadersPanelContextMenu.js",
+ "PropertiesViewContextMenu.js",
+ "RequestBlockingContextMenu.js",
+ "RequestListContextMenu.js",
+ "RequestListHeaderContextMenu.js",
+ "WaterfallBackground.js",
+)