Add Setup and Extending top level navs
Summary: Distinguish between integrating flipper, and developing plugins. Reviewed By: passy Differential Revision: D15148448 fbshipit-source-id: 7c772fa1cea7d5ed789a984039afc37bc0b8a927
This commit is contained in:
committed by
Facebook Github Bot
parent
4c282cea1f
commit
b3ec8b052b
16
docs/extending/architecture.md
Normal file
16
docs/extending/architecture.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
id: arch
|
||||
title: Architecture
|
||||
---
|
||||
|
||||
Flipper is built to be a universal pluggable platform for development tools. Currently Flipper focuses on Android and iOS development but its design does not limit it to these platforms. Another way to think of Flipper is a more general purpose implementation of Chrome DevTools.
|
||||
|
||||
### Overview
|
||||
Flipper consists of a desktop interface built with javascript on top of Electron so that it can be packaged to run on any operating system.
|
||||
|
||||
This desktop app connects over a [tcp connection](establishing-a-connection) to applications running on simulators and connected devices. An application running on a device or simulator is what we refer to as a client.
|
||||
|
||||
The connection is bi-directional allowing the desktop to query information from the client as well as the client to push updates directly to the desktop.
|
||||
|
||||
By querying data and responding to pushing from the client a Flipper plugin is able to visualize data, debug problems, and change behavior of running applications. Flipper provides the platform to build these tools on top of and does not limit what kind of tools that may be.
|
||||
There are two kinds of plugins in Flipper, client plugins and desktop plugins. Client plugins expose information as an API to desktop plugins whose responsibility it is to render this information in a easy-to-digest way. Client plugins are written once for each platform in the platform's native language. Desktop plugins are written only once in JavaScript using React and consume the APIs exposed by the client plugins.
|
||||
150
docs/extending/create-plugin.md
Normal file
150
docs/extending/create-plugin.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
id: create-plugin
|
||||
title: Mobile Setup
|
||||
---
|
||||
|
||||
## Implement FlipperPlugin
|
||||
|
||||
Create a class implementing `FlipperPlugin`. The ID that is returned from your implementation needs to match the `name` defined in your JavaScript counterpart's `package.json`.
|
||||
|
||||
### Android
|
||||
|
||||
```java
|
||||
public class MyFlipperPlugin implements FlipperPlugin {
|
||||
private FlipperConnection mConnection;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "MyFlipperPlugin";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(FlipperConnection connection) throws Exception {
|
||||
mConnection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() throws Exception {
|
||||
mConnection = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean runInBackground() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
```objective-c
|
||||
@interface MyFlipperPlugin : NSObject<FlipperPlugin>
|
||||
@end
|
||||
|
||||
@implementation MyFlipperPlugin
|
||||
|
||||
- (NSString*)identifier { return @"MyFlipperPlugin"; }
|
||||
- (void)didConnect:(FlipperConnection*)connection {}
|
||||
- (void)didDisonnect {}
|
||||
- (BOOL)runInBackground {}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### C++
|
||||
|
||||
```c++
|
||||
class MyFlipperPlugin : public FlipperPlugin {
|
||||
public:
|
||||
std::string identifier() const override { return "MyFlipperPlugin"; }
|
||||
void didConnect(std::shared_ptr<FlipperConnection> conn) override;
|
||||
void didDisconnect() override;
|
||||
bool runInBackground() override;
|
||||
};
|
||||
```
|
||||
|
||||
## Using FlipperConnection
|
||||
|
||||
Using the `FlipperConnection` object you can register a receiver of a desktop method call and respond with data.
|
||||
|
||||
### Android
|
||||
|
||||
```java
|
||||
connection.receive("getData", new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
|
||||
responder.success(
|
||||
new FlipperObject.Builder()
|
||||
.put("data", MyData.get())
|
||||
.build());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
```objective-c
|
||||
@interface MyFlipperPlugin : NSObject<FlipperPlugin>
|
||||
@end
|
||||
|
||||
@implementation MyFlipperPlugin
|
||||
|
||||
- (NSString*)identifier { return @"MyFlipperPlugin"; }
|
||||
|
||||
- (void)didConnect:(FlipperConnection*)connection
|
||||
{
|
||||
[connection receive:@"getData" withBlock:^(NSDictionary *params, FlipperResponder *responder) {
|
||||
[responder success:@{
|
||||
@"data":[MyData get],
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)didDisonnect {}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### C++
|
||||
|
||||
```c++
|
||||
void MyFlipperPlugin::didConnect(std::shared_ptr<FlipperConnection> conn) {
|
||||
conn->receive("getData", [](const folly::dynamic ¶ms,
|
||||
std::unique_ptr<FlipperResponder> responder) {
|
||||
dynamic response = folly::dynamic::object("data", getMyData());
|
||||
responder->success(response);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Push data to the desktop
|
||||
|
||||
You don't have to wait for the desktop to request data though, you can also push data directly to the desktop.
|
||||
|
||||
### Android
|
||||
|
||||
```java
|
||||
connection.send("MyMessage",
|
||||
new FlipperObject.Builder()
|
||||
.put("message", "Hello")
|
||||
.build()
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
```objective-c
|
||||
[connection send:@"getData" withParams:@{@"message":@"hello"}];
|
||||
```
|
||||
|
||||
### C++
|
||||
|
||||
```c++
|
||||
void MyFlipperPlugin::didConnect(std::shared_ptr<FlipperConnection> conn) {
|
||||
dynamic message = folly::dynamic::object("message", "hello");
|
||||
conn->send("getData", message);
|
||||
}
|
||||
```
|
||||
|
||||
## Background Plugins
|
||||
|
||||
If the plugin returns false in `runInBackground()`, then the Flipper app will only accept messages from the client side when the plugin is active (i.e. when user is using the plugin in the Flipper app). Whereas with the plugin marked as `runInBackground`, it can send messages even when the plugin is not in active use. The benefit is that the data can be processed in the background and notifications can be fired. It also reduces the number of rerenders and time taken to display the data when the plugin becomes active. As the data comes in the background, it is processed and a state is updated in the Redux store. When the plugin becomes active, the initial render will contain all the data.
|
||||
82
docs/extending/create-table-plugin.md
Normal file
82
docs/extending/create-table-plugin.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
id: create-table-plugin
|
||||
title: Create Table Plugin
|
||||
---
|
||||
|
||||
A very common kind of Flipper plugin is a plugin which fetches some structured data from the device and presents it in a table.
|
||||
|
||||
To make building these kinds of plugins as easy as possible we have created an abstraction we call `createTablePlugin`. This is a function which manages the complexities of building a table plugin but still allows you to customize many things to suite your needs.
|
||||
|
||||
Below is a sample implementation of a desktop plugin based on `createTablePlugin`. It subscribes to updates from a client send using the `newRow` method. A row can have any structure you want as long as it has a unique field `id` of type `string`.
|
||||
|
||||
See "[Create Plugin](create-plugin.md)" for how to create the native counterpart for your plugin.
|
||||
|
||||
```javascript
|
||||
import {ManagedDataInspector, Panel, Text, createTablePlugin} from 'flipper';
|
||||
|
||||
type Id = string;
|
||||
|
||||
type Row = {
|
||||
id: Id,
|
||||
column1: string,
|
||||
column2: string,
|
||||
column3: string,
|
||||
extras: Object,
|
||||
};
|
||||
|
||||
function buildRow(row: Row) {
|
||||
return {
|
||||
columns: {
|
||||
column1: {
|
||||
value: <Text>{row.column1}</Text>,
|
||||
filterValue: row.column1,
|
||||
},
|
||||
column2: {
|
||||
value: <Text>{row.column2}</Text>,
|
||||
filterValue: row.column2,
|
||||
},
|
||||
column3: {
|
||||
value: <Text>{row.column3}</Text>,
|
||||
filterValue: row.column3,
|
||||
},
|
||||
},
|
||||
key: row.id,
|
||||
copyText: JSON.stringify(row),
|
||||
filterValue: `${row.column1} ${row.column2} ${row.column3}`,
|
||||
};
|
||||
}
|
||||
|
||||
function renderSidebar(row: Row) {
|
||||
return (
|
||||
<Panel floating={false} heading={'Extras'}>
|
||||
<ManagedDataInspector data={JSON.parse(row.extras)} expandRoot={true} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
const columns = {
|
||||
time: {
|
||||
value: 'Column1',
|
||||
},
|
||||
module: {
|
||||
value: 'Column2',
|
||||
},
|
||||
name: {
|
||||
value: 'Column3',
|
||||
},
|
||||
};
|
||||
|
||||
const columnSizes = {
|
||||
time: '15%',
|
||||
module: '20%',
|
||||
name: 'flex',
|
||||
};
|
||||
|
||||
export default createTablePlugin({
|
||||
method: 'newRow', // Method which should be subscribed to to get new rows with share Row (from above),
|
||||
columns,
|
||||
columnSizes,
|
||||
renderSidebar,
|
||||
buildRow,
|
||||
});
|
||||
```
|
||||
31
docs/extending/error-handling.md
Normal file
31
docs/extending/error-handling.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
id: error-handling
|
||||
title: Error Handling
|
||||
---
|
||||
|
||||
Errors in Flipper plugins should be hidden from the user while providing actionable data to the plugin developer.
|
||||
|
||||
## Android
|
||||
|
||||
To gracefully handle errors in Flipper we provide the `ErrorReportingRunnable` class. This is a custom runnable which catches all exceptions, stopping them from crashing the application and reporting them to the plugin developer.
|
||||
|
||||
```java
|
||||
new ErrorReportingRunnable(mConnection) {
|
||||
@Override
|
||||
public void runOrThrow() throws Exception {
|
||||
mightThrowException();
|
||||
}
|
||||
}.run();
|
||||
```
|
||||
|
||||
Executing this block of code will always finish without error but may transfer any silences error to the Flipper desktop app. During plugin development these java stack traces are surfaced in the chrome dev console. In production the errors are instead sent to and a task is assigned so that you can quickly deploy a fix.
|
||||
|
||||
Always use `ErrorReportingRunnable` for error handling instead of `try`/`catch` or even worse letting errors crash the app. With ErrorReportingRunnable you won't block anyone and you won't hide any stack traces.
|
||||
|
||||
## C++
|
||||
|
||||
To gracefully handle errors in Flipper we perform all transactions inside of a try block which catches all exceptions, stopping them from crashing the application and reporting them to the plugin developer. This includes your own customs implementations of `FlipperPlugin::didConnect()` and `FlipperConnection::send()` and `::receive()`!
|
||||
|
||||
That means you can safely throw exceptions in your plugin code. The exception messages will be sent to the Flipper desktop app. During plugin development the exception messages are surfaced in the Chrome dev console.
|
||||
|
||||
If your plugin performs asynchronous work in which exceptions are thrown, these exceptions will not be caught by the Flipper infrastructure. You should handle them appropriately.
|
||||
31
docs/extending/establishing-a-connection.md
Normal file
31
docs/extending/establishing-a-connection.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
id: establishing-a-connection
|
||||
title: Establishing a connection
|
||||
---
|
||||
|
||||
Below is an outline of how a connection is established between an app with with our SDK integrated, and the desktop app. This all goes on behind the scenes inside the mobile SDK, so users shouldn't need to worry about it.
|
||||
|
||||
## Transport Protocol
|
||||
|
||||
Flipper uses [RSocket](http://rsocket.io/) to communicate between the desktop and mobile apps. RSocket allows for bi-directional communication.
|
||||
|
||||
## Client-Server relationship
|
||||
|
||||
When the desktop app starts up, it opens a secure socket on port 8088.
|
||||
Any mobile app with the Flipper SDK installed will continually attempt to connect to this port on localhost to establish a connection with the desktop app.
|
||||
|
||||
## Certificate Exchange
|
||||
|
||||
To avoid mobile apps from connecting to untrusted ports on localhost, they will only connect to servers that have a valid, trusted TLS certificate.
|
||||
In order for the mobile app to know which certificates it can trust, it conducts a certificate exchange with the desktop app before it can make its first secure connection.
|
||||
|
||||
This is achieved through the following steps:
|
||||
* Desktop app starts an insecure server on port 8089.
|
||||
* Mobile app connects to localhost:8089 and sends a Certificate Signing Request to the desktop app.
|
||||
* Desktop app uses it's private key (this is generated once and stored in ~/.flipper) to sign a client certificate for the mobile app.
|
||||
* The desktop uses ADB (for android), or the mounted file system (for iOS simulators) to write the following files to the mobile app's private data partition
|
||||
* Server certificate that the mobile app can now trust.
|
||||
* Client certificate for the mobile app to use going forward.
|
||||
* Now the mobile app knows which server certificate it can trust, and can connect to the secure server.
|
||||
|
||||
This allows the mobile app to trust a certificate if and only if, it is stored inside it's internal data partition. Typically it's only possible to write there with physical access to the device (i.e. through ADB or a mounted simulator).
|
||||
8
docs/extending/index.md
Normal file
8
docs/extending/index.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
id: index
|
||||
title: Clients and Plugins
|
||||
---
|
||||
|
||||
Flipper was designed with extensibility in mind from the start, to enable engineers to quickly build quality, easy-to-use tools for their own needs and applications.
|
||||
|
||||
In addition to building plugins for the existing platforms, you can also extend the capabilities of Flipper to other platforms by conforming to the `FlipperClient` API. After this, you can make use of the existing desktop plugins by writing client plugins that conform to the same API.
|
||||
52
docs/extending/jssetup.md
Normal file
52
docs/extending/jssetup.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
id: js-setup
|
||||
title: JavaScript Setup
|
||||
---
|
||||
|
||||
## Creating the plugin UI
|
||||
|
||||
To create the desktop part of your plugin, initiate a new JavaScript project using `yarn init` and make sure your package name is the plugin's ID you are using in the native implementation. Create a file called `index.js`, which is the entry point to your plugin. A sample `package.json`-file could look like this:
|
||||
|
||||
```
|
||||
{
|
||||
"name": "myplugin",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"title": "My Plugin",
|
||||
"icon": "apps",
|
||||
"bugs": {
|
||||
"email": "you@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Flipper uses some fields from the `package.json` in the plugin. The `name` is used as, the ID to identify the mobile counterpart. A `title` can be set, that is shown in Flipper's sidebar, same is true for the `icon`. We also strongly encourage to set a `bugs` field specifying an email and/or url, where bugs for the plugin can be reported.
|
||||
|
||||
In `index.js` you can now create your plugin. Take a look at [Writing a plugin](writing-a-plugin.md) to learn how a plugin can look like. Also, make sure to check out [Flipper's UI components](ui-components.md) when building your plugin.
|
||||
|
||||
### Dynamically loading plugins
|
||||
|
||||
Once a plugin is created, Flipper can load it from its folder. The path from where the plugins are loaded is specified in `~/.flipper/config.json`. The paths specified in `pluginPaths` need to point to a folder containing a subfolder for every plugin. For example you can create a directory `~/flipper-plugins` and set `pluginPaths` in Flipper's config to `["~/flipper-plugins"]`. This directory needs to contain a sub-directory for every plugin you create. In this example there would be a directory `~/flipper-plugins/myplugin` that contains your plugin's `package.json` and all code of your plugin.
|
||||
|
||||
### npm dependencies
|
||||
|
||||
If you need any dependencies in your plugin, you can install them using `yarn add`. The Flipper UI components exported from `flipper`, as well as `react` and `react-dom` don't need to be installed as dependencies. Our plugin-loader makes these dependencies available to your plugin.
|
||||
|
||||
### ES6, babel-transforms and bundling
|
||||
|
||||
Our plugin-loader is capable of all ES6 goodness, flow annotations and JSX and applies the required babel-transforms without you having to care about this. Also you don't need to bundle your plugin, you can simply use ES6 imports and it will work out of the box.
|
||||
|
||||
## Working on the core
|
||||
|
||||
If you only want to work on a plugin, you don't need to run the development build of Flipper, but you can use the production release. However, if you want to contribute to Flipper's core, add additional UI components, or do anything outside the scope of a single plugins this is how you run the development version of Flipper.
|
||||
|
||||
Make sure you have a recent version of node.js and yarn installed on your system (node ≥ 8, yarn ≥ 1.5). Then run the following commands:
|
||||
|
||||
```
|
||||
git clone https://github.com/facebook/flipper.git
|
||||
cd flipper
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
49
docs/extending/send-data.md
Normal file
49
docs/extending/send-data.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: send-data
|
||||
title: Sending Data to Plugins
|
||||
---
|
||||
|
||||
It is often useful to get an instance of a Flipper plugin to send data to it. Flipper makes this simple with built-in support.
|
||||
|
||||
Plugins should be treated as singleton instances as there can only be one `FlipperClient` and each `FlipperClient` can only have one instance of a certain plugin. The Flipper API makes this simple by offering a way to get the current client and query it for plugins.
|
||||
|
||||
Plugins are identified by the string that their identifier method returns, in this example, "MyFlipperPlugin":
|
||||
|
||||
### Android
|
||||
|
||||
```java
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
// Client may be null if AndroidFlipperClient.createInstance() was never called
|
||||
// which is the case in production builds.
|
||||
if (client != null) {
|
||||
final MyFlipperPlugin plugin = client.getPluginByClass(MyFlipperPlugin.class);
|
||||
plugin.sendData(myData);
|
||||
}
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
```objective-c
|
||||
FlipperClient *client = [FlipperClient sharedClient];
|
||||
MyFlipperPlugin *myPlugin = [client pluginWithIdentifier:@"MyFlipperPlugin"];
|
||||
[myPlugin sendData:myData];
|
||||
```
|
||||
|
||||
### C++
|
||||
|
||||
```c++
|
||||
auto &client = FlipperClient::instance();
|
||||
|
||||
// "MyFlipperPlugin is the return value of MyFlipperPlugin::identifier()
|
||||
auto aPlugin = client.getPlugin("MyFlipperPlugin");
|
||||
|
||||
// aPlugin is a std::shared_ptr<FlipperPlugin>. Downcast to expected type.
|
||||
auto myPlugin = std::static_pointer_cast<MyFlipperPlugin>(aPlugin);
|
||||
|
||||
// Alternatively, use the templated version
|
||||
myPlugin = client.getPlugin<MyFlipperPlugin>("MyFlipperPlugin");
|
||||
|
||||
myPlugin->sendData(myData);
|
||||
```
|
||||
|
||||
Here, `sendData` is an example of a method that might be implemented by the Flipper plugin.
|
||||
72
docs/extending/styling-components.md
Normal file
72
docs/extending/styling-components.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
id: styling-components
|
||||
title: Styling Components
|
||||
---
|
||||
|
||||
We are using [emotion](https://emotion.sh) to style our components. For more details on how this works, please refer to emotion's documentation. We heavily use their [Styled Components](https://emotion.sh/docs/styled) approach, which allows you to extend our built-in components.
|
||||
|
||||
## Basic tags
|
||||
|
||||
For basic building blocks (views, texts, ...) you can use the styled object.
|
||||
|
||||
```javascript
|
||||
import {styled} from 'flipper';
|
||||
|
||||
const MyView = styled('div')({
|
||||
fontSize: 10,
|
||||
color: colors.red
|
||||
});
|
||||
const MyText = styled('span')({ ... });
|
||||
const MyImage = styled('img')({ ... });
|
||||
const MyInput = styled('input')({ ... });
|
||||
```
|
||||
|
||||
## Extending Flipper Components
|
||||
|
||||
It's very common for components to require customizing Flipper's components in some way. For example changing colors, alignment, or wrapping behavior. Flippers components can be wrapped using the `styled` function which allows adding or overwriting existing style rules.
|
||||
|
||||
```javascript
|
||||
import {FlexRow, styled} from 'flipper';
|
||||
|
||||
const Container = styled(FlexRow)({
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
class MyComponent extends Component {
|
||||
render() {
|
||||
return <Container>...</Container>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
The CSS-in-JS object passed to the styled components takes just any CSS rule, with the difference that it uses camel cased keys for the properties. Pixel-values can be numbers, any other values need to be strings.
|
||||
|
||||
The style object can also be returned from a function for dynamic values. Props can be passed to the styled component using React.
|
||||
|
||||
```javascript
|
||||
const MyView = styled('div')(
|
||||
props => ({
|
||||
fontSize: 10,
|
||||
color: => (props.disabled ? colors.red : colors.black),
|
||||
})
|
||||
);
|
||||
|
||||
// usage
|
||||
<MyView disabled />
|
||||
```
|
||||
|
||||
Pseudo-classes can be used like this:
|
||||
|
||||
```javascript
|
||||
'&:hover': {color: colors.red}`
|
||||
```
|
||||
|
||||
## Colors
|
||||
|
||||
The colors module contains all standard colors used by Flipper. All the available colors are defined in `src/ui/components/colors.js` with comments about suggested usage of them. And we strongly encourage to use them. They can be required like this:
|
||||
|
||||
```javascript
|
||||
import {colors} from 'flipper'
|
||||
```
|
||||
95
docs/extending/testing.md
Normal file
95
docs/extending/testing.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
id: testing
|
||||
title: Testing
|
||||
---
|
||||
|
||||
Developer tools are only used if they work. We have built APIs to test plugins.
|
||||
|
||||
## Android
|
||||
|
||||
Start by creating your first test file in this directory `MyFlipperPluginTest.java`. In the test method body we create our plugin which we want to test as well as a `FlipperConnectionMock`. In this contrived example we simply assert that our plugin's connected status is what we expect.
|
||||
|
||||
```java
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class MyFlipperPluginTest {
|
||||
|
||||
@Test
|
||||
public void myTest() {
|
||||
final MyFlipperPlugin plugin = new MyFlipperPlugin();
|
||||
final FlipperConnectionMock connection = new FlipperConnectionMock();
|
||||
|
||||
plugin.onConnect(connection);
|
||||
assertThat(plugin.connected(), equalTo(true));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There are two mock classes that are used to construct tests `FlipperConnectionMock` and `FlipperResponderMock`. Together these can be used to write very powerful tests to verify the end to end behavior of your plugin. For example we can test if for a given incoming message our plugin responds as we expect.
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void myTest() {
|
||||
final MyFlipperPlugin plugin = new MyFlipperPlugin();
|
||||
final FlipperConnectionMock connection = new FlipperConnectionMock();
|
||||
final FlipperResponderMock responder = new FlipperResponderMock();
|
||||
|
||||
plugin.onConnect(connection);
|
||||
|
||||
final FlipperObject params = new FlipperObject.Builder()
|
||||
.put("phrase", "flipper")
|
||||
.build();
|
||||
connection.receivers.get("myMethod").onReceive(params, responder);
|
||||
|
||||
assertThat(responder.successes, hasItem(
|
||||
new FlipperObject.Builder()
|
||||
.put("phrase", "ranos")
|
||||
.build()));
|
||||
}
|
||||
```
|
||||
|
||||
## C++
|
||||
|
||||
Start by creating your first test file in this directory `MyFlipperPluginTests.cpp` and import the testing utilities from `fbsource//xplat/sonar/xplat:FlipperTestLib`. These utilities mock out core pieces of the communication channel so that you can test your plugin in isolation.
|
||||
|
||||
```
|
||||
#include <MyFlipperPlugin/MyFlipperPlugin.h>
|
||||
#include <FlipperTestLib/FlipperConnectionMock.h>
|
||||
#include <FlipperTestLib/FlipperResponderMock.h>
|
||||
|
||||
#include <folly/json.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace flipper {
|
||||
namespace test {
|
||||
|
||||
TEST(MyFlipperPluginTests, testDummy) {
|
||||
EXPECT_EQ(1 + 1, 2);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace flipper
|
||||
} // namespace facebook
|
||||
```
|
||||
|
||||
Here is a simple test using these mock utilities to create a plugin, send some data, and assert that the result is as expected.
|
||||
|
||||
```
|
||||
TEST(MyFlipperPluginTests, testDummy) {
|
||||
std::vector<folly::dynamic> successfulResponses;
|
||||
auto responder = std::make_unique<FlipperResponderMock>(&successfulResponses);
|
||||
auto conn = std::make_shared<FlipperConnectionMock>();
|
||||
|
||||
MyFlipperPlugin plugin;
|
||||
plugin.didConnect(conn);
|
||||
|
||||
folly::dynamic message = folly::dynamic::object("param1", "hello");
|
||||
folly::dynamic expectedResponse = folly::dynamic::object("response", "Hi there");
|
||||
|
||||
auto receiver = conn->receivers_["someMethod"];
|
||||
receiver(message, std::move(responder));
|
||||
|
||||
EXPECT_EQ(successfulResponses.size(), 1);
|
||||
EXPECT_EQ(successfulResponses.back(), expectedResponse);
|
||||
}
|
||||
```
|
||||
78
docs/extending/writing-a-plugin.md
Normal file
78
docs/extending/writing-a-plugin.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
id: writing-a-plugin
|
||||
title: Writing a plugin in JavaScript
|
||||
---
|
||||
|
||||
Every plugin needs to be self-contained in its own folder. At the root-level of this folder we are expecting a `package.json` for your plugin and an `index.js` file which is the entry-point to your plugin. To learn more about the basic setup of a plugin, have a look at [JavaScript Setup](jssetup.md).
|
||||
|
||||
|
||||
We expect this file to have a default export of type `FlipperPlugin`. A hello-world-plugin could look like this:
|
||||
|
||||
```js
|
||||
import {FlipperPlugin} from 'flipper';
|
||||
|
||||
export default class extends FlipperPlugin {
|
||||
render() {
|
||||
return 'hello world';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## persistedState
|
||||
|
||||
React state will be gone once your plugin unmounts. Every time the user switches between plugins, the React state will be reset. This might be fine for UI state, but doesn't make sense for your plugin's data. To persist your data when switching between plugins, you can use our `persistedState`-API.
|
||||
|
||||
Flipper passes a prop with the current `persistedState` to your plugin. You can access it via `this.props.persistedState`. To changes values in the `persistedState` you call `this.setPersistedState({...})`. Our API works similar to React's state API. You can pass a partial state object to `setPersistedState` and it will be merged with the previous state. You can also define a `static defaultPersistedState` to populate it.
|
||||
|
||||
A common pattern for plugins is to receive data from the client and store this data in `persistedState`. To allow your plugin to receive this data, even when the UI is not visible, you can implement a `static persistedStateReducer`. This reducer's job is to merge the current `persistedState` with the data received from the device. Flipper will call this method, even when your plugin is not visible and the next time your plugin is mounted, `persistedState` will already have the latest data.
|
||||
|
||||
```js
|
||||
static defaultPersistedState = {
|
||||
myData: [],
|
||||
}
|
||||
|
||||
static persistedStateReducer = (
|
||||
persistedState: PersistedState,
|
||||
method: string,
|
||||
newData: Object,
|
||||
): PersistedState => {
|
||||
// Logic to merge current state with new data
|
||||
return {
|
||||
myData: [...persistedState.myData, newData],
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Have a look at the [network plugin](https://github.com/facebook/flipper/blob/14e38c087f099a5afed4d7a1e4b5713468eabb28/src/plugins/network/index.js#L122) to see this in action. To send data while your plugin is not active, you need to opt-in on the native side of your plugin. For more info around this, read the mobile [setup](create-plugin.md).
|
||||
|
||||
## Notifications
|
||||
|
||||
Plugins can publish notifications which are displayed in Flipper's notification panel and trigger system level notifications. This can be used by your plugin to make the user aware of something that happened inside the plugin, even when the user is not looking at the plugin right now. The network plugin uses this for failed network requests. All notifications are aggregated in Flipper's notifications pane, accessible from the sidebar.
|
||||
|
||||
A notification should provide actionable and high-signal information for important events the user is likely to take action on. Notifications are generated from the data in your `persistedState`. To trigger notifications you need to implement a static function called `getActiveNotifications`. This function returns an array of all currently active notifications. To invalidate a notification, you simply stop including this notification in the array you are returning.
|
||||
|
||||
```js
|
||||
type Notification = {|
|
||||
id: string, // used to identify your notification and needs to be unique to your plugin
|
||||
title: string, // title shown in the system-level notification
|
||||
message: string, // detailed information about the event
|
||||
severity: 'warning' | 'error',
|
||||
timestamp?: number, // unix timestamp of when the event occurred
|
||||
category?: string, // used to group similar notifications (not shown to the user)
|
||||
action?: string, // passed to your plugin when navigating from a notification back to the plugin
|
||||
|};
|
||||
|
||||
static getActiveNotifications = (
|
||||
persistedState: PersistedState,
|
||||
): Array<Notification> => {
|
||||
return persistedState.myData.filter(d => d.error).map(d => {
|
||||
id: d.id,
|
||||
title: 'Something is rotten in the state of Denmark',
|
||||
message: d.errorMessage,
|
||||
severity: 'error',
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
When the user clicks on a notification, it links back into your plugin. `this.props.deepLinkPayload` will be set to the string provided in the notification's `action`. This can be used to highlight the particular part of the data that triggered the notification. In many cases the `action` will be some sort of ID for your data. You can use React's `componentDidMount` to check if a `deepLinkPayload` is provided.
|
||||
Reference in New Issue
Block a user