--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -295,16 +295,17 @@ devtools.jar:
content/netmonitor/src/assets/styles/netmonitor.css (netmonitor/src/assets/styles/netmonitor.css)
content/netmonitor/src/assets/styles/NetworkDetailsPanel.css (netmonitor/src/assets/styles/NetworkDetailsPanel.css)
content/netmonitor/src/assets/styles/RequestList.css (netmonitor/src/assets/styles/RequestList.css)
content/netmonitor/src/assets/styles/StatisticsPanel.css (netmonitor/src/assets/styles/StatisticsPanel.css)
content/netmonitor/src/assets/styles/StatusBar.css (netmonitor/src/assets/styles/StatusBar.css)
content/netmonitor/src/assets/styles/Toolbar.css (netmonitor/src/assets/styles/Toolbar.css)
content/netmonitor/src/assets/styles/variables.css (netmonitor/src/assets/styles/variables.css)
content/netmonitor/src/assets/icons/play.svg (netmonitor/src/assets/icons/play.svg)
+ content/netmonitor/src/assets/icons/drop-down.svg (netmonitor/src/assets/icons/drop-down.svg)
content/netmonitor/index.html (netmonitor/index.html)
content/netmonitor/initializer.js (netmonitor/initializer.js)
# Application panel
content/application/index.html (application/index.html)
content/application/initializer.js (application/initializer.js)
# Devtools-components
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -938,17 +938,16 @@ netmonitor.context.copyImageAsDataUri.ac
# LOCALIZATION NOTE (netmonitor.context.saveImageAs): This is the label displayed
# on the context menu that save the Image
netmonitor.context.saveImageAs=Save Image As
# LOCALIZATION NOTE (netmonitor.context.saveImageAs.accesskey): This is the access key
# for the Copy Image As Data URI menu item displayed in the context menu for a request
netmonitor.context.saveImageAs.accesskey=V
-
# LOCALIZATION NOTE (netmonitor.context.copyAllAsHar): This is the label displayed
# on the context menu that copies all as HAR format
netmonitor.context.copyAllAsHar=Copy All As HAR
# LOCALIZATION NOTE (netmonitor.context.copyAllAsHar.accesskey): This is the access key
# for the Copy All As HAR menu item displayed in the context menu for a network panel
netmonitor.context.copyAllAsHar.accesskey=O
@@ -1058,8 +1057,12 @@ netmonitor.status.tooltip.worker = %1$S
# of the column status code, when the request is cached and is from a service worker
# %1$S is the status code, %2$S is the status text.
netmonitor.status.tooltip.cachedworker = %1$S %2$S (cached, service worker)
# LOCALIZATION NOTE (netmonitor.label.dropHarFiles): This is a label
# rendered within the Network panel when *.har file(s) are dragged
# over the content.
netmonitor.label.dropHarFiles = Drop HAR files here
+
+# LOCALIZATION NOTE (netmonitor.label.har): This is a label used
+# as a tooltip for toolbar drop-down button with HAR actions
+netmonitor.label.har=HAR Export/Import
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/assets/icons/drop-down.svg
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="8" height="8" viewBox="0 0 16 16">
+ <path fill="context-fill" d="M7.9 16.3c-.3 0-.6-.1-.8-.4l-4-4.8c-.2-.3-.3-.5-.1-.8.1-.3.5-.3.9-.3h8c.4 0 .7 0 .9.3.2.4.1.6-.1.9l-4 4.8c-.2.3-.5.3-.8.3zM7.8 0c.3 0 .6.1.7.4L12.4 5c.2.3.3.4.1.7-.1.4-.5.3-.8.3H3.9c-.4 0-.8.1-.9-.2-.2-.4-.1-.6.1-.9L7 .3c.2-.3.5-.3.8-.3z"/>
+</svg>
+
--- a/devtools/client/netmonitor/src/assets/styles/Toolbar.css
+++ b/devtools/client/netmonitor/src/assets/styles/Toolbar.css
@@ -56,16 +56,36 @@
.devtools-button.devtools-pause-icon::before {
background-image: var(--pause-icon-url);
}
.devtools-button.devtools-play-icon::before {
background-image: var(--play-icon-url);
}
+/* HAR button in the toolbar has a background only when hovered. */
+.devtools-button.devtools-har-button:not(:hover) {
+ background: transparent;
+}
+
+/* HAR button has label and icon, so make sure they don't overlap */
+.devtools-button.devtools-har-button::before {
+ content: "HAR";
+ width: 21px;
+ padding-right: 12px;
+ background-image: var(--drop-down-icon-url);
+ background-position: right center;
+ fill: var(--theme-toolbar-photon-icon-color);
+}
+
+/* Make sure the HAR button label is vertically centered on Mac */
+:root[platform="mac"] .devtools-button.devtools-har-button::before {
+ height: 14px;
+}
+
.devtools-checkbox {
position: relative;
vertical-align: middle;
bottom: 1px;
}
.devtools-checkbox-label {
margin-inline-start: 10px;
--- a/devtools/client/netmonitor/src/assets/styles/variables.css
+++ b/devtools/client/netmonitor/src/assets/styles/variables.css
@@ -35,16 +35,17 @@
}
:root {
--primary-toolbar-height: 29px;
/* Icons */
--play-icon-url: url("chrome://devtools/content/netmonitor/src/assets/icons/play.svg");
--pause-icon-url: url("chrome://devtools/skin/images/pause.svg");
+ --drop-down-icon-url: url("chrome://devtools/content/netmonitor/src/assets/icons/drop-down.svg");
/* HTTP status codes */
--status-code-color-1xx: var(--theme-highlight-blue);
--status-code-color-2xx: var(--theme-highlight-green);
--status-code-color-3xx: transparent;
--status-code-color-4xx: var(--theme-highlight-pink);
--status-code-color-5xx: var(--theme-highlight-red);
}
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -8,16 +8,17 @@ const Services = require("Services");
const { Component, createFactory } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
const Actions = require("../actions/index");
const { FILTER_SEARCH_DELAY, FILTER_TAGS } = require("../constants");
const {
+ getDisplayedRequests,
getRecordingState,
getTypeFilteredRequests,
} = require("../selectors/index");
const { autocompleteProvider } = require("../utils/filter-autocomplete-provider");
const { L10N } = require("../utils/l10n");
const { fetchNetworkUpdatePacket } = require("../utils/request-utils");
// Components
@@ -25,42 +26,49 @@ const SearchBox = createFactory(require(
const { button, div, input, label, span } = dom;
// Localization
const SEARCH_KEY_SHORTCUT = L10N.getStr("netmonitor.toolbar.filterFreetext.key");
const SEARCH_PLACE_HOLDER = L10N.getStr("netmonitor.toolbar.filterFreetext.label");
const TOOLBAR_CLEAR = L10N.getStr("netmonitor.toolbar.clear");
const TOOLBAR_TOGGLE_RECORDING = L10N.getStr("netmonitor.toolbar.toggleRecording");
+const TOOLBAR_HAR_BUTTON = L10N.getStr("netmonitor.label.har");
// Preferences
const DEVTOOLS_DISABLE_CACHE_PREF = "devtools.cache.disabled";
const DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF = "devtools.netmonitor.persistlog";
const TOOLBAR_FILTER_LABELS = FILTER_TAGS.concat("all").reduce((o, tag) =>
Object.assign(o, { [tag]: L10N.getStr(`netmonitor.toolbar.filter.${tag}`) }), {});
const ENABLE_PERSISTENT_LOGS_TOOLTIP =
L10N.getStr("netmonitor.toolbar.enablePersistentLogs.tooltip");
const ENABLE_PERSISTENT_LOGS_LABEL =
L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label");
const DISABLE_CACHE_TOOLTIP = L10N.getStr("netmonitor.toolbar.disableCache.tooltip");
const DISABLE_CACHE_LABEL = L10N.getStr("netmonitor.toolbar.disableCache.label");
+// Menu
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
+
/**
* Network monitor toolbar component.
*
* Toolbar contains a set of useful tools to control network requests
* as well as set of filters for filtering the content.
*/
class Toolbar extends Component {
static get propTypes() {
return {
connector: PropTypes.object.isRequired,
toggleRecording: PropTypes.func.isRequired,
recording: PropTypes.bool.isRequired,
clearRequests: PropTypes.func.isRequired,
+ // List of currently displayed requests (i.e. filtered & sorted).
+ displayedRequests: PropTypes.array.isRequired,
requestFilterTypes: PropTypes.object.isRequired,
setRequestFilterText: PropTypes.func.isRequired,
enablePersistentLogs: PropTypes.func.isRequired,
togglePersistentLogs: PropTypes.func.isRequired,
persistentLogsEnabled: PropTypes.bool.isRequired,
disableBrowserCache: PropTypes.func.isRequired,
toggleBrowserCache: PropTypes.func.isRequired,
browserCacheDisabled: PropTypes.bool.isRequired,
@@ -246,16 +254,57 @@ class Toolbar extends Component {
onChange: toggleBrowserCache,
}),
DISABLE_CACHE_LABEL,
)
);
}
/**
+ * Render drop down button with HAR related actions.
+ */
+ renderHarButton() {
+ return button({
+ id: "devtools-har-button",
+ title: TOOLBAR_HAR_BUTTON,
+ className: "devtools-button devtools-har-button",
+ onClick: evt => {
+ this.showHarMenu(evt.target);
+ },
+ });
+ }
+
+ showHarMenu(menuButton) {
+ const {
+ connector,
+ displayedRequests
+ } = this.props;
+
+ let menuItems = [];
+
+ menuItems.push({
+ id: "request-list-context-save-all-as-har",
+ label: L10N.getStr("netmonitor.context.saveAllAsHar"),
+ accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
+ disabled: !displayedRequests.length,
+ click: () => HarMenuUtils.saveAllAsHar(displayedRequests, connector),
+ });
+
+ menuItems.push({
+ id: "request-list-context-copy-all-as-har",
+ label: L10N.getStr("netmonitor.context.copyAllAsHar"),
+ accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
+ disabled: !displayedRequests.length,
+ click: () => HarMenuUtils.copyAllAsHar(displayedRequests, connector),
+ });
+
+ showMenu(menuItems, { button: menuButton });
+ }
+
+ /**
* Render filter Searchbox.
*/
renderFilterBox(setRequestFilterText) {
return (
SearchBox({
delay: FILTER_SEARCH_DELAY,
keyShortcut: SEARCH_KEY_SHORTCUT,
placeholder: SEARCH_PLACE_HOLDER,
@@ -293,41 +342,46 @@ class Toolbar extends Component {
this.renderFilterBox(setRequestFilterText),
this.renderSeparator(),
this.renderToggleRecordingButton(recording, toggleRecording),
this.renderSeparator(),
this.renderFilterButtons(requestFilterTypes),
this.renderSeparator(),
this.renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs),
this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache),
+ this.renderSeparator(),
+ this.renderHarButton(),
)
)
) : (
span({ className: "devtools-toolbar devtools-toolbar-container" },
span({ className: "devtools-toolbar-group devtools-toolbar-two-rows-1" },
this.renderClearButton(clearRequests),
this.renderSeparator(),
this.renderFilterBox(setRequestFilterText),
this.renderSeparator(),
this.renderToggleRecordingButton(recording, toggleRecording),
this.renderSeparator(),
this.renderPersistlogCheckbox(persistentLogsEnabled, togglePersistentLogs),
this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache),
+ this.renderSeparator(),
+ this.renderHarButton(),
),
span({ className: "devtools-toolbar-group devtools-toolbar-two-rows-2" },
this.renderFilterButtons(requestFilterTypes)
)
)
);
}
}
module.exports = connect(
(state) => ({
browserCacheDisabled: state.ui.browserCacheDisabled,
+ displayedRequests: getDisplayedRequests(state),
filteredRequests: getTypeFilteredRequests(state),
persistentLogsEnabled: state.ui.persistentLogsEnabled,
recording: getRecordingState(state),
requestFilterTypes: state.filters.requestFilterTypes,
}),
(dispatch) => ({
clearRequests: () => dispatch(Actions.clearRequests()),
disableBrowserCache: (disabled) => dispatch(Actions.disableBrowserCache(disabled)),
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/har/har-menu-utils.js
@@ -0,0 +1,42 @@
+/* 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";
+
+loader.lazyRequireGetter(this, "HarExporter", "devtools/client/netmonitor/src/har/har-exporter", true);
+
+/**
+ * Helper object with HAR related context menu actions.
+ */
+var HarMenuUtils = {
+ /**
+ * Copy HAR from the network panel content to the clipboard.
+ */
+ copyAllAsHar(requests, connector) {
+ return HarExporter.copy(this.getDefaultHarOptions(requests, connector));
+ },
+
+ /**
+ * Save HAR from the network panel content to a file.
+ */
+ saveAllAsHar(requests, connector) {
+ // This will not work in launchpad
+ // document.execCommand(‘cut’/‘copy’) was denied because it was not called from
+ // inside a short running user-generated event handler.
+ // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
+ return HarExporter.save(this.getDefaultHarOptions(requests, connector));
+ },
+
+ getDefaultHarOptions(requests, connector) {
+ return {
+ connector: connector,
+ items: requests,
+ };
+ },
+};
+
+// Exports from this module
+exports.HarMenuUtils = HarMenuUtils;
--- a/devtools/client/netmonitor/src/har/moz.build
+++ b/devtools/client/netmonitor/src/har/moz.build
@@ -4,13 +4,14 @@
DevToolsModules(
'har-automation.js',
'har-builder-utils.js',
'har-builder.js',
'har-collector.js',
'har-exporter.js',
'har-importer.js',
+ 'har-menu-utils.js',
'har-utils.js',
'toolbox-overlay.js',
)
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
@@ -60,26 +60,24 @@ add_task(async function() {
});
/**
* Reload the page and copy all as HAR.
*/
async function reloadAndCopyAllAsHar(tab, monitor) {
let { connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
- let RequestListContextMenu = windowRequire(
- "devtools/client/netmonitor/src/widgets/RequestListContextMenu");
+ let { HarMenuUtils } = windowRequire(
+ "devtools/client/netmonitor/src/har/har-menu-utils");
let { getSortedRequests } = windowRequire(
"devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
let wait = waitForNetworkEvents(monitor, 1);
tab.linkedBrowser.reload();
await wait;
- let contextMenu = new RequestListContextMenu({ connector });
-
- await contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
+ await HarMenuUtils.copyAllAsHar(getSortedRequests(store.getState()), connector);
let jsonString = SpecialPowers.getClipboardData("text/unicode");
return JSON.parse(jsonString);
}
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_import.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_import.js
@@ -9,42 +9,43 @@
add_task(async () => {
let { tab, monitor } = await initNetMonitor(
HAR_EXAMPLE_URL + "html_har_import-test-page.html");
info("Starting test... ");
let { actions, connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
- let RequestListContextMenu = windowRequire(
- "devtools/client/netmonitor/src/widgets/RequestListContextMenu");
+ let { HarMenuUtils } = windowRequire(
+ "devtools/client/netmonitor/src/har/har-menu-utils");
let { getSortedRequests } = windowRequire(
"devtools/client/netmonitor/src/selectors/index");
let { HarImporter } = windowRequire(
"devtools/client/netmonitor/src/har/har-importer");
store.dispatch(Actions.batchEnable(false));
// Execute one POST request on the page and wait till its done.
let wait = waitForNetworkEvents(monitor, 3);
await ContentTask.spawn(tab.linkedBrowser, {}, async () => {
await content.wrappedJSObject.executeTest();
});
await wait;
// Copy HAR into the clipboard
- let contextMenu = new RequestListContextMenu({ connector });
- let json1 = await contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
+ let json1 = await HarMenuUtils.copyAllAsHar(
+ getSortedRequests(store.getState()), connector);
// Import HAR string
let importer = new HarImporter(actions);
importer.import(json1);
// Copy HAR into the clipboard again
- let json2 = await contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
+ let json2 = await HarMenuUtils.copyAllAsHar(
+ getSortedRequests(store.getState()), connector);
// Compare exported HAR data
let har1 = JSON.parse(json1);
let har2 = JSON.parse(json2);
dump("---------------\n");
dump(json1 + "\n");
dump("---------------\n");
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
@@ -9,33 +9,33 @@
add_task(async function() {
let { tab, monitor } = await initNetMonitor(
HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
info("Starting test... ");
let { connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
- let RequestListContextMenu = windowRequire(
- "devtools/client/netmonitor/src/widgets/RequestListContextMenu");
+ let { HarMenuUtils } = windowRequire(
+ "devtools/client/netmonitor/src/har/har-menu-utils");
let { getSortedRequests } = windowRequire(
"devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
// Execute one POST request on the page and wait till its done.
let wait = waitForNetworkEvents(monitor, 1);
await ContentTask.spawn(tab.linkedBrowser, {}, async function() {
content.wrappedJSObject.executeTest();
});
await wait;
// Copy HAR into the clipboard (asynchronous).
- let contextMenu = new RequestListContextMenu({ connector });
- let jsonString = await contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
+ let jsonString = await HarMenuUtils.copyAllAsHar(
+ getSortedRequests(store.getState()), connector);
let har = JSON.parse(jsonString);
// Check out the HAR log.
isnot(har.log, null, "The HAR log must exist");
is(har.log.pages.length, 1, "There must be one page");
is(har.log.entries.length, 1, "There must be one request");
let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
@@ -9,33 +9,33 @@
add_task(async function() {
let { tab, monitor } = await initNetMonitor(
HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
info("Starting test... ");
let { connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
- let RequestListContextMenu = windowRequire(
- "devtools/client/netmonitor/src/widgets/RequestListContextMenu");
+ let { HarMenuUtils } = windowRequire(
+ "devtools/client/netmonitor/src/har/har-menu-utils");
let { getSortedRequests } = windowRequire(
"devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
// Execute one GET request on the page and wait till its done.
let wait = waitForNetworkEvents(monitor, 1);
await ContentTask.spawn(tab.linkedBrowser, {}, async function() {
content.wrappedJSObject.executeTest3();
});
await wait;
// Copy HAR into the clipboard (asynchronous).
- let contextMenu = new RequestListContextMenu({ connector });
- let jsonString = await contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
+ let jsonString = await HarMenuUtils.copyAllAsHar(
+ getSortedRequests(store.getState()), connector);
let har = JSON.parse(jsonString);
// Check out the HAR log.
isnot(har.log, null, "The HAR log must exist");
is(har.log.pages.length, 1, "There must be one page");
is(har.log.entries.length, 1, "There must be one request");
let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
@@ -13,18 +13,18 @@ add_task(async function() {
async function throttleUploadTest(actuallyThrottle) {
let { tab, monitor } = await initNetMonitor(
HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
let { connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
- let RequestListContextMenu = windowRequire(
- "devtools/client/netmonitor/src/widgets/RequestListContextMenu");
+ let { HarMenuUtils } = windowRequire(
+ "devtools/client/netmonitor/src/har/har-menu-utils");
let { getSortedRequests } = windowRequire(
"devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
const size = 4096;
const uploadSize = actuallyThrottle ? size / 3 : 0;
@@ -51,18 +51,18 @@ async function throttleUploadTest(actual
let wait = waitForNetworkEvents(monitor, 1);
await ContentTask.spawn(tab.linkedBrowser, { size }, async function(args) {
content.wrappedJSObject.executeTest2(args.size);
});
await wait;
await onEventTimings;
// Copy HAR into the clipboard (asynchronous).
- let contextMenu = new RequestListContextMenu({ connector });
- let jsonString = await contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
+ let jsonString = await HarMenuUtils.copyAllAsHar(
+ getSortedRequests(store.getState()), connector);
let har = JSON.parse(jsonString);
// Check out the HAR log.
isnot(har.log, null, "The HAR log must exist");
is(har.log.pages.length, 1, "There must be one page");
is(har.log.entries.length, 1, "There must be one request");
let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/utils/menu.js
+++ b/devtools/client/netmonitor/src/utils/menu.js
@@ -2,35 +2,58 @@
* 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 Menu = require("devtools/client/framework/menu");
const MenuItem = require("devtools/client/framework/menu-item");
-function showMenu(evt, items) {
+/**
+ * Helper function for opening context menu.
+ *
+ * @param {Array} items List of menu items.
+ * @param {Object} options:
+ * @property {Number} screenX coordinate of the menu on the screen
+ * @property {Number} screenY coordinate of the menu on the screen
+ * @property {Object} button parent used to open the menu
+ */
+function showMenu(items, options) {
if (items.length === 0) {
return;
}
+ // Build the menu object from provided menu items.
let menu = new Menu();
items.forEach((item) => {
let menuItem = new MenuItem(item);
let subItems = item.submenu;
if (subItems) {
let subMenu = new Menu();
subItems.forEach((subItem) => {
subMenu.append(new MenuItem(subItem));
});
menuItem.submenu = subMenu;
}
menu.append(menuItem);
});
- menu.popup(evt.screenX, evt.screenY, { doc: window.parent.document });
+ let screenX = options.screenX;
+ let screenY = options.screenY;
+
+ // Calculate position on the screen according to
+ // the parent button if available.
+ if (options.button) {
+ const button = options.button;
+ const rect = button.getBoundingClientRect();
+ const defaultView = button.ownerDocument.defaultView;
+ screenX = rect.left + defaultView.mozInnerScreenX;
+ screenY = rect.bottom + defaultView.mozInnerScreenY;
+ }
+
+ menu.popup(screenX, screenY, { doc: window.parent.document });
}
module.exports = {
showMenu,
};
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -14,17 +14,17 @@ const {
parseQueryString,
} = require("../utils/request-utils");
loader.lazyRequireGetter(this, "Curl", "devtools/client/shared/curl", true);
loader.lazyRequireGetter(this, "saveAs", "devtools/client/shared/file-saver", true);
loader.lazyRequireGetter(this, "copyString", "devtools/shared/platform/clipboard", true);
loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
loader.lazyRequireGetter(this, "openRequestInTab", "devtools/client/netmonitor/src/utils/firefox/open-request-in-tab", true);
-loader.lazyRequireGetter(this, "HarExporter", "devtools/client/netmonitor/src/har/har-exporter", true);
+loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
class RequestListContextMenu {
constructor(props) {
this.props = props;
}
open(event, selectedRequest, requests) {
let {
@@ -40,16 +40,17 @@ class RequestListContextMenu {
requestPostDataAvailable,
responseHeaders,
responseHeadersAvailable,
responseContent,
responseContentAvailable,
url,
} = selectedRequest;
let {
+ connector,
cloneSelectedRequest,
openStatistics,
} = this.props;
let menu = [];
let copySubmenu = [];
copySubmenu.push({
id: "request-list-context-copy-url",
@@ -138,33 +139,33 @@ class RequestListContextMenu {
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.size > 0,
- click: () => this.copyAllAsHar(requests),
+ visible: requests.length > 0,
+ click: () => HarMenuUtils.copyAllAsHar(requests, connector),
});
menu.push({
label: L10N.getStr("netmonitor.context.copy"),
accesskey: L10N.getStr("netmonitor.context.copy.accesskey"),
visible: !!selectedRequest,
submenu: copySubmenu,
});
menu.push({
id: "request-list-context-save-all-as-har",
label: L10N.getStr("netmonitor.context.saveAllAsHar"),
accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
- visible: requests.size > 0,
- click: () => this.saveAllAsHar(requests),
+ visible: requests.length > 0,
+ click: () => HarMenuUtils.saveAllAsHar(requests, connector),
});
menu.push({
id: "request-list-context-save-image-as",
label: L10N.getStr("netmonitor.context.saveImageAs"),
accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
visible: !!(selectedRequest && (responseContentAvailable || responseContent) &&
mimeType && mimeType.includes("image/")),
@@ -218,17 +219,20 @@ class RequestListContextMenu {
menu.push({
id: "request-list-context-perf",
label: L10N.getStr("netmonitor.context.perfTools"),
accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
visible: requests.size > 0,
click: () => openStatistics(true),
});
- showMenu(event, menu);
+ showMenu(menu, {
+ screenX: event.screenX,
+ screenY: event.screenY,
+ });
}
/**
* Opens selected item in a new tab.
*/
async openRequestInTab(id, url, requestPostData) {
requestPostData = requestPostData ||
await this.props.connector.requestData(id, "requestPostData");
@@ -389,36 +393,11 @@ class RequestListContextMenu {
* Copy response data as a string.
*/
async copyResponse(id, responseContent) {
responseContent = responseContent ||
await this.props.connector.requestData(id, "responseContent");
copyString(responseContent.content.text);
}
-
- /**
- * Copy HAR from the network panel content to the clipboard.
- */
- copyAllAsHar(requests) {
- return HarExporter.copy(this.getDefaultHarOptions(requests));
- }
-
- /**
- * Save HAR from the network panel content to a file.
- */
- saveAllAsHar(requests) {
- // This will not work in launchpad
- // document.execCommand(‘cut’/‘copy’) was denied because it was not called from
- // inside a short running user-generated event handler.
- // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
- return HarExporter.save(this.getDefaultHarOptions(requests));
- }
-
- getDefaultHarOptions(requests) {
- return {
- connector: this.props.connector,
- items: requests,
- };
- }
}
module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
@@ -65,13 +65,16 @@ class RequestListHeaderContextMenu {
menu.push({ type: "separator" });
menu.push({
id: "request-list-header-reset-columns",
label: L10N.getStr("netmonitor.toolbar.resetColumns"),
click: () => this.props.resetColumns(),
});
- return showMenu(event, menu);
+ showMenu(menu, {
+ screenX: event.screenX,
+ screenY: event.screenY,
+ });
}
}
module.exports = RequestListHeaderContextMenu;