Move plugins to "sonar/desktop/plugins"

Summary:
Plugins moved from "sonar/desktop/src/plugins" to "sonar/desktop/plugins".

Fixed all the paths after moving.

New "desktop" folder structure:
- `src` - Flipper desktop app JS code executing in Electron Renderer (Chrome) process.
- `static` - Flipper desktop app JS code executing in Electron Main (Node.js) process.
- `plugins` - Flipper desktop JS plugins.
- `pkg` - Flipper packaging lib and CLI tool.
- `doctor` - Flipper diagnostics lib and CLI tool.
- `scripts` - Build scripts for Flipper desktop app.
- `headless` - Headless version of Flipper desktop app.
- `headless-tests` - Integration tests running agains Flipper headless version.

Reviewed By: mweststrate

Differential Revision: D20344186

fbshipit-source-id: d020da970b2ea1e001f9061a8782bfeb54e31ba0
This commit is contained in:
Anton Nikolaev
2020-03-14 14:26:07 -07:00
committed by Facebook GitHub Bot
parent beb5c85e69
commit 10d990c32c
133 changed files with 106 additions and 77 deletions

View File

@@ -0,0 +1,111 @@
/**
* 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 type {UpdateTreeGenerationChangesetApplicationPayload} from './Models.js';
import React from 'react';
import {
MarkerTimeline,
Component,
styled,
FlexBox,
ManagedDataInspector,
Panel,
colors,
} from 'flipper';
const NoContent = styled(FlexBox)({
alignItems: 'center',
justifyContent: 'center',
flexGrow: 1,
fontWeight: '500',
color: colors.light30,
});
type Props = {|
changeSets: ?Array<UpdateTreeGenerationChangesetApplicationPayload>,
eventUserInfo: ?Object,
focusedChangeSet: ?UpdateTreeGenerationChangesetApplicationPayload,
onFocusChangeSet: (
focusedChangeSet: ?UpdateTreeGenerationChangesetApplicationPayload,
) => void,
selectedNodeInfo: ?Object,
|};
export default class DetailsPanel extends Component<Props> {
render() {
const {changeSets, eventUserInfo} = this.props;
const firstChangeSet =
(changeSets || []).reduce(
(min, cs) => Math.min(min, cs.timestamp),
Infinity,
) || 0;
return (
<React.Fragment>
{eventUserInfo && (
<Panel
key="eventUserInfo"
collapsable={false}
floating={false}
heading={'Event User Info'}>
<ManagedDataInspector data={eventUserInfo} expandRoot={true} />
</Panel>
)}
{changeSets && changeSets.length > 0 ? (
<Panel
key="Changesets"
collapsable={false}
floating={false}
heading={'Changesets'}>
<MarkerTimeline
points={changeSets.map(p => ({
label:
p.type === 'CHANGESET_GENERATED' ? 'Generated' : 'Rendered',
time: Math.round((p.timestamp || 0) - firstChangeSet),
color:
p.type === 'CHANGESET_GENERATED' ? colors.lemon : colors.teal,
key: p.identifier,
}))}
onClick={ids =>
this.props.onFocusChangeSet(
changeSets.find(c => c.identifier === ids[0]),
)
}
selected={this.props.focusedChangeSet?.identifier}
/>
</Panel>
) : (
<NoContent>No changes sets available</NoContent>
)}
{this.props.focusedChangeSet && (
<Panel
key="Changeset Details"
floating={false}
heading="Changeset Details">
<ManagedDataInspector
data={this.props.focusedChangeSet.changeset}
expandRoot={true}
/>
</Panel>
)}
{this.props.selectedNodeInfo && (
<Panel
key="Selected Node Info"
floating={false}
heading="Selected Node Info">
<ManagedDataInspector
data={this.props.selectedNodeInfo}
expandRoot={true}
/>
</Panel>
)}
</React.Fragment>
);
}
}

View File

@@ -0,0 +1,248 @@
/**
* 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 type {TreeGeneration} from './Models.js';
import {
FlexColumn,
FlexRow,
Component,
Tooltip,
Glyph,
styled,
colors,
} from 'flipper';
import React from 'react';
const PADDING = 15;
const WIDTH = 70;
const LABEL_WIDTH = 140;
const Container = styled(FlexRow)({
flexShrink: 0,
flexGrow: 1,
});
const SurfaceContainer = styled(FlexColumn)(props => ({
position: 'relative',
'::after': {
display: props.scrolled ? 'block' : 'none',
content: '""',
top: 0,
bottom: 0,
right: -15,
width: 15,
background: `linear-gradient(90deg, ${colors.macOSTitleBarBackgroundBlur} 0%, transparent 100%)`,
zIndex: 3,
position: 'absolute',
},
}));
const TimeContainer = styled(FlexColumn)({
overflow: 'scroll',
flexGrow: 1,
flexShrink: 1,
});
const Row = styled(FlexRow)(props => ({
alignItems: 'center',
paddingBottom: 3,
marginTop: 3,
flexGrow: 1,
flexShrink: 0,
maxHeight: 75,
position: 'relative',
minWidth: '100%',
alignSelf: 'flex-start',
'::before': {
display: props.showTimeline ? 'block' : 'none',
zIndex: 1,
content: '""',
position: 'absolute',
borderTop: `1px dotted ${colors.light15}`,
height: 1,
top: '50%',
left: 0,
right: 0,
},
}));
const Label = styled.div({
width: LABEL_WIDTH,
paddingLeft: 10,
paddingRight: 10,
fontWeight: 'bold',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
textAlign: 'right',
flexShrink: 0,
position: 'sticky',
left: 0,
zIndex: 2,
});
const Content = styled.div({
textOverflow: 'ellipsis',
overflow: 'hidden',
fontSize: 11,
textAlign: 'center',
textTransform: 'uppercase',
fontWeight: '500',
color: colors.light50,
});
const Record = styled.div(({highlighted}) => ({
border: `1px solid ${colors.light15}`,
boxShadow: highlighted
? `inset 0 0 0 2px ${colors.macOSTitleBarIconSelected}`
: 'none',
borderRadius: 5,
padding: 5,
marginRight: PADDING,
backgroundColor: colors.white,
zIndex: 2,
position: 'relative',
width: WIDTH,
flexShrink: 0,
alignSelf: 'stretch',
display: 'flex',
alignItems: 'center',
}));
const Empty = styled.div({
width: WIDTH,
padding: '10px 5px',
marginRight: PADDING,
flexShrink: 0,
position: 'relative',
});
const Icon = styled(Glyph)({
position: 'absolute',
right: 5,
top: 5,
});
type Props = {|
generations: Array<TreeGeneration>,
focusedGenerationId: ?string,
onClick: (id: string) => mixed,
|};
type State = {
scrolled: boolean,
};
export default class extends Component<Props, State> {
state = {
scrolled: false,
};
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
}
componentDidUpdate(prevProps: Props) {
const {focusedGenerationId} = this.props;
if (
focusedGenerationId &&
focusedGenerationId !== prevProps.focusedGenerationId
) {
const node = document.querySelector(`[data-id="${focusedGenerationId}"]`);
if (node) {
node.scrollIntoViewIfNeeded();
}
}
}
onKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') {
return;
}
e.preventDefault();
let nextGenerationId = null;
const index = this.props.generations.findIndex(
g => g.id === this.props.focusedGenerationId,
);
const direction = e.key === 'ArrowRight' ? 1 : -1;
const bound = e.key === 'ArrowRight' ? this.props.generations.length : -1;
for (let i = index + direction; i !== bound; i += direction) {
if (
this.props.generations[i].surface_key ===
this.props.generations[index].surface_key
) {
nextGenerationId = this.props.generations[i].id;
break;
}
}
if (nextGenerationId) {
this.props.onClick(nextGenerationId);
}
};
onScroll = (e: SyntheticUIEvent<HTMLElement>) =>
this.setState({scrolled: e.currentTarget.scrollLeft > 0});
render() {
const surfaces = this.props.generations.reduce(
(acc, cv) => acc.add(cv.surface_key),
new Set(),
);
return (
<Container>
<SurfaceContainer scrolled={this.state.scrolled}>
{[...surfaces].map(surface => (
<Row key={surface}>
<Label title={surface}>{surface}</Label>
</Row>
))}
</SurfaceContainer>
<TimeContainer onScroll={this.onScroll}>
{[...surfaces].map(surface => (
<Row key={surface} showTimeline>
{this.props.generations.map((record: TreeGeneration) =>
record.surface_key === surface ? (
<Record
key={`${surface}${record.id}`}
data-id={record.id}
highlighted={record.id === this.props.focusedGenerationId}
onClick={() => this.props.onClick(record.id)}>
<Content>{record.reason}</Content>
{record.reentrant_count > 0 && (
<Tooltip
title={'Reentrant count ' + record.reentrant_count}>
<Icon
color={colors.red}
name="caution-circle"
variant="filled"
size={12}
/>
</Tooltip>
)}
</Record>
) : (
<Empty key={`${surface}${record.id}`} />
),
)}
</Row>
))}
</TimeContainer>
</Container>
);
}
}

View File

@@ -0,0 +1,98 @@
/**
* 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
*/
export type SectionComponentHierarchy = {|
type: string,
children: Array<SectionComponentHierarchy>,
|};
export type AddEventPayload = {|
id: string,
reason: string,
stack_trace: Array<string>,
skip_stack_trace_format?: boolean,
surface_key: string,
event_timestamp: number,
update_mode: number,
reentrant_count: number,
payload: ?Object,
|};
export type UpdateTreeGenerationHierarchyGenerationPayload = {|
hierarchy_generation_timestamp: number,
id: string,
reason: string,
tree?: Array<{
didTriggerStateUpdate?: boolean,
identifier: string,
isDirty?: boolean,
isReused?: boolean,
name: string,
parent: string | '',
inserted?: boolean,
removed?: boolean,
updated?: boolean,
unchanged?: boolean,
isSection?: boolean,
isDataModel?: boolean,
}>,
|};
export type UpdateTreeGenerationChangesetGenerationPayload = {|
timestamp: number,
tree_generation_id: string,
identifier: string,
type: string,
changesets: {
section_key: {
changesets: {
id: {
count: number,
index: number,
toIndex?: number,
type: string,
render_infos?: Array<String>,
prev_data?: Array<String>,
next_data?: Array<String>,
},
},
},
},
|};
export type UpdateTreeGenerationChangesetApplicationPayload = {|
changeset: {
section_key: {
changesets: {
id: {
count: number,
index: number,
toIndex?: number,
type: string,
render_infos?: Array<String>,
prev_data?: Array<String>,
next_data?: Array<String>,
},
},
},
},
type: string,
identifier: string,
timestamp: number,
section_component_hierarchy: SectionComponentHierarchy,
tree_generation_id: string,
payload: ?Object,
|};
export type TreeGeneration = {|
...AddEventPayload,
...$Shape<UpdateTreeGenerationHierarchyGenerationPayload>,
...$Shape<UpdateTreeGenerationChangesetGenerationPayload>,
changeSets: Array<UpdateTreeGenerationChangesetApplicationPayload>,
|};

View File

@@ -0,0 +1,62 @@
/**
* 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 React from 'react';
import {colors, StackTrace} from 'flipper';
const FacebookLibraries = ['Facebook'];
const REGEX = new RegExp(
'(?<library>[A-Za-z0-9]*) *(?<address>0x[A-Za-z0-9]*) (?<caller>(.*)) \\+ (?<lineNumber>[0-9]*)',
);
function isSystemLibrary(libraryName: ?string): boolean {
return !FacebookLibraries.includes(libraryName);
}
type Props = {
data: Array<string>,
skipStackTraceFormat?: boolean,
};
export default class extends React.Component<Props> {
render() {
if (this.props.skipStackTraceFormat) {
return (
<StackTrace backgroundColor={colors.white}>
{this.props.data.map(stack_trace_line => {
return {
caller: stack_trace_line,
};
})}
</StackTrace>
);
}
return (
<StackTrace backgroundColor={colors.white}>
{/* We need to filter out from the stack trace any reference to the plugin such that the information is more coincised and focused */}
{this.props.data
.filter(stack_trace_line => {
return !stack_trace_line.includes('FlipperKitSectionsPlugin');
})
.map(stack_trace_line => {
const trace = REGEX.exec(stack_trace_line)?.groups;
return {
bold: !isSystemLibrary(trace?.library),
library: trace?.library,
address: trace?.address,
caller: trace?.caller,
lineNumber: trace?.lineNumber,
};
})}
</StackTrace>
);
}
}

View File

@@ -0,0 +1,302 @@
/**
* 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 type {SectionComponentHierarchy} from './Models';
import {Glyph, PureComponent, styled, Toolbar, Spacer, colors} from 'flipper';
import {Tree} from 'react-d3-tree';
import {Fragment} from 'react';
const Legend = styled.div(props => ({
color: colors.dark50,
marginLeft: 20,
'&::before': {
content: '""',
display: 'inline-block',
width: 10,
height: 10,
borderRadius: 6,
backgroundColor: props.color,
border: `1px solid rgba(0,0,0,0.2)`,
marginRight: 4,
marginBottom: -1,
},
}));
const Label = styled.div({
position: 'relative',
top: -7,
left: 7,
maxWidth: 270,
overflow: 'hidden',
fontWeight: '500',
textOverflow: 'ellipsis',
paddingLeft: 5,
paddingRight: 5,
background: colors.white,
display: 'inline-block',
});
const Container = styled.div({
width: '100%',
height: '100%',
overflow: 'hidden',
background:
'linear-gradient(-90deg,rgba(0,0,0,.02) 1px,transparent 0),linear-gradient(rgba(0,0,0,.02) 1px,transparent 0),linear-gradient(-90deg,rgba(0,0,0,.03) 1px,transparent 0),linear-gradient(rgba(0,0,0,.03) 1px,transparent 0)',
backgroundSize:
'10px 10px,10px 10px,100px 100px,100px 100px,100px 100px,100px 100px,100px 100px,100px 100px',
});
const LabelContainer = styled.div({
display: 'flex',
});
const IconButton = styled.div({
position: 'relative',
left: 5,
top: -8,
background: colors.white,
});
type TreeData = Array<{
identifier: string,
name: string,
parent: string | '',
didTriggerStateUpdate?: boolean,
isReused?: boolean,
isDirty?: boolean,
inserted?: boolean,
removed?: boolean,
updated?: boolean,
unchanged?: boolean,
isSection?: boolean,
isDataModel?: boolean,
}>;
type Props = {
data: TreeData | SectionComponentHierarchy,
nodeClickHandler?: (node: any, evt: InputEvent) => void,
};
type State = {
translate: {
x: number,
y: number,
},
tree: ?Object,
zoom: number,
};
class NodeLabel extends PureComponent<Props, State> {
state = {
collapsed: false,
};
showNodeData = e => {
e.stopPropagation();
this.props.onLabelClicked(this.props?.nodeData);
};
toggleClicked = () => {
this.setState({
collapsed: !this.state.collapsed,
});
};
render() {
const name = this.props?.nodeData?.name;
const isSection = this.props?.nodeData?.attributes.isSection;
const chevron = this.state.collapsed ? 'chevron-right' : 'chevron-left';
return (
<LabelContainer>
<Label title={name} onClick={this.showNodeData}>
{name}
</Label>
{isSection && (
<IconButton onClick={this.toggleClicked}>
<Glyph
color={colors.blueGreyTint70}
name={chevron}
variant={'filled'}
size={12}
/>
</IconButton>
)}
</LabelContainer>
);
}
}
export default class extends PureComponent<Props, State> {
treeFromFlatArray = (data: TreeData) => {
const tree = data.map(n => {
let fill = colors.blueGreyTint70;
if (n.didTriggerStateUpdate) {
fill = colors.lemon;
} else if (n.isReused) {
fill = colors.teal;
} else if (n.isDirty) {
fill = colors.grape;
}
if (n.removed) {
fill = colors.light20;
} else if (n.inserted) {
fill = colors.pinkDark1;
} else if (n.updated) {
fill = colors.orangeTint15;
} else if (n.unchanged) {
fill = colors.teal;
}
return {
name: n.name,
children: [],
attributes: {...n},
nodeSvgShape: {
shapeProps: {
fill,
r: 6,
strokeWidth: 1,
stroke: 'rgba(0,0,0,0.2)',
},
},
};
});
const parentMap: Map<string, Array<Object>> = tree.reduce((acc, cv) => {
const {parent} = cv.attributes;
if (typeof parent !== 'string') {
return acc;
}
const children = acc.get(parent);
if (children) {
return acc.set(parent, children.concat(cv));
} else {
return acc.set(parent, [cv]);
}
}, new Map());
tree.forEach(n => {
n.children = parentMap.get(n.attributes.identifier) || [];
});
// find the root node
return tree.find(node => !node.attributes.parent);
};
treeFromHierarchy = (data: SectionComponentHierarchy): Object => {
return {
name: data.type,
children: data.children ? data.children.map(this.treeFromHierarchy) : [],
};
};
state = {
translate: {
x: 0,
y: 0,
},
tree: Array.isArray(this.props.data)
? this.treeFromFlatArray(this.props.data)
: this.treeFromHierarchy(this.props.data),
zoom: 1,
};
treeContainer: any = null;
UNSAFE_componentWillReceiveProps(props: Props) {
if (this.props.data === props.data) {
return;
}
this.setState({
tree: Array.isArray(props.data)
? this.treeFromFlatArray(props.data)
: this.treeFromHierarchy(props.data),
});
}
componentDidMount() {
if (this.treeContainer) {
const dimensions = this.treeContainer.getBoundingClientRect();
this.setState({
translate: {
x: 50,
y: dimensions.height / 2,
},
});
}
}
onZoom = (e: SyntheticInputEvent<HTMLInputElement>) => {
this.setState({zoom: e.target.valueAsNumber});
};
render() {
return (
<Fragment>
<Container
innerRef={ref => {
this.treeContainer = ref;
}}>
<style>
{'.rd3t-tree-container foreignObject {overflow: visible;}'}
</style>
{this.state.tree && (
<Tree
transitionDuration={0}
separation={{siblings: 0.5, nonSiblings: 0.5}}
data={this.state.tree}
translate={this.state.translate}
zoom={this.state.zoom}
nodeLabelComponent={{
render: (
<NodeLabel onLabelClicked={this.props.nodeClickHandler} />
),
}}
allowForeignObjects
nodeSvgShape={{
shape: 'circle',
shapeProps: {
stroke: 'rgba(0,0,0,0.2)',
strokeWidth: 1,
},
}}
styles={{
links: {
stroke: '#b3b3b3',
},
}}
nodeSize={{x: 300, y: 100}}
/>
)}
</Container>
<Toolbar position="bottom" compact>
<input
type="range"
onChange={this.onZoom}
value={this.state.zoom}
min="0.1"
max="1"
step="0.01"
/>
<Spacer />
<Legend color={colors.light20}>Item removed</Legend>
<Legend color={colors.pinkDark1}>Item inserted</Legend>
<Legend color={colors.orangeTint15}>Item updated</Legend>
<Legend color={colors.teal}>Item/Section Reused</Legend>
<Legend color={colors.lemon}>Section triggered state update</Legend>
<Legend color={colors.grape}>Section is dirty</Legend>
</Toolbar>
</Fragment>
);
}
}

View File

@@ -0,0 +1,26 @@
Copyright (c) 2010-2016, Michael Bostock
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name Michael Bostock may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

9560
desktop/plugins/sections/d3/d3.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
{
"title": "sections-d3",
"name": "d3",
"version": "3.5.17",
"description": "A JavaScript visualization library for HTML and SVG.",
"bugs": {
"email": "danielbuechele@fb.com"
},
"keywords": [
"dom",
"w3c",
"visualization",
"svg",
"animation",
"canvas"
],
"homepage": "http://d3js.org",
"author": {
"name": "Mike Bostock",
"url": "http://bost.ocks.org/mike"
},
"contributors": [
{
"name": "Jason Davies",
"url": "http://jasondavies.com"
}
],
"repository": {
"type": "git",
"url": "https://github.com/mbostock/d3.git"
},
"main": "d3.js",
"browser": "d3.js",
"jspm": {
"main": "d3",
"shim": {
"d3": {
"exports": "d3"
}
},
"files": [
"d3.js"
],
"buildConfig": {
"uglify": true
}
},
"jam": {
"main": "d3.js",
"shim": {
"exports": "d3"
}
},
"spm": {
"main": "d3.js"
},
"devDependencies": {
"jsdom": "^16.0.1",
"seedrandom": "^3.0.5",
"smash": "0.0",
"uglify-js": "^3.7.5",
"vows": "0.8"
},
"scripts": {
"test": "vows && echo",
"prepublish": "npm test && rm -f package.js src/start.js d3.js d3.min.js d3.zip && bin/start > src/start.js && bin/meteor > package.js && smash src/d3.js | uglifyjs - -b indent-level=2 -o d3.js && bin/uglify d3.js > d3.min.js && chmod a-w d3.js d3.min.js package.js && zip d3.zip LICENSE d3.js d3.min.js",
"postpublish": "VERSION=`node -e 'console.log(require(\"./package.json\").version)'`; git push && git push --tags && cp -v README.md LICENSE d3.js d3.min.js ../d3-bower && cd ../d3-bower && git add README.md LICENSE d3.js d3.min.js && git commit -m \"Release $VERSION.\" && git tag -am \"Release $VERSION.\" v${VERSION} && git push && git push --tags && cd - && cp -v d3.js ../d3.github.com/d3.v3.js && cp -v d3.min.js ../d3.github.com/d3.v3.min.js && cd ../d3.github.com && git add d3.v3.js d3.v3.min.js && git commit -m \"d3 ${VERSION}\" && git push"
},
"license": "BSD-3-Clause"
}

View File

@@ -0,0 +1,307 @@
/**
* 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
* @flow
*/
import type {
TreeGeneration,
AddEventPayload,
UpdateTreeGenerationHierarchyGenerationPayload,
UpdateTreeGenerationChangesetGenerationPayload,
UpdateTreeGenerationChangesetApplicationPayload,
} from './Models.js';
import {FlipperPlugin} from 'flipper';
import React from 'react';
import Tree from './Tree.js';
import StackTrace from './StackTrace.js';
import EventTable from './EventsTable.js';
import DetailsPanel from './DetailsPanel.js';
import {
Toolbar,
Glyph,
Sidebar,
FlexBox,
styled,
Button,
Spacer,
colors,
DetailSidebar,
} from 'flipper';
const Waiting = styled(FlexBox)(props => ({
width: '100%',
height: '100%',
flexGrow: 1,
background: colors.light02,
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
}));
const InfoText = styled.div(props => ({
marginTop: 10,
marginBottom: 10,
fontWeight: '500',
color: colors.light30,
}));
const InfoBox = styled.div(props => ({
maxWidth: 400,
margin: 'auto',
textAlign: 'center',
}));
type State = {
focusedChangeSet: ?UpdateTreeGenerationChangesetApplicationPayload,
userSelectedGenerationId: ?string,
selectedTreeNode: ?Object,
};
type PersistedState = {
generations: {
[id: string]: TreeGeneration,
},
focusedGenerationId: ?string,
recording: boolean,
};
export default class extends FlipperPlugin<State, *, PersistedState> {
static title = 'Sections';
static id = 'Sections';
static icon = 'tree';
static defaultPersistedState = {
generations: {},
focusedGenerationId: null,
recording: true,
};
static persistedStateReducer = (
persistedState: PersistedState,
method: string,
payload: Object,
): $Shape<PersistedState> => {
if (!persistedState.recording) {
return persistedState;
}
const addEvent = (data: AddEventPayload) => ({
...persistedState,
generations: {
...persistedState.generations,
[data.id]: {
...data,
changeSets: [],
},
},
focusedGenerationId: persistedState.focusedGenerationId || data.id,
});
const updateTreeGenerationHierarchyGeneration = (
data: UpdateTreeGenerationHierarchyGenerationPayload,
) => ({
...persistedState,
generations: {
...persistedState.generations,
[data.id]: {
...persistedState.generations[data.id],
...data,
},
},
});
const updateTreeGenerationChangeset = (
data:
| UpdateTreeGenerationChangesetGenerationPayload
| UpdateTreeGenerationChangesetApplicationPayload,
) => ({
...persistedState,
generations: {
...persistedState.generations,
[data.tree_generation_id]: {
...persistedState.generations[data.tree_generation_id],
changeSets: [
...persistedState.generations[data.tree_generation_id].changeSets,
data,
],
},
},
});
if (method === 'addEvent') {
return addEvent(payload);
} else if (method === 'updateTreeGenerationHierarchyGeneration') {
return updateTreeGenerationHierarchyGeneration(payload);
} else if (
method === 'updateTreeGenerationChangesetApplication' ||
method === 'updateTreeGenerationChangesetGeneration'
) {
return updateTreeGenerationChangeset(payload);
} else {
return persistedState;
}
};
state = {
focusedChangeSet: null,
userSelectedGenerationId: null,
selectedTreeNode: null,
};
onTreeGenerationFocused = (focusedGenerationId: ?string) => {
this.setState({
focusedChangeSet: null,
userSelectedGenerationId: focusedGenerationId,
selectedTreeNode: null,
});
};
onFocusChangeSet = (
focusedChangeSet: ?UpdateTreeGenerationChangesetApplicationPayload,
) => {
this.setState({
focusedChangeSet,
selectedTreeNode: null,
});
};
onNodeClicked = (targetNode: any, evt: InputEvent) => {
if (targetNode.attributes.isSection) {
const sectionData = {};
sectionData['global_key'] = targetNode.attributes.identifier;
this.setState({
selectedTreeNode: {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;
}
}
this.setState({
selectedTreeNode: {dataModel},
});
};
renderTreeHierarchy = (generation: ?TreeGeneration) => {
if (generation && generation.tree && generation.tree.length > 0) {
// Display component tree hierarchy, if any
return (
<Tree data={generation.tree} nodeClickHandler={this.onNodeClicked} />
);
} else if (
this.state.focusedChangeSet &&
this.state.focusedChangeSet.section_component_hierarchy
) {
// Display section component hierarchy for specific changeset
return (
<Tree
data={this.state.focusedChangeSet.section_component_hierarchy}
nodeClickHandler={this.onNodeClicked}
/>
);
} else {
return this.renderWaiting();
}
};
renderWaiting = () => (
<Waiting>
<InfoBox>
<Glyph
name="face-unhappy"
variant="outline"
size={24}
color={colors.light30}
/>
<InfoText>No data available...</InfoText>
</InfoBox>
</Waiting>
);
clear = () => {
this.props.setPersistedState({
...this.constructor.defaultPersistedState,
});
};
render() {
const {generations} = this.props.persistedState;
if (Object.values(this.props.persistedState.generations).length === 0) {
return this.renderWaiting();
}
const focusedGenerationId =
this.state.userSelectedGenerationId ||
this.props.persistedState.focusedGenerationId;
const focusedTreeGeneration: ?TreeGeneration = focusedGenerationId
? generations[focusedGenerationId]
: null;
return (
<React.Fragment>
<Toolbar>
<Spacer />
{this.props.persistedState.recording ? (
<Button
onClick={() =>
this.props.setPersistedState({
recording: false,
})
}
iconVariant="filled"
icon="stop-playback">
Stop
</Button>
) : (
<Button onClick={this.clear} icon="trash" iconVariant="outline">
Clear
</Button>
)}
</Toolbar>
<Sidebar position="top" minHeight={80} height={80}>
<EventTable
generations={Object.values(generations)}
focusedGenerationId={focusedGenerationId}
onClick={this.onTreeGenerationFocused}
/>
</Sidebar>
{this.renderTreeHierarchy(focusedTreeGeneration)}
{focusedTreeGeneration && (
<Sidebar position="bottom" minHeight={100} height={250}>
<StackTrace
data={focusedTreeGeneration.stack_trace}
skip_stack_trace_format={
focusedTreeGeneration.skip_stack_trace_format
}
/>
</Sidebar>
)}
<DetailSidebar>
<DetailsPanel
eventUserInfo={focusedTreeGeneration?.payload}
changeSets={focusedTreeGeneration?.changeSets}
onFocusChangeSet={this.onFocusChangeSet}
focusedChangeSet={this.state.focusedChangeSet}
selectedNodeInfo={this.state.selectedTreeNode}
/>
</DetailSidebar>
</React.Fragment>
);
}
}

View File

@@ -0,0 +1,19 @@
{
"name": "flipper-plugin-sections",
"title": "Sections",
"bugs": {
"email": "oncall+ios_componentkit@xmail.facebook.com",
"url": "https://fb.workplace.com/groups/componentkit/"
},
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"keywords": ["flipper-plugin"],
"gatekeeper": "flipper_sections_plugin",
"dependencies": {
"react-d3-tree": "^1.12.1"
},
"resolutions": {
"react-d3-tree/d3": "file:./d3"
}
}

View File

@@ -0,0 +1,100 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/runtime@^7.1.2":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f"
integrity sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==
dependencies:
regenerator-runtime "^0.12.0"
chain-function@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.1.tgz#c63045e5b4b663fb86f1c6e186adaf1de402a1cc"
integrity sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==
clone@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
d3@3.5.17, "d3@file:./d3":
version "3.5.17"
deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
dom-helpers@^3.2.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
loose-envify@^1.0.0, loose-envify@^1.3.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
prop-types@^15.5.10, prop-types@^15.5.6:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==
dependencies:
loose-envify "^1.3.1"
object-assign "^4.1.1"
react-d3-tree@^1.12.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/react-d3-tree/-/react-d3-tree-1.12.1.tgz#30ca3ff727bc6c43cf2d0f0e1796c3b0907f6a3f"
integrity sha512-4yRCXccmkTbMPHNr2gcVrYwYaYzZOWItqRqxItNFfozpKfaymWvwepC9sIm5RAdWOIGnsoK1SR+X67rB4th2Xg==
dependencies:
clone "^2.1.1"
d3 "3.5.17"
deep-equal "^1.0.1"
prop-types "^15.5.10"
react-transition-group "^1.1.3"
uuid "^3.0.1"
react-transition-group@^1.1.3:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6"
integrity sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==
dependencies:
chain-function "^1.0.0"
dom-helpers "^3.2.0"
loose-envify "^1.3.1"
prop-types "^15.5.6"
warning "^3.0.0"
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
uuid@^3.0.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
warning@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=
dependencies:
loose-envify "^1.0.0"