diff --git a/src/Client.js b/src/Client.js index 4d2b11674..f8f570de3 100644 --- a/src/Client.js +++ b/src/Client.js @@ -6,6 +6,9 @@ */ import type {FlipperPlugin, FlipperBasePlugin} from './plugin.js'; +import {FlipperDevicePlugin} from './plugin.js'; +import type BaseDevice from './devices/BaseDevice.js'; + import type {App} from './App.js'; import type Logger from './fb-stubs/Logger.js'; import type {Store} from './reducers/index.js'; @@ -67,6 +70,12 @@ export default class Client extends EventEmitter { }, }); } + getDevice = (): ?BaseDevice => + this.store + .getState() + .connections.devices.find( + (device: BaseDevice) => device.serial === this.query.device_id, + ); on: ((event: 'plugins-change', callback: () => void) => void) & ((event: 'close', callback: () => void) => void); @@ -157,7 +166,12 @@ export default class Client extends EventEmitter { this.store.getState().plugins.clientPlugins.get(params.api) || this.store.getState().plugins.devicePlugins.get(params.api); if (persistingPlugin && persistingPlugin.persistedStateReducer) { - const pluginKey = `${this.id}#${params.api}`; + let pluginKey = `${this.id}#${params.api}`; + //$FlowFixMe + if (persistingPlugin.prototype instanceof FlipperDevicePlugin) { + // For device plugins, we are just using the device id instead of client id as the prefix. + pluginKey = `${this.getDevice()?.serial || ''}#${params.api}`; + } const persistedState = { ...persistingPlugin.defaultPersistedState, ...this.store.getState().pluginStates[pluginKey], diff --git a/src/PluginContainer.js b/src/PluginContainer.js index b609a05bd..564d468c0 100644 --- a/src/PluginContainer.js +++ b/src/PluginContainer.js @@ -6,7 +6,7 @@ */ import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js'; import type LogManager from './fb-stubs/Logger'; -import type BaseDevice from './devices/BaseDevice.js'; +import BaseDevice from './devices/BaseDevice.js'; import type {Props as PluginProps} from './plugin'; import Client from './Client.js'; @@ -128,7 +128,6 @@ class PluginContainer extends Component { if (!activePlugin || !target) { return null; } - const props: PluginProps = { key: pluginKey, logger: this.props.logger, @@ -150,6 +149,9 @@ class PluginContainer extends Component { ) { this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload}); return true; + } else if (target instanceof BaseDevice) { + this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload}); + return true; } else { return false; } diff --git a/src/chrome/MainSidebar.js b/src/chrome/MainSidebar.js index 25dbd39b0..c39bfdfc2 100644 --- a/src/chrome/MainSidebar.js +++ b/src/chrome/MainSidebar.js @@ -170,6 +170,7 @@ type MainSidebarProps = {| selectPlugin: (payload: { selectedPlugin: ?string, selectedApp: ?string, + deepLinkPayload: ?string, }) => void, clients: Array, uninitializedClients: Array<{ @@ -220,6 +221,7 @@ class MainSidebar extends Component { selectPlugin({ selectedPlugin: 'notifications', selectedApp: null, + deepLinkPayload: null, }) }> { selectPlugin({ selectedPlugin: plugin.id, selectedApp: null, + deepLinkPayload: null, }) } plugin={plugin} @@ -282,6 +285,7 @@ class MainSidebar extends Component { selectPlugin({ selectedPlugin: plugin.id, selectedApp: client.id, + deepLinkPayload: null, }) } plugin={plugin} diff --git a/src/dispatcher/server.js b/src/dispatcher/server.js index 6fc6b155a..78adaee50 100644 --- a/src/dispatcher/server.js +++ b/src/dispatcher/server.js @@ -30,7 +30,12 @@ export default (store: Store, logger: Logger) => { }); store.dispatch({ type: 'CLEAR_PLUGIN_STATE', - payload: id, + payload: { + id, + devicePlugins: new Set([ + ...store.getState().plugins.devicePlugins.keys(), + ]), + }, }); }); diff --git a/src/index.js b/src/index.js index 9e9171204..34316b24c 100644 --- a/src/index.js +++ b/src/index.js @@ -14,8 +14,11 @@ export { FlipperPlugin, FlipperDevicePlugin, } from './plugin.js'; - +export {clipboard} from 'electron'; export * from './fb-stubs/constants.js'; +export * from './utils/createPaste.js'; +export {connect} from 'react-redux'; +export {selectPlugin} from './reducers/connections'; export { default as SidebarExtensions, diff --git a/src/plugins/crash_reporter/index.js b/src/plugins/crash_reporter/index.js index 1b05e5ae1..054008a81 100644 --- a/src/plugins/crash_reporter/index.js +++ b/src/plugins/crash_reporter/index.js @@ -6,7 +6,17 @@ * @flow */ -import {FlipperDevicePlugin, Device} from 'flipper'; +import { + FlipperDevicePlugin, + Device, + View, + styled, + FlexColumn, + FlexRow, + ContextMenu, + clipboard, + Button, +} from 'flipper'; import type {Notification} from '../../plugin'; type Crash = {| @@ -19,6 +29,34 @@ type PersistedState = {| crashes: Array, |}; +const Title = styled(View)({ + fontWeight: 'bold', + fontSize: '100%', + color: 'red', +}); + +const Value = styled(View)({ + paddingLeft: '8px', + fontSize: '100%', + fontFamily: 'Monospace', +}); + +const RootColumn = styled(FlexColumn)({ + paddingLeft: '16px', + paddingRight: '16px', + paddingTop: '8px', +}); + +const CrashRow = styled(FlexRow)({ + paddingTop: '8px', +}); + +const CallStack = styled('pre')({ + fontFamily: 'Monospace', + fontSize: '100%', + paddingLeft: '8px', +}); + export default class CrashReporterPlugin extends FlipperDevicePlugin { static title = 'Crash Reporter'; static id = 'CrashReporter'; @@ -67,11 +105,70 @@ export default class CrashReporterPlugin extends FlipperDevicePlugin { message: crash.callStack, severity: 'error', title: 'CRASH: ' + crash.name + ' ' + crash.reason, + action: 'Inspect', }; }); }; + convertCallstackToString(crash: Crash): string { + return crash.callStack.reduce((acc, val) => acc.concat('\n', val)); + } + render() { - return 'Dedicated space to debug crashes. Look out for crash notifications.'; + if (this.props.persistedState.crashes.length > 0) { + const crash = this.props.persistedState.crashes.slice(-1)[0]; + const callStackString = this.convertCallstackToString(crash); + return ( + + + Name + {crash.name} + + + Reason + {crash.reason} + + + CallStack + + + { + clipboard.writeText(callStackString); + }, + }, + ]}> + {callStackString} + + + {this.device.os == 'Android' && ( + + + + )} + + ); + } + return ( + + + Dedicated space to debug crashes. Look out for crash notifications + + + ); } } diff --git a/src/plugins/logs/index.js b/src/plugins/logs/index.js index 5e8062932..1f0d73a94 100644 --- a/src/plugins/logs/index.js +++ b/src/plugins/logs/index.js @@ -280,6 +280,30 @@ export default class LogTable extends FlipperDevicePlugin< })); }; + calculateHighlightedRows = ( + deeplinkPayload: ?string, + rows: Array, + ): Array => { + if (!deeplinkPayload) { + return []; + } + const crash = JSON.parse(deeplinkPayload); + let highlightedRows = rows.filter(x => { + //$FlowFixMe: x.filterValue is not undefined + let matched = x.filterValue.includes(crash.name); + if (!matched) { + return matched; + } + for (let i = 0; i < crash.callStack.length && matched; ++i) { + //$FlowFixMe: x.filterValue is not undefined + matched = x.filterValue.includes(crash.callStack[i]); + } + return matched; + }); + highlightedRows = highlightedRows.map(x => x.key); + return [highlightedRows.pop()]; + }; + state = { rows: [], entries: [], @@ -390,7 +414,6 @@ export default class LogTable extends FlipperDevicePlugin< key: String(this.counter++), }, }; - // batch up logs to be processed every 250ms, if we have lots of log // messages coming in, then calling an setState 200+ times is actually // pretty expensive @@ -425,11 +448,15 @@ export default class LogTable extends FlipperDevicePlugin< this.addRowIfNeeded(rows, row, entry, previousEntry); } - + const highlightedRows = this.calculateHighlightedRows( + this.props.deepLinkPayload, + rows, + ); return { entries, rows, key2entry, + highlightedRows: highlightedRows, }; }); }, 100); @@ -547,8 +574,7 @@ export default class LogTable extends FlipperDevicePlugin< }); render() { - const {rows} = this.state; - + const {rows, highlightedRows} = this.state; const contextMenuItems = [ { type: 'separator', @@ -568,6 +594,9 @@ export default class LogTable extends FlipperDevicePlugin< columnOrder={this.columnOrder} columns={this.columns} rows={rows} + highlightedRows={ + highlightedRows ? new Set(highlightedRows) : new Set([]) + } onRowHighlighted={this.onRowHighlighted} multiHighlight={true} defaultFilters={DEFAULT_FILTERS} diff --git a/src/reducers/pluginStates.js b/src/reducers/pluginStates.js index 0c249da37..b38946994 100644 --- a/src/reducers/pluginStates.js +++ b/src/reducers/pluginStates.js @@ -19,7 +19,7 @@ export type Action = } | { type: 'CLEAR_PLUGIN_STATE', - payload: string, + payload: {id: string, devicePlugins: Set}, }; const INITIAL_STATE: State = {}; @@ -41,7 +41,8 @@ export default function reducer( return Object.keys(state).reduce((newState, pluginKey) => { // Only add the pluginState, if its from a plugin other than the one that // was removed. pluginKeys are in the form of ${clientID}#${pluginID}. - if (!pluginKey.startsWith(payload)) { + const pluginId = pluginKey.split('#').pop(); + if (pluginId !== payload.id || payload.devicePlugins.has(pluginId)) { newState[pluginKey] = state[pluginKey]; } return newState;