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:
committed by
Facebook GitHub Bot
parent
6f6b953c62
commit
d5814ea17c
@@ -60,6 +60,7 @@ export type FrameworkEvent = {
|
||||
type: FrameworkEventType;
|
||||
timestamp: number;
|
||||
payload?: JSON;
|
||||
duration?: number;
|
||||
attribution?: FrameworkEventAttribution;
|
||||
thread?: 'main' | string;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
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}>
|
||||
<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>
|
||||
</div>
|
||||
</Layout.Horizontal>
|
||||
<Layout.ScrollContainer pad="small">
|
||||
{children}
|
||||
</Layout.ScrollContainer>
|
||||
</Layout.Container>
|
||||
</ResizablePanel>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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> = ({
|
||||
|
||||
@@ -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]);
|
||||
const event = events[idx];
|
||||
showExtra?.(
|
||||
'Event details',
|
||||
<EventDetails event={event} node={node} />,
|
||||
);
|
||||
}}
|
||||
timeline={{
|
||||
time: events.map((e, idx) => {
|
||||
time: events.map((event, idx) => {
|
||||
return {
|
||||
moment: e.timestamp,
|
||||
display: e.type.slice(e.type.lastIndexOf(':') + 1),
|
||||
color: theme.primaryColor,
|
||||
moment: event.timestamp,
|
||||
display: `${eventTypeToName(event.type)}`,
|
||||
color: threadToColor(event.thread),
|
||||
key: idx.toString(),
|
||||
properties: e.payload as any,
|
||||
};
|
||||
}),
|
||||
current: (events.length - 1).toString(),
|
||||
current: 'initialNone',
|
||||
}}
|
||||
/>
|
||||
{hasStacktrace(selectedEvent) && (
|
||||
<Button type="ghost" onClick={showStacktrace}>
|
||||
Stacktrace
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user