Summary: Old MasterDetail and DataTable (pre-power search) are using `extraActions` prop which no longer exists in teh new components. This diff adds a fallback. Reviewed By: ivanmisuno Differential Revision: D49869730 fbshipit-source-id: 4c1a658aa94acc770836f9bf296a3df7d6cdcb3e
287 lines
7.7 KiB
TypeScript
287 lines
7.7 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 * as React from 'react';
|
|
import {
|
|
createElement,
|
|
createRef,
|
|
useCallback,
|
|
useState,
|
|
useEffect,
|
|
} from 'react';
|
|
import {DataInspector} from './data-inspector/DataInspector';
|
|
import {DataTable, DataTableProps} from './data-table/DataTableWithPowerSearch';
|
|
import {DataTableManager} from './data-table/DataTableWithPowerSearchManager';
|
|
import {DetailSidebar} from './DetailSidebar';
|
|
import {Layout} from './Layout';
|
|
import {Panel} from './Panel';
|
|
|
|
import {
|
|
DeleteOutlined,
|
|
PauseCircleOutlined,
|
|
PlayCircleOutlined,
|
|
} from '@ant-design/icons';
|
|
import {Button} from 'antd';
|
|
import {usePluginInstance} from '../plugin/PluginContext';
|
|
import {Atom, createState} from 'flipper-plugin-core';
|
|
import {useAssertStableRef} from '../utils/useAssertStableRef';
|
|
import {useValue} from '../state/atom';
|
|
|
|
type MasterDetailProps<T> = {
|
|
/**
|
|
* Where 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
|
|
*/
|
|
sidebarPosition?: 'bottom' | 'right' | 'main' | 'overlay' | 'none';
|
|
/**
|
|
* Component that accepts a 'record' prop that is used to render details.
|
|
* If none is provided, a standard `DataInspector` component will be used to display the entire record.
|
|
*/
|
|
sidebarComponent?: React.FC<{
|
|
record: T;
|
|
tableManagerRef?: React.RefObject<DataTableManager<T> | undefined>;
|
|
}>;
|
|
/**
|
|
* Default size of the sidebar.
|
|
*/
|
|
sidebarSize?: number;
|
|
/**
|
|
* If provided, this atom will be used to store selection in.
|
|
*/
|
|
selection?: Atom<T | undefined>;
|
|
/**
|
|
* If provided, this atom will be used to store pause/resume state in, and a pause/resume toggle will be shown
|
|
*/
|
|
isPaused?: Atom<boolean>;
|
|
/**
|
|
* If set, a clear button will be shown.
|
|
* By default this will clear the dataSource (if applicable).
|
|
*/
|
|
enableClear?: boolean;
|
|
/**
|
|
* Callback to be called when clear action is used.
|
|
*/
|
|
onClear?: () => void;
|
|
/**
|
|
* If provided, standard menu entries will be created for clear, goToBottom and createPaste
|
|
*/
|
|
enableMenuEntries?: boolean;
|
|
};
|
|
|
|
export function MasterDetailWithPowerSearch<T extends object>({
|
|
dataSource,
|
|
records,
|
|
sidebarComponent,
|
|
sidebarPosition,
|
|
sidebarSize,
|
|
onSelect,
|
|
actionsTop,
|
|
extraActions,
|
|
enableMenuEntries,
|
|
enableClear,
|
|
isPaused,
|
|
selection,
|
|
onClear,
|
|
...tableProps
|
|
}: MasterDetailProps<T> & DataTableProps<T>) {
|
|
useAssertStableRef(isPaused, 'isPaused');
|
|
useAssertStableRef(selection, 'selection');
|
|
|
|
const pluginInstance = usePluginInstance();
|
|
const {client} = pluginInstance;
|
|
const connected = useValue(pluginInstance.client.connected);
|
|
|
|
const selectionAtom =
|
|
// if no selection atom is provided, the component is uncontrolled
|
|
// and we maintain our own selection atom
|
|
// eslint-disable-next-line
|
|
selection ?? useState(() => createState<T | undefined>(undefined))[0];
|
|
const selectedRecord = useValue(selectionAtom);
|
|
|
|
// if a tableManagerRef is provided, we piggy back on that same ref
|
|
// eslint-disable-next-line
|
|
const tableManagerRef =
|
|
tableProps.tableManagerRef ?? createRef<undefined | DataTableManager<T>>();
|
|
|
|
const pausedState = useValue(isPaused, false);
|
|
|
|
const sidebar =
|
|
sidebarPosition !== 'none' && selectedRecord && sidebarComponent
|
|
? createElement(sidebarComponent, {
|
|
record: selectedRecord,
|
|
tableManagerRef,
|
|
})
|
|
: null;
|
|
|
|
const handleSelect = useCallback(
|
|
(record: T | undefined, records: T[]) => {
|
|
selectionAtom.set(record);
|
|
onSelect?.(record, records);
|
|
},
|
|
[selectionAtom, onSelect],
|
|
);
|
|
|
|
const handleTogglePause = useCallback(() => {
|
|
isPaused?.set(!isPaused?.get());
|
|
}, [isPaused]);
|
|
|
|
const handleClear = useCallback(() => {
|
|
handleSelect(undefined, []);
|
|
if (dataSource) {
|
|
dataSource.clear();
|
|
onClear?.();
|
|
} else {
|
|
if (!onClear) {
|
|
throw new Error(
|
|
"onClear must be set when using 'enableClear' and 'records'",
|
|
);
|
|
}
|
|
onClear();
|
|
}
|
|
}, [dataSource, onClear, handleSelect]);
|
|
|
|
const handleCreatePaste = useCallback(() => {
|
|
const selection = tableManagerRef.current?.getSelectedItems();
|
|
switch (selection?.length) {
|
|
case undefined:
|
|
case 0:
|
|
return;
|
|
case 1:
|
|
client.createPaste(JSON.stringify(selection[0], null, 2));
|
|
break;
|
|
default:
|
|
client.createPaste(JSON.stringify(selection, null, 2));
|
|
}
|
|
}, [client, tableManagerRef]);
|
|
|
|
const handleGoToBottom = useCallback(() => {
|
|
const size = dataSource ? dataSource.view.size : records!.length;
|
|
tableManagerRef?.current?.selectItem(size - 1);
|
|
}, [dataSource, records, tableManagerRef]);
|
|
|
|
useEffect(
|
|
function setupMenuEntries() {
|
|
if (enableMenuEntries) {
|
|
if (enableClear) {
|
|
client.addMenuEntry({
|
|
action: 'clear',
|
|
handler: handleClear,
|
|
});
|
|
}
|
|
if (client.isFB) {
|
|
client.addMenuEntry({
|
|
action: 'createPaste',
|
|
handler: handleCreatePaste,
|
|
});
|
|
}
|
|
client.addMenuEntry({
|
|
action: 'goToBottom',
|
|
handler: handleGoToBottom,
|
|
});
|
|
}
|
|
},
|
|
[
|
|
client,
|
|
enableClear,
|
|
enableMenuEntries,
|
|
handleClear,
|
|
handleCreatePaste,
|
|
handleGoToBottom,
|
|
],
|
|
);
|
|
|
|
const table = (
|
|
<DataTable<T>
|
|
enableAutoScroll
|
|
{...tableProps}
|
|
dataSource={dataSource as any}
|
|
records={records!}
|
|
tableManagerRef={tableManagerRef}
|
|
onSelect={handleSelect}
|
|
actionsRight={
|
|
<>
|
|
{connected && isPaused && (
|
|
<Button
|
|
size="small"
|
|
type="text"
|
|
style={{height: '100%'}}
|
|
title={`Click to ${pausedState ? 'resume' : 'pause'} the stream`}
|
|
danger={pausedState}
|
|
onClick={handleTogglePause}>
|
|
{pausedState ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
|
</Button>
|
|
)}
|
|
{connected && enableClear && (
|
|
<Button
|
|
size="small"
|
|
type="text"
|
|
title="Clear records"
|
|
onClick={handleClear}
|
|
style={{height: '100%'}}>
|
|
<DeleteOutlined />
|
|
</Button>
|
|
)}
|
|
</>
|
|
}
|
|
actionsTop={actionsTop}
|
|
extraActions={extraActions}
|
|
/>
|
|
);
|
|
|
|
switch (sidebarPosition!) {
|
|
case 'main':
|
|
return (
|
|
<Layout.Container grow>
|
|
{table}
|
|
<DetailSidebar width={sidebarSize}>{sidebar}</DetailSidebar>
|
|
</Layout.Container>
|
|
);
|
|
case 'right':
|
|
return (
|
|
<Layout.Right resizable width={sidebarSize}>
|
|
{table}
|
|
{sidebar}
|
|
</Layout.Right>
|
|
);
|
|
case 'bottom':
|
|
return (
|
|
<Layout.Bottom resizable height={sidebarSize}>
|
|
{table}
|
|
{sidebar}
|
|
</Layout.Bottom>
|
|
);
|
|
case 'overlay':
|
|
return (
|
|
<Layout.Container grow style={{position: 'relative'}}>
|
|
{table}
|
|
{sidebar}
|
|
</Layout.Container>
|
|
);
|
|
case 'none':
|
|
return table;
|
|
}
|
|
}
|
|
|
|
MasterDetailWithPowerSearch.defaultProps = {
|
|
sidebarPosition: 'main',
|
|
sidebarSize: 400,
|
|
sidebarComponent: DefaultRenderSidebar,
|
|
} as Partial<MasterDetailProps<any>>;
|
|
|
|
function DefaultRenderSidebar<T>({record}: {record: T}) {
|
|
return (
|
|
<Panel title="Payload" collapsible={false} pad>
|
|
<DataInspector data={record} expandRoot />
|
|
</Panel>
|
|
);
|
|
}
|