Files
flipper/desktop/plugins/public/ui-debugger/components/main.tsx
Luke De Feo fd673d0535 Infrastructure for stream interceptor transform nodes
Summary:
Added stream interecptor which gets a chance to augment the messages off the wire. Stream interceptor transformations are async and can fail  due to network errors so added error state with a retry button. The retry button will just call the function again.

I am also handling errors better generally when this method fails unexpectedly, logging more clearly what went wrong and communicating it to the user

Did some refactoring of subtree update event to support this

Reviewed By: lblasa

Differential Revision: D44415260

fbshipit-source-id: a5a5542b318775b641d53941808399a8fa4634d3
2023-04-27 07:28:41 -07:00

167 lines
4.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, UINode} from '../types';
import {PerfStats} from './PerfStats';
import {Visualization2D} from './Visualization2D';
import {useKeyboardModifiers} from '../hooks/useKeyboardModifiers';
import {Inspector} from './sidebar/Inspector';
import {Controls} from './Controls';
import {Button, Spin} from 'antd';
import {QueryClientProvider} from 'react-query';
import {Tree2} from './Tree';
export function Component() {
const instance = usePlugin(plugin);
const rootId = useValue(instance.rootId);
const streamInterceptorError = useValue(
instance.uiState.streamInterceptorError,
);
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 {ctrlPressed} = useKeyboardModifiers();
const [bottomPanelComponent, setBottomPanelComponent] = useState<
ReactNode | undefined
>();
const openBottomPanelWithContent = (component: ReactNode) => {
setBottomPanelComponent(component);
};
const dismissBottomPanel = () => {
setBottomPanelComponent(undefined);
};
if (showPerfStats) return <PerfStats events={instance.perfEvents} />;
if (streamInterceptorError != null) {
return streamInterceptorError;
}
if (rootId == null || nodes.size == 0) {
return (
<Centered>
<Spin data-testid="loading-indicator" />
</Centered>
);
} else {
return (
<QueryClientProvider client={instance.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>
<Layout.ScrollContainer vertical>
<Visualization2D
width={visualiserWidth}
nodes={nodes}
onSelectNode={instance.uiActions.onSelectNode}
modifierPressed={ctrlPressed}
/>
</Layout.ScrollContainer>
</ResizablePanel>
<DetailSidebar width={350}>
<Inspector
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>
);
}