Introduce createTablePlugin
Summary: This diff exposes the createTablePlugin from flipper-plugin, so that createTablePlugin based plugins can be converted to Sandy as well Reviewed By: jknoxville Differential Revision: D28031227 fbshipit-source-id: 8e9c82da08a83fddab740b46be9917b6a1023117
This commit is contained in:
committed by
Facebook GitHub Bot
parent
cf2405a466
commit
05bf55419f
@@ -62,6 +62,8 @@ type State = {
|
|||||||
* An optional resetMethod argument can be provided which will replace the current rows with the
|
* An optional resetMethod argument can be provided which will replace the current rows with the
|
||||||
* data provided. This is useful when connecting to Flipper for this first time, or reconnecting to
|
* data provided. This is useful when connecting to Flipper for this first time, or reconnecting to
|
||||||
* the client in an unknown state.
|
* the client in an unknown state.
|
||||||
|
*
|
||||||
|
* @deprecated use createTablePlugin from flipper-plugin instead
|
||||||
*/
|
*/
|
||||||
export function createTablePlugin<T extends RowData>(props: Props<T>) {
|
export function createTablePlugin<T extends RowData>(props: Props<T>) {
|
||||||
return class extends FlipperPlugin<State, any, PersistedState<T>> {
|
return class extends FlipperPlugin<State, any, PersistedState<T>> {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ test('Correct top level API exposed', () => {
|
|||||||
"batch",
|
"batch",
|
||||||
"createDataSource",
|
"createDataSource",
|
||||||
"createState",
|
"createState",
|
||||||
|
"createTablePlugin",
|
||||||
"produce",
|
"produce",
|
||||||
"renderReactRoot",
|
"renderReactRoot",
|
||||||
"sleep",
|
"sleep",
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ export {
|
|||||||
} from './ui/elements-inspector/ElementsInspector';
|
} from './ui/elements-inspector/ElementsInspector';
|
||||||
export {useMemoize} from './utils/useMemoize';
|
export {useMemoize} from './utils/useMemoize';
|
||||||
|
|
||||||
|
export {createTablePlugin} from './utils/createTablePlugin';
|
||||||
|
|
||||||
// It's not ideal that this exists in flipper-plugin sources directly,
|
// It's not ideal that this exists in flipper-plugin sources directly,
|
||||||
// but is the least pain for plugin authors.
|
// but is the least pain for plugin authors.
|
||||||
// Probably we should make sure that testing-library doesn't end up in our final Flipper bundle (which packages flipper-plugin)
|
// Probably we should make sure that testing-library doesn't end up in our final Flipper bundle (which packages flipper-plugin)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {BasePluginInstance, BasePluginClient} from './PluginBase';
|
|||||||
import {FlipperLib} from './FlipperLib';
|
import {FlipperLib} from './FlipperLib';
|
||||||
import {RealFlipperDevice} from './DevicePlugin';
|
import {RealFlipperDevice} from './DevicePlugin';
|
||||||
import {batched} from '../state/batch';
|
import {batched} from '../state/batch';
|
||||||
import {Atom, createState} from '../state/atom';
|
import {Atom, createState, ReadOnlyAtom} from '../state/atom';
|
||||||
|
|
||||||
type EventsContract = Record<string, any>;
|
type EventsContract = Record<string, any>;
|
||||||
type MethodsContract = Record<string, (params: any) => Promise<any>>;
|
type MethodsContract = Record<string, (params: any) => Promise<any>>;
|
||||||
@@ -40,6 +40,7 @@ export interface PluginClient<
|
|||||||
readonly appName: string;
|
readonly appName: string;
|
||||||
|
|
||||||
readonly isConnected: boolean;
|
readonly isConnected: boolean;
|
||||||
|
readonly connected: ReadOnlyAtom<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the onConnect event is fired whenever the plugin is connected to it's counter part on the device.
|
* the onConnect event is fired whenever the plugin is connected to it's counter part on the device.
|
||||||
@@ -169,6 +170,7 @@ export class SandyPluginInstance extends BasePluginInstance {
|
|||||||
get appName() {
|
get appName() {
|
||||||
return realClient.query.app;
|
return realClient.query.app;
|
||||||
},
|
},
|
||||||
|
connected: self.connected,
|
||||||
get isConnected() {
|
get isConnected() {
|
||||||
return self.connected.get();
|
return self.connected.get();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ import {Persistable, registerStorageAtom} from '../plugin/PluginBase';
|
|||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
|
|
||||||
export type Atom<T> = {
|
export interface ReadOnlyAtom<T> {
|
||||||
get(): T;
|
get(): T;
|
||||||
set(newValue: T): void;
|
|
||||||
update(recipe: (draft: Draft<T>) => void): void;
|
|
||||||
subscribe(listener: (value: T, prevValue: T) => void): () => void;
|
subscribe(listener: (value: T, prevValue: T) => void): () => void;
|
||||||
unsubscribe(listener: (value: T, prevValue: T) => void): void;
|
unsubscribe(listener: (value: T, prevValue: T) => void): void;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface Atom<T> extends ReadOnlyAtom<T> {
|
||||||
|
set(newValue: T): void;
|
||||||
|
update(recipe: (draft: Draft<T>) => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
class AtomValue<T> implements Atom<T>, Persistable {
|
class AtomValue<T> implements Atom<T>, Persistable {
|
||||||
value: T;
|
value: T;
|
||||||
@@ -93,9 +96,15 @@ export function createState(
|
|||||||
return atom;
|
return atom;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useValue<T>(atom: Atom<T>): T;
|
export function useValue<T>(atom: ReadOnlyAtom<T>): T;
|
||||||
export function useValue<T>(atom: Atom<T> | undefined, defaultValue: T): T;
|
export function useValue<T>(
|
||||||
export function useValue<T>(atom: Atom<T> | undefined, defaultValue?: T): T {
|
atom: ReadOnlyAtom<T> | undefined,
|
||||||
|
defaultValue: T,
|
||||||
|
): T;
|
||||||
|
export function useValue<T>(
|
||||||
|
atom: ReadOnlyAtom<T> | undefined,
|
||||||
|
defaultValue?: T,
|
||||||
|
): T {
|
||||||
const [localValue, setLocalValue] = useState<T>(
|
const [localValue, setLocalValue] = useState<T>(
|
||||||
atom ? atom.get() : defaultValue!,
|
atom ? atom.get() : defaultValue!,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import React from 'react';
|
|||||||
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
||||||
import {DataTableColumn} from './DataTable';
|
import {DataTableColumn} from './DataTable';
|
||||||
import {DataSource} from '../../state/DataSource';
|
import {DataSource} from '../../state/DataSource';
|
||||||
|
import {toFirstUpper} from '../../utils/toFirstUpper';
|
||||||
|
|
||||||
const {Item, SubMenu} = Menu;
|
const {Item, SubMenu} = Menu;
|
||||||
|
|
||||||
@@ -137,5 +138,5 @@ export function tableContextMenuFactory<T>(
|
|||||||
|
|
||||||
function friendlyColumnTitle(column: DataTableColumn<any>): string {
|
function friendlyColumnTitle(column: DataTableColumn<any>): string {
|
||||||
const name = column.title || column.key;
|
const name = column.title || column.key;
|
||||||
return name[0].toUpperCase() + name.substr(1);
|
return toFirstUpper(name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {CaretDownFilled, CaretUpFilled} from '@ant-design/icons';
|
|||||||
import {Layout} from '../Layout';
|
import {Layout} from '../Layout';
|
||||||
import {Sorting, SortDirection, DataTableDispatch} from './DataTableManager';
|
import {Sorting, SortDirection, DataTableDispatch} from './DataTableManager';
|
||||||
import {FilterButton, FilterIcon} from './ColumnFilter';
|
import {FilterButton, FilterIcon} from './ColumnFilter';
|
||||||
|
import {toFirstUpper} from '../../utils/toFirstUpper';
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
@@ -187,7 +188,13 @@ function TableHeadColumn({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}>
|
tabIndex={0}>
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
{column.title ?? <> </>}
|
{column.title === undefined ? (
|
||||||
|
toFirstUpper(column.key)
|
||||||
|
) : column.title === '' ? (
|
||||||
|
<> </>
|
||||||
|
) : (
|
||||||
|
column.title
|
||||||
|
)}
|
||||||
<SortIcons
|
<SortIcons
|
||||||
direction={sorted}
|
direction={sorted}
|
||||||
onSort={(dir) =>
|
onSort={(dir) =>
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* 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 {createTablePlugin} from '../createTablePlugin';
|
||||||
|
import {startPlugin} from '../../test-utils/test-utils';
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
method: 'method',
|
||||||
|
resetMethod: 'resetMethod',
|
||||||
|
columns: [],
|
||||||
|
renderSidebar: (_row: RowData) => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
type RowData = {
|
||||||
|
id: string;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('createTablePlugin returns FlipperPlugin', () => {
|
||||||
|
const tablePlugin = createTablePlugin(PROPS);
|
||||||
|
const p = startPlugin(tablePlugin);
|
||||||
|
expect(Object.keys(p.instance)).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
"selection",
|
||||||
|
"rows",
|
||||||
|
"clear",
|
||||||
|
"tableManagerRef",
|
||||||
|
"connected",
|
||||||
|
"isPaused",
|
||||||
|
"resumePause",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createTablePlugin can add and reset data', () => {
|
||||||
|
const resetMethod = 'resetMethod';
|
||||||
|
const tablePlugin = createTablePlugin({...PROPS, resetMethod});
|
||||||
|
|
||||||
|
const initialSnapshot = [{id: '1'}];
|
||||||
|
|
||||||
|
const p = startPlugin(tablePlugin, {initialState: {rows: initialSnapshot}});
|
||||||
|
|
||||||
|
expect(p.instance.rows.records()).toEqual(initialSnapshot);
|
||||||
|
|
||||||
|
p.sendEvent('method', {id: '2'});
|
||||||
|
expect(p.instance.rows.records()).toEqual([{id: '1'}, {id: '2'}]);
|
||||||
|
expect(p.exportState()).toEqual({rows: [{id: '1'}, {id: '2'}]});
|
||||||
|
|
||||||
|
p.sendEvent('resetMethod', {});
|
||||||
|
expect(p.instance.rows.records()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createTablePlugin can add and reset data', () => {
|
||||||
|
const resetMethod = 'resetMethod';
|
||||||
|
const tablePlugin = createTablePlugin({...PROPS, resetMethod});
|
||||||
|
|
||||||
|
const initialSnapshot = [{id: '1'}];
|
||||||
|
|
||||||
|
const p = startPlugin(tablePlugin, {initialState: {rows: initialSnapshot}});
|
||||||
|
|
||||||
|
expect(p.instance.rows.records()).toEqual(initialSnapshot);
|
||||||
|
|
||||||
|
p.sendEvent('method', {id: '2'});
|
||||||
|
expect(p.instance.rows.records()).toEqual([{id: '1'}, {id: '2'}]);
|
||||||
|
expect(p.exportState()).toEqual({rows: [{id: '1'}, {id: '2'}]});
|
||||||
|
|
||||||
|
// without key, this will append
|
||||||
|
p.sendEvent('method', {id: '1'});
|
||||||
|
expect(p.instance.rows.records()).toEqual([{id: '1'}, {id: '2'}, {id: '1'}]);
|
||||||
|
|
||||||
|
p.sendEvent('resetMethod', {});
|
||||||
|
expect(p.instance.rows.records()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createTablePlugin can upsert data if key is set', () => {
|
||||||
|
const tablePlugin = createTablePlugin({...PROPS, key: 'id'});
|
||||||
|
|
||||||
|
const initialSnapshot = [{id: '1'}];
|
||||||
|
|
||||||
|
const p = startPlugin(tablePlugin, {initialState: {rows: initialSnapshot}});
|
||||||
|
|
||||||
|
expect(p.instance.rows.records()).toEqual(initialSnapshot);
|
||||||
|
|
||||||
|
p.sendEvent('method', {id: '2'});
|
||||||
|
expect(p.instance.rows.records()).toEqual([{id: '1'}, {id: '2'}]);
|
||||||
|
expect(p.exportState()).toEqual({rows: [{id: '1'}, {id: '2'}]});
|
||||||
|
|
||||||
|
// key set, so we can upsert
|
||||||
|
p.sendEvent('method', {id: '1', value: 'hi'});
|
||||||
|
expect(p.instance.rows.records()).toEqual([
|
||||||
|
{id: '1', value: 'hi'},
|
||||||
|
{id: '2'},
|
||||||
|
]);
|
||||||
|
});
|
||||||
229
desktop/flipper-plugin/src/utils/createTablePlugin.tsx
Normal file
229
desktop/flipper-plugin/src/utils/createTablePlugin.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
DeleteOutlined,
|
||||||
|
PauseCircleOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {Button, notification, Typography} from 'antd';
|
||||||
|
import React, {createRef, useCallback} from 'react';
|
||||||
|
import {PluginClient} from '../plugin/Plugin';
|
||||||
|
import {usePlugin} from '../plugin/PluginContext';
|
||||||
|
import {createState, useValue} from '../state/atom';
|
||||||
|
import {createDataSource, DataSource} from '../state/DataSource';
|
||||||
|
import {DataInspector} from '../ui/data-inspector/DataInspector';
|
||||||
|
import {DataTable, DataTableColumn} from '../ui/data-table/DataTable';
|
||||||
|
import {DataTableManager} from '../ui/data-table/DataTableManager';
|
||||||
|
import {DetailSidebar} from '../ui/DetailSidebar';
|
||||||
|
import {Layout} from '../ui/Layout';
|
||||||
|
import {Panel} from '../ui/Panel';
|
||||||
|
|
||||||
|
function defaultRenderSidebar<T>(record: T) {
|
||||||
|
return (
|
||||||
|
<Panel title="Payload" collapsible={false} pad>
|
||||||
|
<DataInspector data={record} expandRoot />
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginResult<Raw, Row> = {
|
||||||
|
plugin(
|
||||||
|
client: PluginClient<Record<string, Raw | {}>>,
|
||||||
|
): {
|
||||||
|
rows: DataSource<Row>;
|
||||||
|
};
|
||||||
|
Component(): React.ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createTablePlugin creates a Plugin class which handles fetching data from the client and
|
||||||
|
* displaying in in a table. The table handles selection of items and rendering a sidebar where
|
||||||
|
* more detailed information can be presented about the selected row.
|
||||||
|
*
|
||||||
|
* The plugin expects the be able to subscribe to the `method` argument and recieve either an array
|
||||||
|
* of data objects or a single data object. Each data object represents a row in the table which is
|
||||||
|
* build by calling the `buildRow` function argument.
|
||||||
|
*
|
||||||
|
* An optional resetMethod argument can be provided which will replace 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.
|
||||||
|
*/
|
||||||
|
export function createTablePlugin<Row extends object>(props: {
|
||||||
|
method: string;
|
||||||
|
resetMethod?: string;
|
||||||
|
columns: DataTableColumn<Row>[];
|
||||||
|
renderSidebar?: (record: Row) => any;
|
||||||
|
key?: keyof Row;
|
||||||
|
}): PluginResult<Row, Row>;
|
||||||
|
export function createTablePlugin<
|
||||||
|
Raw extends object,
|
||||||
|
Row extends object = Raw
|
||||||
|
>(props: {
|
||||||
|
buildRow: (record: Raw) => Row;
|
||||||
|
method: string;
|
||||||
|
resetMethod?: string;
|
||||||
|
columns: DataTableColumn<Row>[];
|
||||||
|
renderSidebar?: (record: Row) => any;
|
||||||
|
key?: keyof Raw;
|
||||||
|
}): PluginResult<Raw, Row>;
|
||||||
|
export function createTablePlugin<
|
||||||
|
Raw extends object,
|
||||||
|
Method extends string,
|
||||||
|
ResetMethod extends string,
|
||||||
|
Row extends object = Raw
|
||||||
|
>(props: {
|
||||||
|
method: Method;
|
||||||
|
resetMethod?: ResetMethod;
|
||||||
|
columns: DataTableColumn<Row>[];
|
||||||
|
renderSidebar?: (record: Row) => any;
|
||||||
|
buildRow?: (record: Raw) => Row;
|
||||||
|
key?: keyof Raw;
|
||||||
|
}) {
|
||||||
|
function plugin(
|
||||||
|
client: PluginClient<Record<Method, Raw> & Record<ResetMethod, {}>, {}>,
|
||||||
|
) {
|
||||||
|
const rows = createDataSource<Row>([], {
|
||||||
|
persist: 'rows',
|
||||||
|
key: props.key,
|
||||||
|
});
|
||||||
|
const selection = createState<undefined | Row>(undefined);
|
||||||
|
const isPaused = createState(false);
|
||||||
|
const tableManagerRef = createRef<undefined | DataTableManager<Row>>();
|
||||||
|
|
||||||
|
client.onMessage(props.method, (event) => {
|
||||||
|
if (isPaused.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const record = props.buildRow
|
||||||
|
? props.buildRow(event)
|
||||||
|
: ((event as any) as Row);
|
||||||
|
if (props.key) {
|
||||||
|
rows.upsert(record);
|
||||||
|
} else {
|
||||||
|
rows.append(record);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.resetMethod) {
|
||||||
|
client.onMessage(props.resetMethod, () => {
|
||||||
|
clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// help plugin authors with finding out what the events and data shape is from the plugin
|
||||||
|
const unhandledMessagesSeen = new Set<string>();
|
||||||
|
client.onUnhandledMessage((message, params) => {
|
||||||
|
if (unhandledMessagesSeen.has(message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unhandledMessagesSeen.add(message);
|
||||||
|
notification.warn({
|
||||||
|
message: 'Unhandled message: ' + message,
|
||||||
|
description: (
|
||||||
|
<Typography.Paragraph>
|
||||||
|
<pre>{JSON.stringify(params, null, 2)}</pre>
|
||||||
|
</Typography.Paragraph>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.addMenuEntry(
|
||||||
|
{
|
||||||
|
action: 'clear',
|
||||||
|
handler: clear,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'createPaste',
|
||||||
|
handler: createPaste,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'goToBottom',
|
||||||
|
handler: goToBottom,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
rows.clear();
|
||||||
|
tableManagerRef.current?.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPaste() {
|
||||||
|
let selection = tableManagerRef.current?.getSelectedItems();
|
||||||
|
if (!selection?.length) {
|
||||||
|
selection = rows.view.output(0, rows.view.size);
|
||||||
|
}
|
||||||
|
if (selection?.length) {
|
||||||
|
client.createPaste(JSON.stringify(selection, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToBottom() {
|
||||||
|
tableManagerRef?.current?.selectItem(rows.view.size - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selection,
|
||||||
|
rows,
|
||||||
|
clear,
|
||||||
|
tableManagerRef,
|
||||||
|
connected: client.connected,
|
||||||
|
isPaused,
|
||||||
|
resumePause() {
|
||||||
|
isPaused.update((v) => !v);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function Component() {
|
||||||
|
const instance = usePlugin(plugin);
|
||||||
|
const paused = useValue(instance.isPaused);
|
||||||
|
const selection = useValue(instance.selection);
|
||||||
|
const connected = useValue(instance.connected);
|
||||||
|
|
||||||
|
const handleSelect = useCallback((v) => instance.selection.set(v), [
|
||||||
|
instance,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Container grow>
|
||||||
|
<DataTable<Row>
|
||||||
|
columns={props.columns}
|
||||||
|
dataSource={instance.rows}
|
||||||
|
tableManagerRef={instance.tableManagerRef}
|
||||||
|
autoScroll
|
||||||
|
onSelect={handleSelect}
|
||||||
|
extraActions={
|
||||||
|
connected ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
title={`Click to ${paused ? 'resume' : 'pause'} the stream`}
|
||||||
|
danger={paused}
|
||||||
|
onClick={instance.resumePause}>
|
||||||
|
{paused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
||||||
|
</Button>
|
||||||
|
<Button title="Clear records" onClick={instance.clear}>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DetailSidebar>
|
||||||
|
{selection
|
||||||
|
? props.renderSidebar?.(selection) ??
|
||||||
|
defaultRenderSidebar(selection)
|
||||||
|
: null}
|
||||||
|
</DetailSidebar>
|
||||||
|
</Layout.Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {plugin, Component};
|
||||||
|
}
|
||||||
18
desktop/flipper-plugin/src/utils/toFirstUpper.tsx
Normal file
18
desktop/flipper-plugin/src/utils/toFirstUpper.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function toFirstUpper(name: string): string {
|
||||||
|
if (!name) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if (name.length === 1) {
|
||||||
|
return name.toUpperCase();
|
||||||
|
}
|
||||||
|
return name[0].toUpperCase() + name.substr(1);
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ A string that uniquely identifies the current application, is based on a combina
|
|||||||
|
|
||||||
A key that uniquely identifies this plugin instance, captures the current device/client/plugin combination.
|
A key that uniquely identifies this plugin instance, captures the current device/client/plugin combination.
|
||||||
|
|
||||||
|
#### `connected`
|
||||||
#### `isConnected`
|
#### `isConnected`
|
||||||
|
|
||||||
Returns whether there is currently an active connection. This is true if:
|
Returns whether there is currently an active connection. This is true if:
|
||||||
@@ -61,6 +62,9 @@ Returns whether there is currently an active connection. This is true if:
|
|||||||
2. The client is still connected
|
2. The client is still connected
|
||||||
3. The plugin is currently selected by the user _or_ the plugin is running in the background.
|
3. 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 listeners
|
### Events listeners
|
||||||
|
|
||||||
#### `onMessage`
|
#### `onMessage`
|
||||||
@@ -99,7 +103,7 @@ export function plugin(client: PluginClient<Events, {}>) {
|
|||||||
|
|
||||||
Usage: `client.onUnhandledMessage(callback: (event: string, params) => void)`
|
Usage: `client.onUnhandledMessage(callback: (event: string, params) => void)`
|
||||||
|
|
||||||
This method subscribe to all messages arriving from the devices which is not handled by an `onMessage` handler.
|
This method subscribe 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.
|
This handler is untyped, and onMessage should be favored over using onUnhandledMessage if the event name is known upfront.
|
||||||
|
|
||||||
#### `onActivate`
|
#### `onActivate`
|
||||||
@@ -149,8 +153,8 @@ Trigger when the users navigates to this plugin using a deeplink, either from an
|
|||||||
|
|
||||||
Usage: `client.onExport(callback: (idler, onStatusMessage) => Promise<state>)`
|
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.
|
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.
|
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.
|
Serializable is defined as: non-cyclic data, consisting purely of primitive values, plain objects, arrays or Date, Set or Map objects.
|
||||||
|
|
||||||
@@ -477,7 +481,7 @@ const rows = createState<string[]>([], {persist: 'rows'});
|
|||||||
const selectedID = createState<string | null>(null, {persist: 'selection'});
|
const selectedID = createState<string | null>(null, {persist: 'selection'});
|
||||||
|
|
||||||
// Listener will be called on each rows.set() and rows.update() call until unsubscribed.
|
// Listener will be called on each rows.set() and rows.update() call until unsubscribed.
|
||||||
const unsubscribe = rows.subscribe((value, prevValue) => {
|
const unsubscribe = rows.subscribe((value, prevValue) => {
|
||||||
console.log(`Rows state updated. New length: ${value.length}. Prev length: ${prevValue.length}.`);
|
console.log(`Rows state updated. New length: ${value.length}. Prev length: ${prevValue.length}.`);
|
||||||
});
|
});
|
||||||
rows.set(["hello"]) // Listener will be notified about the change
|
rows.set(["hello"]) // Listener will be notified about the change
|
||||||
@@ -493,12 +497,12 @@ console.log(rows.get().length) // 2
|
|||||||
|
|
||||||
Usage: `createDataSource<T>(initialSet?: T[], options?): DataSource<T>`
|
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.
|
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`.
|
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`,
|
`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.
|
which stores items based on insertion order, but also allows for efficient by-id lookups.
|
||||||
|
|
||||||
Each `DataSource` exposes a `view` property, which contains a `DataSourceView`.
|
Each `DataSource` exposes a `view` property, which contains a `DataSourceView`.
|
||||||
A `DataSourceView` is a materialized view which can be sorted, filtered and windowed, and will be kept incrementally up to date with the underlying `DataSource`.
|
A `DataSourceView` is a materialized view which can be sorted, filtered and windowed, and will be 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.
|
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.
|
||||||
@@ -532,7 +536,7 @@ export function devicePlugin(client: DevicePluginClient) {
|
|||||||
|
|
||||||
### DataSource
|
### DataSource
|
||||||
|
|
||||||
Stores large amounts of records efficiently. See [`createDataSource`](#createdatasource) for an introduction.
|
Stores large amounts of records efficiently. See [`createDataSource`](#createdatasource) for an introduction.
|
||||||
|
|
||||||
#### limit
|
#### limit
|
||||||
|
|
||||||
@@ -605,11 +609,11 @@ Usage: `fork(): DataSourceView`. Creates an additional materialized view on this
|
|||||||
|
|
||||||
### DataSourceView
|
### 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.
|
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`.
|
Note that the default window is empty, so after obtaining a `DataSourceView` one should typically call `setWindow`.
|
||||||
See [`createDataSource`](#createdatasource) for an introduction.
|
See [`createDataSource`](#createdatasource) for an introduction.
|
||||||
|
|
||||||
The DataSourceView API is important if are creating your own visualization of a `DataSource`.
|
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.
|
However, if a `DataSource` is visualized using a `DataTable`, there is typically no need to directly interact with this API.
|
||||||
|
|
||||||
#### datasource
|
#### datasource
|
||||||
@@ -646,13 +650,13 @@ Usage: `output(): T[]` or `output(start, end): T[]`. Returns a defensive copy of
|
|||||||
|
|
||||||
#### [Symbol.iterator]
|
#### [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.
|
`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
|
#### 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)`).
|
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.
|
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.
|
Windowing will always be applied only after applying any filters, sorting and reversing.
|
||||||
|
|
||||||
@@ -662,13 +666,13 @@ Usage: `setFilter(filter: (record: T) => boolean)`. Applies a filter to the curr
|
|||||||
|
|
||||||
#### setSortBy
|
#### 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.
|
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.
|
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
|
#### toggleRevered
|
||||||
|
|
||||||
Usage: `toggleReversed()`. Toggles the output order between ascending and descending.
|
Usage: `toggleReversed()`. Toggles the output order between ascending and descending.
|
||||||
|
|
||||||
#### setReversed
|
#### setReversed
|
||||||
|
|
||||||
@@ -684,7 +688,7 @@ Usage: `get(index: number)`. Returns the record at the given position in the out
|
|||||||
|
|
||||||
#### setListener
|
#### 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.
|
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 amount of shifts / updates. Such as changing sorting or filtering, calling `clear()` or `reset()`, or doing a large `shift`.
|
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 amount 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.
|
Currently only one listener is allowed at a time. Please contact the Flipper oncall if that doesn't suffice.
|
||||||
@@ -696,7 +700,7 @@ type OutputChange =
|
|||||||
index: number; // the position at which records were inserted or removed
|
index: number; // the position at which records were inserted or removed
|
||||||
location: 'before' | 'in' | 'after'; // relative to current window
|
location: 'before' | 'in' | 'after'; // relative to current window
|
||||||
delta: number; // how many records were inserted (postive number) or removed (negative number)
|
delta: number; // how many records were inserted (postive number) or removed (negative number)
|
||||||
newCount: number; // the new .size of the DataSourceView
|
newCount: number; // the new .size of the DataSourceView
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
// an item, inside the current window, was changed
|
// an item, inside the current window, was changed
|
||||||
@@ -758,9 +762,9 @@ interface Logger {
|
|||||||
|
|
||||||
Usage: `const eventHandler = useTrackedCallback("Interaction description", handlerFunction, deps)`
|
Usage: `const eventHandler = useTrackedCallback("Interaction description", handlerFunction, deps)`
|
||||||
|
|
||||||
Utility that wraps React's `useCallback` with tracking capabilities.
|
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.
|
The API is similar, except that the first argument describes the interaction handled by the given event handler.
|
||||||
See [Tracked](#tracked) for more info.
|
See [Tracked](#tracked) for more info.
|
||||||
|
|
||||||
### useMemoize
|
### useMemoize
|
||||||
|
|
||||||
@@ -788,7 +792,7 @@ export function findMetroDevice(findMetroDevice, deviceList) {
|
|||||||
|
|
||||||
### useLocalStorageState
|
### useLocalStorageState
|
||||||
|
|
||||||
Like `useState`, but the value will be stored in local storage under the given key, and read back upon initialization.
|
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 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`](#trackingscope)).
|
The storage key will be scoped automatically to the current plugin and any additional tracking scopes. (See [`TrackingScope`](#trackingscope)).
|
||||||
|
|
||||||
@@ -803,7 +807,7 @@ const [showWhitespace, setShowWhitespace] = useLocalStorageState(
|
|||||||
|
|
||||||
### Layout.*
|
### Layout.*
|
||||||
|
|
||||||
Layout elements can be used to organize the screen layout.
|
Layout elements can be used to organize the screen layout.
|
||||||
See `View > Flipper Style Guide` inside the Flipper application for more details.
|
See `View > Flipper Style Guide` inside the Flipper application for more details.
|
||||||
|
|
||||||
### DataTable
|
### DataTable
|
||||||
@@ -860,7 +864,7 @@ See `View > Flipper Style Guide` inside the Flipper application for more details
|
|||||||
### DetailSidebar
|
### DetailSidebar
|
||||||
|
|
||||||
An element that can be passed children which will be shown in the right sidebar of Flipper.
|
An element that can be passed children which will be shown in the right sidebar of Flipper.
|
||||||
Horizontal scrolling will be enabled by default.
|
Horizontal scrolling will be enabled by default.
|
||||||
To fine-tune the default dimensions use `width` and `minWidth`.
|
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.
|
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.
|
||||||
|
|
||||||
@@ -868,7 +872,7 @@ It doesn't really matter where exactly this component is used in your layout, as
|
|||||||
|
|
||||||
### Tracked
|
### Tracked
|
||||||
|
|
||||||
An element that can be used to track user interactions.
|
An element that can be used to track user interactions.
|
||||||
An example scuba query can be found [here](https://fburl.com/scuba/infinity_analytics_events/xryoq5j7).
|
An example scuba query can be found [here](https://fburl.com/scuba/infinity_analytics_events/xryoq5j7).
|
||||||
|
|
||||||
See `View > Flipper Style Guide` inside the Flipper application for more details.
|
See `View > Flipper Style Guide` inside the Flipper application for more details.
|
||||||
@@ -892,25 +896,29 @@ See `View > Flipper Style Guide` inside the Flipper application for more details
|
|||||||
|
|
||||||
## Utilities
|
## Utilities
|
||||||
|
|
||||||
|
### createTablePlugin
|
||||||
|
|
||||||
|
Utility to create a plugin that consists of a master table and details json view with minimal effort. See [../tutorial/js-table.mdx](Showing a table) for more details.
|
||||||
|
|
||||||
### batch
|
### batch
|
||||||
|
|
||||||
Usage: `batch(() => { /* state updates */ })`
|
Usage: `batch(() => { /* state updates */ })`
|
||||||
|
|
||||||
Low-level utility to batch state updates to reduce the amount of potential re-renders by React.
|
Low-level utility to batch state updates to reduce the amount of potential re-renders by React.
|
||||||
Wraps React's `unstable_batchedUpdates`.
|
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.
|
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
|
### produce
|
||||||
|
|
||||||
A convenience re-export of `produce` from [Immer](https://immerjs.github.io/immer/docs/produce).
|
A convenience re-export of `produce` from [Immer](https://immerjs.github.io/immer/docs/produce).
|
||||||
The `update` method of the state atoms returned by `createState` automatically applies `produce` to its updater function.
|
The `update` method of the state atoms returned by `createState` automatically applies `produce` to its updater function.
|
||||||
|
|
||||||
### renderReactRoot
|
### renderReactRoot
|
||||||
|
|
||||||
Usage: `renderReactRoot(handler: (unmount: () => void) => React.ReactElement)`
|
Usage: `renderReactRoot(handler: (unmount: () => void) => React.ReactElement)`
|
||||||
|
|
||||||
Renders an element outside the current DOM tree.
|
Renders an element outside the current DOM tree.
|
||||||
This is a low-level utility that can be used to render for example Modal dialogs.
|
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.
|
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.
|
Once the element can be removed from the DOM, the `unmount` callback should be called.
|
||||||
Example:
|
Example:
|
||||||
@@ -934,7 +942,7 @@ Creates a promise that automatically resolves after the specified amount of mill
|
|||||||
|
|
||||||
## styled
|
## styled
|
||||||
|
|
||||||
A convenience re-export of `styled` from [emotion](https://emotion.sh/docs/styled).
|
A convenience re-export of `styled` from [emotion](https://emotion.sh/docs/styled).
|
||||||
|
|
||||||
## TestUtils
|
## TestUtils
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user