Make flipper messages generally available, remove self inspection infra structure

Summary:
Changelog: Flipper message debugging moved from a separate device to the console tab

This makes message debugging easier accessible, and in production (recently requested at GH). Also it clears up a lot of infra that was created just to make flipper a self recursive inspection device + a separate plugin. While fun, a hardcoded setup is just a bit more simpler (no exception rules and better static verification)

Reviewed By: nikoant

Differential Revision: D29487811

fbshipit-source-id: b412adc3ef5bd831001333443b432b6c0f934a5e
This commit is contained in:
Michel Weststrate
2021-07-01 01:58:41 -07:00
committed by Facebook GitHub Bot
parent 8da7495a1a
commit 328ba9513c
15 changed files with 332 additions and 550 deletions

View File

@@ -31,10 +31,13 @@ import {
_SandyPluginInstance,
_getFlipperLibImplementation,
} from 'flipper-plugin';
import {flipperMessagesClientPlugin} from './utils/self-inspection/plugins/FlipperMessagesClientPlugin';
import {freeze} from 'immer';
import GK from './fb-stubs/GK';
import {message} from 'antd';
import {
isFlipperMessageDebuggingEnabled,
registerFlipperDebugMessage,
} from './chrome/FlipperMessages';
type Plugins = Set<string>;
type PluginsArr = Array<string>;
@@ -384,11 +387,8 @@ export default class Client extends EventEmitter {
const {id, method} = data;
if (
data.params?.api != 'flipper-messages' &&
flipperMessagesClientPlugin.isConnected()
) {
flipperMessagesClientPlugin.newMessage({
if (isFlipperMessageDebuggingEnabled()) {
registerFlipperDebugMessage({
device: this.deviceSync?.displayTitle(),
app: this.query.app,
flipperInternalMethod: method,
@@ -416,7 +416,7 @@ export default class Client extends EventEmitter {
const params: Params = data.params;
const bytes = msg.length * 2; // string lengths are measured in UTF-16 units (not characters), so 2 bytes per char
emitBytesReceived(params.api, bytes);
if (bytes > 5 * 1024 * 1024 && params.api !== 'flipper-messages') {
if (bytes > 5 * 1024 * 1024) {
console.warn(
`Plugin '${params.api}' received excessively large message for '${
params.method
@@ -462,12 +462,7 @@ export default class Client extends EventEmitter {
}
}
}
// TODO: Flipper debug as full client is overkill, clean up
if (
!handled &&
!isProduction() &&
params.api !== 'flipper-messages'
) {
if (!handled && !isProduction()) {
console.warn(`Unhandled message ${params.api}.${params.method}`);
}
}
@@ -597,8 +592,8 @@ export default class Client extends EventEmitter {
this.onResponse(response, resolve, reject);
if (flipperMessagesClientPlugin.isConnected()) {
flipperMessagesClientPlugin.newMessage({
if (isFlipperMessageDebuggingEnabled()) {
registerFlipperDebugMessage({
device: this.deviceSync?.displayTitle(),
app: this.query.app,
flipperInternalMethod: method,
@@ -625,8 +620,8 @@ export default class Client extends EventEmitter {
);
}
if (flipperMessagesClientPlugin.isConnected()) {
flipperMessagesClientPlugin.newMessage({
if (isFlipperMessageDebuggingEnabled()) {
registerFlipperDebugMessage({
device: this.deviceSync?.displayTitle(),
app: this.query.app,
flipperInternalMethod: method,
@@ -711,8 +706,8 @@ export default class Client extends EventEmitter {
this.connection.fireAndForget({data: JSON.stringify(data)});
}
if (flipperMessagesClientPlugin.isConnected()) {
flipperMessagesClientPlugin.newMessage({
if (isFlipperMessageDebuggingEnabled()) {
registerFlipperDebugMessage({
device: this.deviceSync?.displayTitle(),
app: this.query.app,
flipperInternalMethod: method,

View File

@@ -8,7 +8,7 @@
*/
import {useMemo} from 'react';
import {Button, ButtonGroup, Layout} from '../ui';
import {Button, Layout} from '../ui';
import React from 'react';
import {Console, Hook} from 'console-feed';
import type {Methods} from 'console-feed/lib/definitions/Methods';
@@ -92,13 +92,11 @@ export function ConsoleLogs() {
return (
<Layout.Top>
<Toolbar>
<ButtonGroup>
<Toolbar wash>
<Button onClick={clearLogs} icon="trash">
Clear Logs
</Button>
<Button dropdown={dropdown}>Log Levels</Button>
</ButtonGroup>
</Toolbar>
<Layout.ScrollContainer vertical>
<Console

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Layout} from '../ui';
import React from 'react';
import {Tab, Tabs} from 'flipper-plugin';
import {ConsoleLogs} from './ConsoleLogs';
import {FlipperMessages} from './FlipperMessages';
export function FlipperDevTools() {
return (
<Layout.Container pad grow>
<Tabs grow>
<Tab tab="Console">
<ConsoleLogs />
</Tab>
<Tab tab="Messages">
<FlipperMessages />
</Tab>
</Tabs>
</Layout.Container>
);
}

View File

@@ -0,0 +1,209 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
DataInspector,
DataTable,
DataTableColumn,
Layout,
createState,
createDataSource,
DetailSidebar,
Panel,
theme,
styled,
useValue,
} from 'flipper-plugin';
import {Button} from 'antd';
import {
DeleteOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
} from '@ant-design/icons';
import React, {useCallback, useState} from 'react';
export type MessageInfo = {
time?: Date;
device?: string;
app: string;
flipperInternalMethod?: string;
plugin?: string;
pluginMethod?: string;
payload?: any;
direction:
| 'toClient:call'
| 'toClient:send'
| 'toFlipper:message'
| 'toFlipper:response';
};
export interface MessageRow extends MessageInfo {
time: Date;
}
const Placeholder = styled(Layout.Container)({
center: true,
color: theme.textColorPlaceholder,
fontSize: 18,
});
function createRow(message: MessageInfo): MessageRow {
return {
...message,
time: message.time == null ? new Date() : message.time,
};
}
const COLUMN_CONFIG: DataTableColumn<MessageRow>[] = [
{
key: 'time',
title: 'Time',
},
{
key: 'device',
title: 'Device',
},
{
key: 'app',
title: 'App',
},
{
key: 'flipperInternalMethod',
title: 'Flipper Internal Method',
},
{
key: 'plugin',
title: 'Plugin',
},
{
key: 'pluginMethod',
title: 'Method',
},
{
key: 'direction',
title: 'Direction',
},
];
const flipperDebugMessages = createDataSource<MessageRow>([], {
limit: 1024 * 10,
persist: 'messages',
});
const flipperDebugMessagesEnabled = createState(false);
export function registerFlipperDebugMessage(message: MessageInfo) {
if (flipperDebugMessagesEnabled.get()) {
flipperDebugMessages.append(createRow(message));
}
}
export function isFlipperMessageDebuggingEnabled(): boolean {
return flipperDebugMessagesEnabled.get();
}
// exposed for testing
export function setFlipperMessageDebuggingEnabled(value: boolean) {
flipperDebugMessagesEnabled.set(value);
}
// exposed for testing
export function clearFlipperDebugMessages() {
flipperDebugMessages.clear();
}
// exposed for testing ONLY!
export function getFlipperDebugMessages() {
return flipperDebugMessages.records();
}
function Sidebar({selection}: {selection: undefined | MessageRow}) {
const renderExtra = (extra: any) => (
<Panel title={'Payload'} collapsible={false}>
<DataInspector data={extra} expandRoot={false} />
</Panel>
);
return (
<DetailSidebar>
{selection != null ? (
renderExtra(selection.payload)
) : (
<Placeholder grow pad="large">
Select a message to view details
</Placeholder>
)}
</DetailSidebar>
);
}
const PauseResumeButton = () => {
const paused = !useValue(flipperDebugMessagesEnabled);
return (
<Button
title={`Click to enable tracing flipper messages`}
danger={!paused}
onClick={() => {
flipperDebugMessagesEnabled.update((v) => !v);
}}>
{paused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
</Button>
);
};
export function FlipperMessages() {
const [selection, setSelection] = useState<MessageRow | undefined>();
const paused = !useValue(flipperDebugMessagesEnabled);
const clearTableButton = (
<Button
title="Clear logs"
onClick={() => {
clearFlipperDebugMessages();
setSelection(undefined);
}}>
<DeleteOutlined />
</Button>
);
const renderEmpty = useCallback(
() => (
<Layout.Container center pad gap style={{width: '100%', marginTop: 200}}>
{paused ? (
<>
Click to enable debugging Flipper messages between the Flipper
application and connected clients: <PauseResumeButton />
</>
) : (
'Waiting for data...'
)}
</Layout.Container>
),
[paused],
);
return (
<Layout.Container grow>
<DataTable<MessageRow>
dataSource={flipperDebugMessages}
columns={COLUMN_CONFIG}
onSelect={setSelection}
enableAutoScroll
onRenderEmpty={renderEmpty}
extraActions={
<>
<PauseResumeButton />
{clearTableButton}
</>
}
/>
<Sidebar selection={selection} />
</Layout.Container>
);
}

View File

@@ -7,45 +7,67 @@
* @format
*/
import {TestUtils} from 'flipper-plugin';
import * as React from 'react';
import {act, render} from '@testing-library/react';
import * as Plugin from '../';
import {MessageRow} from '../';
import {
clearFlipperDebugMessages,
FlipperMessages,
getFlipperDebugMessages,
MessageRow,
registerFlipperDebugMessage,
setFlipperMessageDebuggingEnabled,
} from '../FlipperMessages';
const fixRowTimestamps = (r: MessageRow): MessageRow => ({
...r,
time: new Date(Date.UTC(0, 0, 0, 0, 0, 0)),
});
beforeEach(() => {
clearFlipperDebugMessages();
setFlipperMessageDebuggingEnabled(true);
});
afterEach(() => {
clearFlipperDebugMessages();
setFlipperMessageDebuggingEnabled(false);
});
test('It can store rows', () => {
const {instance, ...plugin} = TestUtils.startPlugin(Plugin);
expect(instance.rows.records()).toEqual([]);
expect(instance.highlightedRow.get()).toBeUndefined();
plugin.sendEvent('newMessage', {
registerFlipperDebugMessage({
app: 'Flipper',
direction: 'toFlipper',
direction: 'toFlipper:message',
});
plugin.sendEvent('newMessage', {
registerFlipperDebugMessage({
app: 'FB4A',
direction: 'toClient',
direction: 'toClient:call',
device: 'Android Phone',
payload: {hello: 'world'},
});
expect(instance.rows.records().map(fixRowTimestamps)).toMatchInlineSnapshot(`
setFlipperMessageDebuggingEnabled(false);
registerFlipperDebugMessage({
app: 'FB4A',
direction: 'toClient:call',
device: 'Android PhoneTEst',
payload: {hello: 'world'},
});
expect(getFlipperDebugMessages().map(fixRowTimestamps))
.toMatchInlineSnapshot(`
Array [
Object {
"app": "Flipper",
"direction": "toFlipper",
"direction": "toFlipper:message",
"time": 1899-12-31T00:00:00.000Z,
},
Object {
"app": "FB4A",
"device": "Android Phone",
"direction": "toClient",
"direction": "toClient:call",
"payload": Object {
"hello": "world",
},
@@ -56,62 +78,44 @@ test('It can store rows', () => {
});
test('It can clear', () => {
const {instance, ...plugin} = TestUtils.startPlugin(Plugin);
expect(instance.rows.records()).toEqual([]);
expect(instance.highlightedRow.get()).toBeUndefined();
plugin.sendEvent('newMessage', {
registerFlipperDebugMessage({
app: 'Flipper',
direction: 'toFlipper',
direction: 'toFlipper:message',
});
instance.clear();
const newRows = instance.rows.records().map(fixRowTimestamps);
expect(newRows).toEqual([]);
});
test('It can highlight a row', () => {
const {instance, ...plugin} = TestUtils.startPlugin(Plugin);
plugin.sendEvent('newMessage', {
app: 'Flipper',
direction: 'toFlipper',
});
instance.setHighlightedRow(instance.rows.records()[0]);
expect(instance.rows.records()).toHaveLength(1);
expect(instance.highlightedRow.get()?.app).toEqual('Flipper');
clearFlipperDebugMessages();
expect(getFlipperDebugMessages()).toEqual([]);
});
test('It can render empty', async () => {
const {renderer} = TestUtils.renderPlugin(Plugin);
const renderer = render(<FlipperMessages />);
// Default message without any highlighted rows.
expect(
await renderer.findByText('Select a message to view details'),
).not.toBeNull();
renderer.unmount();
});
test('It can render rows', async () => {
const {renderer, ...plugin} = TestUtils.renderPlugin(Plugin);
const renderer = render(<FlipperMessages />);
plugin.sendEvent('newMessage', {
act(() => {
registerFlipperDebugMessage({
time: new Date(0, 0, 0, 0, 0, 0),
app: 'Flipper',
direction: 'toFlipper',
direction: 'toFlipper:message',
});
plugin.sendEvent('newMessage', {
registerFlipperDebugMessage({
time: new Date(0, 0, 0, 0, 0, 0),
app: 'FB4A',
direction: 'toClient',
direction: 'toClient:send',
device: 'Android Phone',
flipperInternalMethod: 'unique-string',
payload: {hello: 'world'},
});
});
expect((await renderer.findByText('unique-string')).parentElement)
.toMatchInlineSnapshot(`
@@ -154,8 +158,10 @@ test('It can render rows', async () => {
class="css-1vr131n-TableBodyColumnContainer e1luu51r0"
width="14%"
>
toClient
toClient:send
</div>
</div>
`);
renderer.unmount();
});

View File

@@ -16,7 +16,7 @@ import {Logger} from '../fb-interfaces/Logger';
import {LeftRail} from './LeftRail';
import {useStore, useDispatch} from '../utils/useStore';
import {ConsoleLogs} from '../chrome/ConsoleLogs';
import {FlipperDevTools} from '../chrome/FlipperDevTools';
import {setStaticView} from '../reducers/connections';
import {
ACTIVE_SHEET_CHANGELOG_RECENT_ONLY,
@@ -79,7 +79,7 @@ export function SandyApp() {
}
switch (newSelection) {
case 'flipperlogs':
dispatch(setStaticView(ConsoleLogs));
dispatch(setStaticView(FlipperDevTools));
break;
default:
}

View File

@@ -37,7 +37,6 @@ import {WebsocketClientFlipperConnection} from './utils/js-client-server-utils/w
import querystring from 'querystring';
import {IncomingMessage} from 'http';
import ws from 'ws';
import {initSelfInpector} from './utils/self-inspection/selfInspectionUtils';
import DummyDevice from './devices/DummyDevice';
import BaseDevice from './devices/BaseDevice';
import {sideEffect} from './utils/sideEffect';
@@ -106,10 +105,6 @@ class Server extends EventEmitter {
}
init() {
if (process.env.NODE_ENV === 'development') {
initSelfInpector(this.store, this.logger, this, this.connections);
}
const {insecure, secure} = this.store.getState().application.serverPorts;
this.initialisePromise = this.certificateProvider
.loadSecureServerConfig()

View File

@@ -9,7 +9,7 @@
import {notification, Typography} from 'antd';
import React from 'react';
import {ConsoleLogs} from '../chrome/ConsoleLogs';
import {FlipperDevTools} from '../chrome/FlipperDevTools';
import {setStaticView} from '../reducers/connections';
import {getStore} from '../store';
import {Layout} from '../ui';
@@ -29,7 +29,7 @@ export function showErrorNotification(message: string, description?: string) {
See{' '}
<Link
onClick={() => {
getStore().dispatch(setStaticView(ConsoleLogs));
getStore().dispatch(setStaticView(FlipperDevTools));
notification.close(key);
}}>
logs

View File

@@ -1,54 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {FlipperConnection, FlipperPlugin} from 'flipper-client-sdk';
export type MessageInfo = {
device?: string;
app: string;
flipperInternalMethod?: string;
plugin?: string;
pluginMethod?: string;
payload?: any;
direction:
| 'toClient:call'
| 'toClient:send'
| 'toFlipper:message'
| 'toFlipper:response';
};
export class FlipperMessagesClientPlugin implements FlipperPlugin {
protected connection: FlipperConnection | null = null;
onConnect(connection: FlipperConnection): void {
this.connection = connection;
}
onDisconnect(): void {
this.connection = null;
}
getId(): string {
return 'flipper-messages';
}
runInBackground(): boolean {
return true;
}
newMessage(message: MessageInfo) {
this.connection?.send('newMessage', message);
}
isConnected() {
return this.connection != null;
}
}
export const flipperMessagesClientPlugin = new FlipperMessagesClientPlugin();

View File

@@ -1,117 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {FlipperClientConnection} from '../../Client';
import {Flowable, Single} from 'rsocket-flowable';
import {Payload, ConnectionStatus, ISubscriber} from 'rsocket-types';
import {FlipperClient} from 'flipper-client-sdk';
// somehow linter isn't happy with next import so type definitions are copied
// import {IFutureSubject} from 'rsocket-flowable/Single';
type CancelCallback = () => void;
interface IFutureSubject<T> {
onComplete: (value: T) => void;
onError: (error: Error) => void;
onSubscribe: (cancel: CancelCallback | null | undefined) => void;
}
export class SelfInspectionFlipperClient<M>
extends FlipperClient
implements FlipperClientConnection<string, M>
{
connStatusSubscribers: Set<ISubscriber<ConnectionStatus>> = new Set();
connStatus: ConnectionStatus = {kind: 'CONNECTED'};
connectionStatus(): Flowable<ConnectionStatus> {
return new Flowable<ConnectionStatus>((subscriber) => {
subscriber.onSubscribe({
cancel: () => {
this.connStatusSubscribers.delete(subscriber);
},
request: (_) => {
this.connStatusSubscribers.add(subscriber);
subscriber.onNext(this.connStatus);
},
});
});
}
close(): void {
this.connStatus = {kind: 'CLOSED'};
this.connStatusSubscribers.forEach((subscriber) => {
subscriber.onNext(this.connStatus);
});
}
fireAndForget(payload: Payload<string, M>): void {
if (payload.data == null) {
return;
}
const message = JSON.parse(payload.data) as {
method: string;
id: number;
params: any;
};
this.onMessageReceived(message);
}
activeRequests = new Map<number, IFutureSubject<Payload<string, M>>>();
requestResponse(payload: Payload<string, M>): Single<Payload<string, M>> {
return new Single((subscriber) => {
subscriber.onSubscribe(() => {});
if (payload.data == null) {
subscriber.onError(new Error('empty payload'));
return;
}
const message = JSON.parse(payload.data) as {
method: string;
id: number;
params: any;
};
this.activeRequests.set(message.id, subscriber);
this.onMessageReceived(message);
});
}
// Client methods
messagesHandler: ((message: any) => void) | undefined;
start(_appName: string): void {
this.onConnect();
}
stop(): void {}
sendData(payload: any): void {
if (payload['success'] != null) {
const message = payload as {id: number; success: unknown};
const sub = this.activeRequests.get(message.id);
sub?.onComplete({data: JSON.stringify(message)});
this.activeRequests.delete(message.id);
return;
}
this.messagesHandler && this.messagesHandler(payload);
}
isAvailable(): boolean {
return true;
}
subscibeForClientMessages(handler: (message: any) => void) {
this.messagesHandler = handler;
}
}
export const selfInspectionClient = new SelfInspectionFlipperClient();

View File

@@ -1,94 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import Client, {ClientQuery} from '../../Client';
import {FlipperClientConnection} from '../../Client';
import {Store} from '../../reducers';
import {Logger} from '../../fb-interfaces/Logger';
import Server from '../../server';
import {buildClientId} from '../clientUtils';
import {selfInspectionClient} from './selfInspectionClient';
import {flipperMessagesClientPlugin} from './plugins/FlipperMessagesClientPlugin';
import {destroyDevice} from '../../reducers/connections';
export function initSelfInpector(
store: Store,
logger: Logger,
flipperServer: Server,
flipperConnections: Map<
string,
{
connection: FlipperClientConnection<any, any> | null | undefined;
client: Client;
}
>,
) {
const appName = 'Flipper';
selfInspectionClient.addPlugin(flipperMessagesClientPlugin);
const hostDevice = store
.getState()
.connections.devices.find((d) => d.serial === '');
if (!hostDevice) {
console.error('Failed to find host device for self inspector');
return;
}
const query: ClientQuery = {
app: appName,
os: 'MacOS',
device: 'emulator',
device_id: '',
sdk_version: 4,
};
const clientId = buildClientId(query);
const client = new Client(
clientId,
query,
selfInspectionClient,
logger,
store,
undefined,
hostDevice,
);
flipperConnections.set(clientId, {
connection: selfInspectionClient,
client: client,
});
selfInspectionClient.connectionStatus().subscribe({
onNext(payload) {
if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') {
console.debug(`Device disconnected ${client.id}`, 'server');
flipperServer.removeConnection(client.id);
destroyDevice(store, logger, client.id);
}
},
onSubscribe(subscription) {
subscription.request(Number.MAX_SAFE_INTEGER);
},
});
client.init().then(() => {
flipperServer.emit('new-client', client);
flipperServer.emit('clients-change');
client.emit('plugins-change');
selfInspectionClient.subscibeForClientMessages((payload: any) => {
// let's break the possible recursion problems here
// for example we want to send init plugin message, but store state is being updated when we enable plugins
setImmediate(() => {
client.onMessage(JSON.stringify(payload));
});
});
});
}

View File

@@ -25,6 +25,13 @@ export function usePluginInstance():
return pluginInstance;
}
export function usePluginInstanceMaybe():
| SandyPluginInstance
| SandyDevicePluginInstance
| undefined {
return useContext(SandyPluginContext);
}
export function usePlugin<
Factory extends PluginFactory<any, any> | DevicePluginFactory,
>(plugin: Factory): ReturnType<Factory> {

View File

@@ -48,7 +48,7 @@ import {Typography} from 'antd';
import {CoffeeOutlined, SearchOutlined, PushpinFilled} from '@ant-design/icons';
import {useAssertStableRef} from '../../utils/useAssertStableRef';
import {Formatter} from '../DataFormatter';
import {usePluginInstance} from '../../plugin/PluginContext';
import {usePluginInstanceMaybe} from '../../plugin/PluginContext';
import {debounce} from 'lodash';
import {useInUnitTest} from '../../utils/useInUnitTest';
import {createDataSource} from 'flipper-plugin/src/state/createDataSource';
@@ -142,7 +142,7 @@ export function DataTable<T extends object>(
const isUnitTest = useInUnitTest();
// eslint-disable-next-line
const scope = isUnitTest ? "" : usePluginInstance().pluginKey;
const scope = isUnitTest ? "" : usePluginInstanceMaybe()?.pluginKey ?? "";
const virtualizerRef = useRef<DataSourceVirtualizer | undefined>();
const [tableState, dispatch] = useReducer(
dataTableManagerReducer as DataTableReducer<T>,

View File

@@ -1,163 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
DataInspector,
DataTable,
DataTableColumn,
Layout,
createState,
PluginClient,
usePlugin,
useValue,
createDataSource,
DetailSidebar,
Panel,
theme,
styled,
} from 'flipper-plugin';
import {Button} from 'antd';
import {DeleteOutlined} from '@ant-design/icons';
import React from 'react';
export interface MessageInfo {
time?: Date;
device?: string;
app: string;
flipperInternalMethod?: string;
plugin?: string;
pluginMethod?: string;
payload?: any;
direction: 'toClient' | 'toFlipper';
}
export interface MessageRow extends MessageInfo {
time: Date;
}
const Placeholder = styled(Layout.Container)({
center: true,
color: theme.textColorPlaceholder,
fontSize: 18,
});
function createRow(message: MessageInfo): MessageRow {
return {
...message,
time: message.time == null ? new Date() : message.time,
};
}
type Events = {
newMessage: MessageInfo;
};
const COLUMN_CONFIG: DataTableColumn<MessageRow>[] = [
{
key: 'time',
title: 'Time',
},
{
key: 'device',
title: 'Device',
},
{
key: 'app',
title: 'App',
},
{
key: 'flipperInternalMethod',
title: 'Flipper Internal Method',
},
{
key: 'plugin',
title: 'Plugin',
},
{
key: 'pluginMethod',
title: 'Method',
},
{
key: 'direction',
title: 'Direction',
},
];
export function plugin(client: PluginClient<Events, {}>) {
const highlightedRow = createState<MessageRow>();
const rows = createDataSource<MessageRow>([], {
limit: 1024 * 10,
persist: 'messages',
});
const setHighlightedRow = (record: MessageRow) => {
highlightedRow.set(record);
};
const clear = () => {
highlightedRow.set(undefined);
rows.clear();
};
client.onMessage('newMessage', (payload) => {
rows.append(createRow(payload));
});
return {
rows,
highlightedRow,
setHighlightedRow,
clear,
};
}
function Sidebar() {
const instance = usePlugin(plugin);
const message = useValue(instance.highlightedRow);
const renderExtra = (extra: any) => (
<Panel title={'Payload'} collapsible={false}>
<DataInspector data={extra} expandRoot={false} />
</Panel>
);
return (
<DetailSidebar>
{message != null ? (
renderExtra(message.payload)
) : (
<Placeholder grow pad="large">
Select a message to view details
</Placeholder>
)}
</DetailSidebar>
);
}
export function Component() {
const instance = usePlugin(plugin);
const clearTableButton = (
<Button title="Clear logs" onClick={instance.clear}>
<DeleteOutlined />
</Button>
);
return (
<Layout.Container grow>
<DataTable<MessageRow>
dataSource={instance.rows}
columns={COLUMN_CONFIG}
onSelect={instance.setHighlightedRow}
extraActions={clearTableButton}
/>
<Sidebar />
</Layout.Container>
);
}

View File

@@ -1,29 +0,0 @@
{
"$schema": "https://fbflipper.com/schemas/plugin-package/v2.json",
"name": "flipper-plugin-flipper-messages",
"id": "flipper-messages",
"title": "Flipper Messages",
"icon": "bird",
"version": "0.0.0",
"description": "Flipper self inspection: Messages to and from client",
"main": "dist/bundle.js",
"flipperBundlerEntry": "index.tsx",
"license": "MIT",
"keywords": [
"flipper-plugin"
],
"bugs": {
"url": "https://github.com/facebook/flipper/issues"
},
"scripts": {
"lint": "flipper-pkg lint",
"build": "flipper-pkg bundle",
"watch": "flipper-pkg bundle --watch",
"prepack": "flipper-pkg lint && flipper-pkg bundle --production"
},
"peerDependencies": {
"flipper": "*",
"flipper-pkg": "*",
"flipper-plugin": "*"
}
}