diff --git a/desktop/plugins/public/ui-debugger/ClientTypes.tsx b/desktop/plugins/public/ui-debugger/ClientTypes.tsx
index 39348de8f..374e7bd6d 100644
--- a/desktop/plugins/public/ui-debugger/ClientTypes.tsx
+++ b/desktop/plugins/public/ui-debugger/ClientTypes.tsx
@@ -60,6 +60,7 @@ export type FrameworkEvent = {
type: FrameworkEventType;
timestamp: number;
payload?: JSON;
+ duration?: number;
attribution?: FrameworkEventAttribution;
thread?: 'main' | string;
};
diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx
index 632ae8a5e..22c778342 100644
--- a/desktop/plugins/public/ui-debugger/components/main.tsx
+++ b/desktop/plugins/public/ui-debugger/components/main.tsx
@@ -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() {
-
+
- {bottomPanelComponent && (
+ {bottomPanel && (
- {bottomPanelComponent}
+ {bottomPanel.component}
)}
@@ -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>
- {children}
-
-
-
+
+
+ {title}
+
+
+
+ {children}
+
+
);
diff --git a/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx
index cd8298206..69353765b 100644
--- a/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx
+++ b/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx
@@ -31,7 +31,7 @@ type Props = {
os: DeviceOS;
nodes: Map;
metadata: Map;
- showExtra: (element: ReactNode) => void;
+ showExtra: (title: string, element: ReactNode) => void;
};
export const Inspector: React.FC = ({
diff --git a/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx
index c44318ccb..2305b3dea 100644
--- a/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx
+++ b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx
@@ -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 = ({
node,
events,
showExtra,
}) => {
- const [selectedEvent, setSelectedEvent] = useState(
- events[events.length - 1],
- );
-
- const showStacktrace = () => {
- const attribution = selectedEvent.attribution;
- if (attribution?.type === 'stacktrace') {
- const stacktraceInspector = (
-
- );
- showExtra?.(stacktraceInspector);
- }
- };
-
- const hasStacktrace = (event: FrameworkEvent) => {
- return event?.attribution?.type === 'stacktrace';
- };
-
return (
- <>
- {
- 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) && (
-
- )}
- >
+ {
+ const idx = parseInt(current, 10);
+ const event = events[idx];
+ showExtra?.(
+ 'Event details',
+ ,
+ );
+ }}
+ 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' ? (
+
+ ) : null;
+
+ const details = (
+
+
+ {event.type}
+
+ {event.thread}
+
+
+ {formatTimestamp(event.timestamp)}
+
+ {event.duration && (
+
+ {formatDuration(event.duration)}
+
+ )}
+ {event.payload && Object.keys(event.payload).length > 0 && (
+
+
+
+ )}
+
+
+ );
+
+ return (
+
+ {details}
+ {stackTrace}
+
+ );
+}
+
+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;
+}