Introduce PluginRenderer to render plugins
Summary: PluginContainer will now wrap Sandy plugins in PluginRenderer. PluginRenderer will also be used by plugin unit tests in the future Reviewed By: jknoxville Differential Revision: D22159359 fbshipit-source-id: 69f9c8f4bec9392022c1d7a14957f5aca0339d97
This commit is contained in:
committed by
Facebook GitHub Bot
parent
ba01fa5bc9
commit
f2c39aed55
@@ -47,6 +47,7 @@ import {Message} from './reducers/pluginMessageQueue';
|
|||||||
import {Idler} from './utils/Idler';
|
import {Idler} from './utils/Idler';
|
||||||
import {processMessageQueue} from './utils/messageQueue';
|
import {processMessageQueue} from './utils/messageQueue';
|
||||||
import {ToggleButton, SmallText} from './ui';
|
import {ToggleButton, SmallText} from './ui';
|
||||||
|
import {SandyPluginRenderer} from 'flipper-plugin';
|
||||||
|
|
||||||
const Container = styled(FlexColumn)({
|
const Container = styled(FlexColumn)({
|
||||||
width: 0,
|
width: 0,
|
||||||
@@ -332,53 +333,73 @@ class PluginContainer extends PureComponent<Props, State> {
|
|||||||
console.warn(`No selected plugin. Rendering empty!`);
|
console.warn(`No selected plugin. Rendering empty!`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
let pluginElement: null | React.ReactElement<any>;
|
||||||
if (isSandyPlugin(activePlugin)) {
|
if (isSandyPlugin(activePlugin)) {
|
||||||
// TODO:
|
if (target instanceof Client) {
|
||||||
return null;
|
// Make sure we throw away the container for different pluginKey!
|
||||||
}
|
pluginElement = (
|
||||||
const props: PluginProps<Object> & {
|
<SandyPluginRenderer
|
||||||
key: string;
|
key={pluginKey}
|
||||||
ref: (
|
plugin={target.sandyPluginStates.get(activePlugin.id)!}
|
||||||
ref:
|
/>
|
||||||
| FlipperPlugin<any, any, any>
|
);
|
||||||
| FlipperDevicePlugin<any, any, any>
|
} else {
|
||||||
| null
|
// TODO: target might be a device as well, support that T68738317
|
||||||
| undefined,
|
pluginElement = null;
|
||||||
) => void;
|
}
|
||||||
} = {
|
} else {
|
||||||
key: pluginKey,
|
const props: PluginProps<Object> & {
|
||||||
logger: this.props.logger,
|
key: string;
|
||||||
selectedApp,
|
ref: (
|
||||||
settingsState,
|
ref:
|
||||||
persistedState: activePlugin.defaultPersistedState
|
| FlipperPlugin<any, any, any>
|
||||||
? {
|
| FlipperDevicePlugin<any, any, any>
|
||||||
...activePlugin.defaultPersistedState,
|
| null
|
||||||
...pluginState,
|
| 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: 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;
|
||||||
}
|
}
|
||||||
: pluginState,
|
},
|
||||||
setStaticView: (payload: StaticView) => this.props.setStaticView(payload),
|
ref: this.refChanged,
|
||||||
setPersistedState: (state) => setPluginState({pluginKey, state}),
|
isArchivedDevice,
|
||||||
target,
|
settingsState,
|
||||||
deepLinkPayload: this.props.deepLinkPayload,
|
};
|
||||||
selectPlugin: (pluginID: string, deepLinkPayload: string | null) => {
|
pluginElement = React.createElement(activePlugin, props);
|
||||||
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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Container key="plugin">
|
<Container key="plugin">
|
||||||
@@ -386,7 +407,7 @@ class PluginContainer extends PureComponent<Props, State> {
|
|||||||
heading={`Plugin "${
|
heading={`Plugin "${
|
||||||
activePlugin.title || 'Unknown'
|
activePlugin.title || 'Unknown'
|
||||||
}" encountered an error during render`}>
|
}" encountered an error during render`}>
|
||||||
{React.createElement(activePlugin, props)}
|
{pluginElement}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Container>
|
</Container>
|
||||||
<SidebarContainer id="detailsSidebar" />
|
<SidebarContainer id="detailsSidebar" />
|
||||||
|
|||||||
@@ -7,10 +7,14 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useContext} from 'react';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import {FlipperPlugin} from '../plugin';
|
import {FlipperPlugin} from '../plugin';
|
||||||
import {renderMockFlipperWithPlugin} from '../test-utils/createMockFlipperWithPlugin';
|
import {
|
||||||
|
renderMockFlipperWithPlugin,
|
||||||
|
createMockPluginDetails,
|
||||||
|
} from '../test-utils/createMockFlipperWithPlugin';
|
||||||
|
import {SandyPluginContext, SandyPluginDefinition} from 'flipper-plugin';
|
||||||
|
|
||||||
interface PersistedState {
|
interface PersistedState {
|
||||||
count: 1;
|
count: 1;
|
||||||
@@ -79,3 +83,55 @@ test('Plugin container can render plugin and receive updates', async () => {
|
|||||||
|
|
||||||
expect((await renderer.findByTestId('counter')).textContent).toBe('2');
|
expect((await renderer.findByTestId('counter')).textContent).toBe('2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('PluginContainer can render Sandy plugins', async () => {
|
||||||
|
let renders = 0;
|
||||||
|
|
||||||
|
function MySandyPlugin() {
|
||||||
|
renders++;
|
||||||
|
const sandyContext = useContext(SandyPluginContext);
|
||||||
|
expect(sandyContext).not.toBe(null);
|
||||||
|
return <div>Hello from Sandy</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin = () => ({});
|
||||||
|
|
||||||
|
const definition = new SandyPluginDefinition(createMockPluginDetails(), {
|
||||||
|
plugin,
|
||||||
|
Component: MySandyPlugin,
|
||||||
|
});
|
||||||
|
// any cast because this plugin is not enriched with the meta data that the plugin loader
|
||||||
|
// normally adds. Our further sandy plugin test infra won't need this, but
|
||||||
|
// for this test we do need to act a s a loaded plugin, to make sure PluginContainer itself can handle it
|
||||||
|
const {renderer, act, sendMessage} = await renderMockFlipperWithPlugin(
|
||||||
|
definition,
|
||||||
|
);
|
||||||
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="css-1orvm1g-View-FlexBox-FlexColumn"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Hello from Sandy
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="css-bxcvv9-View-FlexBox-FlexRow"
|
||||||
|
id="detailsSidebar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
expect(renders).toBe(1);
|
||||||
|
|
||||||
|
// sending a new message doesn't cause a re-render
|
||||||
|
act(() => {
|
||||||
|
sendMessage('inc', {delta: 2});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: check that onConnect is called T68683507
|
||||||
|
// TODO: check that messages have arrived T68683442
|
||||||
|
|
||||||
|
expect(renders).toBe(1);
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
import {
|
||||||
|
createMockFlipperWithPlugin,
|
||||||
|
createMockPluginDetails,
|
||||||
|
} from '../../test-utils/createMockFlipperWithPlugin';
|
||||||
import {Store, Client} from '../../';
|
import {Store, Client} from '../../';
|
||||||
import {selectPlugin, starPlugin} from '../../reducers/connections';
|
import {selectPlugin, starPlugin} from '../../reducers/connections';
|
||||||
import {registerPlugins} from '../../reducers/plugins';
|
import {registerPlugins} from '../../reducers/plugins';
|
||||||
@@ -21,18 +24,7 @@ interface PersistedState {
|
|||||||
count: 1;
|
count: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginDetails = {
|
const pluginDetails = createMockPluginDetails();
|
||||||
id: 'TestPlugin',
|
|
||||||
dir: '',
|
|
||||||
name: 'TestPlugin',
|
|
||||||
specVersion: 0,
|
|
||||||
entry: '',
|
|
||||||
isDefault: false,
|
|
||||||
main: '',
|
|
||||||
source: '',
|
|
||||||
title: 'Testing Plugin',
|
|
||||||
version: '',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
let TestPlugin: SandyPluginDefinition;
|
let TestPlugin: SandyPluginDefinition;
|
||||||
|
|
||||||
@@ -114,11 +106,10 @@ test('it should not initialize a sandy plugin if not enabled', async () => {
|
|||||||
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
|
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||||
|
|
||||||
const Plugin2 = new SandyPluginDefinition(
|
const Plugin2 = new SandyPluginDefinition(
|
||||||
{
|
createMockPluginDetails({
|
||||||
...pluginDetails,
|
|
||||||
name: 'Plugin2',
|
name: 'Plugin2',
|
||||||
id: 'Plugin2',
|
id: 'Plugin2',
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
plugin: jest.fn().mockImplementation((client) => {
|
plugin: jest.fn().mockImplementation((client) => {
|
||||||
const destroyStub = jest.fn();
|
const destroyStub = jest.fn();
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ import Client, {ClientQuery} from '../Client';
|
|||||||
|
|
||||||
import {buildClientId} from '../utils/clientUtils';
|
import {buildClientId} from '../utils/clientUtils';
|
||||||
import {Logger} from '../fb-interfaces/Logger';
|
import {Logger} from '../fb-interfaces/Logger';
|
||||||
import {FlipperPlugin, PluginDefinition} from '../plugin';
|
import {PluginDefinition} from '../plugin';
|
||||||
import {registerPlugins} from '../reducers/plugins';
|
import {registerPlugins} from '../reducers/plugins';
|
||||||
import PluginContainer from '../PluginContainer';
|
import PluginContainer from '../PluginContainer';
|
||||||
import {getPluginKey} from '../utils/pluginUtils';
|
import {getPluginKey} from '../utils/pluginUtils';
|
||||||
import {getInstance} from '../fb-stubs/Logger';
|
import {getInstance} from '../fb-stubs/Logger';
|
||||||
|
import {PluginDetails} from 'flipper-plugin-lib';
|
||||||
|
|
||||||
type MockFlipperResult = {
|
type MockFlipperResult = {
|
||||||
client: Client;
|
client: Client;
|
||||||
@@ -182,7 +183,7 @@ export async function createMockFlipperWithPlugin(
|
|||||||
type Renderer = RenderResult<typeof import('testing-library__dom/queries')>;
|
type Renderer = RenderResult<typeof import('testing-library__dom/queries')>;
|
||||||
|
|
||||||
export async function renderMockFlipperWithPlugin(
|
export async function renderMockFlipperWithPlugin(
|
||||||
pluginClazz: typeof FlipperPlugin,
|
pluginClazz: PluginDefinition,
|
||||||
): Promise<
|
): Promise<
|
||||||
MockFlipperResult & {
|
MockFlipperResult & {
|
||||||
renderer: Renderer;
|
renderer: Renderer;
|
||||||
@@ -219,3 +220,21 @@ export async function renderMockFlipperWithPlugin(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createMockPluginDetails(
|
||||||
|
details?: Partial<PluginDetails>,
|
||||||
|
): PluginDetails {
|
||||||
|
return {
|
||||||
|
id: 'TestPlugin',
|
||||||
|
dir: '',
|
||||||
|
name: 'TestPlugin',
|
||||||
|
specVersion: 0,
|
||||||
|
entry: '',
|
||||||
|
isDefault: false,
|
||||||
|
main: '',
|
||||||
|
source: '',
|
||||||
|
title: 'Testing Plugin',
|
||||||
|
version: '',
|
||||||
|
...details,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,3 +9,5 @@
|
|||||||
|
|
||||||
export * from './plugin/Plugin';
|
export * from './plugin/Plugin';
|
||||||
export * from './plugin/SandyPluginDefinition';
|
export * from './plugin/SandyPluginDefinition';
|
||||||
|
export * from './plugin/PluginRenderer';
|
||||||
|
export * from './plugin/PluginContext';
|
||||||
|
|||||||
23
desktop/flipper-plugin/src/plugin/PluginContext.tsx
Normal file
23
desktop/flipper-plugin/src/plugin/PluginContext.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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 {createContext} from 'react';
|
||||||
|
|
||||||
|
export type SandyPluginContext = {
|
||||||
|
deactivate(): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: to be filled in later with testing and such
|
||||||
|
const stubPluginContext: SandyPluginContext = {
|
||||||
|
deactivate() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SandyPluginContext = createContext<SandyPluginContext>(
|
||||||
|
stubPluginContext,
|
||||||
|
);
|
||||||
38
desktop/flipper-plugin/src/plugin/PluginRenderer.tsx
Normal file
38
desktop/flipper-plugin/src/plugin/PluginRenderer.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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 React, {memo, useEffect, createElement} from 'react';
|
||||||
|
import {SandyPluginContext} from './PluginContext';
|
||||||
|
import {SandyPluginInstance} from './Plugin';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
plugin: SandyPluginInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a Sandy plugin container
|
||||||
|
*/
|
||||||
|
export const SandyPluginRenderer = memo(
|
||||||
|
({plugin}: Props) => {
|
||||||
|
useEffect(() => {
|
||||||
|
plugin.deactivate();
|
||||||
|
}, [plugin]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SandyPluginContext.Provider value={plugin}>
|
||||||
|
{createElement(plugin.definition.module.Component)}
|
||||||
|
</SandyPluginContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// One of the goals of the ModernPluginContainer is that we want to prevent it from rendering
|
||||||
|
// for any outside change. Whatever happens outside of us, we don't care. If it is relevant for use, we take care about it from the insde
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user