/** * Copyright (c) Meta Platforms, Inc. and 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} from './plugin'; import {Logger, isTest} from 'flipper-common'; import {BaseDevice} from 'flipper-frontend-core'; import {pluginKey as getPluginKey} from './utils/pluginKey'; import Client from './Client'; import { ErrorBoundary, FlexColumn, FlexRow, colors, styled, Glyph, Label, VBox, View, } from './ui'; import {StaticView, setStaticView} from './reducers/connections'; import {switchPlugin} from './reducers/pluginManager'; import React, {PureComponent} from 'react'; import {connect, ReactReduxContext, ReactReduxContextValue} from 'react-redux'; import {selectPlugin} from './reducers/connections'; import {State as Store, MiddlewareAPI} from './reducers/index'; import {Message} from './reducers/pluginMessageQueue'; import {IdlerImpl} from './utils/Idler'; import {processMessageQueue} from './utils/messageQueue'; import {Layout} from './ui'; import {theme, _SandyPluginRenderer} from 'flipper-plugin'; import { ActivePluginListItem, isDevicePlugin, isDevicePluginDefinition, } from './utils/pluginUtils'; import {ContentContainer} from './sandy-chrome/ContentContainer'; import {Alert, Typography} from 'antd'; import semver from 'semver'; import {loadPlugin} from './reducers/pluginManager'; import {produce} from 'immer'; import {reportUsage, InstalledPluginDetails} from 'flipper-common'; import {PluginInfo} from './chrome/fb-stubs/PluginInfo'; import {getActiveClient, getActivePlugin} from './selectors/connections'; import {AnyAction} from 'redux'; const {Text, Link} = Typography; export const SidebarContainer = styled(FlexRow)({ backgroundColor: theme.backgroundWash, 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; }; type StateFromProps = { activePlugin: ActivePluginListItem | null; target: Client | BaseDevice | null; pluginKey: string | null; deepLinkPayload: unknown; pendingMessages: Message[] | undefined; latestInstalledVersion: InstalledPluginDetails | undefined; }; type DispatchFromProps = { selectPlugin: typeof selectPlugin; setStaticView: (payload: StaticView) => void; enablePlugin: typeof switchPlugin; loadPlugin: typeof loadPlugin; }; type Props = StateFromProps & DispatchFromProps & OwnProps; type State = { progress: {current: number; total: number}; autoUpdateAlertSuppressed: Set; }; class PluginContainer extends PureComponent { static contextType: React.Context> = ReactReduxContext; constructor(props: Props) { super(props); this.reloadPlugin = this.reloadPlugin.bind(this); } plugin: | FlipperPlugin | FlipperDevicePlugin | null | undefined; 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.details.id) ?.triggerDeepLink(deepLinkPayload); } } processMessageQueue() { const {pluginKey, pendingMessages, activePlugin, 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 || activePlugin.status !== 'enabled' || isDevicePluginDefinition(activePlugin.definition) ) { return; } if ( target instanceof Client && activePlugin && pluginKey && pendingMessages?.length ) { const start = Date.now(); this.idler = new IdlerImpl(); processMessageQueue( target.sandyPluginStates.get(activePlugin.definition.id)!, 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.definition.id, ); }) .catch((err) => console.error('Error while processing plugin message queue', err), ); } } } cancelCurrentQueue() { if (this.idler && !this.idler.isCancelled()) { this.idler.cancel(); } } render() { const {activePlugin, pendingMessages} = this.props; if (!activePlugin) { return this.renderNoPluginActive(); } if (activePlugin.status !== 'enabled') { return this.renderPluginInfo(); } if (!pendingMessages || pendingMessages.length === 0) { return this.renderPlugin(); } return this.renderPluginLoader(); } renderPluginInfo() { if (isTest()) { // Plugin info uses Antd animations, generating a gazillion warnings return 'Stubbed plugin info'; } return ; } renderPluginLoader() { return ( ); } renderNoPluginActive() { if (isTest()) { return <>No plugin selected; // to keep 'nothing' clearly recognisable in unit tests } 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 {activePlugin, pluginKey, target, latestInstalledVersion} = this.props; if ( !activePlugin || !target || !pluginKey || activePlugin.status !== 'enabled' ) { console.warn(`No selected plugin. Rendering empty!`); return this.renderNoPluginActive(); } const showUpdateAlert = latestInstalledVersion && activePlugin && !this.state.autoUpdateAlertSuppressed.has( `${latestInstalledVersion.name}@${latestInstalledVersion.version}`, ) && semver.gt( latestInstalledVersion.version, activePlugin.definition.version, ); // Make sure we throw away the container for different pluginKey! const instance = target.sandyPluginStates.get(activePlugin.definition.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(); } return (
{showUpdateAlert && ( Plugin "{activePlugin.definition.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 /> )}
<_SandyPluginRenderer key={pluginKey} plugin={instance} />
); } } export default connect( (state: Store) => { let pluginKey: string | null = null; let target: BaseDevice | Client | null = null; const { connections: {selectedDevice, deepLinkPayload}, plugins: {installedPlugins}, pluginMessageQueue, } = state; const selectedClient = getActiveClient(state); const activePlugin = getActivePlugin(state); if (activePlugin) { if (selectedDevice && isDevicePlugin(activePlugin)) { target = selectedDevice; pluginKey = getPluginKey( selectedDevice.serial, activePlugin.details.id, ); } else if (selectedClient) { target = selectedClient; pluginKey = getPluginKey(selectedClient.id, activePlugin.details.id); } } const pendingMessages = pluginKey ? pluginMessageQueue[pluginKey] : undefined; const s: StateFromProps = { activePlugin, target, deepLinkPayload, pluginKey, pendingMessages, latestInstalledVersion: installedPlugins.get( activePlugin?.details?.name ?? '', ), }; return s; }, { selectPlugin, setStaticView, enablePlugin: switchPlugin, loadPlugin, }, )(PluginContainer);