Files
flipper/desktop/plugins/public/ui-debugger/components/Controls.tsx
Luke De Feo 272d2d2106 Redesign Split Tree and visualiser into panels
Summary: This looks a lot neater and allows us to have separate dedicated visualiser header

Reviewed By: mweststrate

Differential Revision: D47626867

fbshipit-source-id: 843721853e0ff88837afbb9bf3f510a908160d12
2023-07-26 03:22:38 -07:00

248 lines
6.8 KiB
TypeScript

/**
* 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, {useState} from 'react';
import {plugin} from '../index';
import {
Button,
Input,
Modal,
Tooltip,
Typography,
TreeSelect,
Space,
Switch,
} from 'antd';
import {
EyeOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
SearchOutlined,
} from '@ant-design/icons';
import {usePlugin, useValue, Layout} from 'flipper-plugin';
import {FrameworkEventType} from '../ClientTypes';
export const Controls: React.FC = () => {
const instance = usePlugin(plugin);
const searchTerm = useValue(instance.uiState.searchTerm);
const isPaused = useValue(instance.uiState.isPaused);
const filterMainThreadMonitoring = useValue(
instance.uiState.filterMainThreadMonitoring,
);
const frameworkEventMonitoring: Map<FrameworkEventType, boolean> = useValue(
instance.uiState.frameworkEventMonitoring,
);
const [showFrameworkEventsModal, setShowFrameworkEventsModal] =
useState(false);
return (
<Layout.Horizontal gap="medium" pad="medium">
<Input
value={searchTerm}
onChange={(e) => {
instance.uiActions.onSearchTermUpdated(e.target.value);
}}
prefix={<SearchOutlined />}
placeholder="Search"
/>
<Button
type="default"
shape="circle"
onClick={instance.uiActions.onPlayPauseToggled}
icon={
<Tooltip
title={isPaused ? 'Resume live updates' : 'Pause incoming updates'}>
{isPaused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
</Tooltip>
}></Button>
<Button
type="default"
shape="circle"
onClick={() => {
setShowFrameworkEventsModal(true);
}}
icon={
<Tooltip title="Framework event monitoring">
<EyeOutlined />
</Tooltip>
}></Button>
{frameworkEventMonitoring.size > 0 && (
<FrameworkEventsMonitoringModal
filterMainThreadMonitoring={filterMainThreadMonitoring}
onSetFilterMainThreadMonitoring={
instance.uiActions.onSetFilterMainThreadMonitoring
}
frameworkEventTypes={[...frameworkEventMonitoring.entries()]}
onSetEventMonitored={instance.uiActions.onSetFrameworkEventMonitored}
visible={showFrameworkEventsModal}
onCancel={() => setShowFrameworkEventsModal(false)}
/>
)}
</Layout.Horizontal>
);
};
function FrameworkEventsMonitoringModal({
visible,
onCancel,
onSetEventMonitored,
onSetFilterMainThreadMonitoring,
filterMainThreadMonitoring,
frameworkEventTypes,
}: {
visible: boolean;
onCancel: () => void;
onSetEventMonitored: (
eventType: FrameworkEventType,
monitored: boolean,
) => void;
filterMainThreadMonitoring: boolean;
onSetFilterMainThreadMonitoring: (toggled: 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 counter will show the number
of matching events in the tree
</Typography.Text>
<TreeSelect
treeCheckable
showSearch={false}
showCheckedStrategy={TreeSelect.SHOW_PARENT}
placeholder="Select node types 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);
}
}}
/>
<Layout.Horizontal gap="medium">
<Switch
checked={filterMainThreadMonitoring}
onChange={(event) => {
onSetFilterMainThreadMonitoring(event);
}}
/>
<Typography.Text>
Only highlight events that occured on the main thread
</Typography.Text>
</Layout.Horizontal>
</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) {
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: [],
};
}