Files
flipper/desktop/plugins/sections/src/index.tsx
Andrey Mishanin 4fc63da386 Wrap focusedGenerationId in a type
Summary:
- Use `FocusInfo` type instead of a raw string (I'll add another field to it in the next diff and it made sense to keep the changes separate).
- `userSelectedGenerationId` now has the type of `string | undefined` for consistency.

Reviewed By: fabiomassimo

Differential Revision: D23160820

fbshipit-source-id: f210d93b9ed7cb25eb3c2c8e6e6aadec4dc872d7
2020-08-17 08:04:53 -07:00

334 lines
9.2 KiB
TypeScript

/**
* 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 {FlipperClient, 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',
});
type Events = {
addEvent: AddEventPayload;
updateTreeGenerationHierarchyGeneration: UpdateTreeGenerationHierarchyGenerationPayload;
updateTreeGenerationChangesetApplication: UpdateTreeGenerationChangesetApplicationPayload;
updateTreeGenerationChangesetGeneration: UpdateTreeGenerationChangesetGenerationPayload;
};
type FocusInfo = {
generationId: string;
};
export function plugin(client: FlipperClient<Events, {}>) {
const generations = createState<{[id: string]: TreeGeneration}>(
{},
{persist: 'generations'},
);
const focusInfo = createState<FocusInfo | undefined>(undefined);
const recording = createState<boolean>(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,
});
});
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,
);
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<string>('');
const [focusedChangeSet, setFocusedChangeSet] = useState<
UpdateTreeGenerationChangesetApplicationPayload | null | undefined
>(null);
const [selectedTreeNode, setSelectedTreeNode] = useState<any>();
const focusedTreeGeneration: TreeGeneration | null = useMemo(() => {
const id = userSelectedGenerationId || focusInfo?.generationId;
if (id === undefined) {
return null;
}
return generations[id];
}, [userSelectedGenerationId, focusInfo, generations]);
const filteredGenerations: Array<TreeGeneration> = 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<string> = 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 (
<Layout.Right>
<Layout.Top>
<Toolbar>
<SearchBox tabIndex={-1}>
<SearchIcon
name="magnifying-glass"
color={colors.macOSTitleBarIcon}
size={16}
/>
<SearchInput
placeholder={'Search'}
onChange={(e) => setSearchString(e.target.value)}
value={searchString}
/>
</SearchBox>
<Spacer />
{recording ? (
<Button
onClick={() => instance.setRecording(false)}
iconVariant="filled"
icon="stop-playback">
Stop
</Button>
) : (
<Button onClick={instance.clear} icon="trash" iconVariant="outline">
Clear
</Button>
)}
</Toolbar>
<Layout.Top scrollable={false}>
<Sidebar position="top" minHeight={80} height={80}>
<EventTable
generations={filteredGenerations}
focusedGenerationId={
userSelectedGenerationId || focusInfo?.generationId
}
onClick={(id?: string) => {
setFocusedChangeSet(null);
setUserSelectedGenerationId(id);
setSelectedTreeNode(null);
}}
/>
</Sidebar>
<Layout.Top>
<Sidebar position="top" minHeight={400} height={400}>
<TreeHierarchy
generation={focusedTreeGeneration}
focusedChangeSet={focusedChangeSet}
setSelectedTreeNode={setSelectedTreeNode}
/>
</Sidebar>
{focusedTreeGeneration && (
<StackTrace
data={focusedTreeGeneration.stack_trace}
skipStackTraceFormat={
focusedTreeGeneration.skip_stack_trace_format
}
/>
)}
</Layout.Top>
</Layout.Top>
</Layout.Top>
<DetailSidebar>
<DetailsPanel
eventUserInfo={focusedTreeGeneration?.payload}
changeSets={focusedTreeGeneration?.changeSets}
onFocusChangeSet={(
focusedChangeSet:
| UpdateTreeGenerationChangesetApplicationPayload
| null
| undefined,
) => {
setFocusedChangeSet(focusedChangeSet);
setSelectedTreeNode(null);
}}
focusedChangeSet={focusedChangeSet}
selectedNodeInfo={selectedTreeNode}
/>
</DetailSidebar>
</Layout.Right>
);
}
function TreeHierarchy({
generation,
focusedChangeSet,
setSelectedTreeNode,
}: {
generation: TreeGeneration | null;
focusedChangeSet:
| UpdateTreeGenerationChangesetApplicationPayload
| null
| undefined;
setSelectedTreeNode: (node: any) => void;
}) {
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 <Tree data={generation.tree} nodeClickHandler={onNodeClicked} />;
} else if (focusedChangeSet && focusedChangeSet.section_component_hierarchy) {
// Display section component hierarchy for specific changeset
return (
<Tree
data={focusedChangeSet.section_component_hierarchy}
nodeClickHandler={onNodeClicked}
/>
);
} else {
return (
<Waiting>
<InfoBox>
<Glyph
name="face-unhappy"
variant="outline"
size={24}
color={colors.light30}
/>
<InfoText>No data available...</InfoText>
</InfoBox>
</Waiting>
);
}
}