Summary: Mostly mechanical change from jsx to object based api. However some changes: 1. Managed to get rid of UIDebugger context menu item. its now possible to listen to when any context menu is clicked 2. The construction code is cleaner. no more mutable arrary and pushing, its just a big spliced literal 3. Had to change how the ide function worked. It is dynamic and used react query hook to update the number of items. Added a callback to recreate this behaviour. Reviewed By: aigoncharov Differential Revision: D48910165 fbshipit-source-id: 9a71f5ecd302e6ff72194f83a13839f78e9b0796
248 lines
6.9 KiB
TypeScript
248 lines
6.9 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 React, {ReactNode, useEffect, useRef, useState} from 'react';
|
|
import {plugin} from '../index';
|
|
import {
|
|
DetailSidebar,
|
|
Layout,
|
|
usePlugin,
|
|
useValue,
|
|
_Sidebar as ResizablePanel,
|
|
theme,
|
|
} from 'flipper-plugin';
|
|
import {useHotkeys} from 'react-hotkeys-hook';
|
|
import {Id, Metadata, MetadataId, ClientNode} from '../ClientTypes';
|
|
import {PerfStats} from './PerfStats';
|
|
import {Visualization2D} from './visualizer/Visualization2D';
|
|
import {Inspector} from './sidebar/Inspector';
|
|
import {TreeControls} from './tree/TreeControls';
|
|
import {Button, Spin, Typography} from 'antd';
|
|
import {QueryClientProvider} from 'react-query';
|
|
import {Tree2} from './tree/Tree';
|
|
import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
|
|
import {queryClient} from '../utils/reactQuery';
|
|
import {FrameworkEventsTable} from './FrameworkEventsTable';
|
|
import {Centered} from './shared/Centered';
|
|
|
|
export function Component() {
|
|
const instance = usePlugin(plugin);
|
|
const rootId = useValue(instance.rootId);
|
|
const streamState = useValue(instance.uiState.streamState);
|
|
const visualiserWidth = useValue(instance.uiState.visualiserWidth);
|
|
const nodes: Map<Id, ClientNode> = useValue(instance.nodes);
|
|
const metadata: Map<MetadataId, Metadata> = useValue(instance.metadata);
|
|
|
|
const [showPerfStats, setShowPerfStats] = useState(false);
|
|
|
|
useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show));
|
|
|
|
const viewMode = useValue(instance.uiState.viewMode);
|
|
const [bottomPanel, setBottomPanel] = useState<
|
|
{title: string; component: ReactNode} | undefined
|
|
>();
|
|
const openBottomPanelWithContent = (title: string, component: ReactNode) => {
|
|
setBottomPanel({title, component});
|
|
};
|
|
const dismissBottomPanel = () => {
|
|
setBottomPanel(undefined);
|
|
};
|
|
|
|
const [bottomPanelHeight, setBottomPanelHeight] = useState(400);
|
|
|
|
if (showPerfStats)
|
|
return (
|
|
<PerfStats
|
|
frameworkEvents={instance.frameworkEvents}
|
|
uiState={instance.uiState}
|
|
rootId={rootId}
|
|
nodes={nodes}
|
|
events={instance.perfEvents}
|
|
/>
|
|
);
|
|
|
|
if (streamState.state === 'FatalError') {
|
|
return (
|
|
<StreamInterceptorErrorView
|
|
title="Fatal Error"
|
|
message={`Something has gone horribly wrong, we are aware of this and are looking into it, details ${streamState.error.name} ${streamState.error.message}`}
|
|
button={
|
|
<Button onClick={streamState.clearCallBack} type="primary">
|
|
Reset
|
|
</Button>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (streamState.state === 'StreamInterceptorRetryableError') {
|
|
return (
|
|
<StreamInterceptorErrorView
|
|
message={streamState.error.message}
|
|
title={streamState.error.title}
|
|
button={
|
|
<Button onClick={streamState.retryCallback} type="primary">
|
|
Retry
|
|
</Button>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (rootId == null || streamState.state === 'RetryingAfterError') {
|
|
return (
|
|
<Centered>
|
|
<Spin data-testid="loading-indicator" />
|
|
</Centered>
|
|
);
|
|
}
|
|
|
|
if (viewMode.mode === 'frameworkEventsTable') {
|
|
return (
|
|
<FrameworkEventsTable
|
|
nodeId={viewMode.nodeId}
|
|
isTree={viewMode.isTree}
|
|
nodes={nodes}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<Layout.Container grow>
|
|
<Layout.Horizontal
|
|
grow
|
|
style={{
|
|
borderRadius: theme.borderRadius,
|
|
backgroundColor: theme.backgroundWash,
|
|
}}>
|
|
<Layout.Container
|
|
grow
|
|
style={{
|
|
borderRadius: theme.borderRadius,
|
|
backgroundColor: theme.backgroundDefault,
|
|
}}>
|
|
<TreeControls />
|
|
<Tree2
|
|
additionalHeightOffset={
|
|
bottomPanel != null ? bottomPanelHeight : 0
|
|
}
|
|
nodes={nodes}
|
|
rootId={rootId}
|
|
/>
|
|
</Layout.Container>
|
|
|
|
<ResizablePanel
|
|
position="right"
|
|
minWidth={200}
|
|
width={visualiserWidth + theme.space.large}
|
|
maxWidth={800}
|
|
onResize={(width) => {
|
|
instance.uiActions.setVisualiserWidth(width);
|
|
}}
|
|
gutter>
|
|
<Visualization2D
|
|
width={visualiserWidth}
|
|
nodes={nodes}
|
|
onSelectNode={instance.uiActions.onSelectNode}
|
|
/>
|
|
</ResizablePanel>
|
|
<DetailSidebar width={450}>
|
|
<Inspector
|
|
os={instance.os}
|
|
metadata={metadata}
|
|
nodes={nodes}
|
|
showExtra={openBottomPanelWithContent}
|
|
/>
|
|
</DetailSidebar>
|
|
</Layout.Horizontal>
|
|
{bottomPanel && (
|
|
<BottomPanel
|
|
title={bottomPanel.title}
|
|
height={bottomPanelHeight}
|
|
setHeight={setBottomPanelHeight}
|
|
dismiss={dismissBottomPanel}>
|
|
{bottomPanel.component}
|
|
</BottomPanel>
|
|
)}
|
|
</Layout.Container>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
type BottomPanelProps = {
|
|
title: string;
|
|
dismiss: () => void;
|
|
children: React.ReactNode;
|
|
height: number;
|
|
setHeight: (height: number) => void;
|
|
};
|
|
export function BottomPanel({
|
|
title,
|
|
dismiss,
|
|
children,
|
|
height,
|
|
setHeight,
|
|
}: BottomPanelProps) {
|
|
const bottomPanelRef = useRef<any>(null);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
bottomPanelRef.current &&
|
|
!bottomPanelRef.current.contains(event.target)
|
|
) {
|
|
setTimeout(() => {
|
|
//push to back of event queue so that you can still select item in the tree
|
|
dismiss();
|
|
}, 0);
|
|
}
|
|
};
|
|
// Add event listener when the component is mounted.
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
|
|
// Remove event listener when component is unmounted.
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [bottomPanelRef, dismiss]);
|
|
|
|
if (!children) {
|
|
return <></>;
|
|
}
|
|
|
|
return (
|
|
<div ref={bottomPanelRef}>
|
|
<ResizablePanel
|
|
position="bottom"
|
|
minHeight={200}
|
|
height={height}
|
|
onResize={(_, height) => setHeight(height)}
|
|
gutter>
|
|
<Layout.Container grow>
|
|
<Layout.Horizontal
|
|
center
|
|
pad="small"
|
|
style={{
|
|
justifyContent: 'space-between',
|
|
}}>
|
|
<Typography.Title level={3}>{title}</Typography.Title>
|
|
<Button type="ghost" onClick={dismiss}>
|
|
Dismiss
|
|
</Button>
|
|
</Layout.Horizontal>
|
|
<Layout.ScrollContainer pad="small">
|
|
{children}
|
|
</Layout.ScrollContainer>
|
|
</Layout.Container>
|
|
</ResizablePanel>
|
|
</div>
|
|
);
|
|
}
|