Bug 1456129 - Extend HAR toolbar with import; r=davidwalsh draft
authorJan Odvarko <odvarko@gmail.com>
Thu, 26 Apr 2018 19:28:10 +0200
changeset 788611 8044e25d9af8367c430ad1e0b3ebeaba14993a47
parent 788609 daf7bedce6190eda9ab7d049c41eb83d04ffca6f
child 788624 cac53ddea0fed1b143f55a3992315f5369d79566
push id108034
push userjodvarko@mozilla.com
push dateThu, 26 Apr 2018 17:33:21 +0000
reviewersdavidwalsh
bugs1456129
milestone61.0a1
Bug 1456129 - Extend HAR toolbar with import; r=davidwalsh MozReview-Commit-ID: 2g5sVon7xpk
devtools/client/locales/en-US/netmonitor.properties
devtools/client/netmonitor/src/components/App.js
devtools/client/netmonitor/src/components/DropHarHandler.js
devtools/client/netmonitor/src/components/MonitorPanel.js
devtools/client/netmonitor/src/components/Toolbar.js
devtools/client/netmonitor/src/har/har-menu-utils.js
devtools/client/netmonitor/src/utils/menu.js
--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -954,16 +954,36 @@ netmonitor.context.copyAllAsHar.accesske
 # LOCALIZATION NOTE (netmonitor.context.saveAllAsHar): This is the label displayed
 # on the context menu that saves all as HAR format
 netmonitor.context.saveAllAsHar=Save All As HAR
 
 # LOCALIZATION NOTE (netmonitor.context.saveAllAsHar.accesskey): This is the access key
 # for the Save All As HAR menu item displayed in the context menu for a network panel
 netmonitor.context.saveAllAsHar.accesskey=H
 
+# LOCALIZATION NOTE (netmonitor.context.importHar): This is the label displayed
+# on the context menu that imports HAR files
+netmonitor.context.importHar=Import…
+
+# LOCALIZATION NOTE (netmonitor.context.importHar.accesskey): This is the access key
+# for the Import HAR menu item displayed in the context menu for a network panel
+netmonitor.context.importHar.accesskey=I
+
+# LOCALIZATION NOTE (netmonitor.har.importHarDialogTitle): This is a label
+# used for import file open dialog
+netmonitor.har.importHarDialogTitle=Import HAR File
+
+# LOCALIZATION NOTE (netmonitor.har.importDialogHARFilter):
+# This string is displayed as a filter for importing HAR file
+netmonitor.har.importDialogHARFilter=HAR Files
+
+# LOCALIZATION NOTE (netmonitor.har.importDialogAllFilter):
+# This string is displayed as a filter for importing HAR file
+netmonitor.har.importDialogAllFilter=All Files
+
 # LOCALIZATION NOTE (netmonitor.context.editAndResend): This is the label displayed
 # on the context menu that opens a form to edit and resend the currently
 # displayed request
 netmonitor.context.editAndResend=Edit and Resend
 
 # LOCALIZATION NOTE (netmonitor.context.editAndResend.accesskey): This is the access key
 # for the "Edit and Resend" menu item displayed in the context menu for a request
 netmonitor.context.editAndResend.accesskey=E
--- a/devtools/client/netmonitor/src/components/App.js
+++ b/devtools/client/netmonitor/src/components/App.js
@@ -59,17 +59,19 @@ class App extends Component {
     return (
       div({className: "network-monitor"},
         !statisticsOpen ?
           DropHarHandler({
             actions,
             openSplitConsole,
           },
             MonitorPanel({
+              actions,
               connector,
+              openSplitConsole,
               sourceMapService,
               openLink,
             })
           ) : StatisticsPanel({
             connector
           }),
       )
     );
--- a/devtools/client/netmonitor/src/components/DropHarHandler.js
+++ b/devtools/client/netmonitor/src/components/DropHarHandler.js
@@ -5,19 +5,18 @@
 "use strict";
 
 const { Component } = require("devtools/client/shared/vendor/react");
 const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { L10N } = require("../utils/l10n");
 
-loader.lazyGetter(this, "HarImporter", function() {
-  return require("../har/har-importer").HarImporter;
-});
+loader.lazyRequireGetter(this, "HarMenuUtils",
+  "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
 const { div } = dom;
 
 const DROP_HAR_FILES = L10N.getStr("netmonitor.label.dropHarFiles");
 
 /**
  * Helper component responsible for handling and  importing
  * dropped *.har files.
@@ -65,44 +64,34 @@ class DropHarHandler extends Component {
     event.preventDefault();
     stopDragging(findDOMNode(this));
 
     let files = event.dataTransfer.files;
     if (!files) {
       return;
     }
 
+    let {
+      actions,
+      openSplitConsole,
+    } = this.props;
+
     // Import only the first dragged file for now
     // See also:
     // https://bugzilla.mozilla.org/show_bug.cgi?id=1438792
     if (files.length) {
       let file = files[0];
       readFile(file).then(har => {
         if (har) {
-          this.appendPreview(har);
+          HarMenuUtils.appendPreview(har, actions, openSplitConsole);
         }
       });
     }
   }
 
-  appendPreview(har) {
-    let {
-      openSplitConsole
-    } = this.props;
-
-    try {
-      let importer = new HarImporter(this.props.actions);
-      importer.import(har);
-    } catch (err) {
-      if (openSplitConsole) {
-        openSplitConsole("Error while processing HAR file: " + err.message);
-      }
-    }
-  }
-
   // Rendering
 
   render() {
     return (
       div({
         onDragEnter: this.onDragEnter,
         onDragOver: this.onDragOver,
         onDragExit: this.onDragExit,
--- a/devtools/client/netmonitor/src/components/MonitorPanel.js
+++ b/devtools/client/netmonitor/src/components/MonitorPanel.js
@@ -37,20 +37,23 @@ const MediaQuerySingleRow = window.match
 
 /**
  * Monitor panel component
  * The main panel for displaying various network request information
  */
 class MonitorPanel extends Component {
   static get propTypes() {
     return {
+      actions: PropTypes.object.isRequired,
       connector: PropTypes.object.isRequired,
       isEmpty: PropTypes.bool.isRequired,
       networkDetailsOpen: PropTypes.bool.isRequired,
       openNetworkDetails: PropTypes.func.isRequired,
+      // Callback for opening split console.
+      openSplitConsole: PropTypes.func,
       onNetworkDetailsResized: PropTypes.func.isRequired,
       request: PropTypes.object,
       selectedRequestVisible: PropTypes.bool.isRequired,
       sourceMapService: PropTypes.object,
       openLink: PropTypes.func,
       updateRequest: PropTypes.func.isRequired,
     };
   }
@@ -113,32 +116,36 @@ class MonitorPanel extends Component {
     return this.props.onNetworkDetailsResized(
       isVerticalSpliter ? width : null,
       isVerticalSpliter ? null : height
     );
   }
 
   render() {
     let {
+      actions,
       connector,
       isEmpty,
       networkDetailsOpen,
       openLink,
+      openSplitConsole,
       sourceMapService,
     } = 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({
+          actions,
           connector,
+          openSplitConsole,
           singleRow: this.state.isSingleRow,
         }),
         SplitBox({
           className: "devtools-responsive-container",
           initialWidth: initialWidth,
           initialHeight: initialHeight,
           minSize: "50px",
           maxSize: "80%",
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -53,16 +53,17 @@ loader.lazyRequireGetter(this, "HarMenuU
  * 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 {
+      actions: PropTypes.object.isRequired,
       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,
@@ -72,16 +73,18 @@ class Toolbar extends Component {
       disableBrowserCache: PropTypes.func.isRequired,
       toggleBrowserCache: PropTypes.func.isRequired,
       browserCacheDisabled: PropTypes.bool.isRequired,
       toggleRequestFilterType: PropTypes.func.isRequired,
       filteredRequests: PropTypes.array.isRequired,
       // Set to true if there is enough horizontal space
       // and the toolbar needs just one row.
       singleRow: PropTypes.bool.isRequired,
+      // Callback for opening split console.
+      openSplitConsole: PropTypes.func,
     };
   }
 
   constructor(props) {
     super(props);
     this.autocompleteProvider = this.autocompleteProvider.bind(this);
     this.onSearchBoxFocus = this.onSearchBoxFocus.bind(this);
     this.toggleRequestFilterType = this.toggleRequestFilterType.bind(this);
@@ -269,23 +272,34 @@ class Toolbar extends Component {
       onClick: evt => {
         this.showHarMenu(evt.target);
       },
     });
   }
 
   showHarMenu(menuButton) {
     const {
+      actions,
       connector,
-      displayedRequests
+      displayedRequests,
+      openSplitConsole,
     } = this.props;
 
     let menuItems = [];
 
     menuItems.push({
+      id: "request-list-context-import-har",
+      label: L10N.getStr("netmonitor.context.importHar"),
+      accesskey: L10N.getStr("netmonitor.context.importHar.accesskey"),
+      click: () => HarMenuUtils.openHarFile(actions, openSplitConsole),
+    });
+
+    menuItems.push("-");
+
+    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({
--- a/devtools/client/netmonitor/src/har/har-menu-utils.js
+++ b/devtools/client/netmonitor/src/har/har-menu-utils.js
@@ -1,17 +1,24 @@
 /* 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);
+const { L10N } = require("../utils/l10n");
+
+loader.lazyRequireGetter(this, "HarExporter",
+  "devtools/client/netmonitor/src/har/har-exporter", true);
+
+loader.lazyGetter(this, "HarImporter", function() {
+  return require("../har/har-importer").HarImporter;
+});
 
 /**
  * Helper object with HAR related context menu actions.
  */
 var HarMenuUtils = {
   /**
    * Copy HAR from the network panel content to the clipboard.
    */
@@ -25,18 +32,66 @@ var HarMenuUtils = {
   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));
   },
 
+  /**
+   * Import HAR file and preview its content in the Network panel.
+   */
+  openHarFile(actions, openSplitConsole) {
+    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+    fp.init(window, L10N.getStr("netmonitor.har.importHarDialogTitle"),
+      Ci.nsIFilePicker.modeOpen);
+
+    // Append file filters
+    fp.appendFilter(L10N.getStr("netmonitor.har.importDialogHARFilter"), "*.har");
+    fp.appendFilter(L10N.getStr("netmonitor.har.importDialogAllFilter"), "*.*");
+
+    fp.open(rv => {
+      if (rv == Ci.nsIFilePicker.returnOK) {
+        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        file.initWithPath(fp.file.path);
+        readFile(file).then(har => {
+          if (har) {
+            this.appendPreview(har, actions, openSplitConsole);
+          }
+        });
+      }
+    });
+  },
+
+  appendPreview(har, actions, openSplitConsole) {
+    try {
+      let importer = new HarImporter(actions);
+      importer.import(har);
+    } catch (err) {
+      if (openSplitConsole) {
+        openSplitConsole("Error while processing HAR file: " + err.message);
+      }
+    }
+  },
+
   getDefaultHarOptions(requests, connector) {
     return {
       connector: connector,
       items: requests,
     };
   },
 };
 
+// Helpers
+
+function readFile(file) {
+  return new Promise(resolve => {
+    const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+    OS.File.read(file.path).then(data => {
+      let decoder = new TextDecoder();
+      resolve(decoder.decode(data));
+    });
+  });
+}
+
 // Exports from this module
 exports.HarMenuUtils = HarMenuUtils;
--- a/devtools/client/netmonitor/src/utils/menu.js
+++ b/devtools/client/netmonitor/src/utils/menu.js
@@ -19,16 +19,20 @@ const MenuItem = require("devtools/clien
 function showMenu(items, options) {
   if (items.length === 0) {
     return;
   }
 
   // Build the menu object from provided menu items.
   let menu = new Menu();
   items.forEach((item) => {
+    if (item == "-") {
+      item = { type: "separator" };
+    }
+
     let menuItem = new MenuItem(item);
     let subItems = item.submenu;
 
     if (subItems) {
       let subMenu = new Menu();
       subItems.forEach((subItem) => {
         subMenu.append(new MenuItem(subItem));
       });