Ability to highlight nodes that match monitored event
Summary: Listen to framework events and store in a map based on node id Added UI to allow for monitoring framework event types. The event type is a string separated by : Each segment of this string represents a level in the dialog hierachy. For example Litho:Layout:StateUpdateSync would have levels, Litho Layout StateUpdateSync When event type monitored and event comes in for a node flash the visualiser node briefly Reviewed By: lblasa Differential Revision: D42074988 fbshipit-source-id: 52458ad87ab84bf7b1749e87be516ed73106a6c0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d3df6bc00e
commit
d93c9d45a9
@@ -6,11 +6,26 @@
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import {plugin} from '../index';
|
||||
import {Button, Input, Tooltip} from 'antd';
|
||||
import {PauseCircleOutlined, PlayCircleOutlined} from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Typography,
|
||||
TreeSelect,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import {
|
||||
MoreOutlined,
|
||||
PauseCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {usePlugin, useValue, Layout} from 'flipper-plugin';
|
||||
import {FrameworkEventType} from '../types';
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
@@ -25,6 +40,20 @@ export const Controls: React.FC = () => {
|
||||
const instance = usePlugin(plugin);
|
||||
const searchTerm = useValue(instance.uiState.searchTerm);
|
||||
const isPaused = useValue(instance.uiState.isPaused);
|
||||
|
||||
const frameworkEventMonitoring: Map<FrameworkEventType, boolean> = useValue(
|
||||
instance.uiState.frameworkEventMonitoring,
|
||||
);
|
||||
|
||||
const onSetEventMonitored: (
|
||||
eventType: FrameworkEventType,
|
||||
monitored: boolean,
|
||||
) => void = (eventType: FrameworkEventType, monitored: boolean) => {
|
||||
instance.uiState.frameworkEventMonitoring.update((draft) =>
|
||||
draft.set(eventType, monitored),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout.Horizontal pad="small" gap="small">
|
||||
<Input
|
||||
@@ -41,6 +70,196 @@ export const Controls: React.FC = () => {
|
||||
{isPaused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
||||
</Tooltip>
|
||||
}></Button>
|
||||
<MoreOptionsMenu
|
||||
onSetEventMonitored={onSetEventMonitored}
|
||||
frameworkEventTypes={[...frameworkEventMonitoring.entries()]}
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
);
|
||||
};
|
||||
|
||||
function MoreOptionsMenu({
|
||||
onSetEventMonitored,
|
||||
frameworkEventTypes,
|
||||
}: {
|
||||
onSetEventMonitored: (
|
||||
eventType: FrameworkEventType,
|
||||
monitored: boolean,
|
||||
) => void;
|
||||
frameworkEventTypes: [FrameworkEventType, boolean][];
|
||||
}) {
|
||||
const [showFrameworkEventsModal, setShowFrameworkEventsModal] =
|
||||
useState(false);
|
||||
|
||||
const moreOptionsMenu = (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setShowFrameworkEventsModal(true);
|
||||
}}>
|
||||
Framework event monitoring
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
key="more"
|
||||
mouseLeaveDelay={0.7}
|
||||
overlay={moreOptionsMenu}
|
||||
placement="bottomRight">
|
||||
<Button type="text" icon={<MoreOutlined style={{fontSize: 20}} />} />
|
||||
</Dropdown>
|
||||
|
||||
{/*invisible until shown*/}
|
||||
<FrameworkEventsMonitoringModal
|
||||
frameworkEventTypes={frameworkEventTypes}
|
||||
onSetEventMonitored={onSetEventMonitored}
|
||||
visible={showFrameworkEventsModal}
|
||||
onCancel={() => setShowFrameworkEventsModal(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FrameworkEventsMonitoringModal({
|
||||
visible,
|
||||
onCancel,
|
||||
onSetEventMonitored,
|
||||
frameworkEventTypes,
|
||||
}: {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSetEventMonitored: (
|
||||
eventType: FrameworkEventType,
|
||||
monitored: boolean,
|
||||
) => void;
|
||||
frameworkEventTypes: [FrameworkEventType, boolean][];
|
||||
}) {
|
||||
const selectedFrameworkEvents = frameworkEventTypes
|
||||
.filter(([, selected]) => selected)
|
||||
.map(([eventType]) => eventType);
|
||||
|
||||
const treeData = buildTreeSelectData(
|
||||
frameworkEventTypes.map(([type]) => type),
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Framework event monitoring"
|
||||
visible={visible}
|
||||
footer={null}
|
||||
onCancel={onCancel}>
|
||||
<Space direction="vertical" size="large">
|
||||
<Typography.Text>
|
||||
Monitoring an event will cause the relevant node in the visualizer and
|
||||
tree to highlight briefly. Additionally a running total of the number
|
||||
of each event will be show in the tree inline
|
||||
</Typography.Text>
|
||||
|
||||
<TreeSelect
|
||||
treeCheckable
|
||||
showSearch={false}
|
||||
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||
placeholder="Select nodes to monitor"
|
||||
virtual={false} //for scrollbar
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
treeData={treeData}
|
||||
treeDefaultExpandAll
|
||||
value={selectedFrameworkEvents}
|
||||
onSelect={(_: any, node: any) => {
|
||||
for (const leaf of getAllLeaves(node)) {
|
||||
onSetEventMonitored(leaf, true);
|
||||
}
|
||||
}}
|
||||
onDeselect={(_: any, node: any) => {
|
||||
for (const leaf of getAllLeaves(node)) {
|
||||
onSetEventMonitored(leaf, false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
type TreeSelectNode = {
|
||||
title: string;
|
||||
key: string;
|
||||
value: string;
|
||||
children: TreeSelectNode[];
|
||||
};
|
||||
|
||||
/**
|
||||
* In tree select you can select a parent which implicitly selects all children, we find them all here as the real state
|
||||
* is in terms of the leaf nodes
|
||||
*/
|
||||
function getAllLeaves(treeSelectNode: TreeSelectNode) {
|
||||
const result: string[] = [];
|
||||
function getAllLeavesRec(node: TreeSelectNode) {
|
||||
console.log(node);
|
||||
if (node.children.length > 0) {
|
||||
for (const child of node.children) {
|
||||
getAllLeavesRec(child);
|
||||
}
|
||||
} else {
|
||||
result.push(node.key);
|
||||
}
|
||||
}
|
||||
getAllLeavesRec(treeSelectNode);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* transformed flat event type data structure into tree
|
||||
*/
|
||||
function buildTreeSelectData(eventTypes: string[]): TreeSelectNode[] {
|
||||
const root: TreeSelectNode = buildTreeSelectNode('root', 'root');
|
||||
|
||||
eventTypes.forEach((eventType) => {
|
||||
const eventSubtypes = eventType.split(':');
|
||||
let currentNode = root;
|
||||
|
||||
// Find the parent node for the current id
|
||||
for (let i = 0; i < eventSubtypes.length - 1; i++) {
|
||||
let foundChild = false;
|
||||
for (const child of currentNode.children) {
|
||||
if (child.title === eventSubtypes[i]) {
|
||||
currentNode = child;
|
||||
foundChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundChild) {
|
||||
const newNode: TreeSelectNode = buildTreeSelectNode(
|
||||
eventSubtypes[i],
|
||||
eventSubtypes.slice(0, i + 1).join(':'),
|
||||
);
|
||||
|
||||
currentNode.children.push(newNode);
|
||||
currentNode = newNode;
|
||||
}
|
||||
}
|
||||
// Add the current id as a child of the parent node
|
||||
currentNode.children.push(
|
||||
buildTreeSelectNode(
|
||||
eventSubtypes[eventSubtypes.length - 1],
|
||||
eventSubtypes.slice(0, eventSubtypes.length).join(':'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return root.children;
|
||||
}
|
||||
|
||||
function buildTreeSelectNode(title: string, fullValue: string): TreeSelectNode {
|
||||
return {
|
||||
title: title,
|
||||
key: fullValue,
|
||||
value: fullValue,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ function Visualization2DNode({
|
||||
|
||||
const isSelected = selectedNode === node.id;
|
||||
const {isHovered, isLongHovered} = useHoverStates(node.id);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
let nestedChildren: NestedNode[];
|
||||
|
||||
//if there is an active child don't draw the other children
|
||||
@@ -210,6 +210,10 @@ function Visualization2DNode({
|
||||
/>
|
||||
));
|
||||
|
||||
const isHighlighted = useValue(instance.uiState.highlightedNodes).has(
|
||||
node.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
visible={isLongHovered}
|
||||
@@ -223,6 +227,7 @@ function Visualization2DNode({
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
ref={ref}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
@@ -230,8 +235,10 @@ function Visualization2DNode({
|
||||
top: toPx(node.bounds.y),
|
||||
width: toPx(node.bounds.width),
|
||||
height: toPx(node.bounds.height),
|
||||
opacity: isSelected ? 0.5 : 1,
|
||||
backgroundColor: isSelected
|
||||
opacity: isSelected || isHighlighted ? 0.3 : 1,
|
||||
backgroundColor: isHighlighted
|
||||
? 'red'
|
||||
: isSelected
|
||||
? theme.selectionBackgroundColor
|
||||
: 'transparent',
|
||||
}}
|
||||
|
||||
@@ -8,15 +8,17 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
PluginClient,
|
||||
createState,
|
||||
createDataSource,
|
||||
produce,
|
||||
Atom,
|
||||
createDataSource,
|
||||
createState,
|
||||
PluginClient,
|
||||
produce,
|
||||
} from 'flipper-plugin';
|
||||
import {
|
||||
Events,
|
||||
Id,
|
||||
FrameworkEvent,
|
||||
FrameworkEventType,
|
||||
Metadata,
|
||||
MetadataId,
|
||||
PerfStatsEvent,
|
||||
@@ -37,8 +39,10 @@ type UIState = {
|
||||
searchTerm: Atom<string>;
|
||||
isContextMenuOpen: Atom<boolean>;
|
||||
hoveredNodes: Atom<Id[]>;
|
||||
highlightedNodes: Atom<Set<Id>>;
|
||||
focusedNode: Atom<Id | undefined>;
|
||||
expandedNodes: Atom<Set<Id>>;
|
||||
frameworkEventMonitoring: Atom<Map<FrameworkEventType, boolean>>;
|
||||
};
|
||||
|
||||
export function plugin(client: PluginClient<Events>) {
|
||||
@@ -72,12 +76,23 @@ export function plugin(client: PluginClient<Events>) {
|
||||
});
|
||||
|
||||
const nodes = createState<Map<Id, UINode>>(new Map());
|
||||
const frameworkEvents = createState<Map<Id, FrameworkEvent[]>>(new Map());
|
||||
|
||||
const highlightedNodes = createState(new Set<Id>());
|
||||
const snapshot = createState<SnapshotInfo | null>(null);
|
||||
|
||||
const uiState: UIState = {
|
||||
//used to disabled hover effects which cause rerenders and mess up the existing context menu
|
||||
isContextMenuOpen: createState<boolean>(false),
|
||||
|
||||
highlightedNodes,
|
||||
|
||||
//used to indicate whether we will higher the visualizer / tree when a matching event comes in
|
||||
//also whether or not will show running total in the tree
|
||||
frameworkEventMonitoring: createState(
|
||||
new Map<FrameworkEventType, boolean>(),
|
||||
),
|
||||
|
||||
isPaused: createState(false),
|
||||
|
||||
//The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser.
|
||||
@@ -131,23 +146,60 @@ export function plugin(client: PluginClient<Events>) {
|
||||
};
|
||||
|
||||
const seenNodes = new Set<Id>();
|
||||
client.onMessage('subtreeUpdate', (event) => {
|
||||
client.onMessage('subtreeUpdate', (subtreeUpdate) => {
|
||||
uiState.frameworkEventMonitoring.update((draft) => {
|
||||
(subtreeUpdate.frameworkEvents ?? []).forEach((frameworkEvent) => {
|
||||
if (!draft.has(frameworkEvent.type))
|
||||
draft.set(frameworkEvent.type, false);
|
||||
});
|
||||
});
|
||||
|
||||
frameworkEvents.update((draft) => {
|
||||
if (subtreeUpdate.frameworkEvents) {
|
||||
subtreeUpdate.frameworkEvents.forEach((frameworkEvent) => {
|
||||
if (
|
||||
uiState.frameworkEventMonitoring.get().get(frameworkEvent.type) ===
|
||||
true &&
|
||||
uiState.isPaused.get() === false
|
||||
) {
|
||||
highlightedNodes.update((draft) => {
|
||||
draft.add(frameworkEvent.nodeId);
|
||||
});
|
||||
}
|
||||
|
||||
const frameworkEventsForNode = draft.get(frameworkEvent.nodeId);
|
||||
if (frameworkEventsForNode) {
|
||||
frameworkEventsForNode.push(frameworkEvent);
|
||||
} else {
|
||||
draft.set(frameworkEvent.nodeId, [frameworkEvent]);
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
highlightedNodes.update((laterDraft) => {
|
||||
for (const event of subtreeUpdate.frameworkEvents!!.values()) {
|
||||
laterDraft.delete(event.nodeId);
|
||||
}
|
||||
});
|
||||
}, HighlightTime);
|
||||
}
|
||||
});
|
||||
|
||||
liveClientData = produce(liveClientData, (draft) => {
|
||||
if (event.snapshot) {
|
||||
if (subtreeUpdate.snapshot) {
|
||||
draft.snapshotInfo = {
|
||||
nodeId: event.rootId,
|
||||
base64Image: event.snapshot,
|
||||
nodeId: subtreeUpdate.rootId,
|
||||
base64Image: subtreeUpdate.snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
event.nodes.forEach((node) => {
|
||||
subtreeUpdate.nodes.forEach((node) => {
|
||||
draft.nodes.set(node.id, {...node});
|
||||
});
|
||||
setParentPointers(rootId.get()!!, undefined, draft.nodes);
|
||||
});
|
||||
|
||||
uiState.expandedNodes.update((draft) => {
|
||||
for (const node of event.nodes) {
|
||||
for (const node of subtreeUpdate.nodes) {
|
||||
if (!seenNodes.has(node.id)) {
|
||||
draft.add(node.id);
|
||||
}
|
||||
@@ -176,6 +228,7 @@ export function plugin(client: PluginClient<Events>) {
|
||||
uiState,
|
||||
uiActions: uiActions(uiState),
|
||||
nodes,
|
||||
frameworkEvents,
|
||||
snapshot,
|
||||
metadata,
|
||||
perfEvents,
|
||||
@@ -289,6 +342,8 @@ function collapseinActiveChildren(node: UINode, expandedNodes: Draft<Set<Id>>) {
|
||||
}
|
||||
}
|
||||
|
||||
const HighlightTime = 300;
|
||||
|
||||
export {Component} from './components/main';
|
||||
|
||||
setLogger({
|
||||
|
||||
@@ -26,6 +26,18 @@ export type SubtreeUpdateEvent = {
|
||||
rootId: Id;
|
||||
nodes: UINode[];
|
||||
snapshot: Snapshot;
|
||||
frameworkEvents?: FrameworkEvent[];
|
||||
};
|
||||
|
||||
export type Thread = 'Main' | 'Background';
|
||||
|
||||
export type FrameworkEventType = string;
|
||||
|
||||
export type FrameworkEvent = {
|
||||
nodeId: Id;
|
||||
type: FrameworkEventType;
|
||||
thread: Thread;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type InitEvent = {
|
||||
|
||||
Reference in New Issue
Block a user