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;
|
type: FrameworkEventType;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
payload?: JSON;
|
payload?: JSON;
|
||||||
|
duration?: number;
|
||||||
attribution?: FrameworkEventAttribution;
|
attribution?: FrameworkEventAttribution;
|
||||||
thread?: 'main' | string;
|
thread?: 'main' | string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {PerfStats} from './PerfStats';
|
|||||||
import {Visualization2D} from './visualizer/Visualization2D';
|
import {Visualization2D} from './visualizer/Visualization2D';
|
||||||
import {Inspector} from './sidebar/Inspector';
|
import {Inspector} from './sidebar/Inspector';
|
||||||
import {TreeControls} from './tree/TreeControls';
|
import {TreeControls} from './tree/TreeControls';
|
||||||
import {Button, Spin} from 'antd';
|
import {Button, Spin, Typography} from 'antd';
|
||||||
import {QueryClientProvider} from 'react-query';
|
import {QueryClientProvider} from 'react-query';
|
||||||
import {Tree2} from './tree/Tree';
|
import {Tree2} from './tree/Tree';
|
||||||
import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
|
import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
|
||||||
@@ -43,14 +43,14 @@ export function Component() {
|
|||||||
useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show));
|
useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show));
|
||||||
|
|
||||||
const viewMode = useValue(instance.uiState.viewMode);
|
const viewMode = useValue(instance.uiState.viewMode);
|
||||||
const [bottomPanelComponent, setBottomPanelComponent] = useState<
|
const [bottomPanel, setBottomPanel] = useState<
|
||||||
ReactNode | undefined
|
{title: string; component: ReactNode} | undefined
|
||||||
>();
|
>();
|
||||||
const openBottomPanelWithContent = (component: ReactNode) => {
|
const openBottomPanelWithContent = (title: string, component: ReactNode) => {
|
||||||
setBottomPanelComponent(component);
|
setBottomPanel({title, component});
|
||||||
};
|
};
|
||||||
const dismissBottomPanel = () => {
|
const dismissBottomPanel = () => {
|
||||||
setBottomPanelComponent(undefined);
|
setBottomPanel(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [bottomPanelHeight, setBottomPanelHeight] = useState(400);
|
const [bottomPanelHeight, setBottomPanelHeight] = useState(400);
|
||||||
@@ -124,7 +124,7 @@ export function Component() {
|
|||||||
<TreeControls />
|
<TreeControls />
|
||||||
<Tree2
|
<Tree2
|
||||||
additionalHeightOffset={
|
additionalHeightOffset={
|
||||||
bottomPanelComponent != null ? bottomPanelHeight : 0
|
bottomPanel != null ? bottomPanelHeight : 0
|
||||||
}
|
}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
rootId={rootId}
|
rootId={rootId}
|
||||||
@@ -146,7 +146,7 @@ export function Component() {
|
|||||||
onSelectNode={instance.uiActions.onSelectNode}
|
onSelectNode={instance.uiActions.onSelectNode}
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<DetailSidebar width={350}>
|
<DetailSidebar width={450}>
|
||||||
<Inspector
|
<Inspector
|
||||||
os={instance.os}
|
os={instance.os}
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
@@ -155,12 +155,13 @@ export function Component() {
|
|||||||
/>
|
/>
|
||||||
</DetailSidebar>
|
</DetailSidebar>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
{bottomPanelComponent && (
|
{bottomPanel && (
|
||||||
<BottomPanel
|
<BottomPanel
|
||||||
|
title={bottomPanel.title}
|
||||||
height={bottomPanelHeight}
|
height={bottomPanelHeight}
|
||||||
setHeight={setBottomPanelHeight}
|
setHeight={setBottomPanelHeight}
|
||||||
dismiss={dismissBottomPanel}>
|
dismiss={dismissBottomPanel}>
|
||||||
{bottomPanelComponent}
|
{bottomPanel.component}
|
||||||
</BottomPanel>
|
</BottomPanel>
|
||||||
)}
|
)}
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
@@ -179,12 +180,14 @@ export function Centered(props: {children: React.ReactNode}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BottomPanelProps = {
|
type BottomPanelProps = {
|
||||||
|
title: string;
|
||||||
dismiss: () => void;
|
dismiss: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
height: number;
|
height: number;
|
||||||
setHeight: (height: number) => void;
|
setHeight: (height: number) => void;
|
||||||
};
|
};
|
||||||
export function BottomPanel({
|
export function BottomPanel({
|
||||||
|
title,
|
||||||
dismiss,
|
dismiss,
|
||||||
children,
|
children,
|
||||||
height,
|
height,
|
||||||
@@ -198,7 +201,10 @@ export function BottomPanel({
|
|||||||
bottomPanelRef.current &&
|
bottomPanelRef.current &&
|
||||||
!bottomPanelRef.current.contains(event.target)
|
!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.
|
// Add event listener when the component is mounted.
|
||||||
@@ -222,12 +228,22 @@ export function BottomPanel({
|
|||||||
height={height}
|
height={height}
|
||||||
onResize={(_, height) => setHeight(height)}
|
onResize={(_, height) => setHeight(height)}
|
||||||
gutter>
|
gutter>
|
||||||
<Layout.ScrollContainer>{children}</Layout.ScrollContainer>
|
<Layout.Container grow>
|
||||||
<div style={{margin: 10}}>
|
<Layout.Horizontal
|
||||||
<Button type="ghost" style={{float: 'right'}} onClick={dismiss}>
|
center
|
||||||
Dismiss
|
pad="small"
|
||||||
</Button>
|
style={{
|
||||||
</div>
|
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>
|
</ResizablePanel>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type Props = {
|
|||||||
os: DeviceOS;
|
os: DeviceOS;
|
||||||
nodes: Map<Id, ClientNode>;
|
nodes: Map<Id, ClientNode>;
|
||||||
metadata: Map<MetadataId, Metadata>;
|
metadata: Map<MetadataId, Metadata>;
|
||||||
showExtra: (element: ReactNode) => void;
|
showExtra: (title: string, element: ReactNode) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Inspector: React.FC<Props> = ({
|
export const Inspector: React.FC<Props> = ({
|
||||||
|
|||||||
@@ -7,70 +7,133 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Button} from 'antd';
|
import {
|
||||||
import {theme, TimelineDataDescription} from 'flipper-plugin';
|
DataInspector,
|
||||||
|
Layout,
|
||||||
|
theme,
|
||||||
|
TimelineDataDescription,
|
||||||
|
} from 'flipper-plugin';
|
||||||
import {FrameworkEvent, ClientNode} from '../../../ClientTypes';
|
import {FrameworkEvent, ClientNode} from '../../../ClientTypes';
|
||||||
import React, {ReactNode, useState} from 'react';
|
import React, {ReactNode} from 'react';
|
||||||
import {StackTraceInspector} from './StackTraceInspector';
|
import {StackTraceInspector} from './StackTraceInspector';
|
||||||
|
import {Descriptions, Tag} from 'antd';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
node: ClientNode;
|
node: ClientNode;
|
||||||
events: readonly FrameworkEvent[];
|
events: readonly FrameworkEvent[];
|
||||||
showExtra?: (element: ReactNode) => void;
|
showExtra?: (title: string, element: ReactNode) => void;
|
||||||
};
|
};
|
||||||
export const FrameworkEventsInspector: React.FC<Props> = ({
|
export const FrameworkEventsInspector: React.FC<Props> = ({
|
||||||
node,
|
node,
|
||||||
events,
|
events,
|
||||||
showExtra,
|
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 (
|
return (
|
||||||
<>
|
<TimelineDataDescription
|
||||||
<TimelineDataDescription
|
key={node.id}
|
||||||
key={node.id}
|
canSetCurrent={false}
|
||||||
canSetCurrent={false}
|
onClick={(current) => {
|
||||||
onClick={(current) => {
|
const idx = parseInt(current, 10);
|
||||||
const idx = parseInt(current, 10);
|
const event = events[idx];
|
||||||
setSelectedEvent(events[idx]);
|
showExtra?.(
|
||||||
}}
|
'Event details',
|
||||||
timeline={{
|
<EventDetails event={event} node={node} />,
|
||||||
time: events.map((e, idx) => {
|
);
|
||||||
return {
|
}}
|
||||||
moment: e.timestamp,
|
timeline={{
|
||||||
display: e.type.slice(e.type.lastIndexOf(':') + 1),
|
time: events.map((event, idx) => {
|
||||||
color: theme.primaryColor,
|
return {
|
||||||
key: idx.toString(),
|
moment: event.timestamp,
|
||||||
properties: e.payload as any,
|
display: `${eventTypeToName(event.type)}`,
|
||||||
};
|
color: threadToColor(event.thread),
|
||||||
}),
|
key: idx.toString(),
|
||||||
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