Bug 1494552 - Allow user to refresh USB devices. r=jdescottes,daisuke
authorBelén Albeza <balbeza@mozilla.com>
Thu, 25 Oct 2018 11:29:11 +0000
changeset 491314 6dddfbd0f3fb9ff61ed5b061c5687ff07f0978f4
parent 491313 bb7b0a61d22c955cb068bedaafcfc59b083d73f6
child 491315 0fc121f7a403b4dfef2efc11a48da848f60bd473
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersjdescottes, daisuke
bugs1494552
milestone65.0a1
Bug 1494552 - Allow user to refresh USB devices. r=jdescottes,daisuke Adds a "Refresh devices button". I was unsure on whether this button should use state for this or just plug directly into the adb module. In the end I opted for doing it via actions/state because it would also allow us to show somewhere else an indication of whether the scanner is running or not (in case we need it). But if you think this is overkill, I'll gladly change it. To try it, with the device connected, open and close firefox. If you press Refresh you should see the list update. Differential Revision: https://phabricator.services.mozilla.com/D9500
devtools/client/aboutdebugging-new/src/actions/ui.js
devtools/client/aboutdebugging-new/src/components/App.js
devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.js
devtools/client/aboutdebugging-new/src/components/sidebar/RefreshDevicesButton.js
devtools/client/aboutdebugging-new/src/components/sidebar/Sidebar.css
devtools/client/aboutdebugging-new/src/components/sidebar/Sidebar.js
devtools/client/aboutdebugging-new/src/components/sidebar/SidebarItem.css
devtools/client/aboutdebugging-new/src/components/sidebar/SidebarItem.js
devtools/client/aboutdebugging-new/src/components/sidebar/moz.build
devtools/client/aboutdebugging-new/src/constants.js
devtools/client/aboutdebugging-new/src/modules/usb-runtimes.js
devtools/client/aboutdebugging-new/src/reducers/ui-state.js
devtools/client/aboutdebugging-new/tmp-locale/en-US/aboutdebugging.notftl
--- a/devtools/client/aboutdebugging-new/src/actions/ui.js
+++ b/devtools/client/aboutdebugging-new/src/actions/ui.js
@@ -11,20 +11,23 @@ const {
   ADB_ADDON_UNINSTALL_START,
   ADB_ADDON_UNINSTALL_SUCCESS,
   ADB_ADDON_UNINSTALL_FAILURE,
   ADB_ADDON_STATUS_UPDATED,
   DEBUG_TARGET_COLLAPSIBILITY_UPDATED,
   NETWORK_LOCATIONS_UPDATED,
   PAGE_SELECTED,
   PAGES,
+  USB_RUNTIMES_SCAN_START,
+  USB_RUNTIMES_SCAN_SUCCESS,
 } = require("../constants");
 
 const NetworkLocationsModule = require("../modules/network-locations");
 const { adbAddon } = require("devtools/shared/adb/adb-addon");
+const { refreshUSBRuntimes } = require("../modules/usb-runtimes");
 
 const Actions = require("./index");
 
 // XXX: Isolating the code here, because it feels wrong to rely solely on the page "not"
 // being CONNECT to decide what to do. Should we have a page "type" on top of page "id"?
 function _isRuntimePage(page) {
   return page && page !== PAGES.CONNECT;
 }
@@ -97,18 +100,32 @@ function uninstallAdbAddon() {
       await adbAddon.uninstall();
       dispatch({ type: ADB_ADDON_UNINSTALL_SUCCESS });
     } catch (e) {
       dispatch({ type: ADB_ADDON_UNINSTALL_FAILURE, error: e });
     }
   };
 }
 
+function scanUSBRuntimes() {
+  return async (dispatch, getState) => {
+    // do not re-scan if we are already doing it
+    if (getState().ui.isScanningUsb) {
+      return;
+    }
+
+    dispatch({ type: USB_RUNTIMES_SCAN_START });
+    await refreshUSBRuntimes();
+    dispatch({ type: USB_RUNTIMES_SCAN_SUCCESS });
+  };
+}
+
 module.exports = {
   addNetworkLocation,
   installAdbAddon,
   removeNetworkLocation,
+  scanUSBRuntimes,
   selectPage,
   uninstallAdbAddon,
   updateAdbAddonStatus,
   updateDebugTargetCollapsibility,
   updateNetworkLocations,
 };
--- a/devtools/client/aboutdebugging-new/src/components/App.js
+++ b/devtools/client/aboutdebugging-new/src/components/App.js
@@ -23,16 +23,17 @@ class App extends PureComponent {
   static get propTypes() {
     return {
       adbAddonStatus: PropTypes.string,
       // The "dispatch" helper is forwarded to the App component via connect.
       // From that point, components are responsible for forwarding the dispatch
       // property to all components who need to dispatch actions.
       dispatch: PropTypes.func.isRequired,
       fluentBundles: PropTypes.arrayOf(PropTypes.object).isRequired,
+      isScanningUsb: PropTypes.bool.isRequired,
       networkEnabled: PropTypes.bool.isRequired,
       networkLocations: PropTypes.arrayOf(PropTypes.string).isRequired,
       networkRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
       selectedPage: PropTypes.string,
       usbRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
       wifiEnabled: PropTypes.bool.isRequired,
     };
   }
@@ -67,30 +68,32 @@ class App extends PureComponent {
     }
   }
 
   render() {
     const {
       adbAddonStatus,
       dispatch,
       fluentBundles,
+      isScanningUsb,
       networkRuntimes,
       selectedPage,
       usbRuntimes,
     } = this.props;
 
     return LocalizationProvider(
       { messages: fluentBundles },
       dom.div(
         { className: "app" },
         Sidebar(
           {
             adbAddonStatus,
             className: "app__sidebar",
             dispatch,
+            isScanningUsb,
             networkRuntimes,
             selectedPage,
             usbRuntimes,
           }
         ),
         dom.main(
           { className: "app__content" },
           this.getSelectedPageComponent()
@@ -98,16 +101,17 @@ class App extends PureComponent {
       )
     );
   }
 }
 
 const mapStateToProps = state => {
   return {
     adbAddonStatus: state.ui.adbAddonStatus,
+    isScanningUsb: state.ui.isScanningUsb,
     networkEnabled: state.ui.networkEnabled,
     networkLocations: state.ui.networkLocations,
     networkRuntimes: state.runtimes.networkRuntimes,
     selectedPage: state.ui.selectedPage,
     usbRuntimes: state.runtimes.usbRuntimes,
     wifiEnabled: state.ui.wifiEnabled,
   };
 };
--- a/devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.js
+++ b/devtools/client/aboutdebugging-new/src/components/connect/ConnectPage.js
@@ -116,17 +116,17 @@ class ConnectPage extends PureComponent 
     const disabled = usbStatus === USB_STATES.UPDATING_USB;
 
     return Localized(
       {
         id: localizedState,
       },
       dom.button(
         {
-          className: "std-button connect-page__usb__toggle-button " +
+          className: "default-button connect-page__usb__toggle-button " +
                      "js-connect-usb-toggle-button",
           disabled,
           onClick: () => this.onToggleUSBClick(),
         },
         localizedState
       )
     );
   }
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/sidebar/RefreshDevicesButton.js
@@ -0,0 +1,43 @@
+/* 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 { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const FluentReact = require("devtools/client/shared/vendor/fluent-react");
+const Localized = createFactory(FluentReact.Localized);
+
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Actions = require("../../actions/index");
+
+class RefreshDevicesButton extends PureComponent {
+  static get propTypes() {
+    return {
+      dispatch: PropTypes.func.isRequired,
+      isScanning: PropTypes.bool.isRequired,
+    };
+  }
+
+  refreshDevices() {
+    this.props.dispatch(Actions.scanUSBRuntimes());
+  }
+
+  render() {
+    return Localized(
+      { id: "about-debugging-refresh-usb-devices-button" },
+      dom.button(
+        {
+          className: "default-button",
+          disabled: this.props.isScanning,
+          onClick: () => this.refreshDevices(),
+        },
+        "Refresh devices"
+      )
+    );
+  }
+}
+
+module.exports = RefreshDevicesButton;
--- a/devtools/client/aboutdebugging-new/src/components/sidebar/Sidebar.css
+++ b/devtools/client/aboutdebugging-new/src/components/sidebar/Sidebar.css
@@ -3,9 +3,13 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .sidebar__devices__message {
   color: var(--grey-40);
   display: inline-block;
   padding: 12px 0;
   text-align: center;
   width: 100%;
-}
\ No newline at end of file
+}
+
+.sidebar__refresh-usb {
+  text-align: center;
+}
--- a/devtools/client/aboutdebugging-new/src/components/sidebar/Sidebar.js
+++ b/devtools/client/aboutdebugging-new/src/components/sidebar/Sidebar.js
@@ -10,29 +10,32 @@ const PropTypes = require("devtools/clie
 
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
 
 const { PAGES, RUNTIMES } = require("../../constants");
 const Types = require("../../types");
 loader.lazyRequireGetter(this, "ADB_ADDON_STATES", "devtools/shared/adb/adb-addon", true);
 
+const SidebarItem = createFactory(require("./SidebarItem"));
 const SidebarFixedItem = createFactory(require("./SidebarFixedItem"));
 const SidebarRuntimeItem = createFactory(require("./SidebarRuntimeItem"));
+const RefreshDevicesButton = createFactory(require("./RefreshDevicesButton"));
 const FIREFOX_ICON = "chrome://devtools/skin/images/aboutdebugging-firefox-logo.svg";
 const CONNECT_ICON = "chrome://devtools/skin/images/aboutdebugging-connect-icon.svg";
 const GLOBE_ICON = "chrome://devtools/skin/images/globe.svg";
 const USB_ICON = "chrome://devtools/skin/images/aboutdebugging-connect-icon.svg";
 
 class Sidebar extends PureComponent {
   static get propTypes() {
     return {
       adbAddonStatus: PropTypes.string,
       className: PropTypes.string,
       dispatch: PropTypes.func.isRequired,
+      isScanningUsb: PropTypes.bool.isRequired,
       networkRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
       selectedPage: PropTypes.string,
       usbRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
     };
   }
 
   renderAdbAddonStatus() {
     const isAddonInstalled = this.props.adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
@@ -45,31 +48,44 @@ class Sidebar extends PureComponent {
         {
           className: "sidebar__devices__message js-sidebar-usb-status",
         },
         localizationId
       )
     );
   }
 
-  renderDevices() {
-    const { networkRuntimes, usbRuntimes } = this.props;
-    if (!networkRuntimes.length && !usbRuntimes.length) {
-      return Localized(
+  renderDevicesEmpty() {
+    return SidebarItem(
+      {
+        isSelected: false,
+        selectable: false,
+      },
+      Localized(
         {
           id: "about-debugging-sidebar-no-devices",
-        }, dom.aside(
+        },
+        dom.aside(
           {
             className: "sidebar__devices__message js-sidebar-no-devices",
           },
           "No devices discovered"
         )
-      );
+      )
+    );
+  }
+
+  renderDevices() {
+    const { networkRuntimes, usbRuntimes } = this.props;
+
+    // render a "no devices" messages when the lists are empty
+    if (!networkRuntimes.length && !usbRuntimes.length) {
+      return this.renderDevicesEmpty();
     }
-
+    // render all devices otherwise
     return [
       ...this.renderSidebarItems(GLOBE_ICON, networkRuntimes),
       ...this.renderSidebarItems(USB_ICON, usbRuntimes),
     ];
   }
 
   renderSidebarItems(icon, runtimes) {
     const { dispatch, selectedPage } = this.props;
@@ -88,17 +104,17 @@ class Sidebar extends PureComponent {
         key: pageId,
         name: runtime.name,
         runtimeId: runtime.id,
       });
     });
   }
 
   render() {
-    const { dispatch, selectedPage } = this.props;
+    const { dispatch, selectedPage, isScanningUsb } = this.props;
 
     return dom.aside(
       {
         className: `sidebar ${this.props.className || ""}`,
       },
       dom.ul(
         {},
         Localized(
@@ -117,17 +133,34 @@ class Sidebar extends PureComponent {
           SidebarFixedItem({
             id: PAGES.CONNECT,
             dispatch,
             icon: CONNECT_ICON,
             isSelected: PAGES.CONNECT === selectedPage,
             name: "Connect",
           })
         ),
-        dom.hr({ className: "separator" }),
-        this.renderAdbAddonStatus(),
-        this.renderDevices()
+        SidebarItem(
+          {
+            isSelected: false,
+            selectable: false,
+          },
+          dom.hr({ className: "separator" }),
+          this.renderAdbAddonStatus(),
+        ),
+        this.renderDevices(),
+        SidebarItem(
+          {
+            className: "sidebar-item--breathe sidebar__refresh-usb",
+            isSelected: false,
+            selectable: false,
+          },
+          RefreshDevicesButton({
+            dispatch,
+            isScanning: isScanningUsb,
+          })
+        ),
       )
     );
   }
 }
 
 module.exports = Sidebar;
--- a/devtools/client/aboutdebugging-new/src/components/sidebar/SidebarItem.css
+++ b/devtools/client/aboutdebugging-new/src/components/sidebar/SidebarItem.css
@@ -23,8 +23,13 @@
 
 .sidebar-item--selectable:hover {
   background-color: var(--sidebar-background-hover);
 }
 
 .sidebar-item--selected {
   color: var(--sidebar-selected-color);
 }
+
+.sidebar-item--breathe {
+  margin-block-start: calc(2 * var(--base-distance));
+  margin-block-end: calc(2 * var(--base-distance));
+}
--- a/devtools/client/aboutdebugging-new/src/components/sidebar/SidebarItem.js
+++ b/devtools/client/aboutdebugging-new/src/components/sidebar/SidebarItem.js
@@ -9,21 +9,29 @@ const dom = require("devtools/client/sha
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 /**
  * This component is used as a wrapper by items in the sidebar.
  */
 class SidebarItem extends PureComponent {
   static get propTypes() {
     return {
-      children: PropTypes.arrayOf(PropTypes.element).isRequired,
+      children: PropTypes.node.isRequired,
       className: PropTypes.string,
       isSelected: PropTypes.bool.isRequired,
       selectable: PropTypes.bool.isRequired,
-      onSelect: PropTypes.func.isRequired,
+      // only require `onSelect` function when `selectable` is true
+      onSelect: (props, propName, componentName) => {
+        const isFn = props[propName] && typeof props[propName] === "function";
+        if (props.selectable && !isFn) {
+          return new Error(`Missing ${propName} function supplied to ${componentName}. ` +
+            "(you must set this prop when selectable is true)");
+        }
+        return null; // for eslint (consistent-return rule)
+      },
     };
   }
 
   onItemClick() {
     this.props.onSelect();
   }
 
   render() {
--- a/devtools/client/aboutdebugging-new/src/components/sidebar/moz.build
+++ b/devtools/client/aboutdebugging-new/src/components/sidebar/moz.build
@@ -1,13 +1,14 @@
 # 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(
+    'RefreshDevicesButton.js',
     'Sidebar.css',
     'Sidebar.js',
     'SidebarFixedItem.css',
     'SidebarFixedItem.js',
     'SidebarItem.css',
     'SidebarItem.js',
     'SidebarRuntimeItem.css',
     'SidebarRuntimeItem.js',
--- a/devtools/client/aboutdebugging-new/src/constants.js
+++ b/devtools/client/aboutdebugging-new/src/constants.js
@@ -31,16 +31,18 @@ const actionTypes = {
   REQUEST_WORKERS_START: "REQUEST_WORKERS_START",
   REQUEST_WORKERS_SUCCESS: "REQUEST_WORKERS_SUCCESS",
   UNWATCH_RUNTIME_FAILURE: "UNWATCH_RUNTIME_FAILURE",
   UNWATCH_RUNTIME_START: "UNWATCH_RUNTIME_START",
   UNWATCH_RUNTIME_SUCCESS: "UNWATCH_RUNTIME_SUCCESS",
   UPDATE_CONNECTION_PROMPT_SETTING_FAILURE: "UPDATE_CONNECTION_PROMPT_SETTING_FAILURE",
   UPDATE_CONNECTION_PROMPT_SETTING_START: "UPDATE_CONNECTION_PROMPT_SETTING_START",
   UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS: "UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS",
+  USB_RUNTIMES_SCAN_START: "USB_RUNTIMES_SCAN_START",
+  USB_RUNTIMES_SCAN_SUCCESS: "USB_RUNTIMES_SCAN_SUCCESS",
   USB_RUNTIMES_UPDATED: "USB_RUNTIMES_UPDATED",
   WATCH_RUNTIME_FAILURE: "WATCH_RUNTIME_FAILURE",
   WATCH_RUNTIME_START: "WATCH_RUNTIME_START",
   WATCH_RUNTIME_SUCCESS: "WATCH_RUNTIME_SUCCESS",
 };
 
 const DEBUG_TARGETS = {
   EXTENSION: "EXTENSION",
--- a/devtools/client/aboutdebugging-new/src/modules/usb-runtimes.js
+++ b/devtools/client/aboutdebugging-new/src/modules/usb-runtimes.js
@@ -32,8 +32,13 @@ function getUSBRuntimes() {
   return adbScanner.listRuntimes();
 }
 exports.getUSBRuntimes = getUSBRuntimes;
 
 function removeUSBRuntimesObserver(listener) {
   adbScanner.off("runtime-list-updated", listener);
 }
 exports.removeUSBRuntimesObserver = removeUSBRuntimesObserver;
+
+function refreshUSBRuntimes() {
+  return adbScanner.scan();
+}
+exports.refreshUSBRuntimes = refreshUSBRuntimes;
--- a/devtools/client/aboutdebugging-new/src/reducers/ui-state.js
+++ b/devtools/client/aboutdebugging-new/src/reducers/ui-state.js
@@ -4,23 +4,26 @@
 
 "use strict";
 
 const {
   ADB_ADDON_STATUS_UPDATED,
   DEBUG_TARGET_COLLAPSIBILITY_UPDATED,
   NETWORK_LOCATIONS_UPDATED,
   PAGE_SELECTED,
+  USB_RUNTIMES_SCAN_START,
+  USB_RUNTIMES_SCAN_SUCCESS,
 } = require("../constants");
 
 function UiState(locations = [], debugTargetCollapsibilities = {},
                  networkEnabled = false, wifiEnabled = false) {
   return {
     adbAddonStatus: null,
     debugTargetCollapsibilities,
+    isScanningUsb: false,
     networkEnabled,
     networkLocations: locations,
     selectedPage: null,
     wifiEnabled,
   };
 }
 
 function uiReducer(state = UiState(), action) {
@@ -42,16 +45,24 @@ function uiReducer(state = UiState(), ac
       return Object.assign({}, state, { networkLocations: locations });
     }
 
     case PAGE_SELECTED: {
       const { page } = action;
       return Object.assign({}, state, { selectedPage: page });
     }
 
+    case USB_RUNTIMES_SCAN_START: {
+      return Object.assign({}, state, { isScanningUsb: true });
+    }
+
+    case USB_RUNTIMES_SCAN_SUCCESS: {
+      return Object.assign({}, state, { isScanningUsb: false });
+    }
+
     default:
       return state;
   }
 }
 
 module.exports = {
   UiState,
   uiReducer,
--- a/devtools/client/aboutdebugging-new/tmp-locale/en-US/aboutdebugging.notftl
+++ b/devtools/client/aboutdebugging-new/tmp-locale/en-US/aboutdebugging.notftl
@@ -31,16 +31,20 @@ about-debugging-sidebar-no-devices = No 
 # Text displayed in buttons found in sidebar items representing remote runtimes.
 # Clicking on the button will attempt to connect to the runtime.
 about-debugging-sidebar-item-connect-button = Connect
 
 # Temporary text displayed in sidebar items representing remote runtimes after
 # successfully connecting to them. Temporary UI, do not localize.
 about-debugging-sidebar-item-connected-label = Connected
 
+# Temporary text displayed in a sidebar button to refresh USB devices. Temporary
+# UI, do not localize.
+about-debugging-refresh-usb-devices-button = Refresh devices
+
 # Title of the Connect page.
 about-debugging-connect-title = Connect a Device
 
 # WiFi section of the Connect page
 about-debugging-connect-wifi
   .title = Via WiFi
 
 # Temporary text displayed when wifi support is turned off via preferences.