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:
John Knox
2019-04-30 11:02:56 -07:00
committed by Facebook Github Bot
parent 4c282cea1f
commit b3ec8b052b
15 changed files with 62 additions and 65 deletions

View 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.

View 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 &params,
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.

View 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,
});
```

View 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.

View 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
View 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
View 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
```

View 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.

View 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
View 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);
}
```

View 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.