Summary: To avoid showing the Ugly spinner in the context menu as well as a better UX we prefetch the IDE resolved path. It was important to limit the concurrency of the running arc jobs otherwise lots of bad things happen and the whole machine stalls out. The general idea is as the frame comes off the wire we send them to react query to prefetch. by setting the cache time sending the same key twice will not result in 2 fetches, so we dont need to worry about deduplication on our side Reviewed By: antonk52 Differential Revision: D47210292 fbshipit-source-id: 4a1d8efdfae754c1a73c6a868b02d1f3a0a5b567
194 lines
5.6 KiB
TypeScript
194 lines
5.6 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, UINode} from '../types';
|
|
import {PerfStats} from './PerfStats';
|
|
import {Visualization2D} from './Visualization2D';
|
|
import {Inspector} from './sidebar/Inspector';
|
|
import {Controls} from './Controls';
|
|
import {Button, Spin} from 'antd';
|
|
import {QueryClientProvider} from 'react-query';
|
|
import {Tree2} from './Tree';
|
|
import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
|
|
import {queryClient} from '../reactQuery';
|
|
|
|
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, UINode> = useValue(instance.nodes);
|
|
const metadata: Map<MetadataId, Metadata> = useValue(instance.metadata);
|
|
|
|
const [showPerfStats, setShowPerfStats] = useState(false);
|
|
|
|
useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show));
|
|
|
|
const [bottomPanelComponent, setBottomPanelComponent] = useState<
|
|
ReactNode | undefined
|
|
>();
|
|
const openBottomPanelWithContent = (component: ReactNode) => {
|
|
setBottomPanelComponent(component);
|
|
};
|
|
const dismissBottomPanel = () => {
|
|
setBottomPanelComponent(undefined);
|
|
};
|
|
|
|
if (showPerfStats)
|
|
return (
|
|
<PerfStats
|
|
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>
|
|
);
|
|
} else {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<Layout.Container grow padh="small" padv="medium">
|
|
<Layout.Top>
|
|
<>
|
|
<Controls />
|
|
<Layout.Horizontal grow pad="small">
|
|
<Tree2 nodes={nodes} rootId={rootId} />
|
|
|
|
<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={350}>
|
|
<Inspector
|
|
os={instance.os}
|
|
metadata={metadata}
|
|
nodes={nodes}
|
|
showExtra={openBottomPanelWithContent}
|
|
/>
|
|
</DetailSidebar>
|
|
</Layout.Horizontal>
|
|
</>
|
|
<BottomPanel dismiss={dismissBottomPanel}>
|
|
{bottomPanelComponent}
|
|
</BottomPanel>
|
|
</Layout.Top>
|
|
</Layout.Container>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
}
|
|
|
|
export function Centered(props: {children: React.ReactNode}) {
|
|
return (
|
|
<Layout.Horizontal center grow>
|
|
<Layout.Container center grow>
|
|
{props.children}
|
|
</Layout.Container>
|
|
</Layout.Horizontal>
|
|
);
|
|
}
|
|
|
|
type BottomPanelProps = {
|
|
dismiss: () => void;
|
|
children: React.ReactNode;
|
|
};
|
|
export function BottomPanel({dismiss, children}: BottomPanelProps) {
|
|
const bottomPanelRef = useRef<any>(null);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
bottomPanelRef.current &&
|
|
!bottomPanelRef.current.contains(event.target)
|
|
) {
|
|
dismiss();
|
|
}
|
|
};
|
|
// 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={400} gutter>
|
|
<Layout.ScrollContainer>{children}</Layout.ScrollContainer>
|
|
<div style={{margin: 10}}>
|
|
<Button type="ghost" style={{float: 'right'}} onClick={dismiss}>
|
|
Dismiss
|
|
</Button>
|
|
</div>
|
|
</ResizablePanel>
|
|
</div>
|
|
);
|
|
}
|