Added ability to pause the incoming updates from the client

Summary: There were a few subtleties around what to the auto expanding / collapsing for active children but otherwise this is quite straightforward

Reviewed By: lblasa

Differential Revision: D41548252

fbshipit-source-id: c153d00210d859463a51753dadf2e5aabeb7ea35
This commit is contained in:
Luke De Feo
2022-11-28 05:09:20 -08:00
committed by Facebook GitHub Bot
parent ced04c7cec
commit 57dcf72763
2 changed files with 101 additions and 27 deletions

View File

@@ -11,13 +11,14 @@ import React, {useState} from 'react';
import {plugin} from '../index'; import {plugin} from '../index';
import {DetailSidebar, Layout, usePlugin, useValue} from 'flipper-plugin'; import {DetailSidebar, Layout, usePlugin, useValue} from 'flipper-plugin';
import {useHotkeys} from 'react-hotkeys-hook'; import {useHotkeys} from 'react-hotkeys-hook';
import {Id, Metadata, MetadataId, Snapshot, UINode} from '../types'; import {Id, Metadata, MetadataId, UINode} from '../types';
import {PerfStats} from './PerfStats'; import {PerfStats} from './PerfStats';
import {Tree} from './Tree'; import {Tree} from './Tree';
import {Visualization2D} from './Visualization2D'; import {Visualization2D} from './Visualization2D';
import {useKeyboardModifiers} from '../hooks/useKeyboardModifiers'; import {useKeyboardModifiers} from '../hooks/useKeyboardModifiers';
import {Inspector} from './sidebar/Inspector'; import {Inspector} from './sidebar/Inspector';
import {Input, Spin} from 'antd'; import {Button, Input, Spin, Tooltip} from 'antd';
import {PauseCircleOutlined, PlayCircleOutlined} from '@ant-design/icons';
export function Component() { export function Component() {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
@@ -33,6 +34,7 @@ export function Component() {
const searchTerm = useValue(instance.uiState.searchTerm); const searchTerm = useValue(instance.uiState.searchTerm);
const {ctrlPressed} = useKeyboardModifiers(); const {ctrlPressed} = useKeyboardModifiers();
const isPaused = useValue(instance.uiState.isPaused);
function renderSidebar( function renderSidebar(
node: UINode | undefined, node: UINode | undefined,
metadata: Map<MetadataId, Metadata>, metadata: Map<MetadataId, Metadata>,
@@ -53,10 +55,26 @@ export function Component() {
return ( return (
<Layout.Horizontal grow> <Layout.Horizontal grow>
<Layout.Container grow pad="medium" gap="small"> <Layout.Container grow pad="medium" gap="small">
<Input <Layout.Horizontal padh="small" gap="small">
value={searchTerm} <Input
onChange={(e) => instance.uiState.searchTerm.set(e.target.value)} value={searchTerm}
/> onChange={(e) => instance.uiState.searchTerm.set(e.target.value)}
/>
<Button
type="default"
shape="circle"
onClick={() =>
instance.setPlayPause(!instance.uiState.isPaused.get())
}
icon={
<Tooltip
title={
isPaused ? 'Resume live updates' : 'Pause incoming updates'
}>
{isPaused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
</Tooltip>
}></Button>
</Layout.Horizontal>
<Layout.ScrollContainer> <Layout.ScrollContainer>
<Tree <Tree
selectedNode={selectedNode} selectedNode={selectedNode}

View File

@@ -7,7 +7,12 @@
* @format * @format
*/ */
import {PluginClient, createState, createDataSource} from 'flipper-plugin'; import {
PluginClient,
createState,
createDataSource,
produce,
} from 'flipper-plugin';
import { import {
Events, Events,
Id, Id,
@@ -20,6 +25,12 @@ import {
} from './types'; } from './types';
import './node_modules/react-complex-tree/lib/style.css'; import './node_modules/react-complex-tree/lib/style.css';
type SnapshotInfo = {nodeId: Id; base64Image: Snapshot};
type LiveClientState = {
snapshotInfo: SnapshotInfo | null;
nodes: Map<Id, UINode>;
};
export function plugin(client: PluginClient<Events>) { export function plugin(client: PluginClient<Events>) {
const rootId = createState<Id | undefined>(undefined); const rootId = createState<Id | undefined>(undefined);
const metadata = createState<Map<MetadataId, Metadata>>(new Map()); const metadata = createState<Map<MetadataId, Metadata>>(new Map());
@@ -49,14 +60,14 @@ export function plugin(client: PluginClient<Events>) {
}); });
const nodes = createState<Map<Id, UINode>>(new Map()); const nodes = createState<Map<Id, UINode>>(new Map());
const snapshot = createState<{nodeId: Id; base64Image: Snapshot} | null>( const snapshot = createState<SnapshotInfo | null>(null);
null,
);
const uiState = { const uiState = {
//used to disabled hover effects which cause rerenders and mess up the existing context menu //used to disabled hover effects which cause rerenders and mess up the existing context menu
isContextMenuOpen: createState<boolean>(false), isContextMenuOpen: createState<boolean>(false),
isPaused: createState(false),
//The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser. //The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser.
//The nodes are sorted by area since you most likely want to select the smallest node under your cursor //The nodes are sorted by area since you most likely want to select the smallest node under your cursor
hoveredNodes: createState<Id[]>([]), hoveredNodes: createState<Id[]>([]),
@@ -67,8 +78,8 @@ export function plugin(client: PluginClient<Events>) {
}; };
client.onMessage('coordinateUpdate', (event) => { client.onMessage('coordinateUpdate', (event) => {
nodes.update((draft) => { liveClientData = produce(liveClientData, (draft) => {
const node = draft.get(event.nodeId); const node = draft.nodes.get(event.nodeId);
if (!node) { if (!node) {
console.warn(`Coordinate update for non existing node `, event); console.warn(`Coordinate update for non existing node `, event);
} else { } else {
@@ -76,17 +87,48 @@ export function plugin(client: PluginClient<Events>) {
node.bounds.y = event.coordinate.y; node.bounds.y = event.coordinate.y;
} }
}); });
if (uiState.isPaused.get()) {
return;
}
nodes.set(liveClientData.nodes);
}); });
const setPlayPause = (isPaused: boolean) => {
uiState.isPaused.set(isPaused);
if (!isPaused) {
//When going back to play mode then set the atoms to the live state to rerender the latest
//Also need to fixed expanded state for any change in active child state
uiState.treeState.update((draft) => {
liveClientData.nodes.forEach((node) => {
collapseinActiveChildren(node, draft);
});
});
nodes.set(liveClientData.nodes);
snapshot.set(liveClientData.snapshotInfo);
}
};
//this is the client data is what drives all of desktop UI
//it is always up-to-date with the client regardless of whether we are paused or not
let liveClientData: LiveClientState = {
snapshotInfo: null,
nodes: new Map(),
};
const seenNodes = new Set<Id>(); const seenNodes = new Set<Id>();
client.onMessage('subtreeUpdate', (event) => { client.onMessage('subtreeUpdate', (event) => {
if (event.snapshot) { liveClientData = produce(liveClientData, (draft) => {
snapshot.set({nodeId: event.rootId, base64Image: event.snapshot}); if (event.snapshot) {
} draft.snapshotInfo = {
nodeId: event.rootId,
base64Image: event.snapshot,
};
}
nodes.update((draft) => {
event.nodes.forEach((node) => { event.nodes.forEach((node) => {
draft.set(node.id, node); draft.nodes.set(node.id, node);
}); });
}); });
@@ -97,28 +139,42 @@ export function plugin(client: PluginClient<Events>) {
} }
seenNodes.add(node.id); seenNodes.add(node.id);
if (node.activeChild) { if (!uiState.isPaused.get()) {
const inactiveChildren = node.children.filter( //we need to not do this while paused as you may move to another screen / tab
(child) => child !== node.activeChild, //and it would collapse the tree node for the activity you were paused on.
); collapseinActiveChildren(node, draft);
draft.expandedNodes = draft.expandedNodes.filter(
(nodeId) => !inactiveChildren.includes(nodeId),
);
draft.expandedNodes.push(node.activeChild);
} }
} }
}); });
if (!uiState.isPaused.get()) {
nodes.set(liveClientData.nodes);
snapshot.set(liveClientData.snapshotInfo);
}
}); });
return { return {
rootId, rootId,
uiState, uiState,
nodes, nodes,
metadata,
snapshot, snapshot,
metadata,
perfEvents, perfEvents,
setPlayPause,
}; };
} }
function collapseinActiveChildren(node: UINode, draft: TreeState) {
if (node.activeChild) {
const inactiveChildren = node.children.filter(
(child) => child !== node.activeChild,
);
draft.expandedNodes = draft.expandedNodes.filter(
(nodeId) => !inactiveChildren.includes(nodeId),
);
draft.expandedNodes.push(node.activeChild);
}
}
export {Component} from './components/main'; export {Component} from './components/main';