Files
flipper/desktop/app/src/PluginContainer.tsx
Michel Weststrate 1029a6c97c Introduce types for Sandy plugins through code base
Summary:
So far there were 2 types of plugins: `FlipperPlugin` and `FlipperDevicePlugin`. This introduces a third kind: `SandyPluginDefinition`.

Unlike with the old plugins, the export of the module is not directly exposed as the plugin definition. Rather, we use class `SandyPluginDefinition` (instance) that holds a loaded definition and its meta data separately (`PluginDetails`). This means that we don't have to mix in and mutate loaded definitions, and that for unit tests we can avoid needing to provide a bunch of meta data. This also prevents a bunch of meta data existing on two places: on the loaded classes as static fields, and in the meta data field of the loaded class as well. Finally, we can now freely extends the `PluginDetails` interface in flipper, without needing to store it on the loaded classes and we are sure that no naming conflicts are caused by this in the future.

For compatibility with the existing code base, common fields are delegated from the `SandyPluginDefinition` class to the meta data.

Also cleaned up types around plugins a little bit and removed some unnecessary casts.

For all features that reason about plugins in general (such as exports), sandy plugins are ignored for now.

`SandyPluginInstance` is worked out in further diffs

The `instanceof` calls are replaced by a utility function in later diffs.

{F241363645}

Reviewed By: jknoxville

Differential Revision: D22091432

fbshipit-source-id: 3aa6b12fda5925268913779f3c3c9e84494438f8
2020-07-01 09:12:35 -07:00

470 lines
12 KiB
TypeScript

/**
* 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,
} 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,
ArchivedDevice,
Glyph,
Label,
VBox,
View,
} from 'flipper';
import {
StaticView,
setStaticView,
pluginIsStarred,
starPlugin,
} from './reducers/connections';
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 {Idler} from './utils/Idler';
import {processMessageQueue} from './utils/messageQueue';
import {ToggleButton, SmallText} from './ui';
import {SandyPluginDefinition} from 'flipper-plugin';
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,
background: colors.light02,
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
});
function ProgressBar({progress}: {progress: number}) {
return (
<ProgressBarContainer>
<ProgressBarBar progress={progress} />
</ProgressBarContainer>
);
}
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 = {
pluginState: Object;
activePlugin: PluginDefinition | undefined;
target: Client | BaseDevice | null;
pluginKey: string | null;
deepLinkPayload: string | null;
selectedApp: string | null;
isArchivedDevice: boolean;
pendingMessages: Message[] | undefined;
pluginIsEnabled: boolean;
settingsState: Settings;
};
type DispatchFromProps = {
selectPlugin: (payload: {
selectedPlugin: string | null;
selectedApp?: string | null;
deepLinkPayload: string | null;
}) => any;
setPluginState: (payload: {pluginKey: string; state: any}) => void;
setStaticView: (payload: StaticView) => void;
starPlugin: typeof starPlugin;
};
type Props = StateFromProps & DispatchFromProps & OwnProps;
type State = {
progress: {current: number; total: number};
};
class PluginContainer extends PureComponent<Props, State> {
static contextType = ReactReduxContext;
plugin:
| FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any>
| null
| undefined;
refChanged = (
ref:
| FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any>
| null
| undefined,
) => {
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?: Idler;
pluginBeingProcessed: string | null = null;
state = {progress: {current: 0, total: 0}};
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();
}
processMessageQueue() {
const {
pluginKey,
pendingMessages,
activePlugin,
pluginIsEnabled,
} = this.props;
if (pluginKey !== this.pluginBeingProcessed) {
this.pluginBeingProcessed = pluginKey;
this.cancelCurrentQueue();
this.setState({progress: {current: 0, total: 0}});
if (
pluginIsEnabled &&
activePlugin &&
// TODO: support sandy: T68683442
!(activePlugin instanceof SandyPluginDefinition) &&
activePlugin.persistedStateReducer &&
pluginKey &&
pendingMessages?.length
) {
const start = Date.now();
this.idler = new Idler();
processMessageQueue(
activePlugin,
pluginKey,
this.store,
(progress) => {
this.setState({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) {
console.warn(`No selected plugin. Rendering empty!`);
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 (
<View grow>
<Waiting>
<VBox>
<FlexRow>
<Label
style={{
fontSize: '16px',
color: colors.light30,
textTransform: 'uppercase',
}}>
{activePlugin.title}
</Label>
</FlexRow>
</VBox>
<VBox>
<ToggleButton
toggled={false}
onClick={() => {
this.props.starPlugin({
selectedPlugin: activePlugin.id,
selectedApp: (this.props.target as Client).query.app,
});
}}
large
/>
</VBox>
<VBox>
<SmallText>Click to enable this plugin</SmallText>
</VBox>
</Waiting>
</View>
);
}
renderPluginLoader() {
return (
<View grow>
<Waiting>
<VBox>
<Glyph
name="dashboard"
variant="outline"
size={24}
color={colors.light30}
/>
</VBox>
<VBox>
<Label>
Processing {this.state.progress.total} events for{' '}
{this.props.activePlugin?.id ?? 'plugin'}
</Label>
</VBox>
<VBox>
<ProgressBar
progress={this.state.progress.current / this.state.progress.total}
/>
</VBox>
</Waiting>
</View>
);
}
renderPlugin() {
const {
pluginState,
setPluginState,
activePlugin,
pluginKey,
target,
isArchivedDevice,
selectedApp,
settingsState,
} = this.props;
if (!activePlugin || !target || !pluginKey) {
console.warn(`No selected plugin. Rendering empty!`);
return null;
}
if (activePlugin instanceof SandyPluginDefinition) {
// TODO:
return null;
}
const props: PluginProps<Object> & {
key: string;
ref: (
ref:
| FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any>
| null
| undefined,
) => void;
} = {
key: pluginKey,
logger: this.props.logger,
selectedApp,
settingsState,
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: string | null) => {
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,
};
return (
<React.Fragment>
<Container key="plugin">
<ErrorBoundary
heading={`Plugin "${
activePlugin.title || 'Unknown'
}" encountered an error during render`}>
{React.createElement(activePlugin, props)}
</ErrorBoundary>
</Container>
<SidebarContainer id="detailsSidebar" />
</React.Fragment>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
connections: {
selectedPlugin,
selectedDevice,
selectedApp,
clients,
deepLinkPayload,
userStarredPlugins,
},
pluginStates,
plugins: {devicePlugins, clientPlugins},
pluginMessageQueue,
settingsState,
}) => {
let pluginKey = null;
let target = null;
let activePlugin: PluginDefinition | undefined;
let pluginIsEnabled = false;
if (selectedPlugin) {
activePlugin = devicePlugins.get(selectedPlugin);
target = selectedDevice;
if (selectedDevice && activePlugin) {
pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id);
pluginIsEnabled = true;
} else {
target =
clients.find((client: Client) => client.id === selectedApp) || null;
activePlugin = clientPlugins.get(selectedPlugin);
if (activePlugin && target) {
pluginKey = getPluginKey(target.id, activePlugin.id);
pluginIsEnabled = pluginIsStarred(
userStarredPlugins,
selectedApp,
activePlugin.id,
);
}
}
}
const isArchivedDevice = !selectedDevice
? false
: selectedDevice instanceof ArchivedDevice;
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,
};
return s;
},
{
setPluginState,
selectPlugin,
setStaticView,
starPlugin,
},
)(PluginContainer);