Use bottom panel as detail view for framework events

Summary: Now when you click on an event the bottom bar appears automatically showing you every thing you need

Reviewed By: lblasa

Differential Revision: D48318694

fbshipit-source-id: 6505e439d949941dc0e091b9576d7d1321d8a05f
This commit is contained in:
Luke De Feo
2023-08-21 04:24:16 -07:00
committed by Facebook GitHub Bot
parent 6f6b953c62
commit d5814ea17c
4 changed files with 150 additions and 70 deletions

View File

@@ -23,7 +23,7 @@ import {PerfStats} from './PerfStats';
import {Visualization2D} from './visualizer/Visualization2D';
import {Inspector} from './sidebar/Inspector';
import {TreeControls} from './tree/TreeControls';
import {Button, Spin} from 'antd';
import {Button, Spin, Typography} from 'antd';
import {QueryClientProvider} from 'react-query';
import {Tree2} from './tree/Tree';
import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
@@ -43,14 +43,14 @@ export function Component() {
useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show));
const viewMode = useValue(instance.uiState.viewMode);
const [bottomPanelComponent, setBottomPanelComponent] = useState<
ReactNode | undefined
const [bottomPanel, setBottomPanel] = useState<
{title: string; component: ReactNode} | undefined
>();
const openBottomPanelWithContent = (component: ReactNode) => {
setBottomPanelComponent(component);
const openBottomPanelWithContent = (title: string, component: ReactNode) => {
setBottomPanel({title, component});
};
const dismissBottomPanel = () => {
setBottomPanelComponent(undefined);
setBottomPanel(undefined);
};
const [bottomPanelHeight, setBottomPanelHeight] = useState(400);
@@ -124,7 +124,7 @@ export function Component() {
<TreeControls />
<Tree2
additionalHeightOffset={
bottomPanelComponent != null ? bottomPanelHeight : 0
bottomPanel != null ? bottomPanelHeight : 0
}
nodes={nodes}
rootId={rootId}
@@ -146,7 +146,7 @@ export function Component() {
onSelectNode={instance.uiActions.onSelectNode}
/>
</ResizablePanel>
<DetailSidebar width={350}>
<DetailSidebar width={450}>
<Inspector
os={instance.os}
metadata={metadata}
@@ -155,12 +155,13 @@ export function Component() {
/>
</DetailSidebar>
</Layout.Horizontal>
{bottomPanelComponent && (
{bottomPanel && (
<BottomPanel
title={bottomPanel.title}
height={bottomPanelHeight}
setHeight={setBottomPanelHeight}
dismiss={dismissBottomPanel}>
{bottomPanelComponent}
{bottomPanel.component}
</BottomPanel>
)}
</Layout.Container>
@@ -179,12 +180,14 @@ export function Centered(props: {children: React.ReactNode}) {
}
type BottomPanelProps = {
title: string;
dismiss: () => void;
children: React.ReactNode;
height: number;
setHeight: (height: number) => void;
};
export function BottomPanel({
title,
dismiss,
children,
height,
@@ -198,7 +201,10 @@ export function BottomPanel({
bottomPanelRef.current &&
!bottomPanelRef.current.contains(event.target)
) {
dismiss();
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.
@@ -222,12 +228,22 @@ export function BottomPanel({
height={height}
onResize={(_, height) => setHeight(height)}
gutter>
<Layout.ScrollContainer>{children}</Layout.ScrollContainer>
<div style={{margin: 10}}>
<Button type="ghost" style={{float: 'right'}} onClick={dismiss}>
Dismiss
</Button>
</div>
<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>
);

View File

@@ -31,7 +31,7 @@ type Props = {
os: DeviceOS;
nodes: Map<Id, ClientNode>;
metadata: Map<MetadataId, Metadata>;
showExtra: (element: ReactNode) => void;
showExtra: (title: string, element: ReactNode) => void;
};
export const Inspector: React.FC<Props> = ({

View File

@@ -7,70 +7,133 @@
* @format
*/
import {Button} from 'antd';
import {theme, TimelineDataDescription} from 'flipper-plugin';
import {
DataInspector,
Layout,
theme,
TimelineDataDescription,
} from 'flipper-plugin';
import {FrameworkEvent, ClientNode} from '../../../ClientTypes';
import React, {ReactNode, useState} from 'react';
import React, {ReactNode} from 'react';
import {StackTraceInspector} from './StackTraceInspector';
import {Descriptions, Tag} from 'antd';
type Props = {
node: ClientNode;
events: readonly FrameworkEvent[];
showExtra?: (element: ReactNode) => void;
showExtra?: (title: string, element: ReactNode) => void;
};
export const FrameworkEventsInspector: React.FC<Props> = ({
node,
events,
showExtra,
}) => {
const [selectedEvent, setSelectedEvent] = useState<FrameworkEvent>(
events[events.length - 1],
);
const showStacktrace = () => {
const attribution = selectedEvent.attribution;
if (attribution?.type === 'stacktrace') {
const stacktraceInspector = (
<StackTraceInspector
stacktrace={attribution.stacktrace}
tags={node.tags}
/>
);
showExtra?.(stacktraceInspector);
}
};
const hasStacktrace = (event: FrameworkEvent) => {
return event?.attribution?.type === 'stacktrace';
};
return (
<>
<TimelineDataDescription
key={node.id}
canSetCurrent={false}
onClick={(current) => {
const idx = parseInt(current, 10);
setSelectedEvent(events[idx]);
}}
timeline={{
time: events.map((e, idx) => {
return {
moment: e.timestamp,
display: e.type.slice(e.type.lastIndexOf(':') + 1),
color: theme.primaryColor,
key: idx.toString(),
properties: e.payload as any,
};
}),
current: (events.length - 1).toString(),
}}
/>
{hasStacktrace(selectedEvent) && (
<Button type="ghost" onClick={showStacktrace}>
Stacktrace
</Button>
)}
</>
<TimelineDataDescription
key={node.id}
canSetCurrent={false}
onClick={(current) => {
const idx = parseInt(current, 10);
const event = events[idx];
showExtra?.(
'Event details',
<EventDetails event={event} node={node} />,
);
}}
timeline={{
time: events.map((event, idx) => {
return {
moment: event.timestamp,
display: `${eventTypeToName(event.type)}`,
color: threadToColor(event.thread),
key: idx.toString(),
};
}),
current: 'initialNone',
}}
/>
);
};
function EventDetails({
event,
node,
}: {
event: FrameworkEvent;
node: ClientNode;
}) {
const stackTrace =
event?.attribution?.type === 'stacktrace' ? (
<StackTraceInspector
stacktrace={event.attribution.stacktrace}
tags={node.tags}
/>
) : null;
const details = (
<Layout.Container>
<Descriptions size="small" bordered column={1}>
<Descriptions.Item label="Event type">{event.type}</Descriptions.Item>
<Descriptions.Item label="Thread">
<Tag color={threadToColor(event.thread)}>{event.thread}</Tag>
</Descriptions.Item>
<Descriptions.Item label="Timestamp">
{formatTimestamp(event.timestamp)}
</Descriptions.Item>
{event.duration && (
<Descriptions.Item label="Duration">
{formatDuration(event.duration)}
</Descriptions.Item>
)}
{event.payload && Object.keys(event.payload).length > 0 && (
<Descriptions.Item label="Attributes">
<DataInspector data={event.payload} />
</Descriptions.Item>
)}
</Descriptions>
</Layout.Container>
);
return (
<Layout.Horizontal>
{details}
{stackTrace}
</Layout.Horizontal>
);
}
const options: Intl.DateTimeFormatOptions = {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false,
};
function formatTimestamp(timestamp: number): string {
const date = new Date(timestamp);
const formattedDate = new Intl.DateTimeFormat('en-US', options).format(date);
const milliseconds = date.getMilliseconds();
return `${formattedDate}.${milliseconds.toString().padStart(3, '0')}`;
}
function formatDuration(nanoseconds: number): string {
if (nanoseconds < 1_000) {
return `${nanoseconds} nanoseconds`;
} else if (nanoseconds < 1_000_000) {
return `${(nanoseconds / 1_000).toFixed(2)} microseconds`;
} else if (nanoseconds < 1_000_000_000) {
return `${(nanoseconds / 1_000_000).toFixed(2)} milliseconds`;
} else if (nanoseconds < 1_000_000_000_000) {
return `${(nanoseconds / 1_000_000_000).toFixed(2)} seconds`;
} else {
return `${(nanoseconds / 1_000_000_000_000).toFixed(2)} minutes`;
}
}
function eventTypeToName(eventType: string) {
return eventType.slice(eventType.lastIndexOf('.') + 1);
}
function threadToColor(thread?: string) {
return thread === 'main' ? theme.warningColor : theme.primaryColor;
}