Desktop Plugin API
PluginClient
is the type of the client
passed into a standard Sandy plugin.
It takes two generic arguments:
Event
- mapping of an event name to the data structure of the payload, as explained in the Tutorial.Methods
- used to describe the methods that are offered by the plugin implementation on the device. 'Methods' is a mapping of a method name to a function that describes the signature of a method. The first argument of that function describes the parameters that can be passed to the client. The return type of the function describes what is returned from the client. Wrapped with aPromise
.
Following is a short example of how the above arguments can be used:
type LogEntry = {
message: string
}
// Events that can be send by the client implementation:
type Events = {
addLogEntry: LogEntry,
flushLogs: {},
}
// Methods we can invoke on the client:
type Methods = {
retrieveLogsSince(params: { since: number }): Promise<{ message: string }>,
}
export function plugin(client: PluginClient<Events, Methods>) {
// etc
}
The PluginClient
received by the plugin
exposes the following members, which are detailed in the following sub-sections.
Propertiesβ
device
β
Returns the Device this plugin is connected to.
appName
β
The name of the application, for example 'Facebook', 'Instagram' or 'Slack'.
appId
β
A string that uniquely identifies the current application and is based on a combination of the application name and device serial on which the application is running.
pluginKey
β
A key that uniquely identifies this plugin instance and captures the current device/client/plugin combination.
connected
β
isConnected
β
Returns whether there is currently an active connection; 'true' for any of the following:
- The device is still connected.
- The client is still connected.
- The plugin is currently selected by the user or the plugin is running in the background.
The connected
field provides the atom that can be used in combination with useValue
to subscribe to future updates in a component.
In contrast, isConnected
returns a boolean that merely captures the current state.
Eventsβ
onMessage
β
Usage: client.onMessage(event: string, callback: (params) => void)
Subscribes the plugin to a specific event that is fired from the client plugin (using connection.send). Typically used to update some of the state. For background plugins that are currently not active in the UI, messages won't arrive immediately, but are queued until the user opens the plugin.
Example:
type Events = {
newRow: {
id: number;
title: string;
url: string;
};
};
export function plugin(client: PluginClient<Events, {}>) {
const rows = createState<Record<string, Row>>({}, {persist: 'rows'});
client.onMessage('newRow', (row /* type will be inferred correctly */) => {
rows.update((draft) => {
draft[row.id] = row;
});
});
// etc
}
The Flipper Sample application contains a plugin that demonstrates these API, see the bi-directional-communication-demo.
onActivate
β
Usage: client.onActivate(callback: () => void)
Called when the plugin is selected by the user and mounted into the Flipper Desktop UI. See also the closely related onConnect event. Unlike onConnect
, onActivate
triggers for archived / imported devices.
onDeactivate
β
Usage: client.onDeactivate(callback: () => void)
Triggered when the plugin is unmounted from the Flipper Desktop UI because the user navigates to some other plugin. In the case the plugin is destroyed while being active, onDeactivate will still be called.
onConnect
β
Usage: client.onConnect(callback: () => void)
Triggered once the connection with the plugin on the client is established, and, for example, send can be called safely. Typically, this occurs when the plugin is activated (opened) in the Flipper Desktop. However, for background plugins, this happens immediately after the plugin has been instantiated. This lifecycle is never triggered for archived / imported devices,
onDisconnect
β
Usage: client.onDisconnect(callback: () => void)
Triggered once the connection with the plugin on the client has been lost. Typically, this occurs when the user leaves the plugin in the Flipper Desktop, when the plugin is disabled, or when the app or device has disconnected. However, for background plugins, this event won't fire when the user merely navigates somewhere else. In that case, onDeactivate can be used instead.
onDestroy
β
Usage: client.onDestroy(callback: () => void)
Called when the plugin is unloaded. This happens if the device or client has been disconnected, or when the user disables the plugin.
Note that there is no corresponding onCreate
event, since the function body of the plugin definition acts already as 'what needs to be done when the plugin is loaded/enabled'.
onDeepLink
β
Usage: client.onDeepLink(callback: (payload: unknown) => void)
Triggerred when the users navigates to this plugin using a deeplink, either from an external flipper://
plugin URL, or because the user was linked here from another plugin.
onExport
β
Usage: client.onExport(callback: (idler, onStatusMessage) => Promise<state>)
Overrides the default serialization behavior of this plugin. Should return a promise with persistable state that is to be stored, or nothing at all. This process is async, so it is possible to first fetch some additional state from the device.
Serializable is defined as 'non-cyclic data, consisting purely of primitive values, plain objects, arrays or Date, Set or Map objects'.
If nothing is returned, the handler will be run, and after the handler has finished the persist
keys of the different states will be used as export basis.
onImport
β
Usage: client.onImport(callback: (snapshot) => void)
Overrides the default de-serialization behavior of this plugin. Use it to update the state based on the snapshot data. This hook will be called immediately after constructing the plugin instance.
To synchonize the types of the data between onImport
and onExport
, it's possible to provide a type as generic to both hooks. The next example stores counter
under the count
field and stores it as string rather than as number.
type SerializedState = {
count: string;
}
export function plugin(client: PluginClient) {
const counter = createState(0);
client.onExport<SerializedState>(() => {
return {
count: "" + counter.get()
}
})
client.onImport<SerializedState>((data) => {
counter.set(parseInt(data.count, 10));
});
}
onUnhandledMessage
β
Usage: client.onUnhandledMessage(callback: (event: string, params) => void)
This method subscribes to all messages arriving from the devices which is not handled by an onMessage
handler.
This handler is untyped, and onMessage should be favored over using onUnhandledMessage if the event name is known upfront.
onServerAddOnStart
β
Experimental!
Usage: client.onServerAddOnStart(callback: () => void)
Triggered when a server add-on starts. You should send messages to the server add-on only after it connects. Do not forget to stop all communication when the add-on stops.
onServerAddOnStop
β
Experimental!
Usage: client.onServerAddOnStop(callback: () => void)
Triggered when a server add-on stops. You should stop all communication with the server add-on when the add-on stops.
onServerAddOnMessage
β
Experimental!
Usage: client.onServerAddOnMessage(event: string, callback: (params) => void)
This subscribes the plugin to a specific event that is fired from the server add-on.
Example:
type ServerAddOnEvents = {
newRow: {
id: number;
title: string;
url: string;
};
};
export function plugin(client: PluginClient<{}, {}, ServerAddOnEvents>) {
const rows = createState<Record<string, Row>>({}, {persist: 'rows'});
client.onServerAddOnMessage('newRow', (row /* type will be inferred correctly */) => {
rows.update((draft) => {
draft[row.id] = row;
});
});
// etc
}
onServerAddOnUnhandledMessage
β
Experimental!
Usage: client.onServerAddOnUnhandledMessage(callback: (event: string, params) => void)
Subscribe to all messages arriving from the server add-ons not handled by another listener. This handler is untyped, and onMessage should be favored over using onUnhandledMessage if the event name is known upfront.
onReady
β
The onReady
event is triggered immediately after a plugin has been initialized and any pending state was restored.
This event fires after onImport
/ the interpretation of any persist
flags and indicates that the initialization process has finished.
This event does not signal that the plugin is loaded in the UI yet (see onActivated
) and does fire before deeplinks (see onDeeplink
) are handled.
If a plugin has complex initialization logic it is recommended to put it in the onReady
hook, as an error in the onReady hook won't cause the plugin not to be loaded.
Methodsβ
send
β
Usage: client.send(method: string, params: object): Promise<object>
If the plugin is connected, send
can be used to invoke a method on the client implementation of the plugin.
Note that if client.isConnected
returns false
, calling client.send
will throw an exception. This is the case if for example the connection with the device or application was lost.
Generally, one should guard client.send
calls with a check to client.isConnected
.
Example:
type Methods = {
currentLogs(params: {since: number}): Promise<string[]>;
};
export function plugin(client: PluginClient<{}, Methods>) {
const logs = createState<string[]>([])
client.onConnect(async () => {
try {
const currentLogs = await client.send('currentLogs', {
since: Date.now()
})
logs.set(currentLogs)
} catch (e) {
console.error("Failed to retrieve current logs: ", e)
}
})
//etc
}
The Flipper Sample application contains a plugin that demonstrates these API, see bi-directional-communication-demo.
addMenuEntry
β
Usage: client.addMenuEntry(...entry: MenuEntry[])
This method can be used to add menu entries to the Flipper main menu while this plugin is active. It supports registering global keyboard shortcuts as well. Example:
client.addMenuEntry({
label: 'Reset Selection',
accelerator: 'CmdOrCtrl+R'
handler: () => {
// Event handling
}
}
The accelerator
argument is optional but describes the keyboard shortcut.
It is possible to leave out the label
, and accelerator
fields if a pre-defined action
is set, which configures all three of them.
The currently pre-defined actions are "Clear"
, "Go To Bottom"
and "Create Paste"
.
Example of using a pre-defined action:
client.addMenuEntry({
action: 'createPaste',
handler: async () => {
// Event handling
}
})
isPluginAvailable
β
Usage: isPluginAvailable(pluginId: string): boolean
Returns true
if a plugin with the given id is available by for consumption, that is: supported by the current application / device and enabled by the user.
selectPlugin
β
Usage: selectPlugin(pluginId: string, deeplinkPayload?: unknown): void
Opens a different plugin by id, optionally providing a deeplink to bring the target plugin to a certain state.
supportsMethod
β
Usage: client.supportsMethod(method: string): Promise<Boolean>
Method must be called within onConnect, otherwise will throw error.
Resolves to true if the client supports the specified method. Useful when adding functionality to existing plugins, when connectivity to older clients is still required. Also useful when client plugins are implemented on multiple platforms and don't all have feature parity.
showNotification
β
Usage: client.showNotification(notification)
Shows an urgent, system wide notification, that will also be registered in Flipper's notification pane.
For on-screen notifications, we recommend using either the message
or notification
API from antd
directly.
Clicking the notification will open the sending plugin. If the action
id is set, it will be used as deeplink.
The notification interface is defined as:
interface Notification {
id: string;
title: string;
message: string | React.ReactNode;
severity: 'warning' | 'error';
timestamp?: number;
category?: string;
action?: string;
};
writeTextToClipboard
β
Usage: writeTextToClipboard(text: string)
Writes text to the OS-level clipboard.
createPaste
β
This is a Facebook only method.
Usage: client.createPaste(value: string): Promise<string|undefined>
Creates a Facebook Paste (similar to a GitHub Gist) for the given value
.
The returned promise either contains a string with the URL of the paste, or undefined
if the process failed.
Details of the failure will be communicated back directly to the user through Flipper notifications. For example, if the user is currently not signed in.
GK
β
This is a Facebook only method.
Usage: client.GK(gatekeeper: string): boolean
Returns true
if the current user is part of the given GK. false
in all other cases.
logger
β
Logger instance that logs information to the console, but also to the internal logging (in FB only builds) and which can be used to track performance (see useLogger).
sendToServerAddOn
β
Usage: client.sendToServerAddOn(method: string, params: object): Promise<object>
Send a message to the server add-on.
Example:
type ServerAddOnMethods = {
currentLogs(params: {since: number}): Promise<string[]>;
};
export function plugin(client: PluginClient<{}, {}, {}, ServerAddOnMethods}>) {
const logs = createState<string[]>([])
client.onServerAddOnStart(async () => {
try {
const currentLogs = await client.sendToServerAddOn('currentLogs', {
since: Date.now()
})
logs.set(currentLogs)
} catch (e) {
console.error("Failed to retrieve current logs: ", e)
}
})
//etc
}
For an example of how to use server add-ons, see the React DevTools plugin.
DevicePluginClientβ
Propertiesβ
device
- returns the Device this plugin is connected to.
Eventsβ
The following DevicePluginClient events are identical to those with the same name for the PluginClient (each of the following events links to the relative PluginClient description):
- onDestroy
- onActivate
- onDeactivate
- onDeepLink
- onExport
- onImport
- onServerAddOnStart
- onServerAddOnStop
- onServerAddOnMessage
- onServerAddOnUnhandledMessage
Methodsβ
The following DevicePluginClient methods are identical to those with the same name for the PluginClient (each of the following methods links to the relative PluginClient description):
Deviceβ
Device
captures the metadata of the device the plugin is currently connected to.
Device objects are passed into the supportsDevice
method of a device plugin and are available as the device
field on a DevicePluginClient
Propertiesβ
Property | Datatype | Description | Values |
---|---|---|---|
os | String | Operating System of the device. | 'iOS' , 'Android' , 'Windows' ,'MacOS' , or 'Metro' |
deviceType | String | Whether the device is a physical device or an emulator. | 'emulator' or 'physical' |
isArchived | Boolean | A flag: true indicates the current device is coming from an import Flipper snapshot, and not an actually connected device. | True or False |
isConnected | Boolean | A flag: true indicates the connection to the device is still alive. | True or False |
Eventsβ
onLogEntry
β
Usage: device.onLogEntry(callback: (logEntry: DeviceLogEntry) => void)
Use this event to subscribe to the log stream that is emitted by the device. For Android, this is using adb
behind the scenes, for iOS idb
, for Metro it connects to the webserver for the Metro log output, etc.
The DeviceLogEntry
exposes the following fields:
Field | Datatype |
---|---|
date | Date |
type | string |
message | string |
pid | number |
tid | number |
app | string |
tag | string |
Possible values for type
are: 'unknown'
, 'verbose'
, 'debug'
, 'info'
, 'warn'
, 'error'
, and 'fatal'
.
State Managementβ
State in Sandy plugins is stored in small containers that hold immutable values and can be consumed in React components using the useValue hook.
Methodsβ
createStateβ
Usage: createState<T>(initialValue: T, options?): StateAtom<T>
The createState
method can be used to create a small state container that lives inside a Sandy plugin.
Its value should be treated as immutable and is initialized by default using the initialValue
parameter.
Optionsβ
Optionally, options
can be provided when creating state. Supported options:
persist: string
- if thepersist
value is set, this state container is serialized when a Flipper snapshot export is being made. When a snapshot is imported into Flipper, and plugins are initialized, this state container loads its initial value from the snapshot, rather than using theinitialValue
parameter. Thepersist
key should be unique within the plugin and only be set if the state stored in this container is serializable and won't become unreasonably large. See alsoexportState
andinitialState
in the TestUtils section of the 'Utlities' page.persistToLocalStorage: boolean
- indicates if this option is set in combination with thepersist
option. The atom stores its state in local storage instead of as part of the plugin import / export. State stored in local storage is shared between the same plugin across multiple clients/ devices, but not actively synced.
Serializable is defined as: non-cyclic data, consisting purely of primitive values, plain objects and arrays. Precisely as the root, Date
, Set
or Map
objects are allowed as well but shouldn't appear deeper in the tree.
The state atom objectβ
A state atom object is returned by createState
, exposing the following methods:
get(): T
- returns the current value stored. If you want to use the atom object in a React component, consider using theuseValue
hook instead, to make sure the component is notified about future updates of this atom.set(newValue: T)
- stores a new value into the atom. If the new value is not reference-equal to the previous one, all observing components are notified.update(updater: (draft: Draft<T>) => void)
- updates the current state using an Immer recipe. In theupdater
, thedraft
object can be safely (deeply) mutated. Once theupdater
finishes, Immer will compute a new immutable object based on the changes, and store that. This is often simpler than using a combination ofget
andset
if deep updates need to be made to the stored object.subscribe(listener: (value: T, prevValue: T) => void): () => void
- subscribes a listener function to the state updates. The Listener function receives the next and previous value on each update. The method also returns function which can be called to unsubscribe the listener from further updates.unsubscribe(listener: (value: T, prevValue: T) => void): void
- unsubscribes a listener function from the state updates if it was subscribed before.
Exampleβ
import {createState} from 'flipper-plugin'
const rows = createState<string[]>([], {persist: 'rows'});
const selectedID = createState<string | null>(null, {persist: 'selection'});
// Listener is called on each rows.set() and rows.update() call until unsubscribed.
const unsubscribe = rows.subscribe((value, prevValue) => {
console.log(`Rows state updated. New length: ${value.length}. Prev length: ${prevValue.length}.`);
});
rows.set(["hello"]) // Listener will be notified about the change
console.log(rows.get().length) // 1
unsubscribe(); // Do not notify listener anymore
rows.update(draft => { // Listener won't be notified about the change
draft.push("world")
})
console.log(rows.get().length) // 2
createDataSourceβ
Usage: createDataSource<T>(initialSet?: T[], options?): DataSource<T>
Most Flipper plugins follow the basic concept of receiving events from the device, store them, and being able to tail, filter and search them.
To optimise for this situation, there is a dedicated createDataSource
abstraction which creates a DataSource
.
DataSource
is a data collection that is heavily optimized for append
and update
,
which stores items based on insertion order, but also allows for efficient by-id lookups.
Each DataSource
exposes a default view
property, which contains a DataSourceView
.
A DataSourceView
is a materialized view that can be sorted, filtered and windowed, and is kept incrementally up to date with the underlying DataSource
.
When using the DataTable
component, this view
will be managed by the table automatically, giving plugin users the capability to freely sort, filter, search and tail your datasource.
Alternatively, you could also pass in a different view(like from additionalViews) to the DataTable
component which allows for different filters, searches, etc. than the default view
.
Valid options
are:
key
- if a key is set, the given field of the records is assumed to be unique, and its value can be used to perform lookups and upserts.limit
- the maximum number of records that this DataSource will store. If the limit is exceeded, the oldest records will automatically be dropped to make place for the new ones. Defaults to 100.000 records.persist
- see thecreateState
persist
option: If set, this data source will automatically be part of Flipper imports / exports; it's recommended to set this option.indices
- If set, secondary indices will be maintained for this table that allows fast lookups. Indices is an array of keys with 1 or more items. SeegetAllRecordsByIndex
for more details.
All records stored in a data source should be treated as being immutable. To update a record, replace it with a new value using the update
or upsert
operations.
Exampleβ
export function devicePlugin(client: DevicePluginClient) {
const rows = createDataSource<ExtendedLogEntry>([], {
limit: 200000,
persist: 'logs',
});
client.device.onLogEntry(entry => {
rows.append(entry);
});
return {
rows,
}
}
DataSourceβ
Stores large amounts of records efficiently. For an introducrion, see createDataSource.
limitβ
The maximum number of records that can be stored in this DataSource to constrain memory usage. Defaults to 100.000 rows. If the limit is exceeded, the oldest 10% of records is dropped. This field is writable but does not immediately truncate if changed.
viewβ
Returns the currently active default view on the data source. Note that be default it is windowed on the impractical [0, 0)
range. For more details, see DataSourceView.
additionalViewsβ
Holds additional DataSourceView
on this DataSource
. It contains key of viewId: string
and value of DataSourceView<T, KeyType>
. The default view in DataSource
has DEFAULT_VIEW_ID = '0'
, so all additionalViews cannot use this as the viewId
. This is a readonly
array and additional views are appended to the array by calling getAdditionalView(viewId: string)
with a new viewId
sizeβ
The total number of records stored in this data source.
recordsβ
Usage: records(): T[]
. Returns all values stored in this data source in a defensive copy. Note that this operation performs O(n)
, so typically one should operate on a subset of the records using size
and get
.
getβ
Usage: get(index: number): T
. Returns the record at the given index, which is insertion order based. This operation does not take into consideration the current view. See also view.get
to get a record based on visible position. To look items up based on their id, use getById
.
getByIdβ
Usage: getById(key: string): T | undefined
(for example, users.getById("jane")
. Returns the record associated with the given key, or undefined
. This method can only be used if the key
option was passed to createDataSource
.
keysβ
Usage: keys()
, returns an iterator that will iterate all keys in the data source. For example, to create an array of all keys: const usernames = Array.from(users.keys())
. This method can only be used if the key
option was passed to createDataSource
.
entriesβ
Usage: entries()
. Similar to keys()
, but will return an iterator that generate entry tuples, in the shape of [key, value]
.
[Symbol.iterator]β
DataSource
supports the iterator protocol, so to visit all stored records one can use for (const user of users) {....}
.
getIndexOfKeyβ
Usage: getById(key: string): number
. Returns the insertion index of the record associated with the given key, or -1
. This method can only be used if the key
option was passed to createDataSource
.
appendβ
Usage: append(record: T)
. Appends a new record to the data collection. This method will throw if a duplicate key is inserted. Use upsert
to automatically append or update. Mutations like append
will be reflected in the view
automatically.
updateβ
Usage: update(index: number, record: T)
. Replaces the given record in the data sources.
deleteβ
Usage: delete(index: number)
. Remove the record at the given index from the datasource. Note that if a the key
option of the datasource is set, this operation degrades to O(n)
performance and should typically be avoided.
deleteByIdβ
Usage: delete(key: string): boolean
. Removes the record with the given key. Returns true
if the record existed and has been removed. This operation is O(n)
expensive and should generally be avoided.
shiftβ
Usage: shift(amount: number)
. Removes the first amount
records from the datasource. This is generally a performant operation.
clearβ
Usage: clear()
. Removes all records from this data source.
getAdditionalViewβ
Usage: getAdditionalView(viewId: string)
. Gets an additional DataSourceView
of the DataSource
by passing in an identifier viewId
. If there already exists a DataSourceView
with the viewId
, we simply return that view instead.
getAllRecordsByIndexβ
Usage: getAllRecordsByIndex({ indexedAttribute: value, indexAttribute2: value2, .... })
This method allows fast lookups for objects that match specific attributes exactly. Returns all items matching the specified index query. Note that the results are unordered, unless records have not been updated using upsert / update, in that case insertion order is maintained. If no index has been specified for this exact keyset in the indexQuery (see options.indices), this method will throw.
Example:
const ds = createDataSource([eatCookie, drinkCoffee, submitBug], {
key: 'id',
indices: [
['title']
['id', 'title'],
['title', 'done'],
],
});
// Find first element with title === cookie (or undefined)
const todo = ds.getFirstRecordByIndex({
title: 'cookie',
})
// Find all elements where title === cookie, and done === false
const todos = ds.getAllRecordsByIndex({
title: 'cookie',
done: false,
})
DataSourceViewβ
A materialized view on a DataSource, which can apply windowing, sorting and filtering and will be kept incrementally up to date with the underlying datasource.
Note that the default window is empty, so after obtaining a DataSourceView
one should typically call setWindow
. For an introduction, see [createDataSource].
The DataSourceView API is important if are creating your own visualization of a DataSource
.
However, if a DataSource
is visualized using a DataTable
, there is typically no need to directly interact with this API.
datasourceβ
A reference to the underlying DataSource.
windowStartβ
See setWindow.
windowEndβ
See setWindow.
sizeβ
The total size of the current view after applying filtering. Note that size
does not reflect windowing. To get the window size use: windowEnd - windowStart
. To get the total amount of records, without respecting the current filter, use datasource.size
.
isSortedβ
Returns true
if a sort criterium is set.
isFilteredβ
Returns true
if a filter criterium is set.
isReveredβ
Return true
if the current view will be shown in reverse order.
outputβ
Usage: output(): T[]
or output(start, end): T[]
. Returns a defensive copy of all items visible in the provided range window. If start
and end
are omitted, the current window will be used. To get all items visible in the current view, ignoring the window, use view.output(0, view.size)
.
[Symbol.iterator]β
DataSourceView
supports the iterator protocol, so the currently visible output can be iterated using for example for (const user in users.view) { ... }
. The iterator will always apply the current window.
setWindowβ
Usage: setWindow(start, end)
. This method sets the current visible window to the specified range (which will include start
, but not end
, so [start, end)
).
Setting a window impacts the default behavior of output
and iterator
and, more importantly, the behavior of any listener: update
events that happen outside the window will not be propagated to any listeners, and shift
events will describe whether the happened in
, before
, or after
the current window.
Windowing will always be applied only after applying any filters, sorting and reversing.
setFilterβ
Usage: setFilter(filter: (record: T) => boolean)
. Applies a filter to the current records. This will typically reduce size
of this view. Example: users.view.setFilter(user => user.age >= 18)
.
setSortByβ
Usage: setSortBy(field: string)
or setSortBy(sortBy: (irecord: T) => primitive)
. For example: users.view.setSortBy("age")
or users.viewSetSortBy(user =>
${user.lastName} ${user.firstName})
. setSortBy
will cause the data source to be sorted by the given field or criterium function. Sort is implemented efficiently by using a binary search to insert / remove newly arriving records, rather than performing a full sort. But this means that the sort function should be stable and pure.
Sorting will always happen in ascending order, and if duplicate sort values appear, the insertion order will take precedence. To sort in descending order, use setReversed
. If a view doesn't have sorting specified, it will always show records in insertion order.
toggleReveredβ
Usage: toggleReversed()
. Toggles the output order between ascending and descending.
setReversedβ
Usage: setReversed(ascending: boolean)
. Defines whether the output colletion is shown normal (ascending) or reverse (descending) order.
resetβ
Usage: reset()
. Resets the window, filtering, sorting and reverse to their defaults. Note that this puts the window back to [0, 0)
as well, meaning now records will be part of the output.
getβ
Usage: get(index: number)
. Returns the record at the given position in the output. The index
parameter respects sorting, filtering and reversing, but does not respect any window offset. So get(0)
will return the first record in the datasource according to the given filtering, sorting and reversing, while get(windowStart)
will return the first of the records visible in the current window.
setListenerβ
Usage: setListener(callback: undefined | (event: OutputChange) => void)
. Sets up a listener that will get notified whenever the output
or size
of this view changes. This can be used to, for example, update the UI and is used by DataTable
under the hood.
The following events can be emitted. These events respect the current sorting, filtering and reversing. The shift location
is expressed relatively to the current window. Now update
events that are outside the current window will be emitted. reset
events are typically emitted if a change happened that cannot be expressed in a limited number of shifts / updates. Such as changing sorting or filtering, calling clear()
or reset()
, or doing a large shift
.
Currently only one listener is allowed at a time. Please contact the Flipper oncall if that doesn't suffice.
type OutputChange =
| {
type: 'shift';
index: number; // the position at which records were inserted or removed
location: 'before' | 'in' | 'after'; // relative to current window
delta: number; // how many records were inserted (positive number) or removed (negative number)
newCount: number; // the new .size of the DataSourceView
}
| {
// an item, inside the current window, was changed
type: 'update';
index: number;
}
| {
// something big and awesome happened. Drop earlier updates to the floor and start again
// like: clear, filter or sorting change, etc
type: 'reset';
newCount: number;
};
React Hooksβ
Methodsβ
usePluginβ
Usage: const instance = usePlugin(plugin)
Can be used by any component in the plugin, and gives the current instance
that corresponds with the currently loaded plugin.
The plugin
parameter isn't actually used; it verifies that a component is used correctly inside a mounted component and helps with type inference.
The returned instance
method corresponds to the object that is returned from the plugin
/ devicePlugin
definition.
See the Tutorial for how this hook is used in practice.
useValueβ
Usage: const currentValue = useValue(stateAtom)
Returns the current value of a state atom, and also subscribes the current component to future changes of the atom (in contrast to using stateAtom.get()
directly).
See the Tutorial for how this hook is used in practice.
useLoggerβ
Usage: const logger = useLogger()
Provides the default logger that can be used for console logging, error reporting and performance measurements.
In internal Meta builds, this is wired up to the internal statistic reporting. Use logger
rather than using console
directly.
The logger API is defined as:
interface Logger {
track(type: TrackType, event: string, data?: any, plugin?: string): void;
trackTimeSince(
mark: string,
eventName?: string | null | undefined,
data?: any,
): void;
info(data: any, category: string): void;
warn(data: any, category: string): void;
error(data: any, category: string): void;
debug(data: any, category: string): void;
}
useTrackedCallbackβ
Usage: const eventHandler = useTrackedCallback("Interaction description", handlerFunction, deps)
Utility that wraps React's useCallback
with tracking capabilities.
The API is similar, except that the first argument describes the interaction handled by the given event handler.
For more information, see Tracked in the UI Components page.
useMemoizeβ
Slight variation on useMemo that encourages to create hoistable memoization functions, which encourages reuse and testability by no longer closing over variables that are used by the memoized function, but rather receiving them as arguments so that these functions beome pure.
function MyComponent() {
const {findMetroDevice} = props;
const connections = useSomeHook();
const metroDevice = useMemoize(
findMetroDevice,
[connections.devices],
);
// etc
}
export function findMetroDevice(findMetroDevice, deviceList) {
return deviceList.find(findMetroDevice);
}
useLocalStorageStateβ
Like useState
, but the value will be stored in local storage under the given key and read back upon initialization.
The hook signature is similar to useState
, except that the first argument is the storage key.
The storage key will be scoped automatically to the current plugin and any additional tracking scopes. (See TrackingScope in the 'UI Components' page).
const [showWhitespace, setShowWhitespace] = useLocalStorageState(
`showWhitespace`,
true
);
UI componentsβ
Layout.*β
Layout elements can be used to organize the screen layout. See the Style Guide for more details.
HighlightContextβ
HighlightProviderβ
React context provider for Highlight context. All wrapped componets can access context or use the useHighligher helper. Example
<HighlightProvider
text={searchTerm}
highlightColor={theme.searchHighlightBackground.yellow}>
<HighlightedText text='Lorem itsum'/>
</HighlightProvider>
useHighlighterβ
Hook to be used inside a Highlight context to render text with highlighting applied. Example
function HighlightedText(props: {text: string}) {
const highlightManager: HighlightManager = useHighlighter();
return <span>{highlightManager.render(props.text)}</span>;
}
PowerSearchβ
DataTableβ
DataFormatterβ
DataInspectorβ
DataDescriptionβ
MarkerTimelineβ
ElementsInspectorβ
ElementAttributeβ
ElementDataβ
ElementExtraInfoβ
ElementIDβ
ElementSearchResultSetβ
ElementsInspectorElementβ
ElementsInspectorPropsβ
TimelineDataDescriptionβ
dataTablePowerSearchOperatorsβ
MasterDetailLegacyβ
DataTableLegacyβ
Coming soon
MasterDetailβ
The MasterDetail
provides a default setup for a DataTable
in combination with a sidebar, and some super common features like keyboard shortcuts, paste integration, and clear / pause-resume buttons.
The MasterDetail
component accepts all DataTable
props, and beyond that the following props.
- The
sidebarPosition
prop controls here to display the details of the currently selected record:'main'
(default): show the details in the standard, centrally controlled right sidebar'right'
: show a resizable pane to the right'bottom'
: show a resizable pane to the bottom'none'
: don't show details at all
sidebarComponent
: Component that accepts a 'record' prop that is used to render details.If none is provided, a standardDataInspector
component will be used to display the entire record.sidebarSize
: Default size of the sidebar.selection
: If provided, this atom will be used to store selection in.isPaused
: If provided, this atom will be used to store pause/resume state in, and a pause/resume toggle will be shown- `enableClear: If set, a clear button will be shown. By default, this will clear the dataSource (if any).
onClear
: Callback to be called when clear action is used.enableMenuEntries
: If provided, standard menu entries will be created for clear, goToBottom and createPaste.
Panelβ
A collapsible UI region. The collapsed state of the pane will automatically be persisted so that the collapsed state is restored the next time user visits the plugin again. Note that the children of a Panel should have some size, either a fixed or a natural size. Elements that grow to their parent size will become invisible.
For demos and property documentation see the 'Flipper style guide'.
Tabsβ
Tabβ
The Tabs
and Tab
component re-expose the TabControl as provided by Antd, and support the same properties. For more details, see the official Ant Design docs.
The following additional property is supported:
grow
. If set, the tab control will use all available vertical space. By defaultfalse
.
Toolbarβ
A horizontal panel to organize buttons and alike. Basically a Layout.Horizontal
with a border, padding, gap and wrapping enabled.
CodeBlockβ
A preformatted paragraph that supports wrapping, preserves whitespace and uses monospace.
DataListβ
The DataList can be used to display a set of items efficiently, and where a single item can be selected.
Properties:
items
: Items to display. Per item at least a title and unique id should be provided.value
: The current selectiononSelect
onRenderItem
: A custom render function. By default, the component will render thetitle
in bold and description (if any) below it.type
:default
or `dropdown. Defines the styling of the component. By default shows a list, but alternatively the items can be displayed in a drop downscrollable
: By default the data list will take all available space and scroll if items aren't otherwise visible. By settingscrollable={false}
the list will only take its natural size
Dialogβ
The Dialog
namespace provides a set of utility to prompt the user with feedback of input. Rather than spawning dialogs by hand, the benefit of the Dialog
utilities is that they all return promises capture the results.
The promises returned by Dialog
will resolve to false
if the user intentionally closed the dialog (typically by using cancel / escape / clicking the close button).
The promises returned by Dialog
utilities will expose a close()
method that can be used to programmatically close a dialog. In which case the pending promise will resolve to false
as well.
General properties accepted by the Dialog
utility:
title
- Overrides the title of the dialog, defaults to empty.width
- Overrides the default width (400) for dialogs. Number in pixels.okText
- Overrides the caption of the OK buttoncancelText
- Overrides the caption of the Cancel button
Available utilities
Dialog.confirm(options): Promise<boolean>
. Show a confirmation dialog twithino the user. Options:message
: Description of what the user is confirming.
Dialog.prompt(options): Promise<string | false>
. Inspired bywindow.prompt
. Prompt the user for some input. Options:message
: Text accompanying the inputdefaultValue
onConfirm(value) => Promise<string>
. Can be used to transform the inputted value before resolving the prompt promise. If the handler throws, this will be shown as validation error in the dialog.
Dialog.loading(options): Promise<void>
. Shows a dialog with a loading spinner. This dialog cannot be closed by the user, so instead.close()
should be called programmatically on the returned promise.message
: Message to display with the loading spinner.
Dialog.show<T>(options): Promise<T | false
. Low level building block to build dialogs. Options:children
: React Element to render as children of the dialog.onConfirm: () => Promise<T>
. Handler to handle the OK button, which should produce the value theDialog.show
call will resolve to.
Dialog.alert(options)
show a small dialog, inspired by window.alert. Returns aPromise<void>
. Options:type
, determines style. One of:'info' | 'error' | 'warning' | 'success'
.message
parameter to specify the content of the dialog.
Spinnerβ
Shows a loading spinner. Accept an optional size
to make the spinner larger / smaller.
NUXβ
An element that can be used to provide a New User eXperience: Hints that give a one-time introduction to new features to the current user.
For more details, see the Style Guide.
FileSelectorβ
Enables file uploading. Shows an input with an upload button. User can select and upload files by clicking on the button, on the input, by pressing enter when the input is focused, and by dropping a file on the input. The input's value is a path to a file or its name if path is not available (in browsers).
Exports FileSelector
components with the following props:
label
- placeholder text displayed in the input when it is empty.onChange
- callback called when new files are selected or when the existing selection is reset.multi
- [optional] allows selecting multiple files at once.extensions
- [optional] list of allowed file extensions.required
- [optional] boolean to mark the file selection input as required.encoding
- [optional] imported file encoding. Default: UTF-8.className
- [optional] class name string.style
- [optional] CSS styles object.
DetailSidebarβ
An element that can be passed children which will be shown in the right sidebar of Flipper.
Horizontal scrolling will be enabled by default.
To fine-tune the default dimensions use width
and minWidth
.
It doesn't really matter where exactly this component is used in your layout, as the contents will be moved to the main Flipper chrome, rather than being rendered in place.
theme objectβ
Provides a standard set of colors and spacings, used by the Flipper Style Guide. The colors exposed here support dark mode.
Utilitiesβ
suggestNewPluginβ
Display a pop-up prompting users switch to a new plugin
getFlipperLibβ
A set of globally available utilities such as opening links, interacting with the clipboard (see the following example), and many more.
Example:
getFlipperLib().writeTextToClipboard("hello from Flipper");
The full set of utilities is listed in FlipperLib.tsx.
Since the Node.js system API's cannot be used directly by Flipper plugins (when running in a browser), the utilties exposed on getFlipperLib().remoteServerContext
should be used instead, as detailed in Using Node.js APIs.
createTablePluginβ
A utility to create a plugin that consists of a main table and details JSON view with minimal effort. For an example, see the Building a Desktop Plugin - Showing a Table tutorial.
createTablePlugin
creates a plugin that handles receiving data from the client and
displaying it in a table. The table handles selection of items, sorting, filtering and rendering a sidebar where more detailed information can be presented about the selected row.
The plugin expects to be able to subscribe to the method
argument and receive single data objects. Each data object represents a row in the table.
An optional resetMethod
argument can be provided that replaces the current rows with the data provided. This is useful when connecting to Flipper for this first time, or reconnecting to the client in an unknown state.
Since the createTablePlugin
defines both the plugin
and Component
for the plugin in one go, making the result is most easily done by using module.exports = createTablePlugin(....)
so that both are exported from the plugin package.
Valid options are:
method: string
- the event that is sent from the corresponding client plugin and should be collected.resetMethod?: string
- an event name, that, when sent from the client, should clear the current table.columns: DataTableColumn
- a description of the columns to display (see DataTable in the 'UI Components' page.key?: string
- if set, the specified field of the incoming data will be treated as unique identifier. Receiving new data for existing rows will replace the existing rows. Without this property the table will only be appended.onCopyRows?: (rows) => string
- a function that can be used to customize how records are copied to the clipboard. By default, they areJSON.stringify
-ed.buildRow?: (rawData) => row
- a function that can be used to preprocess the incoming data before it is handed off to the table.renderSidebar?: (row) => React.Element
- a function that can be used to customize how the sidebar is rendered.
batchedβ
batchβ
Usage: batch(() => { /* state updates */ })
Low-level utility to batch state updates to reduce the number of potential re-renders by React.
Wraps React's unstable_batchedUpdates
.
Event handlers provided by React or flipper-plugin
already apply batch
automatically, so using this utility is only recommended when updating plugin state in an asynchronous process.
produceβ
A convenience re-export of produce
from Immer.
The update
method of the state atoms returned by createState
automatically applies produce
to its updater function.
renderReactRootβ
Usage: renderReactRoot(handler: (unmount: () => void) => React.ReactElement)
Renders an element outside the current DOM tree.
This is a low-level utility that can be used to render for example Modal dialogs.
The provided handler
function should return the root element to be rendered.
Once the element can be removed from the DOM, the unmount
callback should be called.
Example:
renderReactRoot((unmount) => (
<RequiredParametersDialog
onHide={unmount}
uri={filteredQuery}
requiredParameters={params}
onSubmit={navigateTo}
/>
));
sleepβ
Usage: await sleep(1000)
Creates a promise that automatically resolves after the specified number of milliseconds.
timeoutβ
Usage await timeout(1000, promise, message?)
styledβ
A convenience re-export of styled
from emotion.
textContentβ
Given a string or React element, returns a text representation of that element, that is suitable as plain text.
pathβ
A set of utilizities to handle file paths. A subset of Node.js' path.
path.basename(path[, ext])
β
path
{string}ext
{string} An optional file extension- Returns: {string}
The path.basename()
method returns the last portion of a path
, similar to
the Unix basename
command. Trailing directory separators are ignored.
path.basename('/foo/bar/baz/asdf/quux.html');
// Returns: 'quux.html'
path.basename('/foo/bar/baz/asdf/quux.html', '.html');
// Returns: 'quux'
path.extname(path)
β
path
{string}- Returns: {string}
The path.extname()
method returns the extension of the path
, from the last
occurrence of the .
(period) character to end of string in the last portion of
the path
. If there is no .
in the last portion of the path
, or if
there are no .
characters other than the first character of
the basename of path
(see path.basename()
) , an empty string is returned.
path.extname('index.html');
// Returns: '.html'
path.extname('index.coffee.md');
// Returns: '.md'
path.extname('index.');
// Returns: '.'
path.extname('index');
// Returns: ''
path.extname('.index');
// Returns: ''
path.extname('.index.md');
// Returns: '.md'
path.join([...paths])
β
...paths
{string} A sequence of path segments- Returns: {string}
The path.join()
method joins all given path
segments together using the
platform-specific separator as a delimiter, then normalizes the resulting path.
Zero-length path
segments are ignored. If the joined path string is a
zero-length string then '.'
will be returned, representing the current
working directory.
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// Returns: '/foo/bar/baz/asdf'
path.normalize(path)
β
path
{string}- Returns: {string}
The path.normalize()
method normalizes the given path
, resolving '..'
and
'.'
segments.
When multiple, sequential path segment separation characters are found (e.g.
/
), they are replaced by a single
instance of /
. Trailing separators are preserved.
If the path
is a zero-length string, '.'
is returned, representing the
current working directory.
path.normalize('/foo/bar//baz/asdf/quux/..');
// Returns: '/foo/bar/baz/asdf'
uuidβ
Usage: uuid()
Returns UUID V4.
safeStringifyβ
Usage: safeStringify(dataStructure)
Serialises the given data structure using JSON.stringify
, but doesn't throw if the processes failed, but rather returns a <unserializable ...>
string.
createControlledPromiseβ
Creates a promise and functions to resolve/reject it externally. Also provides its current state.
Returns:
// When the promise is pending
type Res<T> = {
promise: Promise<T>;
resolve: (...res: T extends void ? [] : [T]) => void;
reject: (reason: unknown) => void;
state: 'pending';
promiseVal: undefined;
} | {
promise: Promise<T>;
resolve: (...res: T extends void ? [] : [T]) => void;
reject: (reason: unknown) => void;
state: 'resolved';
// Resolved value
promiseVal: T;
} | {
promise: Promise<T>;
resolve: (...res: T extends void ? [] : [T]) => void;
reject: (reason: unknown) => void;
state: 'rejected';
// Rejection reason
promiseVal: unknown;
}
Usage:
const controllerPromise = createControlledPromise()
someService.on('event', (val) => controllerPromise.resolve(val))
await controllerPromise.promise
isAtomβ
Usage: isAtom(value)
Checks if value is an Atom. Returns boolean.
reportPluginFailuresβ
Wraps a promise together with a description of the action that's performed and the name of the plugin to track success rates and duration of the actions. This is a no-op for the open-source version of Flipper as we do not collect any data from users.
Example usage:
import {reportPluginFailures} from 'flipper-plugin';
const lastCreatedBisect: PreviousBisects = await reportPluginFailures(
graphQLQuery(
`query MyQuery {
viewer {
data (first: 1) {
nodes {
id
date_created
}
}
}
}`,
60 * 1000,
),
'queryMyQuery',
'myplugin',
);
tryCatchReportPluginFailuresβ
See above for reportPluginFailures. This takes a callback and wraps it in a try/catch block instead of expecting a promise.
tryCatchReportPluginFailuresAsyncβ
Same as tryCatchReportPluginFailures but for async functions.
TestUtilsβ
The object TestUtils
as exposed from flipper-plugin
exposes utilities to write unit tests for Sandy plugins.
Different utilities are exposed depending on whether you want to test a client or device plugin, and whether or not the component should be rendered or only the logic itself is going to be tested.
It is recommended to follow the Tutorial first, as it explains how unit tests should be setup.
Starting a pluginβ
Usage:
const runner = TestUtils.startPlugin(pluginModule, options?)
const runner = TestUtils.renderPlugin(pluginModule, options?)
const runner = TestUtils.startDevicePlugin(devicePluginModule, options?)
const runner = TestUtils.renderDevicePlugin(devicePluginModule, options?)
Starts a client plugin in a fully mocked environment, but without rendering support.
The pluginModule is an object that has a plugin
(or devicePlugin
and supportsDevice
) and Component
property.
Typically, it is invoked with startPlugin(PluginUnderTest)
, where PluginUnderTest
is loaded like import * as PluginUnderTest from "../index.tsx"
(the path to the actual definition).
However, it doesn't have to be loaded with an entire module, and a local object with the same signature can be constructed as well.
startPlugin optionsβ
The options
argument is optional, but can specify the following fields:
initialState
: Can be used to start the plugin in a certain state, rather than in the default state.initialState
should be an object that specifies for all the state atoms that have thepersist
option set, their initial value. For example:{ initialState: { rows: ["hello", "world"]}}
, whererows
matches thepersist
key of an atom.isArchived: boolean
: Setting this flag, will set theisArchived
on the mocked device as well. Set it if you want to test the behavior of your plugin for imported devices (see also Device.isArchived). Defaults tofalse
.isBackgroundPlugin
: This makes sure the test runner emits life-cycle events in a way that is typical for background plugins. Defaults tofalse
. The notable difference in behavior is that calling.active()
on the test runner won't trigger theconnect
event to be fired, nor the.deactivate()
thedisconnect
event.startUnactivated
: This does not activate the plugin;connect
needs to be explicitly called. This can be used in case setting mock implementation foronSend
is required to make sure Client plugin works as expected. Defaults tofalse
.GKs
: A string array of gatekeeper names for whichclient.GK
willtrue
inside the test. By default GKs are assumed to be disabled inside unit tests.
The test runner objectβ
startPlugin
returns an object that can be used to inspect and interact with your plugin instance.
Again, see the tutorial how to interact with this object in general.
The test runner is a bag full of utilities, but typically it is fine to just destructure the utilities relevant for the test.
Exposed members:
instance
: The object (public API) returned from your plugin definition. You will typically use this in most tests, either to trigger updates or to inspect the current state of the plugin.exportState()
: Grabs the current state of allpersist
enabled state atoms. The object format returned here is the same as in theinitialState
option.activate()
: Emulate theonActivate
event. By default,startPlugin
already starts the plugin in activated state and callingactivate
to test theonActivate
event should be preceded by adeactivate()
call first.deactivate()
: Emulates a user navigating away from the plugin.destroy()
: Emulates the plugin being cleaned up, for example because the plugin is disabled by the user, or because the device / client has disconnected. After callingdestroy
the currentrunner
is unusable.triggerDeepLink(payload)
: Emulates a deepLink being triggered and fires theonDeepLink
event.triggerMenuEntry(label)
: Emulates the user clicking a menu entry in the Flipper main menu.flipperLib
: An object that exposedjest.fn()
mocks for all built-in Flipper APIs that can be called by your plugin. So, assertions can be made that the plugin did actually invoke those methods. For example:expect(runner.flipperLib.createPaste).toBeCalledWith("test message")
. Currently supported mocks:createPaste
,enableMenuEntries
.
The following members are available when using the render...
variant rather than the start...
variant:
renderer
: This object can be used to query the DOM and further interact with it. It is provided by react-testing-library, and further documented in the render Result page.act
: Use this function to wrap interactions with the plugin under test into a transaction, after which the DOM updates will be flushed by React. See also the React.js act() documentation.
The following members are only available for Client plugins:
sendEvent(event, params)
: Emulates an event being sent by the client plugin. Will trigger the correspondingonMessage
handler in the plugin.sendEvents({ method: string, params: object}[])
: LikesendEvent
, but sends multiple events at once.onSend
: Ajest.fn()
that can be used to assert thatclient.send()
was called by the plugin under test. For example,expect(runner.onSend).toBeCalledWith('currentLogs', { since: 0})
.connect()
: Triggers theonConnect()
event. (For non-background pluginsactivate()
could as well be used for this).disconnect()
: Triggers theonDisconnect()
event. (For non-background pluginsdeactivate()
could as well be used for this).
The following members are only available for Device plugins:
sendLogEntry(logEntry)
: Emulates a log message arriving from the device. Triggers theclient.device.onLogEntry
listener.