diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx
index 1f84bfe2b..7bc0d39a1 100644
--- a/desktop/plugins/public/ui-debugger/components/main.tsx
+++ b/desktop/plugins/public/ui-debugger/components/main.tsx
@@ -7,7 +7,7 @@
* @format
*/
-import React, {useState} from 'react';
+import React, {ReactNode, useEffect, useRef, useState} from 'react';
import {plugin} from '../index';
import {
DetailSidebar,
@@ -23,7 +23,7 @@ import {Visualization2D} from './Visualization2D';
import {useKeyboardModifiers} from '../hooks/useKeyboardModifiers';
import {Inspector} from './sidebar/Inspector';
import {Controls} from './Controls';
-import {Spin} from 'antd';
+import {Button, Spin} from 'antd';
import {QueryClientProvider} from 'react-query';
import {Tree2} from './Tree';
@@ -40,38 +40,59 @@ export function Component() {
const {ctrlPressed} = useKeyboardModifiers();
+ const [bottomPanelComponent, setBottomPanelComponent] = useState<
+ ReactNode | undefined
+ >();
+ const openBottomPanelWithContent = (component: ReactNode) => {
+ setBottomPanelComponent(component);
+ };
+ const dismissBottomPanel = () => {
+ setBottomPanelComponent(undefined);
+ };
+
if (showPerfStats) return ;
if (rootId) {
return (
-
-
-
+
+ <>
+
+
+
- {
- instance.uiActions.setVisualiserWidth(width);
- }}
- gutter>
-
-
-
-
-
-
-
-
+ maxWidth={800}
+ onResize={(width) => {
+ instance.uiActions.setVisualiserWidth(width);
+ }}
+ gutter>
+
+
+
+
+
+
+
+
+ >
+
+ {bottomPanelComponent}
+
+
);
@@ -93,3 +114,45 @@ export function Centered(props: {children: React.ReactNode}) {
);
}
+
+type BottomPanelProps = {
+ dismiss: () => void;
+ children: React.ReactNode;
+};
+export function BottomPanel({dismiss, children}: BottomPanelProps) {
+ const bottomPanelRef = useRef(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 (
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx
index f8d064b7f..57cb0b86b 100644
--- a/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx
+++ b/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx
@@ -7,33 +7,25 @@
* @format
*/
-import React from 'react';
+import React, {ReactNode} from 'react';
// eslint-disable-next-line rulesdir/no-restricted-imports-clone
import {Glyph} from 'flipper';
-import {
- Layout,
- Tab,
- Tabs,
- theme,
- usePlugin,
- useValue,
- TimelineDataDescription,
-} from 'flipper-plugin';
-
+import {Layout, Tab, Tabs, theme, usePlugin, useValue} from 'flipper-plugin';
import {Id, Metadata, MetadataId, UINode} from '../../types';
-
import {IdentityInspector} from './inspector/IdentityInspector';
import {AttributesInspector} from './inspector/AttributesInspector';
import {Tooltip} from 'antd';
import {NoData} from './inspector/NoData';
import {plugin} from '../../index';
+import {FrameworkEventsInspector} from './inspector/FrameworkEventsInspector';
type Props = {
nodes: Map;
metadata: Map;
+ showExtra: (element: ReactNode) => void;
};
-export const Inspector: React.FC = ({nodes, metadata}) => {
+export const Inspector: React.FC = ({nodes, metadata, showExtra}) => {
const instance = usePlugin(plugin);
const selectedNodeId = useValue(instance.uiState.selectedNode);
const frameworkEvents = useValue(instance.frameworkEvents);
@@ -43,7 +35,7 @@ export const Inspector: React.FC = ({nodes, metadata}) => {
return ;
}
- const events = selectedNodeId
+ const selectedFrameworkEvents = selectedNodeId
? frameworkEvents?.get(selectedNodeId)
: undefined;
@@ -96,7 +88,7 @@ export const Inspector: React.FC = ({nodes, metadata}) => {
metadata={metadata}
/>
- {events && (
+ {selectedFrameworkEvents && (
= ({nodes, metadata}) => {
}>
- {
- return {
- moment: e.timestamp,
- display: e.type.slice(e.type.lastIndexOf(':') + 1),
- color: theme.primaryColor,
- key: e.timestamp.toString(),
- properties: e.payload as any,
- };
- }),
- current: '',
- }}
+
)}
diff --git a/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx
new file mode 100644
index 000000000..5dce08fe2
--- /dev/null
+++ b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx
@@ -0,0 +1,76 @@
+/**
+ * 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 {Button} from 'antd';
+import {theme, TimelineDataDescription} from 'flipper-plugin';
+import {FrameworkEvent, UINode} from '../../../types';
+import React, {ReactNode, useState} from 'react';
+import {StackTraceInspector} from './StackTraceInspector';
+
+type Props = {
+ node: UINode;
+ events: FrameworkEvent[];
+ showExtra?: (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) && (
+
+ )}
+ >
+ );
+};
diff --git a/desktop/plugins/public/ui-debugger/components/sidebar/inspector/StackTraceInspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/StackTraceInspector.tsx
new file mode 100644
index 000000000..ccc07a473
--- /dev/null
+++ b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/StackTraceInspector.tsx
@@ -0,0 +1,47 @@
+/**
+ * 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 from 'react';
+// eslint-disable-next-line rulesdir/no-restricted-imports-clone
+import {StackTrace} from 'flipper';
+import {Tag} from '../../../types';
+
+const FacebookLibraries = ['Facebook'];
+const CKFilter = ['UIDCKAnalyticsListener'];
+
+const REGEX =
+ /\d+\s+(?[\s\w\.]+\w)\s+(?0x\w+?)\s+(?.+) \+ (?\d+)/;
+
+function isSystemLibrary(libraryName: string | null | undefined): boolean {
+ return libraryName ? !FacebookLibraries.includes(libraryName) : false;
+}
+
+type Props = {
+ stacktrace: string[];
+ tags: Tag[];
+};
+export const StackTraceInspector: React.FC = ({stacktrace, tags}) => {
+ const filters = tags.includes('CK') ? CKFilter : [];
+ return (
+
+ {stacktrace
+ ?.filter((line) => filters.every((filter) => !line.includes(filter)))
+ .map((line) => {
+ const trace = REGEX.exec(line)?.groups;
+ return {
+ bold: !isSystemLibrary(trace?.library),
+ library: trace?.library,
+ address: trace?.address,
+ caller: trace?.caller,
+ lineNumber: trace?.lineNumber,
+ };
+ }) ?? [{caller: 'No stacktrace available'}]}
+
+ );
+};