show only one device in sidbar
Summary: Refactors the plugin architecture of Sonar: - Before plugin rendering had it's own implementation of the react lifecycle. This means the `render`-function was not called by react, but rather by the application it self. In this diff, the render method is now called from react, which enables better debugging and allows react to do optimizations. - Business logic for querying emulators is moved away from the view components into its own dispatcher - All plugin handling is moved from `App.js` to `PluginContainer`. - The sidebar only shows one selected device. This allows us to add the screenshot feature as part of the Sonar main app and not a plugin. - This also fixes the inconsistency between the devices button and the sidebar Reviewed By: jknoxville Differential Revision: D8186933 fbshipit-source-id: 46404443025bcf18d6eeba0679e098d5440822d5
This commit is contained in:
committed by
Facebook Github Bot
parent
0c2f4d7cff
commit
cbab597236
@@ -9,7 +9,6 @@
|
||||
.*/website/.*
|
||||
|
||||
[libs]
|
||||
lib
|
||||
flow-typed
|
||||
|
||||
[options]
|
||||
|
||||
182
src/App.js
182
src/App.js
@@ -4,11 +4,9 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
import {ErrorBoundary, FlexColumn, FlexRow} from 'sonar';
|
||||
import {FlexColumn, FlexRow} from 'sonar';
|
||||
import {connect} from 'react-redux';
|
||||
import {toggleBugDialogVisible} from './reducers/application.js';
|
||||
import {setupMenu, activateMenuItems} from './MenuBar.js';
|
||||
import {devicePlugins} from './device-plugins/index.js';
|
||||
import WelcomeScreen from './chrome/WelcomeScreen.js';
|
||||
import SonarTitleBar from './chrome/SonarTitleBar.js';
|
||||
import BaseDevice from './devices/BaseDevice.js';
|
||||
@@ -16,7 +14,6 @@ import MainSidebar from './chrome/MainSidebar.js';
|
||||
import {SonarBasePlugin} from './plugin.js';
|
||||
import Server from './server.js';
|
||||
import Client from './Client.js';
|
||||
import * as reducers from './reducers.js';
|
||||
import React from 'react';
|
||||
import BugReporter from './fb-stubs/BugReporter.js';
|
||||
import BugReporterDialog from './chrome/BugReporterDialog.js';
|
||||
@@ -47,7 +44,6 @@ export type State = {
|
||||
activeAppKey: ?string,
|
||||
plugins: StatePlugins,
|
||||
error: ?string,
|
||||
server: Server,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -55,6 +51,8 @@ type Props = {
|
||||
leftSidebarVisible: boolean,
|
||||
bugDialogVisible: boolean,
|
||||
pluginManagerVisible: boolean,
|
||||
selectedDeviceIndex: number,
|
||||
selectedApp: ?string,
|
||||
toggleBugDialogVisible: (visible?: boolean) => void,
|
||||
};
|
||||
|
||||
@@ -67,6 +65,7 @@ export class App extends React.Component<Props, State> {
|
||||
setupEnvironment();
|
||||
this.logger = new Logger();
|
||||
replaceGlobalConsole(this.logger);
|
||||
this.server = this.initServer();
|
||||
|
||||
this.state = {
|
||||
activeAppKey: null,
|
||||
@@ -74,14 +73,13 @@ export class App extends React.Component<Props, State> {
|
||||
error: null,
|
||||
devices: {},
|
||||
plugins: {},
|
||||
server: this.initServer(),
|
||||
};
|
||||
|
||||
this.bugReporter = new BugReporter(this.logger);
|
||||
this.commandLineArgs = yargs.parse(electron.remote.process.argv);
|
||||
setupMenu(this.sendKeyboardAction);
|
||||
}
|
||||
|
||||
server: Server;
|
||||
bugReporter: BugReporter;
|
||||
logger: Logger;
|
||||
commandLineArgs: Object;
|
||||
@@ -92,40 +90,35 @@ export class App extends React.Component<Props, State> {
|
||||
|
||||
// close socket before reloading
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.state.server.close();
|
||||
this.server.close();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.devices !== this.props.devices) {
|
||||
this.ensurePluginSelected();
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return null;
|
||||
}
|
||||
|
||||
initServer(): Server {
|
||||
const server = new Server(this);
|
||||
|
||||
server.addListener('new-client', client => {
|
||||
client.addListener('close', () => {
|
||||
this.setState(state =>
|
||||
reducers.TeardownClient(this, state, {appKey: client.id}),
|
||||
);
|
||||
this.setState(state => {
|
||||
this.forceUpdate();
|
||||
// TODO:
|
||||
//reducers.TeardownClient(this, state, {appKey: client.id}),
|
||||
});
|
||||
if (this.state.activeAppKey === client.id) {
|
||||
setTimeout(this.ensurePluginSelected);
|
||||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
client.addListener('plugins-change', () => {
|
||||
this.setState({}, this.ensurePluginSelected);
|
||||
this.forceUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
server.addListener('clients-change', () => {
|
||||
this.setState({}, this.ensurePluginSelected);
|
||||
this.forceUpdate();
|
||||
});
|
||||
|
||||
server.addListener('error', err => {
|
||||
@@ -178,143 +171,21 @@ export class App extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
sendKeyboardAction = (action: string) => {
|
||||
const {activeAppKey, activePluginKey} = this.state;
|
||||
|
||||
if (activeAppKey != null && activePluginKey != null) {
|
||||
const clientPlugins = this.state.plugins[activeAppKey];
|
||||
const pluginInfo = clientPlugins && clientPlugins[activePluginKey];
|
||||
const plugin = pluginInfo && pluginInfo.plugin;
|
||||
if (plugin && typeof plugin.onKeyboardAction === 'function') {
|
||||
plugin.onKeyboardAction(action);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getDevice = (id: string): ?BaseDevice => {
|
||||
getDevice = (id: string): ?BaseDevice =>
|
||||
this.props.devices.find((device: BaseDevice) => device.serial === id);
|
||||
};
|
||||
|
||||
ensurePluginSelected = () => {
|
||||
// check if we need to rehydrate this client as it may have been previously active
|
||||
const {activeAppKey, activePluginKey, server} = this.state;
|
||||
const {devices} = this.props;
|
||||
|
||||
if (!this._hasActivatedPreferredPlugin) {
|
||||
for (const connection of server.connections.values()) {
|
||||
const {client} = connection;
|
||||
const {plugins} = client;
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (plugin !== this.commandLineArgs.plugin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._hasActivatedPreferredPlugin = true;
|
||||
this.onActivatePlugin(client.id, plugin);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (devices.length > 0) {
|
||||
const device = devices[0];
|
||||
for (const plugin of devicePlugins) {
|
||||
if (plugin.id !== this.commandLineArgs.plugin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._hasActivatedPreferredPlugin = true;
|
||||
this.onActivatePlugin(device.serial, plugin.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeAppKey != null && activePluginKey != null) {
|
||||
const client = this.getClient(activeAppKey);
|
||||
if (client != null && client.plugins.includes(activePluginKey)) {
|
||||
this.onActivatePlugin(client.id, activePluginKey);
|
||||
return;
|
||||
}
|
||||
|
||||
const device: ?BaseDevice = this.getDevice(activeAppKey);
|
||||
if (device != null) {
|
||||
this.onActivatePlugin(device.serial, activePluginKey);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// No plugin selected, let's select one
|
||||
const deviceList = ((Object.values(devices): any): Array<BaseDevice>);
|
||||
if (deviceList.length > 0) {
|
||||
const device = deviceList[0];
|
||||
this.onActivatePlugin(device.serial, devicePlugins[0].id);
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = Array.from(server.connections.values());
|
||||
if (connections.length > 0) {
|
||||
const client = connections[0].client;
|
||||
const plugins = client.plugins;
|
||||
if (plugins.length > 0) {
|
||||
this.onActivatePlugin(client.id, client.plugins[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getClient(appKey: ?string): ?Client {
|
||||
if (appKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = this.state.server.connections.get(appKey);
|
||||
const info = this.server.connections.get(appKey);
|
||||
if (info != null) {
|
||||
return info.client;
|
||||
}
|
||||
}
|
||||
|
||||
onActivatePlugin = (appKey: string, pluginKey: string) => {
|
||||
activateMenuItems(pluginKey);
|
||||
|
||||
this.setState(state =>
|
||||
reducers.ActivatePlugin(this, state, {
|
||||
appKey,
|
||||
pluginKey,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {state} = this;
|
||||
const hasDevices =
|
||||
this.props.devices.length > 0 || state.server.connections.size > 0;
|
||||
let mainView = null;
|
||||
|
||||
const {activeAppKey, activePluginKey} = state;
|
||||
if (activeAppKey != null && activePluginKey != null) {
|
||||
const clientPlugins = state.plugins[activeAppKey];
|
||||
const pluginInfo = clientPlugins && clientPlugins[activePluginKey];
|
||||
const plugin = pluginInfo && pluginInfo.plugin;
|
||||
if (plugin) {
|
||||
mainView = this.props.pluginManagerVisible ? (
|
||||
<PluginManager />
|
||||
) : (
|
||||
<ErrorBoundary
|
||||
heading={`Plugin "${
|
||||
plugin.constructor.title
|
||||
}" encountered an error during render`}
|
||||
logger={this.logger}>
|
||||
<PluginContainer
|
||||
logger={this.logger}
|
||||
plugin={plugin}
|
||||
state={plugin.state}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FlexColumn fill={true}>
|
||||
<SonarTitleBar />
|
||||
@@ -324,25 +195,26 @@ export class App extends React.Component<Props, State> {
|
||||
close={() => this.props.toggleBugDialogVisible(false)}
|
||||
/>
|
||||
)}
|
||||
{hasDevices ? (
|
||||
{this.props.selectedDeviceIndex > -1 ? (
|
||||
<FlexRow fill={true}>
|
||||
{this.props.leftSidebarVisible && (
|
||||
<MainSidebar
|
||||
activePluginKey={state.activePluginKey}
|
||||
activeAppKey={state.activeAppKey}
|
||||
devices={this.props.devices}
|
||||
server={state.server}
|
||||
onActivatePlugin={this.onActivatePlugin}
|
||||
clients={Array.from(this.server.connections.values()).map(
|
||||
({client}) => client,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{mainView}
|
||||
<PluginContainer
|
||||
logger={this.logger}
|
||||
client={this.getClient(this.props.selectedApp)}
|
||||
/>
|
||||
</FlexRow>
|
||||
) : this.props.pluginManagerVisible ? (
|
||||
<PluginManager />
|
||||
) : (
|
||||
<WelcomeScreen />
|
||||
)}
|
||||
<ErrorBar text={state.error} />
|
||||
<ErrorBar text={this.state.error} />
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
@@ -351,12 +223,14 @@ export class App extends React.Component<Props, State> {
|
||||
export default connect(
|
||||
({
|
||||
application: {pluginManagerVisible, bugDialogVisible, leftSidebarVisible},
|
||||
devices,
|
||||
connections: {devices, selectedDeviceIndex, selectedApp},
|
||||
}) => ({
|
||||
pluginManagerVisible,
|
||||
bugDialogVisible,
|
||||
leftSidebarVisible,
|
||||
devices,
|
||||
selectedDeviceIndex,
|
||||
selectedApp,
|
||||
}),
|
||||
{toggleBugDialogVisible},
|
||||
)(App);
|
||||
|
||||
@@ -60,7 +60,16 @@ export type KeyboardActions = Array<DefaultKeyboardAction | KeyboardAction>;
|
||||
|
||||
const menuItems: Map<string, Object> = new Map();
|
||||
|
||||
export function setupMenu(actionHandler: (action: string) => void) {
|
||||
let pluginActionHandler;
|
||||
function actionHandler(action: string) {
|
||||
if (pluginActionHandler) {
|
||||
pluginActionHandler(action);
|
||||
} else {
|
||||
console.warn(`Unhandled keybaord action "${action}".`);
|
||||
}
|
||||
}
|
||||
|
||||
export function setupMenuBar() {
|
||||
const template = getTemplate(electron.remote.app, electron.remote.shell);
|
||||
|
||||
// collect all keyboard actions from all plugins
|
||||
@@ -126,20 +135,20 @@ function appendMenuItem(
|
||||
}
|
||||
}
|
||||
|
||||
export function activateMenuItems(activePluginKey: ?string) {
|
||||
const activePlugin: ?Class<SonarBasePlugin<>> = [
|
||||
...devicePlugins,
|
||||
...plugins,
|
||||
].find((plugin: Class<SonarBasePlugin<>>) => plugin.id === activePluginKey);
|
||||
|
||||
export function activateMenuItems(activePlugin: SonarBasePlugin<>) {
|
||||
// disable all keyboard actions
|
||||
for (const item of menuItems) {
|
||||
item[1].enabled = false;
|
||||
}
|
||||
|
||||
// set plugin action handler
|
||||
if (activePlugin.onKeyboardAction) {
|
||||
pluginActionHandler = activePlugin.onKeyboardAction;
|
||||
}
|
||||
|
||||
// enable keyboard actions for the current plugin
|
||||
if (activePlugin != null && activePlugin.keyboardActions != null) {
|
||||
(activePlugin.keyboardActions || []).forEach(keyboardAction => {
|
||||
if (activePlugin.constructor.keyboardActions != null) {
|
||||
(activePlugin.constructor.keyboardActions || []).forEach(keyboardAction => {
|
||||
const action =
|
||||
typeof keyboardAction === 'string'
|
||||
? keyboardAction
|
||||
|
||||
@@ -4,125 +4,172 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
import {Component, FlexColumn, Sidebar, colors} from 'sonar';
|
||||
import Intro from './ui/components/intro/intro.js';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
toggleRightSidebarAvailable,
|
||||
toggleRightSidebarVisible,
|
||||
} from './reducers/application.js';
|
||||
import type {SonarBasePlugin} from './plugin.js';
|
||||
import type {SonarPlugin, SonarBasePlugin} from './plugin.js';
|
||||
import type LogManager from './fb-stubs/Logger';
|
||||
import type Client from './Client.js';
|
||||
import type BaseDevice from './devices/BaseDevice.js';
|
||||
import type {Props as PluginProps} from './plugin.js';
|
||||
|
||||
type Props = {
|
||||
plugin: SonarBasePlugin<>,
|
||||
state?: any,
|
||||
logger: LogManager,
|
||||
rightSidebarVisible: boolean,
|
||||
rightSidebarAvailable: boolean,
|
||||
toggleRightSidebarVisible: (available: ?boolean) => void,
|
||||
toggleRightSidebarAvailable: (available: ?boolean) => void,
|
||||
};
|
||||
import {SonarDevicePlugin} from './plugin.js';
|
||||
import {ErrorBoundary, Component, FlexColumn, FlexRow, colors} from 'sonar';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {setPluginState} from './reducers/pluginStates.js';
|
||||
import {devicePlugins} from './device-plugins/index.js';
|
||||
import plugins from './plugins/index.js';
|
||||
import {activateMenuItems} from './MenuBar.js';
|
||||
|
||||
type State = {
|
||||
showIntro: boolean,
|
||||
};
|
||||
|
||||
class PluginContainer extends Component<Props, State> {
|
||||
state = {
|
||||
showIntro:
|
||||
typeof this.props.plugin.renderIntro === 'function' &&
|
||||
window.localStorage.getItem(
|
||||
`${this.props.plugin.constructor.id}.introShown`,
|
||||
) !== 'true',
|
||||
};
|
||||
|
||||
_sidebar: ?React$Node;
|
||||
|
||||
static Container = FlexColumn.extends({
|
||||
const Container = FlexColumn.extends({
|
||||
width: 0,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
backgroundColor: colors.white,
|
||||
});
|
||||
|
||||
componentWillUnmount() {
|
||||
performance.mark(`init_${this.props.plugin.constructor.id}`);
|
||||
}
|
||||
const SidebarContainer = FlexRow.extends({
|
||||
backgroundColor: colors.light02,
|
||||
height: '100%',
|
||||
overflow: 'scroll',
|
||||
});
|
||||
|
||||
type Props = {
|
||||
logger: LogManager,
|
||||
selectedDeviceIndex: number,
|
||||
selectedPlugin: ?string,
|
||||
pluginStates: Object,
|
||||
client: ?Client,
|
||||
devices: Array<BaseDevice>,
|
||||
setPluginState: (payload: {
|
||||
pluginKey: string,
|
||||
state: Object,
|
||||
}) => void,
|
||||
};
|
||||
|
||||
type State = {
|
||||
activePlugin: ?Class<SonarBasePlugin<>>,
|
||||
target: Client | BaseDevice | null,
|
||||
pluginKey: string,
|
||||
};
|
||||
|
||||
function withPluginLifecycleHooks(
|
||||
PluginComponent: Class<SonarBasePlugin<>>,
|
||||
target: Client | BaseDevice,
|
||||
) {
|
||||
return class extends React.Component<PluginProps<any>> {
|
||||
plugin: ?SonarBasePlugin<>;
|
||||
|
||||
static displayName = `${PluginComponent.title}Plugin`;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.logger.trackTimeSince(
|
||||
`init_${this.props.plugin.constructor.id}`,
|
||||
const {plugin} = this;
|
||||
if (plugin) {
|
||||
activateMenuItems(plugin);
|
||||
plugin._setup(target);
|
||||
plugin._init();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.plugin) {
|
||||
this.plugin._teardown();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PluginComponent
|
||||
ref={(ref: ?SonarBasePlugin<>) => {
|
||||
if (ref) {
|
||||
this.plugin = ref;
|
||||
}
|
||||
}}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.plugin !== this.props.plugin) {
|
||||
this.props.logger.trackTimeSince(
|
||||
`init_${this.props.plugin.constructor.id}`,
|
||||
class PluginContainer extends Component<Props, State> {
|
||||
static getDerivedStateFromProps(props: Props) {
|
||||
let activePlugin = devicePlugins.find(
|
||||
(p: Class<SonarDevicePlugin<>>) => p.id === props.selectedPlugin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps: Props) {
|
||||
if (this.props.plugin !== nextProps.plugin) {
|
||||
performance.mark(`init_${nextProps.plugin.constructor.id}`);
|
||||
}
|
||||
let sidebarContent;
|
||||
if (typeof nextProps.plugin.renderSidebar === 'function') {
|
||||
sidebarContent = nextProps.plugin.renderSidebar();
|
||||
}
|
||||
|
||||
if (sidebarContent == null) {
|
||||
this._sidebar = null;
|
||||
nextProps.toggleRightSidebarAvailable(false);
|
||||
const device: BaseDevice = props.devices[props.selectedDeviceIndex];
|
||||
let target = device;
|
||||
let pluginKey = 'unknown';
|
||||
if (activePlugin) {
|
||||
pluginKey = `${device.serial}#${activePlugin.id}`;
|
||||
} else {
|
||||
this._sidebar = (
|
||||
<Sidebar position="right" width={400} key="sidebar">
|
||||
{sidebarContent}
|
||||
</Sidebar>
|
||||
activePlugin = plugins.find(
|
||||
(p: Class<SonarPlugin<>>) => p.id === props.selectedPlugin,
|
||||
);
|
||||
nextProps.toggleRightSidebarAvailable(true);
|
||||
if (!activePlugin || !props.client) {
|
||||
return null;
|
||||
}
|
||||
target = props.client;
|
||||
pluginKey = `${target.id}#${activePlugin.id}`;
|
||||
}
|
||||
|
||||
onDismissIntro = () => {
|
||||
const {plugin} = this.props;
|
||||
window.localStorage.setItem(`${plugin.constructor.id}.introShown`, 'true');
|
||||
this.setState({
|
||||
showIntro: false,
|
||||
});
|
||||
return {
|
||||
pluginKey,
|
||||
activePlugin,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
pluginKey: 'unknown',
|
||||
activePlugin: null,
|
||||
target: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {plugin} = this.props;
|
||||
const {pluginStates, setPluginState} = this.props;
|
||||
const {activePlugin, pluginKey, target} = this.state;
|
||||
|
||||
return [
|
||||
<PluginContainer.Container key="plugin">
|
||||
{this.state.showIntro ? (
|
||||
<Intro
|
||||
title={plugin.constructor.title}
|
||||
icon={plugin.constructor.icon}
|
||||
screenshot={plugin.constructor.screenshot}
|
||||
onDismiss={this.onDismissIntro}>
|
||||
{typeof plugin.renderIntro === 'function' && plugin.renderIntro()}
|
||||
</Intro>
|
||||
) : (
|
||||
plugin.render()
|
||||
if (!activePlugin || !target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
// $FlowFixMe: Flow doesn't know of React.Fragment yet
|
||||
<React.Fragment>
|
||||
<Container key="plugin">
|
||||
<ErrorBoundary
|
||||
heading={`Plugin "${
|
||||
activePlugin.title
|
||||
}" encountered an error during render`}
|
||||
logger={this.props.logger}>
|
||||
{React.createElement(
|
||||
withPluginLifecycleHooks(activePlugin, target),
|
||||
{
|
||||
key: pluginKey,
|
||||
logger: this.props.logger,
|
||||
persistedState: pluginStates[pluginKey],
|
||||
setPersistedState: state => setPluginState({pluginKey, state}),
|
||||
},
|
||||
)}
|
||||
</PluginContainer.Container>,
|
||||
this.props.rightSidebarVisible === false ? null : this._sidebar,
|
||||
];
|
||||
</ErrorBoundary>
|
||||
</Container>
|
||||
<SidebarContainer id="sonarSidebar" />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({application: {rightSidebarVisible, rightSidebarAvailable}}) => ({
|
||||
rightSidebarVisible,
|
||||
rightSidebarAvailable,
|
||||
({
|
||||
application: {rightSidebarVisible, rightSidebarAvailable},
|
||||
connections: {selectedPlugin, devices, selectedDeviceIndex},
|
||||
pluginStates,
|
||||
}) => ({
|
||||
selectedPlugin,
|
||||
devices,
|
||||
selectedDeviceIndex,
|
||||
pluginStates,
|
||||
}),
|
||||
{
|
||||
toggleRightSidebarAvailable,
|
||||
toggleRightSidebarVisible,
|
||||
setPluginState,
|
||||
},
|
||||
)(PluginContainer);
|
||||
|
||||
@@ -5,293 +5,96 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Component, styled, Glyph, Button, colors} from 'sonar';
|
||||
import {Component, Button} from 'sonar';
|
||||
import {connect} from 'react-redux';
|
||||
import BaseDevice from '../devices/BaseDevice.js';
|
||||
import child_process from 'child_process';
|
||||
import DevicesList from './DevicesList.js';
|
||||
import {exec} from 'child_process';
|
||||
import {selectDevice} from '../reducers/connections.js';
|
||||
import type BaseDevice from '../devices/BaseDevice.js';
|
||||
|
||||
const adb = require('adbkit-fb');
|
||||
|
||||
const Light = styled.view(
|
||||
{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '999em',
|
||||
backgroundColor: props => (props.active ? '#70f754' : colors.light20),
|
||||
border: props => `1px solid ${props.active ? '#52d936' : colors.light30}`,
|
||||
},
|
||||
{
|
||||
ignoreAttributes: ['active'],
|
||||
},
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
type Props = {
|
||||
selectedDeviceIndex: number,
|
||||
androidEmulators: Array<string>,
|
||||
devices: Array<BaseDevice>,
|
||||
|};
|
||||
|
||||
type Emulator = {|
|
||||
name: string,
|
||||
os?: string,
|
||||
isRunning: boolean,
|
||||
|};
|
||||
|
||||
type State = {
|
||||
androidEmulators: Array<Emulator>,
|
||||
iOSSimulators: Array<Emulator>,
|
||||
popoverVisible: boolean,
|
||||
selectDevice: (i: number) => void,
|
||||
};
|
||||
|
||||
type IOSSimulatorList = {
|
||||
devices: {
|
||||
[os: string]: Array<{
|
||||
state: 'Shutdown' | 'Booted',
|
||||
availability: string,
|
||||
name: string,
|
||||
udid: string,
|
||||
os?: string,
|
||||
}>,
|
||||
},
|
||||
};
|
||||
|
||||
class DevicesButton extends Component<Props, State> {
|
||||
state = {
|
||||
androidEmulators: [],
|
||||
iOSSimulators: [],
|
||||
popoverVisible: false,
|
||||
};
|
||||
|
||||
client = adb.createClient();
|
||||
_iOSSimulatorRefreshInterval: ?number;
|
||||
|
||||
componentDidMount() {
|
||||
this.updateEmulatorState(this.openMenuWhenNoDevicesConnected);
|
||||
this.fetchIOSSimulators();
|
||||
this._iOSSimulatorRefreshInterval = window.setInterval(
|
||||
this.fetchIOSSimulators,
|
||||
5000,
|
||||
);
|
||||
|
||||
this.client.trackDevices().then(tracker => {
|
||||
tracker.on('add', () => this.updateEmulatorState());
|
||||
tracker.on('remove', () => this.updateEmulatorState());
|
||||
tracker.on('end', () => this.updateEmulatorState());
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._iOSSimulatorRefreshInterval != null) {
|
||||
window.clearInterval(this._iOSSimulatorRefreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
fetchIOSSimulators = () => {
|
||||
child_process.exec(
|
||||
'xcrun simctl list devices --json',
|
||||
(err: ?Error, data: ?string) => {
|
||||
if (data != null && err == null) {
|
||||
const devicesList: IOSSimulatorList = JSON.parse(data);
|
||||
const iOSSimulators = Object.keys(devicesList.devices)
|
||||
.map(os =>
|
||||
devicesList.devices[os].map(device => {
|
||||
device.os = os;
|
||||
return device;
|
||||
}),
|
||||
)
|
||||
.reduce((acc, cv) => acc.concat(cv), [])
|
||||
.filter(device => device.state === 'Booted')
|
||||
.map(device => ({
|
||||
name: device.name,
|
||||
os: device.os,
|
||||
isRunning: true,
|
||||
}));
|
||||
this.setState({iOSSimulators});
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
openMenuWhenNoDevicesConnected = () => {
|
||||
const numberOfEmulators = this.state.androidEmulators.filter(
|
||||
e => e.isRunning,
|
||||
).length;
|
||||
const numberOfDevices = Object.values(this.props.devices).length;
|
||||
if (numberOfEmulators + numberOfDevices === 0) {
|
||||
this.setState({popoverVisible: true});
|
||||
}
|
||||
};
|
||||
|
||||
updateEmulatorState = async (cb?: Function) => {
|
||||
try {
|
||||
const devices = await this.getEmulatorNames();
|
||||
const ports = await this.getRunningEmulatorPorts();
|
||||
const runningDevices = await Promise.all(
|
||||
ports.map(port => this.getRunningName(port)),
|
||||
);
|
||||
this.setState(
|
||||
{
|
||||
androidEmulators: devices.map(name => ({
|
||||
name,
|
||||
isRunning: runningDevices.indexOf(name) > -1,
|
||||
})),
|
||||
},
|
||||
cb,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
getEmulatorNames(): Promise<Array<string>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
child_process.exec(
|
||||
'$ANDROID_HOME/tools/emulator -list-avds',
|
||||
(error: ?Error, data: ?string) => {
|
||||
if (error == null && data != null) {
|
||||
resolve(data.split('\n').filter(name => name !== ''));
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getRunningEmulatorPorts(): Promise<Array<string>> {
|
||||
const EMULATOR_PREFIX = 'emulator-';
|
||||
return adb
|
||||
.createClient()
|
||||
.listDevices()
|
||||
.then((devices: Array<{id: string}>) =>
|
||||
devices
|
||||
.filter(d => d.id.startsWith(EMULATOR_PREFIX))
|
||||
.map(d => d.id.replace(EMULATOR_PREFIX, '')),
|
||||
)
|
||||
.catch((e: Error) => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
getRunningName(port: string): Promise<?string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
child_process.exec(
|
||||
`echo "avd name" | nc -w 1 localhost ${port}`,
|
||||
(error: ?Error, data: ?string) => {
|
||||
if (error == null && data != null) {
|
||||
const match = data.trim().match(/(.*)\r\nOK$/);
|
||||
resolve(match != null && match.length > 0 ? match[1] : null);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class DevicesButton extends Component<Props> {
|
||||
launchEmulator = (name: string) => {
|
||||
if (/^[a-zA-Z0-9-_\s]+$/.test(name)) {
|
||||
child_process.exec(
|
||||
`$ANDROID_HOME/tools/emulator -avd "${name}"`,
|
||||
this.updateEmulatorState,
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`Can not launch emulator named ${name}, because it's name contains invalid characters.`,
|
||||
);
|
||||
exec(`$ANDROID_HOME/tools/emulator @${name}`, error => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
createEmualtor = () => {};
|
||||
|
||||
onClick = () => {
|
||||
this.setState({popoverVisible: !this.state.popoverVisible});
|
||||
this.updateEmulatorState();
|
||||
this.fetchIOSSimulators();
|
||||
};
|
||||
|
||||
onDismissPopover = () => {
|
||||
this.setState({popoverVisible: false});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
let text = 'No devices running';
|
||||
let glyph = 'minus-circle';
|
||||
const {
|
||||
devices,
|
||||
androidEmulators,
|
||||
selectedDeviceIndex,
|
||||
selectDevice,
|
||||
} = this.props;
|
||||
let text = 'No device selected';
|
||||
let icon = 'minus-circle';
|
||||
|
||||
const runnningEmulators = this.state.androidEmulators.filter(
|
||||
emulator => emulator.isRunning,
|
||||
);
|
||||
|
||||
const numberOfRunningDevices =
|
||||
runnningEmulators.length + this.state.iOSSimulators.length;
|
||||
|
||||
if (numberOfRunningDevices > 0) {
|
||||
text = `${numberOfRunningDevices} device${
|
||||
numberOfRunningDevices > 1 ? 's' : ''
|
||||
} running`;
|
||||
glyph = 'mobile';
|
||||
if (selectedDeviceIndex > -1) {
|
||||
text = devices[selectedDeviceIndex].title;
|
||||
icon = 'mobile';
|
||||
}
|
||||
|
||||
const connectedDevices = this.props.devices;
|
||||
const dropdown = [];
|
||||
|
||||
if (devices.length > 0) {
|
||||
dropdown.push(
|
||||
{
|
||||
label: 'Running devices',
|
||||
enabled: false,
|
||||
},
|
||||
...devices.map((device: BaseDevice, i: number) => ({
|
||||
click: () => selectDevice(i),
|
||||
checked: i === selectedDeviceIndex,
|
||||
label: `${device.deviceType === 'physical' ? '📱 ' : ''}${
|
||||
device.title
|
||||
}`,
|
||||
type: 'checkbox',
|
||||
})),
|
||||
);
|
||||
}
|
||||
if (androidEmulators.length > 0) {
|
||||
const emulators = Array.from(androidEmulators)
|
||||
.filter(
|
||||
(name: string) =>
|
||||
devices.findIndex((device: BaseDevice) => device.title === name) ===
|
||||
-1,
|
||||
)
|
||||
.map((name: string) => ({
|
||||
label: name,
|
||||
click: () => this.launchEmulator(name),
|
||||
}));
|
||||
|
||||
if (emulators.length > 0) {
|
||||
dropdown.push(
|
||||
{type: 'separator'},
|
||||
{
|
||||
label: 'Launch Android emulators',
|
||||
enabled: false,
|
||||
},
|
||||
...emulators,
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
compact={true}
|
||||
onClick={this.onClick}
|
||||
icon={glyph}
|
||||
disabled={this.state.androidEmulators.length === 0}>
|
||||
<Button compact={true} icon={icon} dropdown={dropdown} disabled={false}>
|
||||
{text}
|
||||
{this.state.popoverVisible && (
|
||||
<DevicesList
|
||||
onDismiss={this.onDismissPopover}
|
||||
sections={[
|
||||
{
|
||||
title: 'Running',
|
||||
items: [
|
||||
...connectedDevices
|
||||
.filter(device => device.deviceType === 'physical')
|
||||
.map(device => ({
|
||||
title: device.title,
|
||||
subtitle: device.os,
|
||||
icon: <Light active={true} />,
|
||||
})),
|
||||
...runnningEmulators.map(emulator => ({
|
||||
title: emulator.name,
|
||||
subtitle: 'Android Emulator',
|
||||
icon: <Light active={true} />,
|
||||
})),
|
||||
...this.state.iOSSimulators.map(simulator => ({
|
||||
title: simulator.name,
|
||||
subtitle: `${String(simulator.os)} Simulator`,
|
||||
icon: <Light active={true} />,
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Not Running',
|
||||
items: [
|
||||
...this.state.androidEmulators
|
||||
.filter(emulator => !emulator.isRunning)
|
||||
.map(emulator => ({
|
||||
title: emulator.name,
|
||||
subtitle: 'Android Emulator',
|
||||
onClick: () => this.launchEmulator(emulator.name),
|
||||
icon: <Light active={false} />,
|
||||
})),
|
||||
{
|
||||
title: 'Connect a device',
|
||||
subtitle: 'Plugins will load automatically',
|
||||
icon: <Glyph name="mobile" size={12} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({devices}) => ({
|
||||
export default connect(
|
||||
({connections: {devices, androidEmulators, selectedDeviceIndex}}) => ({
|
||||
devices,
|
||||
}))(DevicesButton);
|
||||
androidEmulators,
|
||||
selectedDeviceIndex,
|
||||
}),
|
||||
{selectDevice},
|
||||
)(DevicesButton);
|
||||
|
||||
@@ -5,24 +5,29 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {SonarBasePlugin} from '../plugin.js';
|
||||
import type {
|
||||
SonarPlugin,
|
||||
SonarDevicePlugin,
|
||||
SonarBasePlugin,
|
||||
} from '../plugin.js';
|
||||
import type BaseDevice from '../devices/BaseDevice.js';
|
||||
import type Client from '../Client.js';
|
||||
|
||||
import {
|
||||
Component,
|
||||
Sidebar,
|
||||
FlexBox,
|
||||
ClickableList,
|
||||
ClickableListItem,
|
||||
colors,
|
||||
brandColors,
|
||||
Text,
|
||||
Glyph,
|
||||
} from 'sonar';
|
||||
import React from 'react';
|
||||
import {devicePlugins} from '../device-plugins/index.js';
|
||||
import type BaseDevice from '../devices/BaseDevice.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import plugins from '../plugins/index.js';
|
||||
import {selectPlugin} from '../reducers/connections.js';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
const CustomClickableListItem = ClickableListItem.extends({
|
||||
paddingLeft: 10,
|
||||
@@ -85,29 +90,17 @@ function PluginIcon({
|
||||
}
|
||||
|
||||
class PluginSidebarListItem extends Component<{
|
||||
activePluginKey: ?string,
|
||||
activeAppKey: ?string,
|
||||
onActivatePlugin: (appKey: string, pluginKey: string) => void,
|
||||
appKey: string,
|
||||
appName?: string,
|
||||
onClick: () => void,
|
||||
isActive: boolean,
|
||||
Plugin: Class<SonarBasePlugin<>>,
|
||||
windowFocused: boolean,
|
||||
plugin: Class<SonarBasePlugin<>>,
|
||||
app?: ?string,
|
||||
}> {
|
||||
onClick = () => {
|
||||
const {props} = this;
|
||||
props.onActivatePlugin(props.appKey, props.Plugin.id);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isActive, Plugin, windowFocused, appKey, appName} = this.props;
|
||||
const {isActive, plugin} = this.props;
|
||||
const app = this.props.app || 'Facebook';
|
||||
let iconColor = brandColors[app];
|
||||
|
||||
let iconColor;
|
||||
if (appName != null) {
|
||||
iconColor = brandColors[appName];
|
||||
}
|
||||
|
||||
if (iconColor == null) {
|
||||
if (!iconColor) {
|
||||
const pluginColors = [
|
||||
colors.seaFoam,
|
||||
colors.teal,
|
||||
@@ -120,186 +113,121 @@ class PluginSidebarListItem extends Component<{
|
||||
colors.grape,
|
||||
];
|
||||
|
||||
iconColor = pluginColors[parseInt(appKey, 36) % pluginColors.length];
|
||||
iconColor = pluginColors[parseInt(app, 36) % pluginColors.length];
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomClickableListItem
|
||||
active={isActive}
|
||||
onClick={this.onClick}
|
||||
windowFocused={windowFocused}>
|
||||
<CustomClickableListItem active={isActive} onClick={this.props.onClick}>
|
||||
<PluginIcon
|
||||
name={Plugin.icon}
|
||||
backgroundColor={
|
||||
isActive
|
||||
? windowFocused
|
||||
? colors.white
|
||||
: colors.macOSSidebarSectionTitle
|
||||
: iconColor
|
||||
}
|
||||
color={
|
||||
isActive
|
||||
? windowFocused
|
||||
? colors.macOSTitleBarIconSelected
|
||||
: colors.white
|
||||
: colors.white
|
||||
}
|
||||
name={plugin.icon}
|
||||
backgroundColor={iconColor}
|
||||
color={colors.white}
|
||||
/>
|
||||
<PluginName>{Plugin.title}</PluginName>
|
||||
<PluginName>{plugin.title}</PluginName>
|
||||
</CustomClickableListItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function PluginSidebarList(props: {|
|
||||
activePluginKey: ?string,
|
||||
activeAppKey: ?string,
|
||||
onActivatePlugin: (appKey: string, pluginKey: string) => void,
|
||||
appKey: string,
|
||||
appName?: string,
|
||||
enabledPlugins: Array<Class<SonarBasePlugin<>>>,
|
||||
windowFocused: boolean,
|
||||
|}) {
|
||||
if (props.enabledPlugins.length === 0) {
|
||||
return <Text>No available plugins for this device</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClickableList>
|
||||
{props.enabledPlugins.map(Plugin => {
|
||||
const isActive =
|
||||
props.activeAppKey === props.appKey &&
|
||||
props.activePluginKey === Plugin.id;
|
||||
return (
|
||||
<PluginSidebarListItem
|
||||
key={Plugin.id}
|
||||
activePluginKey={props.activePluginKey}
|
||||
activeAppKey={props.activeAppKey}
|
||||
onActivatePlugin={props.onActivatePlugin}
|
||||
appKey={props.appKey}
|
||||
appName={props.appName}
|
||||
isActive={isActive}
|
||||
Plugin={Plugin}
|
||||
windowFocused={props.windowFocused}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ClickableList>
|
||||
);
|
||||
}
|
||||
|
||||
function AppSidebarInfo(props: {|
|
||||
client: Client,
|
||||
appKey: string,
|
||||
activePluginKey: ?string,
|
||||
activeAppKey: ?string,
|
||||
onActivatePlugin: (appKey: string, pluginKey: string) => void,
|
||||
windowFocused: boolean,
|
||||
|}): any {
|
||||
const {appKey, client, windowFocused} = props;
|
||||
|
||||
let enabledPlugins = [];
|
||||
for (const Plugin of plugins) {
|
||||
if (client.supportsPlugin(Plugin)) {
|
||||
enabledPlugins.push(Plugin);
|
||||
}
|
||||
}
|
||||
enabledPlugins = enabledPlugins.sort((a, b) => {
|
||||
return (a.title || '').localeCompare(b.title);
|
||||
});
|
||||
|
||||
return [
|
||||
<SidebarHeader key={client.query.app}>{`${client.query.app} (${
|
||||
client.query.os
|
||||
}) - ${client.query.device}`}</SidebarHeader>,
|
||||
<PluginSidebarList
|
||||
key={`list-${appKey}`}
|
||||
activePluginKey={props.activePluginKey}
|
||||
activeAppKey={props.activeAppKey}
|
||||
onActivatePlugin={props.onActivatePlugin}
|
||||
appKey={appKey}
|
||||
appName={client.query.app}
|
||||
enabledPlugins={enabledPlugins}
|
||||
windowFocused={windowFocused}
|
||||
/>,
|
||||
];
|
||||
}
|
||||
|
||||
type MainSidebarProps = {|
|
||||
activePluginKey: ?string,
|
||||
activeAppKey: ?string,
|
||||
onActivatePlugin: (appKey: string, pluginKey: string) => void,
|
||||
selectedPlugin: ?string,
|
||||
selectedApp: ?string,
|
||||
selectedDeviceIndex: number,
|
||||
selectPlugin: (payload: {
|
||||
selectedPlugin: ?string,
|
||||
selectedApp: ?string,
|
||||
}) => void,
|
||||
devices: Array<BaseDevice>,
|
||||
server: Server,
|
||||
clients: Array<Client>,
|
||||
|};
|
||||
|
||||
export default class MainSidebar extends Component<MainSidebarProps> {
|
||||
static contextTypes = {
|
||||
windowIsFocused: PropTypes.bool,
|
||||
};
|
||||
|
||||
class MainSidebar extends Component<MainSidebarProps> {
|
||||
render() {
|
||||
const connections = Array.from(this.props.server.connections.values()).sort(
|
||||
(a, b) => {
|
||||
return (a.client.query.app || '').localeCompare(b.client.query.app);
|
||||
},
|
||||
);
|
||||
const {
|
||||
devices,
|
||||
selectedDeviceIndex,
|
||||
selectedPlugin,
|
||||
selectedApp,
|
||||
selectPlugin,
|
||||
} = this.props;
|
||||
let {clients} = this.props;
|
||||
const device: BaseDevice = devices[selectedDeviceIndex];
|
||||
|
||||
const sidebarContent = connections.map(conn => {
|
||||
const {client} = conn;
|
||||
|
||||
return (
|
||||
<AppSidebarInfo
|
||||
key={`app=${client.id}`}
|
||||
client={client}
|
||||
appKey={client.id}
|
||||
activePluginKey={this.props.activePluginKey}
|
||||
activeAppKey={this.props.activeAppKey}
|
||||
onActivatePlugin={this.props.onActivatePlugin}
|
||||
windowFocused={this.context.windowIsFocused}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
let {devices} = this.props;
|
||||
devices = devices.sort((a, b) => {
|
||||
return (a.title || '').localeCompare(b.title);
|
||||
});
|
||||
|
||||
for (const device of devices) {
|
||||
let enabledPlugins = [];
|
||||
for (const DevicePlugin of devicePlugins) {
|
||||
if (device.supportsPlugin(DevicePlugin)) {
|
||||
enabledPlugins.push(DevicePlugin);
|
||||
for (const devicePlugin of devicePlugins) {
|
||||
if (device.supportsPlugin(devicePlugin)) {
|
||||
enabledPlugins.push(devicePlugin);
|
||||
}
|
||||
}
|
||||
enabledPlugins = enabledPlugins.sort((a, b) => {
|
||||
return (a.title || '').localeCompare(b.title);
|
||||
});
|
||||
|
||||
sidebarContent.unshift([
|
||||
<SidebarHeader key={device.title}>{device.title}</SidebarHeader>,
|
||||
<PluginSidebarList
|
||||
key={`list-${device.serial}`}
|
||||
activePluginKey={this.props.activePluginKey}
|
||||
activeAppKey={this.props.activeAppKey}
|
||||
onActivatePlugin={this.props.onActivatePlugin}
|
||||
appKey={device.serial}
|
||||
enabledPlugins={enabledPlugins}
|
||||
windowFocused={this.context.windowIsFocused}
|
||||
/>,
|
||||
]);
|
||||
}
|
||||
clients = clients
|
||||
// currently we can't filter clients for a device, because all clients
|
||||
// are reporting `unknown` as their deviceID, due to a change in Android's
|
||||
// API.
|
||||
//.filter((client: Client) => client.getDevice() === device)
|
||||
.sort((a, b) => (a.query.app || '').localeCompare(b.query.app));
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
position="left"
|
||||
width={200}
|
||||
backgroundColor={
|
||||
this.context.windowIsFocused ? 'transparent' : '#f6f6f6'
|
||||
}>
|
||||
{sidebarContent}
|
||||
<Sidebar position="left" width={200}>
|
||||
{devicePlugins
|
||||
.filter(device.supportsPlugin)
|
||||
.map((plugin: Class<SonarDevicePlugin<>>) => (
|
||||
<PluginSidebarListItem
|
||||
key={plugin.id}
|
||||
isActive={plugin.id === selectedPlugin}
|
||||
onClick={() =>
|
||||
selectPlugin({
|
||||
selectedPlugin: plugin.id,
|
||||
selectedApp: null,
|
||||
})
|
||||
}
|
||||
plugin={plugin}
|
||||
/>
|
||||
))}
|
||||
{clients.map((client: Client) => (
|
||||
// $FlowFixMe: Flow doesn't know of React.Fragment yet
|
||||
<React.Fragment key={client.id}>
|
||||
<SidebarHeader>{client.query.app}</SidebarHeader>
|
||||
{plugins
|
||||
.filter(
|
||||
(p: Class<SonarPlugin<>>) => client.plugins.indexOf(p.id) > -1,
|
||||
)
|
||||
.map((plugin: Class<SonarPlugin<>>) => (
|
||||
<PluginSidebarListItem
|
||||
key={plugin.id}
|
||||
isActive={
|
||||
plugin.id === selectedPlugin && selectedApp === client.id
|
||||
}
|
||||
onClick={() =>
|
||||
selectPlugin({
|
||||
selectedPlugin: plugin.id,
|
||||
selectedApp: client.id,
|
||||
})
|
||||
}
|
||||
plugin={plugin}
|
||||
app={client.query.app}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({
|
||||
connections: {devices, selectedDeviceIndex, selectedPlugin, selectedApp},
|
||||
}) => ({
|
||||
devices,
|
||||
selectedDeviceIndex,
|
||||
selectedPlugin,
|
||||
selectedApp,
|
||||
}),
|
||||
{
|
||||
selectPlugin,
|
||||
},
|
||||
)(MainSidebar);
|
||||
|
||||
61
src/chrome/SonarSidebar.js
Normal file
61
src/chrome/SonarSidebar.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Sidebar} from 'sonar';
|
||||
import {connect} from 'react-redux';
|
||||
import {toggleRightSidebarAvailable} from '../reducers/application.js';
|
||||
|
||||
type Props = {
|
||||
children: any,
|
||||
rightSidebarVisible: boolean,
|
||||
rightSidebarAvailable: boolean,
|
||||
toggleRightSidebarAvailable: (visible: boolean) => void,
|
||||
};
|
||||
|
||||
class SonarSidebar extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.updateSidebarAvailablility();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateSidebarAvailablility();
|
||||
}
|
||||
|
||||
updateSidebarAvailablility() {
|
||||
const available = Boolean(this.props.children);
|
||||
if (available !== this.props.rightSidebarAvailable) {
|
||||
this.props.toggleRightSidebarAvailable(available);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const domNode = document.getElementById('sonarSidebar');
|
||||
return (
|
||||
this.props.children &&
|
||||
this.props.rightSidebarVisible &&
|
||||
domNode &&
|
||||
ReactDOM.createPortal(
|
||||
<Sidebar width={300} position="right">
|
||||
{this.props.children}
|
||||
</Sidebar>,
|
||||
domNode,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({application: {rightSidebarVisible, rightSidebarAvailable}}) => ({
|
||||
rightSidebarVisible,
|
||||
rightSidebarAvailable,
|
||||
}),
|
||||
{
|
||||
toggleRightSidebarAvailable,
|
||||
},
|
||||
)(SonarSidebar);
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
TableColumnSizes,
|
||||
TableColumns,
|
||||
} from 'sonar';
|
||||
import {FlexColumn, Button} from 'sonar';
|
||||
import {FlexColumn, Button, SonarSidebar} from 'sonar';
|
||||
import textContent from './utils/textContent.js';
|
||||
import createPaste from './utils/createPaste.js';
|
||||
import {SonarPlugin, SearchableTable} from 'sonar';
|
||||
@@ -199,6 +199,7 @@ export function createTablePlugin<T: RowData>(props: Props<T>) {
|
||||
stickyBottom={true}
|
||||
actions={<Button onClick={this.clear}>Clear Table</Button>}
|
||||
/>
|
||||
<SonarSidebar>{this.renderSidebar()}</SonarSidebar>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {SonarDevicePlugin} from '../plugin.js';
|
||||
|
||||
import {GK} from 'sonar';
|
||||
import logs from './logs/index.js';
|
||||
import cpu from './cpu/index.js';
|
||||
import screen from './screen/index.js';
|
||||
|
||||
const plugins = [logs];
|
||||
const plugins: Array<Class<SonarDevicePlugin<any>>> = [logs];
|
||||
|
||||
if (GK.get('sonar_uiperf')) {
|
||||
plugins.push(cpu);
|
||||
|
||||
@@ -121,6 +121,13 @@ export default class ScreenPlugin extends SonarDevicePlugin<State> {
|
||||
device: AndroidDevice;
|
||||
adbClient: AdbClient;
|
||||
|
||||
state = {
|
||||
pullingData: false,
|
||||
recording: false,
|
||||
recordingEnabled: false,
|
||||
capturingScreenshot: false,
|
||||
};
|
||||
|
||||
init() {
|
||||
this.adbClient = this.device.adb;
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import type {DeviceType, DeviceShell, DeviceLogListener} from './BaseDevice.js';
|
||||
|
||||
import {Priority} from 'adbkit-logcat-fb';
|
||||
import child_process from 'child_process';
|
||||
|
||||
// TODO
|
||||
import BaseDevice from './BaseDevice.js';
|
||||
|
||||
type ADBClient = any;
|
||||
@@ -39,6 +37,7 @@ export default class AndroidDevice extends BaseDevice {
|
||||
os = 'Android';
|
||||
adb: ADBClient;
|
||||
pidAppMapping: {[key: number]: string} = {};
|
||||
logReader: any;
|
||||
|
||||
supportedColumns(): Array<string> {
|
||||
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
|
||||
|
||||
@@ -53,9 +53,9 @@ export default class BaseDevice {
|
||||
// possible src of icon to display next to the device title
|
||||
icon: ?string;
|
||||
|
||||
supportsPlugin(DevicePlugin: Class<SonarDevicePlugin<>>) {
|
||||
supportsPlugin = (DevicePlugin: Class<SonarDevicePlugin<>>): boolean => {
|
||||
return this.supportedPlugins.includes(DevicePlugin.id);
|
||||
}
|
||||
};
|
||||
|
||||
// ensure that we don't serialise devices
|
||||
toJSON() {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import AndroidDevice from '../devices/AndroidDevice';
|
||||
import child_process from 'child_process';
|
||||
import type {Store} from '../reducers/index.js';
|
||||
import type BaseDevice from '../devices/BaseDevice';
|
||||
const adb = require('adbkit-fb');
|
||||
@@ -16,30 +17,61 @@ function createDecive(client, device): Promise<AndroidDevice> {
|
||||
device.type !== 'device' || device.id.startsWith('emulator')
|
||||
? 'emulator'
|
||||
: 'physical';
|
||||
client.getProperties(device.id).then(props => {
|
||||
const androidDevice = new AndroidDevice(
|
||||
device.id,
|
||||
type,
|
||||
props['ro.product.model'],
|
||||
client,
|
||||
);
|
||||
|
||||
client.getProperties(device.id).then(async props => {
|
||||
let name = props['ro.product.model'];
|
||||
if (type === 'emulator') {
|
||||
name = (await getRunningEmulatorName(device.id)) || name;
|
||||
}
|
||||
const androidDevice = new AndroidDevice(device.id, type, name, client);
|
||||
androidDevice.reverse();
|
||||
resolve(androidDevice);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getRunningEmulatorName(id: string): Promise<?string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const port = id.replace('emulator-', '');
|
||||
child_process.exec(
|
||||
`echo "avd name" | nc -w 1 localhost ${port}`,
|
||||
(error: ?Error, data: ?string) => {
|
||||
if (error == null && data != null) {
|
||||
const match = data.trim().match(/(.*)\r\nOK$/);
|
||||
resolve(match != null && match.length > 0 ? match[1] : null);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default (store: Store) => {
|
||||
const client = adb.createClient();
|
||||
|
||||
// get emulators
|
||||
child_process.exec(
|
||||
'$ANDROID_HOME/tools/emulator -list-avds',
|
||||
(error: ?Error, data: ?string) => {
|
||||
if (error == null && data != null) {
|
||||
const payload = data.split('\n').filter(Boolean);
|
||||
store.dispatch({
|
||||
type: 'REGISTER_ANDROID_EMULATORS',
|
||||
payload,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
client
|
||||
.trackDevices()
|
||||
.then(tracker => {
|
||||
tracker.on('error', err => {
|
||||
if (err.message === 'Connection closed') {
|
||||
// adb server has shutdown, remove all android devices
|
||||
const {devices} = store.getState();
|
||||
const deviceIDsToRemove: Array<string> = devices
|
||||
const {connections} = store.getState();
|
||||
const deviceIDsToRemove: Array<string> = connections.devices
|
||||
.filter((device: BaseDevice) => device instanceof AndroidDevice)
|
||||
.map((device: BaseDevice) => device.serial);
|
||||
|
||||
@@ -56,8 +88,8 @@ export default (store: Store) => {
|
||||
});
|
||||
|
||||
tracker.on('add', async device => {
|
||||
const androidDevice = await createDecive(client, device);
|
||||
if (device.type !== 'offline') {
|
||||
const androidDevice = await createDecive(client, device);
|
||||
store.dispatch({
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: androidDevice,
|
||||
|
||||
@@ -54,9 +54,8 @@ export default (store: Store) => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
let simulatorUpdateInterval = setInterval(() => {
|
||||
const {devices} = store.getState();
|
||||
clearInterval(simulatorUpdateInterval);
|
||||
const simulatorUpdateInterval = setInterval(() => {
|
||||
const {connections} = store.getState();
|
||||
querySimulatorDevices()
|
||||
.then((simulatorDevices: IOSDeviceMap) => {
|
||||
const simulators: Array<iOSSimulatorDevice> = Object.values(
|
||||
@@ -65,7 +64,7 @@ export default (store: Store) => {
|
||||
).reduce((acc, cv) => acc.concat(cv), []);
|
||||
|
||||
const currentDeviceIDs: Set<string> = new Set(
|
||||
devices
|
||||
connections.devices
|
||||
.filter(device => device instanceof IOSDevice)
|
||||
.map(device => device.serial),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ export * from './utils/index.js';
|
||||
export {default as GK} from './fb-stubs/GK.js';
|
||||
export {SonarBasePlugin, SonarPlugin, SonarDevicePlugin} from './plugin.js';
|
||||
export {createTablePlugin} from './createTablePlugin.js';
|
||||
export {default as SonarSidebar} from './chrome/SonarSidebar.js';
|
||||
|
||||
export * from './init.js';
|
||||
export {default} from './init.js';
|
||||
|
||||
@@ -14,6 +14,7 @@ import App from './App.js';
|
||||
import {createStore} from 'redux';
|
||||
import reducers from './reducers/index.js';
|
||||
import dispatcher from './dispatcher/index.js';
|
||||
import {setupMenuBar} from './MenuBar.js';
|
||||
const path = require('path');
|
||||
|
||||
const store = createStore(
|
||||
@@ -24,6 +25,7 @@ const store = createStore(
|
||||
dispatcher(store);
|
||||
|
||||
GK.init();
|
||||
setupMenuBar();
|
||||
|
||||
const AppFrame = () => (
|
||||
<ContextMenuProvider>
|
||||
|
||||
106
src/plugin.js
106
src/plugin.js
@@ -7,8 +7,10 @@
|
||||
|
||||
import type {KeyboardActions} from './MenuBar.js';
|
||||
import type {App} from './App.js';
|
||||
import type Logger from './fb-stubs/Logger.js';
|
||||
import type Client from './Client.js';
|
||||
|
||||
import React from 'react';
|
||||
import BaseDevice from './devices/BaseDevice.js';
|
||||
import {AndroidDevice, IOSDevice} from 'sonar';
|
||||
|
||||
@@ -22,37 +24,20 @@ export type PluginClient = {|
|
||||
|
||||
type PluginTarget = BaseDevice | Client;
|
||||
|
||||
/**
|
||||
* This is a wrapper for a plugin instance and state. We have a special toJSON method that removes the plugin
|
||||
* instance and any state if it's not set to be persisted.
|
||||
*/
|
||||
export class PluginStateContainer {
|
||||
constructor(plugin: SonarBasePlugin<>, state: Object) {
|
||||
this.plugin = plugin;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
plugin: ?SonarBasePlugin<>;
|
||||
state: Object;
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
plugin: null,
|
||||
state: this.plugin != null ? this.state : null,
|
||||
export type Props<T> = {
|
||||
logger: Logger,
|
||||
persistedState: T,
|
||||
setPersistedState: (state: $Shape<T>) => void,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class SonarBasePlugin<State: Object = any, Actions = any> {
|
||||
constructor() {
|
||||
// $FlowFixMe: this is fine
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
export class SonarBasePlugin<
|
||||
State = *,
|
||||
Actions = *,
|
||||
PersistedState = *,
|
||||
> extends React.Component<Props<PersistedState>, State> {
|
||||
static title: string = 'Unknown';
|
||||
static id: string = 'Unknown';
|
||||
static icon: string = 'apps';
|
||||
static persist: boolean = true;
|
||||
static keyboardActions: ?KeyboardActions;
|
||||
static screenshot: ?string;
|
||||
|
||||
@@ -60,19 +45,18 @@ export class SonarBasePlugin<State: Object = any, Actions = any> {
|
||||
title: empty;
|
||||
id: empty;
|
||||
persist: empty;
|
||||
icon: empty;
|
||||
keyboardActions: empty;
|
||||
screenshot: empty;
|
||||
|
||||
namespaceKey: string;
|
||||
reducers: {
|
||||
[actionName: string]: (state: State, actionData: Object) => $Shape<State>,
|
||||
} = {};
|
||||
app: App;
|
||||
state: State;
|
||||
renderSidebar: ?() => ?React.Element<*>;
|
||||
renderIntro: ?() => ?React.Element<*>;
|
||||
onKeyboardAction: ?(action: string) => void;
|
||||
|
||||
toJSON() {
|
||||
return null;
|
||||
return this.constructor.title;
|
||||
}
|
||||
|
||||
// methods to be overriden by plugins
|
||||
@@ -81,37 +65,7 @@ export class SonarBasePlugin<State: Object = any, Actions = any> {
|
||||
// methods to be overridden by subclasses
|
||||
_init(): void {}
|
||||
_teardown(): void {}
|
||||
_setup(target: PluginTarget, app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
setState(
|
||||
state: $Shape<State> | ((state: State) => $Shape<State>),
|
||||
callback?: () => void,
|
||||
) {
|
||||
if (typeof state === 'function') {
|
||||
state = state(this.state);
|
||||
}
|
||||
this.state = Object.assign({}, this.state, state);
|
||||
|
||||
const pluginKey = this.constructor.id;
|
||||
const namespaceKey = this.namespaceKey;
|
||||
const appState = this.app.state;
|
||||
|
||||
// update app state
|
||||
this.app.setState(
|
||||
{
|
||||
plugins: {
|
||||
...appState.plugins,
|
||||
[namespaceKey]: {
|
||||
...(appState.plugins[namespaceKey] || {}),
|
||||
[pluginKey]: new PluginStateContainer(this, this.state),
|
||||
},
|
||||
},
|
||||
},
|
||||
callback,
|
||||
);
|
||||
}
|
||||
_setup(target: PluginTarget) {}
|
||||
|
||||
dispatchAction(actionData: Actions) {
|
||||
// $FlowFixMe
|
||||
@@ -128,29 +82,17 @@ export class SonarBasePlugin<State: Object = any, Actions = any> {
|
||||
throw new TypeError(`Reducer ${actionData.type} isn't a function`);
|
||||
}
|
||||
}
|
||||
|
||||
render(): any {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class SonarDevicePlugin<
|
||||
State: Object = any,
|
||||
Actions = any,
|
||||
> extends SonarBasePlugin<State, Actions> {
|
||||
export class SonarDevicePlugin<S = *, A = *> extends SonarBasePlugin<S, A> {
|
||||
device: BaseDevice;
|
||||
|
||||
_setup(target: PluginTarget, app: App) {
|
||||
_setup(target: PluginTarget) {
|
||||
invariant(target instanceof BaseDevice, 'expected instanceof Client');
|
||||
const device: BaseDevice = target;
|
||||
|
||||
this.namespaceKey = device.serial;
|
||||
this.device = device;
|
||||
super._setup(device, app);
|
||||
}
|
||||
|
||||
_teardown() {
|
||||
this.teardown();
|
||||
super._setup(device);
|
||||
}
|
||||
|
||||
_init() {
|
||||
@@ -158,10 +100,7 @@ export class SonarDevicePlugin<
|
||||
}
|
||||
}
|
||||
|
||||
export class SonarPlugin<
|
||||
State: Object = any,
|
||||
Actions = any,
|
||||
> extends SonarBasePlugin<State, Actions> {
|
||||
export class SonarPlugin<S = *, A = *> extends SonarBasePlugin<S, A> {
|
||||
constructor() {
|
||||
super();
|
||||
this.subscriptions = [];
|
||||
@@ -197,14 +136,13 @@ export class SonarPlugin<
|
||||
return device;
|
||||
}
|
||||
|
||||
_setup(target: any, app: App) {
|
||||
_setup(target: any) {
|
||||
/* We have to type the above as `any` since if we import the actual Client we have an
|
||||
unresolvable dependency cycle */
|
||||
|
||||
const realClient: Client = target;
|
||||
const id: string = this.constructor.id;
|
||||
|
||||
this.namespaceKey = realClient.id;
|
||||
this.realClient = realClient;
|
||||
this.client = {
|
||||
call: (method, params) => realClient.call(id, method, params),
|
||||
@@ -218,7 +156,7 @@ export class SonarPlugin<
|
||||
},
|
||||
};
|
||||
|
||||
super._setup(realClient, app);
|
||||
super._setup(realClient);
|
||||
}
|
||||
|
||||
_teardown() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {GK} from 'sonar';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Sonar from 'sonar';
|
||||
import {SonarBasePlugin} from '../plugin.js';
|
||||
import {SonarPlugin, SonarBasePlugin} from '../plugin.js';
|
||||
|
||||
const plugins = new Map();
|
||||
|
||||
@@ -52,7 +52,9 @@ bundledPlugins
|
||||
}))
|
||||
.forEach(addIfNotAdded);
|
||||
|
||||
export default Array.from(plugins.values())
|
||||
const exportedPlugins: Array<Class<SonarPlugin<>>> = Array.from(
|
||||
plugins.values(),
|
||||
)
|
||||
.map(plugin => {
|
||||
if (
|
||||
(plugin.gatekeeper && !GK.get(plugin.gatekeeper)) ||
|
||||
@@ -70,3 +72,5 @@ export default Array.from(plugins.values())
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter(plugin => plugin.prototype instanceof SonarBasePlugin);
|
||||
|
||||
export default exportedPlugins;
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
SearchBox,
|
||||
SearchInput,
|
||||
SearchIcon,
|
||||
SonarSidebar,
|
||||
} from 'sonar';
|
||||
|
||||
// $FlowFixMe
|
||||
@@ -313,7 +314,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
||||
this.dispatchAction({elements: [element], type: 'UpdateElements'});
|
||||
this.dispatchAction({root: element.id, type: 'SetRoot'});
|
||||
this.performInitialExpand(element).then(() => {
|
||||
this.app.logger.trackTimeSince('LayoutInspectorInitialize');
|
||||
this.props.logger.trackTimeSince('LayoutInspectorInitialize');
|
||||
this.setState({initialised: true});
|
||||
});
|
||||
});
|
||||
@@ -400,7 +401,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
||||
return this.client
|
||||
.call('getNodes', {ids})
|
||||
.then(({elements}: GetNodesResult) => {
|
||||
this.app.logger.trackTimeSince('LayoutInspectorGetNodes');
|
||||
this.props.logger.trackTimeSince('LayoutInspectorGetNodes');
|
||||
return Promise.resolve(elements);
|
||||
});
|
||||
} else {
|
||||
@@ -425,7 +426,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
||||
performance.mark('LayoutInspectorExpandElement');
|
||||
if (expand) {
|
||||
return this.getChildren(key).then((elements: Array<Element>) => {
|
||||
this.app.logger.trackTimeSince('LayoutInspectorExpandElement');
|
||||
this.props.logger.trackTimeSince('LayoutInspectorExpandElement');
|
||||
this.dispatchAction({elements, type: 'UpdateElements'});
|
||||
return Promise.resolve(elements);
|
||||
});
|
||||
@@ -468,7 +469,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
||||
} else {
|
||||
this.expandElement(key);
|
||||
}
|
||||
this.app.logger.track('usage', 'layout:element-expanded', {
|
||||
this.props.logger.track('usage', 'layout:element-expanded', {
|
||||
id: key,
|
||||
deep: deep,
|
||||
});
|
||||
@@ -494,7 +495,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
||||
|
||||
onDataValueChanged = (path: Array<string>, value: any) => {
|
||||
this.client.send('setData', {id: this.state.selected, path, value});
|
||||
this.app.logger.track('usage', 'layout:value-changed', {
|
||||
this.props.logger.track('usage', 'layout:value-changed', {
|
||||
id: this.state.selected,
|
||||
value: value,
|
||||
path: path,
|
||||
@@ -567,6 +568,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
||||
</Center>
|
||||
)}
|
||||
</FlexRow>
|
||||
<SonarSidebar>{this.renderSidebar()}</SonarSidebar>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Glyph,
|
||||
colors,
|
||||
PureComponent,
|
||||
SonarSidebar,
|
||||
} from 'sonar';
|
||||
|
||||
import {SonarPlugin, SearchableTable} from 'sonar';
|
||||
@@ -188,6 +189,7 @@ export default class extends SonarPlugin<State> {
|
||||
clear={this.clearLogs}
|
||||
onRowHighlighted={this.onRowHighlighted}
|
||||
/>
|
||||
<SonarSidebar>{this.renderSidebar()}</SonarSidebar>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
|
||||
184
src/reducers.js
184
src/reducers.js
@@ -1,184 +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
|
||||
*/
|
||||
|
||||
import type {
|
||||
App,
|
||||
State,
|
||||
StatePluginInfo,
|
||||
StatePlugins,
|
||||
StateClientPlugins,
|
||||
} from './App.js';
|
||||
import type {SonarBasePlugin} from 'sonar';
|
||||
import {devicePlugins} from './device-plugins/index.js';
|
||||
import {SonarPlugin, SonarDevicePlugin} from 'sonar';
|
||||
import {PluginStateContainer} from './plugin.js';
|
||||
import BaseDevice from './devices/BaseDevice.js';
|
||||
import plugins from './plugins/index.js';
|
||||
import Client from './Client.js';
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
type ActivatePluginAction = {|
|
||||
appKey: string,
|
||||
pluginKey: string,
|
||||
|};
|
||||
|
||||
type TeardownClientAction = {|
|
||||
appKey: string,
|
||||
|};
|
||||
|
||||
export function ActivatePlugin(
|
||||
app: App,
|
||||
state: State,
|
||||
{appKey, pluginKey}: ActivatePluginAction,
|
||||
) {
|
||||
const {activePluginKey, activeAppKey} = state;
|
||||
|
||||
// get currently active plugin
|
||||
const activeClientPlugins: ?StateClientPlugins =
|
||||
activeAppKey != null ? state.plugins[activeAppKey] : null;
|
||||
const activePluginInfo: ?StatePluginInfo =
|
||||
activePluginKey != null && activeClientPlugins
|
||||
? activeClientPlugins[activePluginKey]
|
||||
: null;
|
||||
|
||||
// check if this plugin is already active
|
||||
if (
|
||||
activePluginKey === pluginKey &&
|
||||
activeAppKey === appKey &&
|
||||
activePluginInfo &&
|
||||
activePluginInfo.plugin
|
||||
) {
|
||||
// this is a noop
|
||||
return state;
|
||||
}
|
||||
|
||||
// produce new plugins object
|
||||
const newPluginsState: StatePlugins = {
|
||||
...state.plugins,
|
||||
};
|
||||
|
||||
// check if the currently active plugin needs to be torn down after being deactivated
|
||||
if (
|
||||
activeAppKey != null &&
|
||||
activePluginKey != null &&
|
||||
activePluginInfo &&
|
||||
activeClientPlugins
|
||||
) {
|
||||
const activePlugin: ?SonarBasePlugin<> = activePluginInfo.plugin;
|
||||
if (activePlugin && !activePlugin.constructor.persist) {
|
||||
// teardown the currently active plugin
|
||||
activePlugin._teardown();
|
||||
|
||||
// and remove it's plugin instance so next time it's made active it'll be reloaded
|
||||
newPluginsState[activeAppKey] = {
|
||||
...activeClientPlugins,
|
||||
[activePluginKey]: {
|
||||
plugin: null,
|
||||
state: activePluginInfo.state,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// get the plugin state associated with the new client
|
||||
const newClientPluginsState: StateClientPlugins = {
|
||||
...(newPluginsState[appKey] || {}),
|
||||
};
|
||||
newPluginsState[appKey] = newClientPluginsState;
|
||||
|
||||
// find the Plugin constructor with this key
|
||||
let Plugin: Class<SonarBasePlugin<>>;
|
||||
for (const FindPlugin of plugins) {
|
||||
if (FindPlugin.id === pluginKey) {
|
||||
Plugin = FindPlugin;
|
||||
}
|
||||
}
|
||||
for (const FindPlugin of devicePlugins) {
|
||||
if (FindPlugin.id === pluginKey) {
|
||||
Plugin = FindPlugin;
|
||||
}
|
||||
}
|
||||
invariant(Plugin, 'expected plugin');
|
||||
|
||||
// get target, this could be an app connection or a device
|
||||
const clientInfo = state.server.connections.get(appKey);
|
||||
let target: void | Client | BaseDevice;
|
||||
if (clientInfo) {
|
||||
target = clientInfo.client;
|
||||
invariant(
|
||||
// $FlowFixMe prototype not known
|
||||
Plugin.prototype instanceof SonarPlugin,
|
||||
'expected plugin to be an app Plugin',
|
||||
);
|
||||
} else {
|
||||
target = app.props.devices.find(
|
||||
(device: BaseDevice) => device.serial === appKey,
|
||||
);
|
||||
invariant(
|
||||
// $FlowFixMe prototype not known
|
||||
Plugin.prototype instanceof SonarDevicePlugin,
|
||||
'expected plugin to be DevicePlugin',
|
||||
);
|
||||
}
|
||||
invariant(target, 'expected target');
|
||||
|
||||
// initialise the client if it hasn't alreadu been
|
||||
const thisPluginState: ?StatePluginInfo = newClientPluginsState[pluginKey];
|
||||
if (!thisPluginState || !thisPluginState.plugin) {
|
||||
const plugin = new Plugin();
|
||||
|
||||
// setup plugin, this is to avoid consumers having to pass args to super
|
||||
plugin._setup(target, app);
|
||||
|
||||
// if we already have state for this plugin then rehydrate it
|
||||
if (thisPluginState && thisPluginState.state) {
|
||||
plugin.state = thisPluginState.state;
|
||||
}
|
||||
|
||||
// init plugin - setup broadcasts, initial messages etc
|
||||
plugin._init();
|
||||
|
||||
newClientPluginsState[pluginKey] = new PluginStateContainer(
|
||||
plugin,
|
||||
plugin.state,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
activeAppKey: appKey,
|
||||
activePluginKey: pluginKey,
|
||||
plugins: newPluginsState,
|
||||
};
|
||||
}
|
||||
|
||||
export function TeardownClient(
|
||||
app: App,
|
||||
state: State,
|
||||
{appKey}: TeardownClientAction,
|
||||
) {
|
||||
const allPlugins: StatePlugins = {...state.plugins};
|
||||
|
||||
// teardown all plugins
|
||||
const clientPlugins: StateClientPlugins = allPlugins[appKey];
|
||||
for (const pluginKey in clientPlugins) {
|
||||
const {plugin} = clientPlugins[pluginKey];
|
||||
if (plugin) {
|
||||
plugin._teardown();
|
||||
}
|
||||
}
|
||||
|
||||
// remove this client
|
||||
delete allPlugins[appKey];
|
||||
|
||||
return {
|
||||
activeAppKey: state.activeAppKey === appKey ? null : state.activeAppKey,
|
||||
activePluginKey:
|
||||
state.activeAppKey === appKey ? null : state.activePluginKey,
|
||||
plugins: allPlugins,
|
||||
};
|
||||
}
|
||||
148
src/reducers/connections.js
Normal file
148
src/reducers/connections.js
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import type BaseDevice from '../devices/BaseDevice';
|
||||
export type State = {
|
||||
devices: Array<BaseDevice>,
|
||||
androidEmulators: Array<string>,
|
||||
selectedDeviceIndex: number,
|
||||
selectedPlugin: ?string,
|
||||
selectedApp: ?string,
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'UNREGISTER_DEVICES',
|
||||
payload: Set<string>,
|
||||
}
|
||||
| {
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: BaseDevice,
|
||||
}
|
||||
| {
|
||||
type: 'REGISTER_ANDROID_EMULATORS',
|
||||
payload: Array<string>,
|
||||
}
|
||||
| {
|
||||
type: 'SELECT_DEVICE',
|
||||
payload: number,
|
||||
}
|
||||
| {
|
||||
type: 'SELECT_PLUGIN',
|
||||
payload: {
|
||||
selectedPlugin: ?string,
|
||||
selectedApp: ?string,
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_PLUGIN = 'DeviceLogs';
|
||||
|
||||
const INITAL_STATE: State = {
|
||||
devices: [],
|
||||
androidEmulators: [],
|
||||
selectedDeviceIndex: -1,
|
||||
selectedApp: null,
|
||||
selectedPlugin: DEFAULT_PLUGIN,
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = INITAL_STATE,
|
||||
action: Action,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'SELECT_DEVICE': {
|
||||
const {payload} = action;
|
||||
return {
|
||||
...state,
|
||||
selectedApp: null,
|
||||
selectedPlugin: DEFAULT_PLUGIN,
|
||||
selectedDeviceIndex: payload,
|
||||
};
|
||||
}
|
||||
case 'REGISTER_ANDROID_EMULATORS': {
|
||||
const {payload} = action;
|
||||
return {
|
||||
...state,
|
||||
androidEmulators: payload,
|
||||
};
|
||||
}
|
||||
case 'REGISTER_DEVICE': {
|
||||
const {payload} = action;
|
||||
const devices = state.devices.concat(payload);
|
||||
let {selectedDeviceIndex} = state;
|
||||
let selection = {};
|
||||
if (selectedDeviceIndex === -1) {
|
||||
selectedDeviceIndex = devices.length - 1;
|
||||
selection = {
|
||||
selectedApp: null,
|
||||
selectedPlugin: DEFAULT_PLUGIN,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
devices,
|
||||
// select device if none was selected before
|
||||
selectedDeviceIndex,
|
||||
...selection,
|
||||
};
|
||||
}
|
||||
case 'UNREGISTER_DEVICES': {
|
||||
const {payload} = action;
|
||||
const {selectedDeviceIndex} = state;
|
||||
let selectedDeviceWasRemoved = false;
|
||||
const devices = state.devices.filter((device: BaseDevice, i: number) => {
|
||||
if (payload.has(device.serial)) {
|
||||
if (selectedDeviceIndex === i) {
|
||||
// removed device is the selected
|
||||
selectedDeviceWasRemoved = true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
let selection = {};
|
||||
if (selectedDeviceWasRemoved) {
|
||||
selection = {
|
||||
selectedDeviceIndex: devices.length - 1,
|
||||
selectedApp: null,
|
||||
selectedPlugin: DEFAULT_PLUGIN,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
devices,
|
||||
...selection,
|
||||
};
|
||||
}
|
||||
case 'SELECT_PLUGIN': {
|
||||
const {payload} = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const selectDevice = (payload: number): Action => ({
|
||||
type: 'SELECT_DEVICE',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const selectPlugin = (payload: {
|
||||
selectedPlugin: ?string,
|
||||
selectedApp: ?string,
|
||||
}): Action => ({
|
||||
type: 'SELECT_PLUGIN',
|
||||
payload,
|
||||
});
|
||||
@@ -1,39 +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
|
||||
*/
|
||||
|
||||
import type BaseDevice from '../devices/BaseDevice';
|
||||
export type State = Array<BaseDevice>;
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'UNREGISTER_DEVICES',
|
||||
payload: Set<string>,
|
||||
}
|
||||
| {
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: BaseDevice,
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = [];
|
||||
|
||||
export default function reducer(
|
||||
state: State = INITIAL_STATE,
|
||||
action: Action,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'REGISTER_DEVICE': {
|
||||
const {payload} = action;
|
||||
return state.concat(payload);
|
||||
}
|
||||
case 'UNREGISTER_DEVICES': {
|
||||
const {payload} = action;
|
||||
return state.filter((device: BaseDevice) => !payload.has(device.serial));
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
import application from './application.js';
|
||||
import devices from './devices.js';
|
||||
import connections from './connections.js';
|
||||
import pluginStates from './pluginStates.js';
|
||||
import type {
|
||||
State as ApplicationState,
|
||||
Action as ApplicationAction,
|
||||
@@ -15,15 +16,20 @@ import type {
|
||||
import type {
|
||||
State as DevicesState,
|
||||
Action as DevicesAction,
|
||||
} from './devices.js';
|
||||
} from './connections.js';
|
||||
import type {
|
||||
State as PluginsState,
|
||||
Action as PluginsAction,
|
||||
} from './pluginStates.js';
|
||||
import type {Store as ReduxStore} from 'redux';
|
||||
|
||||
export type Store = ReduxStore<
|
||||
{
|
||||
application: ApplicationState,
|
||||
devices: DevicesState,
|
||||
connections: DevicesState,
|
||||
pluginStates: PluginsState,
|
||||
},
|
||||
ApplicationAction | DevicesAction,
|
||||
ApplicationAction | DevicesAction | PluginsAction,
|
||||
>;
|
||||
|
||||
export default combineReducers({application, devices});
|
||||
export default combineReducers({application, connections, pluginStates});
|
||||
|
||||
42
src/reducers/pluginStates.js
Normal file
42
src/reducers/pluginStates.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 = {
|
||||
[pluginKey: string]: Object,
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
type: 'SET_PLUGIN_STATE',
|
||||
payload: {
|
||||
pluginKey: string,
|
||||
state: Object,
|
||||
},
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {};
|
||||
|
||||
export default function reducer(
|
||||
state: State = INITIAL_STATE,
|
||||
action: Action,
|
||||
): State {
|
||||
if (action.type === 'SET_PLUGIN_STATE') {
|
||||
return {
|
||||
...state,
|
||||
[action.payload.pluginKey]: action.payload.state,
|
||||
};
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const setPluginState = (payload: {
|
||||
pluginKey: string,
|
||||
state: Object,
|
||||
}): Action => ({
|
||||
type: 'SET_PLUGIN_STATE',
|
||||
payload,
|
||||
});
|
||||
@@ -125,6 +125,7 @@ const StyledButton = styled.view(
|
||||
},
|
||||
{
|
||||
ignoreAttributes: [
|
||||
'dropdown',
|
||||
'dispatch',
|
||||
'compact',
|
||||
'large',
|
||||
|
||||
@@ -19,6 +19,7 @@ const ErrorBoundaryContainer = View.extends({
|
||||
|
||||
const ErrorBoundaryStack = ErrorBlock.extends({
|
||||
marginBottom: 10,
|
||||
whiteSpace: 'pre',
|
||||
});
|
||||
|
||||
type ErrorBoundaryProps = {
|
||||
@@ -43,8 +44,7 @@ export default class ErrorBoundary extends Component<
|
||||
}
|
||||
|
||||
componentDidCatch(err: Error) {
|
||||
this.props.logger &&
|
||||
console.error(err.toString(), 'ErrorBoundary');
|
||||
this.props.logger && console.error(err.toString(), 'ErrorBoundary');
|
||||
this.setState({error: err});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user