From f282a5eb8a1940b425fdb5f782b11e52909fdb8f Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Tue, 25 Oct 2022 07:10:38 -0700 Subject: [PATCH] Ant tree -> React complex tree Summary: Upgraded from ant tree library to the much more capable React complex tree. Added the following: 1. Ability to expand / collapse nodes while automatically expanding / collapsing active/inactive children when they change 2. Keyboard controls of tree all the time 3. Basic search functionality 4. Selecting node in tree focuses and scrolls in the tree 5. Hover state for tree Reviewed By: lblasa Differential Revision: D40633876 fbshipit-source-id: 8dcef5ec2c277e476a3eb3cdaef62b15c25323c0 --- .../public/ui-debugger/components/Tree.tsx | 142 +++++++++++------- .../components/Visualization2D.tsx | 83 +++++----- .../public/ui-debugger/components/main.tsx | 43 +++--- desktop/plugins/public/ui-debugger/index.tsx | 34 ++++- .../plugins/public/ui-debugger/package.json | 1 + desktop/plugins/public/ui-debugger/types.tsx | 6 +- desktop/plugins/public/yarn.lock | 5 + desktop/themes/base.less | 1 + 8 files changed, 191 insertions(+), 124 deletions(-) diff --git a/desktop/plugins/public/ui-debugger/components/Tree.tsx b/desktop/plugins/public/ui-debugger/components/Tree.tsx index 02461c43b..07091083a 100644 --- a/desktop/plugins/public/ui-debugger/components/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/Tree.tsx @@ -8,76 +8,108 @@ */ import {Id, UINode} from '../types'; -import {Tree as AntTree, TreeDataNode} from 'antd'; -import {DownOutlined} from '@ant-design/icons'; -import React from 'react'; +import React, {useEffect, useRef} from 'react'; +import { + Tree as ComplexTree, + ControlledTreeEnvironment, + TreeItem, +} from 'react-complex-tree'; + +import {plugin} from '../index'; +import {usePlugin, useValue} from 'flipper-plugin'; +import { + InteractionMode, + TreeEnvironmentRef, +} from 'react-complex-tree/lib/esm/types'; export function Tree(props: { rootId: Id; nodes: Map; selectedNode?: Id; + hoveredNode?: Id; onSelectNode: (id: Id) => void; onHoveredNode: (id?: Id) => void; }) { - const [antTree, inactive] = nodesToAntTree(props.rootId, props.nodes); + const instance = usePlugin(plugin); + const expandedItems = useValue(instance.treeState).expandedNodes; + const items = toComplexTree(props.nodes); + const treeRef = useRef(); + useEffect(() => { + //this makes the keyboard arrow controls work always, even when using the visualiser + treeRef.current?.focusTree('tree', true); + }, [props.hoveredNode, props.selectedNode]); return ( -
{ - //This div exists so when mouse exits the entire tree then unhover - props.onHoveredNode(undefined); - }}> - { - return ( -
{ - props.onHoveredNode(node.key as Id); - }}> - {node.title} -
+ item.data.name} + canRename={false} + canDragAndDrop={false} + canSearch + autoFocus + viewState={{ + tree: { + focusedItem: props.hoveredNode, + expandedItems, + selectedItems: props.selectedNode ? [props.selectedNode] : [], + }, + }} + onFocusItem={(item) => props.onHoveredNode(item.index)} + onExpandItem={(item) => { + instance.treeState.update((draft) => { + draft.expandedNodes.push(item.index); + }); + }} + onCollapseItem={(item) => + instance.treeState.update((draft) => { + draft.expandedNodes = draft.expandedNodes.filter( + (expandedItemIndex) => expandedItemIndex !== item.index, ); - }} - selectedKeys={[props.selectedNode ?? '']} - onSelect={(selected) => { - props.onSelectNode(selected[0] as Id); - }} - defaultExpandAll - expandedKeys={[...props.nodes.keys()].filter( - (key) => !inactive.includes(key), - )} - switcherIcon={} - treeData={[antTree]} + }) + } + onSelectItems={(items) => props.onSelectNode(items[0])} + defaultInteractionMode={{ + mode: 'custom', + extends: InteractionMode.DoubleClickItemToExpand, + createInteractiveElementProps: ( + item, + treeId, + actions, + renderFlags, + ) => ({ + onClick: () => { + if (renderFlags.isSelected) { + actions.unselectItem(); + } else { + actions.selectItem(); + } + }, + onMouseOver: () => { + props.onHoveredNode(item.index); + }, + }), + }}> + -
+ ); } -function nodesToAntTree( - root: Id, - nodes: Map, -): [TreeDataNode, Id[]] { - const inactive: Id[] = []; - - function uiNodeToAntNode(id: Id): TreeDataNode { - const node = nodes.get(id); - - if (node?.activeChild) { - for (const child of node.children) { - if (child !== node?.activeChild) { - inactive.push(child); - } - } - } - - return { - key: id, - title: node?.name, - children: node?.children.map((id) => uiNodeToAntNode(id)), +function toComplexTree(nodes: Map): Record> { + const res: Record> = {}; + for (const node of nodes.values()) { + res[node.id] = { + index: node.id, + canMove: false, + canRename: false, + children: node.children, + data: node, + hasChildren: node.children.length > 0, }; } - - return [uiNodeToAntNode(root), inactive]; + return res; } diff --git a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx index 67bec8390..e0e388a35 100644 --- a/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx +++ b/desktop/plugins/public/ui-debugger/components/Visualization2D.tsx @@ -10,11 +10,10 @@ import React from 'react'; import {Id, Snapshot, Tag, UINode} from '../types'; import {styled, Layout, theme} from 'flipper-plugin'; -import {Typography} from 'antd'; export const Visualization2D: React.FC< { - root: Id; + rootId: Id; nodes: Map; snapshots: Map; hoveredNode?: Id; @@ -24,7 +23,7 @@ export const Visualization2D: React.FC< modifierPressed: boolean; } & React.HTMLAttributes > = ({ - root, + rootId, nodes, snapshots, hoveredNode, @@ -34,53 +33,49 @@ export const Visualization2D: React.FC< modifierPressed, }) => { //todo, do a bfs search for the first bounds found - const rootBounds = nodes.get(root)?.bounds; - const rootSnapshot = snapshots.get(root); + const rootBounds = nodes.get(rootId)?.bounds; + const rootSnapshot = snapshots.get(rootId); if (!rootBounds) { return null; } return ( - - Visualizer - -
{ - e.stopPropagation(); - onHoverNode(undefined); - }} - style={{ - /** - * This relative position is so the root visualization 2DNode and outer border has a non static element to - * position itself relative to. - * - * Subsequent Visualization2DNode are positioned relative to their parent as each one is position absolute - * which despite the name acts are a reference point for absolute positioning... - */ - position: 'relative', - width: toPx(rootBounds.width), - height: toPx(rootBounds.height), - overflow: 'hidden', - }}> - - {rootSnapshot ? ( - - ) : null} - { + e.stopPropagation(); + onHoverNode(undefined); + }} + style={{ + /** + * This relative position is so the root visualization 2DNode and outer border has a non static element to + * position itself relative to. + * + * Subsequent Visualization2DNode are positioned relative to their parent as each one is position absolute + * which despite the name acts are a reference point for absolute positioning... + */ + position: 'relative', + width: toPx(rootBounds.width), + height: toPx(rootBounds.height), + overflow: 'hidden', + }}> + + {rootSnapshot ? ( + -
-
+ ) : null} + + ); }; diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index 265363fea..83dd4e633 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -47,32 +47,31 @@ export function Component() { if (rootId) { return ( - <> + - - - - + + {selectedNode && renderSidebar(nodes.get(selectedNode))} - + ); } - return
Nothing yet
; + return
Loading...
; } diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index 6032ee3d8..2bfa99e32 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -8,7 +8,7 @@ */ import {PluginClient, createState, createDataSource} from 'flipper-plugin'; -import {Events, Id, PerfStatsEvent, Snapshot, UINode} from './types'; +import {Events, Id, PerfStatsEvent, Snapshot, TreeState, UINode} from './types'; export function plugin(client: PluginClient) { const rootId = createState(undefined); @@ -25,6 +25,8 @@ export function plugin(client: PluginClient) { const nodesAtom = createState>(new Map()); const snapshotsAtom = createState>(new Map()); + const treeState = createState({expandedNodes: []}); + client.onMessage('coordinateUpdate', (event) => { nodesAtom.update((draft) => { const node = draft.get(event.nodeId); @@ -36,6 +38,8 @@ export function plugin(client: PluginClient) { } }); }); + + const seenNodes = new Set(); client.onMessage('subtreeUpdate', (event) => { snapshotsAtom.update((draft) => { draft.set(event.rootId, event.snapshot); @@ -45,9 +49,35 @@ export function plugin(client: PluginClient) { draft.set(node.id, node); } }); + + treeState.update((draft) => { + for (const node of event.nodes) { + if (!seenNodes.has(node.id)) { + draft.expandedNodes.push(node.id); + } + seenNodes.add(node.id); + + 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); + } + } + }); }); - return {rootId, snapshots: snapshotsAtom, nodes: nodesAtom, perfEvents}; + return { + rootId, + snapshots: snapshotsAtom, + nodes: nodesAtom, + perfEvents, + treeState, + }; } export {Component} from './components/main'; diff --git a/desktop/plugins/public/ui-debugger/package.json b/desktop/plugins/public/ui-debugger/package.json index fef2532ac..f51191943 100644 --- a/desktop/plugins/public/ui-debugger/package.json +++ b/desktop/plugins/public/ui-debugger/package.json @@ -14,6 +14,7 @@ ], "dependencies": { "react-color": "^2.19.3", + "react-complex-tree" : "^1.1.11", "react-hotkeys-hook": "^3.4.7" }, "bugs": { diff --git a/desktop/plugins/public/ui-debugger/types.tsx b/desktop/plugins/public/ui-debugger/types.tsx index 557fb26ee..48dbbd3f6 100644 --- a/desktop/plugins/public/ui-debugger/types.tsx +++ b/desktop/plugins/public/ui-debugger/types.tsx @@ -7,6 +7,8 @@ * @format */ +import {TreeItemIndex} from 'react-complex-tree'; + export type Events = { init: InitEvent; subtreeUpdate: SubtreeUpdateEvent; @@ -89,7 +91,9 @@ export type Color = { }; export type Snapshot = string; -export type Id = number; +export type Id = number | TreeItemIndex; + +export type TreeState = {expandedNodes: Id[]}; export type Tag = 'Native' | 'Declarative' | 'Android' | 'Litho '; diff --git a/desktop/plugins/public/yarn.lock b/desktop/plugins/public/yarn.lock index 573cd4398..9d8cf2472 100644 --- a/desktop/plugins/public/yarn.lock +++ b/desktop/plugins/public/yarn.lock @@ -1644,6 +1644,11 @@ react-color@^2.19.3: reactcss "^1.2.0" tinycolor2 "^1.4.1" +react-complex-tree@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/react-complex-tree/-/react-complex-tree-1.1.11.tgz#430520d12908b033a4b278be0dfd8d0aa6654a85" + integrity sha512-hAkm2ZRH2lwZd7NEzZMQI8db/jI5T2fJsbwHX8oNPrG/WPdakc3eNpm2A4gLk2SBa88HeU6mnauVXg6Q6fJLow== + react-devtools-core@^4.26.1: version "4.26.1" resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.26.1.tgz#2893fea58089be64c5356d5bd0eebda8d1bbf317" diff --git a/desktop/themes/base.less b/desktop/themes/base.less index 2a31b08ad..dbb02ef92 100644 --- a/desktop/themes/base.less +++ b/desktop/themes/base.less @@ -7,6 +7,7 @@ */ @import '../node_modules/antd/dist/antd.less'; @import './typography.less'; +@import (inline) './plugins/public/node_modules/react-complex-tree/lib/style.css'; /* Based on: https://www.figma.com/file/4e6BMdm2SuZ1L7FSuOPQVC/Flipper?node-id=620%3A84636 */ @background-transparent-hover: rgba(0, 0, 0, 0.1);