in desktop/flipper-plugin/src/ui/MasterDetail.tsx [76:249]
export function MasterDetail<T extends object>({
dataSource,
records,
sidebarComponent,
sidebarPosition,
sidebarSize,
onSelect,
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,
})
: 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}
extraActions={
<>
{connected && isPaused && (
<Button
title={`Click to ${pausedState ? 'resume' : 'pause'} the stream`}
danger={pausedState}
onClick={handleTogglePause}>
{pausedState ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
</Button>
)}
{connected && enableClear && (
<Button title="Clear records" onClick={handleClear}>
<DeleteOutlined />
</Button>
)}
{extraActions}
</>
}
/>
);
switch (sidebarPosition!) {
case 'main':
return (
<Layout.Container grow>
{table}
<DetailSidebar>{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 'none':
return table;
}
}