/** * Copyright (c) Facebook, Inc. and its 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 { TreeGeneration, AddEventPayload, UpdateTreeGenerationHierarchyGenerationPayload, UpdateTreeGenerationChangesetGenerationPayload, UpdateTreeGenerationChangesetApplicationPayload, } from './Models'; import Tree from './Tree'; import StackTrace from './StackTrace'; import EventTable from './EventsTable'; import DetailsPanel from './DetailsPanel'; import React, {useState, useMemo} from 'react'; import { Toolbar, Glyph, Sidebar, FlexBox, styled, Button, Spacer, colors, DetailSidebar, SearchInput, SearchBox, SearchIcon, Layout, } from 'flipper'; import {PluginClient, createState, usePlugin, useValue} from 'flipper-plugin'; const Waiting = styled(FlexBox)({ width: '100%', height: '100%', flexGrow: 1, background: colors.light02, alignItems: 'center', justifyContent: 'center', textAlign: 'center', }); const InfoText = styled.div({ marginTop: 10, marginBottom: 10, fontWeight: 500, color: colors.light30, }); const InfoBox = styled.div({ maxWidth: 400, margin: 'auto', textAlign: 'center', }); const TreeContainer = styled.div({ width: '100%', height: '100%', overflow: 'hidden', }); type Events = { addEvent: AddEventPayload; updateTreeGenerationHierarchyGeneration: UpdateTreeGenerationHierarchyGenerationPayload; updateTreeGenerationChangesetApplication: UpdateTreeGenerationChangesetApplicationPayload; updateTreeGenerationChangesetGeneration: UpdateTreeGenerationChangesetGenerationPayload; }; type FocusInfo = { generationId: string; treeNodeIndexPath?: number[]; }; export function plugin(client: PluginClient) { const generations = createState<{[id: string]: TreeGeneration}>( {}, {persist: 'generations'}, ); const focusInfo = createState(undefined); const recording = createState(true); client.onMessage('addEvent', (data) => { if (!recording.get()) { return; } generations.update((draft) => { draft[data.id] = {...data, changeSets: []}; }); focusInfo.set({ generationId: focusInfo.get()?.generationId || data.id, treeNodeIndexPath: undefined, }); }); client.onMessage('updateTreeGenerationHierarchyGeneration', (data) => { generations.update((draft) => { draft[data.id] = {...draft[data.id], ...data}; }); }); function updateTreeGenerationChangeset( data: | UpdateTreeGenerationChangesetGenerationPayload | UpdateTreeGenerationChangesetApplicationPayload, ) { generations.update((draft) => { draft[data.tree_generation_id].changeSets.push(data); }); } client.onMessage( 'updateTreeGenerationChangesetApplication', updateTreeGenerationChangeset, ); client.onMessage( 'updateTreeGenerationChangesetGeneration', updateTreeGenerationChangeset, ); client.onDeepLink((payload) => { if (typeof payload === 'string') { handleDeepLinkPayload(payload); } }); function handleDeepLinkPayload(payload: string) { // Payload expected to be something like // 1.1.FBAnimatingComponent[0].CKFlexboxComponent[2].CKComponent where path components are separated by '.' // - The first '1' is the scope root ID. // - The numbers in square brackets are the indexes of the following component in the children array // of the preceding component. In this example, the last CKComponent is the 2nd child of CKFlexboxComponent. const pathComponents = payload.split('.'); const rootId = pathComponents[0]; const generationValues = Object.values(generations.get()); const mostRecentTreeBuild = generationValues.reverse().find((g) => { return g.surface_key == rootId && g.reason == 'Tree Build'; }); if (mostRecentTreeBuild) { const regex = /\w+\[(\d+)\]/; const indexPath = pathComponents.reduce((acc, component) => { const match = regex.exec(component); if (match) { acc.push(+match[1]); } return acc; }, [] as number[]); focusInfo.set({ generationId: mostRecentTreeBuild.id, treeNodeIndexPath: indexPath, }); } } function setRecording(value: boolean) { recording.set(value); } function clear() { generations.set({}); focusInfo.set(undefined); recording.set(true); } return { generations, focusInfo, recording, setRecording, clear, }; } export function Component() { const instance = usePlugin(plugin); const generations = useValue(instance.generations); const focusInfo = useValue(instance.focusInfo); const recording = useValue(instance.recording); const [userSelectedGenerationId, setUserSelectedGenerationId] = useState< string | undefined >(); const [searchString, setSearchString] = useState(''); const [focusedChangeSet, setFocusedChangeSet] = useState< UpdateTreeGenerationChangesetApplicationPayload | null | undefined >(null); const [selectedTreeNode, setSelectedTreeNode] = useState(); const focusedTreeGeneration: TreeGeneration | null = useMemo(() => { const id = userSelectedGenerationId || focusInfo?.generationId; if (id === undefined) { return null; } return generations[id]; }, [userSelectedGenerationId, focusInfo, generations]); const filteredGenerations: Array = useMemo(() => { const generationValues = Object.values(generations); if (searchString.length <= 0) { return generationValues; } const matchesCurrentSearchString = (s: string): boolean => { return s.toLowerCase().includes(searchString.toLowerCase()); }; const matchingKeys: Array = generationValues .filter((g) => { if (g.payload) { const componentClassName: string | null | undefined = g.payload.component_class_name; if (componentClassName) { return matchesCurrentSearchString(componentClassName); } } return g.tree?.some((node) => { return matchesCurrentSearchString(node.name); }); }) .map((g) => { return g.surface_key; }); return generationValues.filter((g) => matchingKeys.includes(g.surface_key)); }, [generations, searchString]); return ( setSearchString(e.target.value)} value={searchString} /> {recording ? ( ) : ( )} { setFocusedChangeSet(null); setUserSelectedGenerationId(id); setSelectedTreeNode(null); }} /> {focusedTreeGeneration && ( )} { setFocusedChangeSet(focusedChangeSet); setSelectedTreeNode(null); }} focusedChangeSet={focusedChangeSet} selectedNodeInfo={selectedTreeNode} /> ); } function TreeHierarchy({ generation, focusedChangeSet, setSelectedTreeNode, selectedNodeIndexPath, }: { generation: TreeGeneration | null; focusedChangeSet: | UpdateTreeGenerationChangesetApplicationPayload | null | undefined; setSelectedTreeNode: (node: any) => void; selectedNodeIndexPath?: number[]; }) { const onNodeClicked = useMemo( () => (targetNode: any) => { if (targetNode.attributes.isSection) { const sectionData: any = {}; sectionData.global_key = targetNode.attributes.identifier; setSelectedTreeNode({sectionData}); return; } let dataModel; // Not all models can be parsed. if (targetNode.attributes.isDataModel) { try { dataModel = JSON.parse(targetNode.attributes.identifier); } catch (e) { dataModel = targetNode.attributes.identifier; } } setSelectedTreeNode({dataModel}); }, [setSelectedTreeNode], ); if (generation && generation.tree && generation.tree.length > 0) { // Display component tree hierarchy, if any return ( ); } else if (focusedChangeSet && focusedChangeSet.section_component_hierarchy) { // Display section component hierarchy for specific changeset return ( ); } else { return ( No data available... ); } }