/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import { FlipperPlugin, FlipperDevicePlugin, Props as PluginProps, PluginDefinition, isSandyPlugin, } from './plugin'; import {Logger} from './fb-interfaces/Logger'; import BaseDevice from './devices/BaseDevice'; import {pluginKey as getPluginKey} from './reducers/pluginStates'; import Client from './Client'; import { ErrorBoundary, FlexColumn, FlexRow, colors, styled, Glyph, Label, VBox, View, } from './ui'; import { StaticView, setStaticView, pluginIsStarred, } from './reducers/connections'; import {starPlugin} from './reducers/pluginManager'; import React, {PureComponent} from 'react'; import {connect, ReactReduxContext} from 'react-redux'; import {setPluginState} from './reducers/pluginStates'; import {Settings} from './reducers/settings'; import {selectPlugin} from './reducers/connections'; import {State as Store, MiddlewareAPI} from './reducers/index'; import {activateMenuItems} from './MenuBar'; import {Message} from './reducers/pluginMessageQueue'; import {IdlerImpl} from './utils/Idler'; import {processMessageQueue} from './utils/messageQueue'; import {ToggleButton, SmallText, Layout} from './ui'; import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin'; import {isDevicePluginDefinition} from './utils/pluginUtils'; import {ContentContainer} from './sandy-chrome/ContentContainer'; import {Alert, Typography} from 'antd'; import {InstalledPluginDetails} from 'plugin-lib'; import semver from 'semver'; import {loadPlugin} from './reducers/pluginManager'; import {produce} from 'immer'; import {reportUsage} from './utils/metrics'; const {Text, Link} = Typography; const Container = styled(FlexColumn)({ width: 0, flexGrow: 1, flexShrink: 1, backgroundColor: colors.white, }); export const SidebarContainer = styled(FlexRow)({ backgroundColor: colors.light02, height: '100%', overflow: 'auto', }); const Waiting = styled(FlexColumn)({ width: '100%', height: '100%', flexGrow: 1, alignItems: 'center', justifyContent: 'center', textAlign: 'center', }); function ProgressBar({progress}: {progress: number}) { return ( ); } const ProgressBarContainer = styled.div({ border: `1px solid ${colors.cyan}`, borderRadius: 4, width: 300, }); const ProgressBarBar = styled.div<{progress: number}>(({progress}) => ({ background: colors.cyan, width: `${Math.min(100, Math.round(progress * 100))}%`, height: 8, })); type OwnProps = { logger: Logger; isSandy?: boolean; }; type StateFromProps = { pluginState: Object; activePlugin: PluginDefinition | undefined; target: Client | BaseDevice | null; pluginKey: string | null; deepLinkPayload: unknown; selectedApp: string | null; isArchivedDevice: boolean; pendingMessages: Message[] | undefined; pluginIsEnabled: boolean; settingsState: Settings; latestInstalledVersion: InstalledPluginDetails | undefined; }; type DispatchFromProps = { selectPlugin: (payload: { selectedPlugin: string | null; selectedApp?: string | null; deepLinkPayload: unknown; }) => any; setPluginState: (payload: {pluginKey: string; state: any}) => void; setStaticView: (payload: StaticView) => void; starPlugin: typeof starPlugin; loadPlugin: typeof loadPlugin; }; type Props = StateFromProps & DispatchFromProps & OwnProps; type State = { progress: {current: number; total: number}; autoUpdateAlertSuppressed: Set; }; class PluginContainer extends PureComponent { static contextType = ReactReduxContext; constructor(props: Props) { super(props); this.reloadPlugin = this.reloadPlugin.bind(this); } plugin: | FlipperPlugin | FlipperDevicePlugin | null | undefined; refChanged = ( ref: | FlipperPlugin | FlipperDevicePlugin | null | undefined, ) => { // N.B. for Sandy plugins this lifecycle is managed by PluginRenderer if (this.plugin) { this.plugin._teardown(); this.plugin = null; } if (ref && this.props.target) { activateMenuItems(ref); ref._init(); this.props.logger.trackTimeSince(`activePlugin-${ref.constructor.id}`); this.plugin = ref; } }; idler?: IdlerImpl; pluginBeingProcessed: string | null = null; state = { progress: {current: 0, total: 0}, autoUpdateAlertSuppressed: new Set(), }; get store(): MiddlewareAPI { return this.context.store; } componentWillUnmount() { if (this.plugin) { this.plugin._teardown(); this.plugin = null; } this.cancelCurrentQueue(); } componentDidMount() { this.processMessageQueue(); } componentDidUpdate() { this.processMessageQueue(); // make sure deeplinks are propagated const {deepLinkPayload, target, activePlugin} = this.props; if (deepLinkPayload && activePlugin && target) { target.sandyPluginStates .get(activePlugin.id) ?.triggerDeepLink(deepLinkPayload); } } processMessageQueue() { const { pluginKey, pendingMessages, activePlugin, pluginIsEnabled, target, } = this.props; if (pluginKey !== this.pluginBeingProcessed) { this.pluginBeingProcessed = pluginKey; this.cancelCurrentQueue(); this.setState((state) => produce(state, (draft) => { draft.progress = {current: 0, total: 0}; }), ); // device plugins don't have connections so no message queues if (!activePlugin || isDevicePluginDefinition(activePlugin)) { return; } if ( pluginIsEnabled && target instanceof Client && activePlugin && (isSandyPlugin(activePlugin) || activePlugin.persistedStateReducer) && pluginKey && pendingMessages?.length ) { const start = Date.now(); this.idler = new IdlerImpl(); processMessageQueue( isSandyPlugin(activePlugin) ? target.sandyPluginStates.get(activePlugin.id)! : activePlugin, pluginKey, this.store, (progress) => { this.setState((state) => produce(state, (draft) => { draft.progress = progress; }), ); }, this.idler, ).then((completed) => { const duration = Date.now() - start; this.props.logger.track( 'duration', 'queue-processing-before-plugin-open', { completed, duration, }, activePlugin.id, ); }); } } } cancelCurrentQueue() { if (this.idler && !this.idler.isCancelled()) { this.idler.cancel(); } } render() { const { activePlugin, pluginKey, target, pendingMessages, pluginIsEnabled, } = this.props; if (!activePlugin || !target || !pluginKey) { return null; } if (!pluginIsEnabled) { return this.renderPluginEnabler(); } if (!pendingMessages || pendingMessages.length === 0) { return this.renderPlugin(); } return this.renderPluginLoader(); } renderPluginEnabler() { const activePlugin = this.props.activePlugin!; return ( { this.props.starPlugin({ plugin: activePlugin, selectedApp: (this.props.target as Client)?.query?.app, }); }} large /> Click to enable this plugin ); } renderPluginLoader() { return ( ); } renderNoPluginActive() { return ( ); } reloadPlugin() { const {loadPlugin, latestInstalledVersion} = this.props; if (latestInstalledVersion) { reportUsage( 'plugin-auto-update:alert:reloadClicked', { version: latestInstalledVersion.version, }, latestInstalledVersion.id, ); loadPlugin({ plugin: latestInstalledVersion, enable: false, notifyIfFailed: true, }); } } renderPlugin() { const { pluginState, setPluginState, activePlugin, pluginKey, target, isArchivedDevice, selectedApp, settingsState, isSandy, latestInstalledVersion, } = this.props; if (!activePlugin || !target || !pluginKey) { console.warn(`No selected plugin. Rendering empty!`); return this.renderNoPluginActive(); } let pluginElement: null | React.ReactElement; const showUpdateAlert = latestInstalledVersion && activePlugin && !this.state.autoUpdateAlertSuppressed.has( `${latestInstalledVersion.name}@${latestInstalledVersion.version}`, ) && semver.gt(latestInstalledVersion.version, activePlugin.version); if (isSandyPlugin(activePlugin)) { // Make sure we throw away the container for different pluginKey! const instance = target.sandyPluginStates.get(activePlugin.id); if (!instance) { // happens if we selected a plugin that is not enabled on a specific app or not supported on a specific device. return this.renderNoPluginActive(); } pluginElement = ( <_SandyPluginRenderer key={pluginKey} plugin={instance} /> ); } else { const props: PluginProps & { key: string; ref: ( ref: | FlipperPlugin | FlipperDevicePlugin | null | undefined, ) => void; } = { key: pluginKey, logger: this.props.logger, selectedApp, persistedState: activePlugin.defaultPersistedState ? { ...activePlugin.defaultPersistedState, ...pluginState, } : pluginState, setStaticView: (payload: StaticView) => this.props.setStaticView(payload), setPersistedState: (state) => setPluginState({pluginKey, state}), target, deepLinkPayload: this.props.deepLinkPayload, selectPlugin: (pluginID: string, deepLinkPayload: unknown) => { const {target} = this.props; // check if plugin will be available if ( target instanceof Client && target.plugins.some((p) => p === pluginID) ) { this.props.selectPlugin({ selectedPlugin: pluginID, deepLinkPayload, }); return true; } else if (target instanceof BaseDevice) { this.props.selectPlugin({ selectedPlugin: pluginID, deepLinkPayload, }); return true; } else { return false; } }, ref: this.refChanged, isArchivedDevice, settingsState, }; pluginElement = ( {React.createElement(activePlugin, props)} ); } return isSandy ? (
{showUpdateAlert && ( Plugin "{activePlugin.title}" v {latestInstalledVersion?.version} is downloaded and ready to install. Reload to start using the new version. } type="info" onClose={() => this.setState((state) => produce(state, (draft) => { draft.autoUpdateAlertSuppressed.add( `${latestInstalledVersion?.name}@${latestInstalledVersion?.version}`, ); }), ) } style={{marginBottom: theme.space.large}} showIcon closable /> )}
{pluginElement}
) : ( {pluginElement} ); } } export default connect( ({ connections: { selectedPlugin, selectedDevice, selectedApp, clients, deepLinkPayload, userStarredPlugins, userStarredDevicePlugins, }, pluginStates, plugins: {devicePlugins, clientPlugins, installedPlugins}, pluginMessageQueue, settingsState, }) => { let pluginKey = null; let target = null; let activePlugin: PluginDefinition | undefined; let pluginIsEnabled = false; if (selectedPlugin) { activePlugin = devicePlugins.get(selectedPlugin); if (selectedDevice && activePlugin) { target = selectedDevice; pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id); } else { target = clients.find((client: Client) => client.id === selectedApp) || null; activePlugin = clientPlugins.get(selectedPlugin); if (activePlugin && target) { pluginKey = getPluginKey(target.id, activePlugin.id); } } pluginIsEnabled = activePlugin !== undefined && pluginIsStarred( userStarredPlugins, userStarredDevicePlugins, selectedApp, activePlugin.id, ); } const isArchivedDevice = !selectedDevice ? false : selectedDevice.isArchived; if (isArchivedDevice) { pluginIsEnabled = true; } const pendingMessages = pluginKey ? pluginMessageQueue[pluginKey] : undefined; const s: StateFromProps = { pluginState: pluginStates[pluginKey as string], activePlugin: activePlugin, target, deepLinkPayload, pluginKey, isArchivedDevice, selectedApp: selectedApp || null, pendingMessages, pluginIsEnabled, settingsState, latestInstalledVersion: installedPlugins.get( activePlugin?.packageName ?? '', ), }; return s; }, { setPluginState, selectPlugin, setStaticView, starPlugin, loadPlugin: loadPlugin, }, )(PluginContainer);