diff --git a/src/App.js b/src/App.js index dba3d8999..ac60810f3 100644 --- a/src/App.js +++ b/src/App.js @@ -71,8 +71,7 @@ export class App extends React.Component { export default connect( ({ application: {pluginManagerVisible, bugDialogVisible, leftSidebarVisible}, - connections: {selectedDevice}, - server: {error}, + connections: {selectedDevice, error}, }) => ({ pluginManagerVisible, bugDialogVisible, diff --git a/src/PluginContainer.js b/src/PluginContainer.js index 35e33723e..40e517d11 100644 --- a/src/PluginContainer.js +++ b/src/PluginContainer.js @@ -147,9 +147,8 @@ class PluginContainer extends Component { export default connect( ({ application: {rightSidebarVisible, rightSidebarAvailable}, - connections: {selectedPlugin, selectedDevice, selectedApp}, + connections: {selectedPlugin, selectedDevice, selectedApp, clients}, pluginStates, - server: {clients}, }) => ({ selectedPlugin, selectedDevice, diff --git a/src/chrome/DevicesButton.js b/src/chrome/DevicesButton.js index d341bd27e..722d64e19 100644 --- a/src/chrome/DevicesButton.js +++ b/src/chrome/DevicesButton.js @@ -7,8 +7,8 @@ import {Component, Button} from 'sonar'; import {connect} from 'react-redux'; -import {exec} from 'child_process'; -import {selectDevice} from '../reducers/connections.js'; +import {spawn} from 'child_process'; +import {selectDevice, preferDevice} from '../reducers/connections.js'; import type BaseDevice from '../devices/BaseDevice.js'; type Props = { @@ -16,15 +16,21 @@ type Props = { androidEmulators: Array, devices: Array, selectDevice: (device: BaseDevice) => void, + preferDevice: (device: string) => void, }; class DevicesButton extends Component { launchEmulator = (name: string) => { - exec(`$ANDROID_HOME/tools/emulator @${name}`, error => { - if (error) { - console.error(error); - } - }); + const child = spawn( + `${process.env.ANDROID_HOME || ''}/tools/emulator`, + [`@${name}`], + { + detached: true, + stdio: 'ignore', + }, + ); + child.on('error', console.error); + this.props.preferDevice(name); }; render() { @@ -96,5 +102,5 @@ export default connect( androidEmulators, selectedDevice, }), - {selectDevice}, + {selectDevice, preferDevice}, )(DevicesButton); diff --git a/src/chrome/MainSidebar.js b/src/chrome/MainSidebar.js index 678ca5854..e2b0074d5 100644 --- a/src/chrome/MainSidebar.js +++ b/src/chrome/MainSidebar.js @@ -225,10 +225,7 @@ class MainSidebar extends Component { } export default connect( - ({ - connections: {selectedDevice, selectedPlugin, selectedApp}, - server: {clients}, - }) => ({ + ({connections: {selectedDevice, selectedPlugin, selectedApp, clients}}) => ({ selectedDevice, selectedPlugin, selectedApp, diff --git a/src/dispatcher/server.js b/src/dispatcher/server.js index 72568d4ca..6c9869a0e 100644 --- a/src/dispatcher/server.js +++ b/src/dispatcher/server.js @@ -9,6 +9,7 @@ import Server from '../server.js'; import type {Store} from '../reducers/index.js'; import type Logger from '../fb-stubs/Logger.js'; +import type Client from '../Client.js'; export default (store: Store, logger: Logger) => { const server = new Server(logger); diff --git a/src/reducers/connections.js b/src/reducers/connections.js index 8bc69785f..729d955a9 100644 --- a/src/reducers/connections.js +++ b/src/reducers/connections.js @@ -6,12 +6,19 @@ */ import type BaseDevice from '../devices/BaseDevice'; +import type Client from '../Client'; + export type State = { devices: Array, androidEmulators: Array, selectedDevice: ?BaseDevice, selectedPlugin: ?string, selectedApp: ?string, + userPreferredDevice: ?string, + userPreferredPlugin: ?string, + userPreferredApp: ?string, + error: ?string, + clients: Array, }; export type Action = @@ -37,6 +44,22 @@ export type Action = selectedPlugin: ?string, selectedApp: ?string, }, + } + | { + type: 'SERVER_ERROR', + payload: ?string, + } + | { + type: 'NEW_CLIENT', + payload: Client, + } + | { + type: 'CLIENT_REMOVED', + payload: string, + } + | { + type: 'PREFER_DEVICE', + payload: string, }; const DEFAULT_PLUGIN = 'DeviceLogs'; @@ -47,6 +70,11 @@ const INITAL_STATE: State = { selectedDevice: null, selectedApp: null, selectedPlugin: DEFAULT_PLUGIN, + userPreferredDevice: null, + userPreferredPlugin: null, + userPreferredApp: null, + error: null, + clients: [], }; export default function reducer( @@ -61,6 +89,7 @@ export default function reducer( selectedApp: null, selectedPlugin: DEFAULT_PLUGIN, selectedDevice: payload, + userPreferredDevice: payload.title, }; } case 'REGISTER_ANDROID_EMULATORS': { @@ -75,13 +104,17 @@ export default function reducer( const devices = state.devices.concat(payload); let {selectedDevice} = state; let selection = {}; + if (!selectedDevice) { selectedDevice = payload; selection = { selectedApp: null, selectedPlugin: DEFAULT_PLUGIN, }; + } else if (payload.title === state.userPreferredDevice) { + selectedDevice = payload; } + return { ...state, devices, @@ -127,8 +160,59 @@ export default function reducer( return { ...state, ...payload, + userPreferredApp: payload.selectedApp, + userPreferredPlugin: payload.selectedPlugin, }; } + + case 'NEW_CLIENT': { + const {payload} = action; + const {userPreferredApp, userPreferredPlugin} = state; + let {selectedApp, selectedPlugin} = state; + + if ( + userPreferredApp && + userPreferredPlugin && + payload.id === userPreferredApp && + payload.plugins.includes(userPreferredPlugin) + ) { + // user preferred client did reconnect, so let's select it + selectedApp = userPreferredApp; + selectedPlugin = userPreferredPlugin; + } + + return { + ...state, + clients: state.clients.concat(payload), + selectedApp, + selectedPlugin, + }; + } + case 'CLIENT_REMOVED': { + const {payload} = action; + + let selected = {}; + if (state.selectedApp === payload) { + selected.selectedApp = null; + selected.selectedPlugin = DEFAULT_PLUGIN; + } + + return { + ...state, + ...selected, + clients: state.clients.filter( + (client: Client) => client.id !== payload, + ), + }; + } + case 'PREFER_DEVICE': { + const {payload: userPreferredDevice} = action; + return {...state, userPreferredDevice}; + } + case 'SERVER_ERROR': { + const {payload} = action; + return {...state, error: payload}; + } default: return state; } @@ -139,6 +223,11 @@ export const selectDevice = (payload: BaseDevice): Action => ({ payload, }); +export const preferDevice = (payload: string): Action => ({ + type: 'PREFER_DEVICE', + payload, +}); + export const selectPlugin = (payload: { selectedPlugin: ?string, selectedApp: ?string, diff --git a/src/reducers/index.js b/src/reducers/index.js index f16a3e02a..403b07c55 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -8,8 +8,8 @@ import {combineReducers} from 'redux'; import application from './application.js'; import connections from './connections.js'; -import server from './server.js'; import pluginStates from './pluginStates.js'; + import type { State as ApplicationState, Action as ApplicationAction, @@ -22,7 +22,6 @@ import type { State as PluginsState, Action as PluginsAction, } from './pluginStates.js'; -import type {State as ServerState, Action as ServerAction} from './server.js'; import type {Store as ReduxStore} from 'redux'; export type Store = ReduxStore< @@ -30,14 +29,12 @@ export type Store = ReduxStore< application: ApplicationState, connections: DevicesState, pluginStates: PluginsState, - server: ServerState, }, - ApplicationAction | DevicesAction | PluginsAction | ServerAction, + ApplicationAction | DevicesAction | PluginsAction, >; export default combineReducers({ application, connections, pluginStates, - server, }); diff --git a/src/reducers/server.js b/src/reducers/server.js deleted file mode 100644 index 84e342855..000000000 --- a/src/reducers/server.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright 2018-present Facebook. - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * @format - */ - -export type State = { - error: ?string, - clients: Array, -}; - -export type Action = - | { - type: 'SERVER_ERROR', - payload: ?string, - } - | { - type: 'NEW_CLIENT', - payload: Client, - } - | { - type: 'CLIENT_REMOVED', - payload: string, - }; - -const INITIAL_STATE: State = { - error: null, - clients: [], -}; - -export default function reducer( - state: State = INITIAL_STATE, - action: Action, -): State { - if (action.type === 'NEW_CLIENT') { - const {payload} = action; - return { - ...state, - clients: state.clients.concat(payload), - }; - } else if (action.type === 'CLIENT_REMOVED') { - const {payload} = action; - return { - ...state, - clients: state.clients.filter((client: Client) => client.id !== payload), - }; - } else if (action.type === 'SERVER_ERROR') { - const {payload} = action; - return {...state, error: payload}; - } else { - return state; - } -}