MarkerTimeline
Summary: Moving the `MarkerTimeline` from the QPL plugin to the Flipper UI library, so it can be used by other plugin. Reviewed By: priteshrnandgaonkar Differential Revision: D13377065 fbshipit-source-id: 9ef1f0e044fa85b68a01e23071042600aa5c3c63
This commit is contained in:
committed by
Facebook Github Bot
parent
c9131963d5
commit
3b45976217
210
src/ui/components/MarkerTimeline.js
Normal file
210
src/ui/components/MarkerTimeline.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2018-present Facebook.
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Component} from 'react';
|
||||||
|
import styled from '../styled/index.js';
|
||||||
|
import Text from './Text.js';
|
||||||
|
import FlexRow from './FlexRow.js';
|
||||||
|
import {colors} from './colors.js';
|
||||||
|
|
||||||
|
type DataPoint = {
|
||||||
|
time: number,
|
||||||
|
color?: string,
|
||||||
|
label: string,
|
||||||
|
key: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {|
|
||||||
|
onClick?: (keys: Array<string>) => mixed,
|
||||||
|
selected?: ?string,
|
||||||
|
points: Array<DataPoint>,
|
||||||
|
lineHeight: number,
|
||||||
|
maxGap: number,
|
||||||
|
|};
|
||||||
|
|
||||||
|
const Markers = styled('div')(props => ({
|
||||||
|
position: 'relative',
|
||||||
|
margin: 10,
|
||||||
|
height: props.totalTime,
|
||||||
|
'::before': {
|
||||||
|
content: '""',
|
||||||
|
width: 1,
|
||||||
|
borderLeft: `1px dotted ${colors.light30}`,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 5,
|
||||||
|
bottom: 20,
|
||||||
|
left: 5,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Point = styled(FlexRow)(props => ({
|
||||||
|
position: 'absolute',
|
||||||
|
top: props.positionY,
|
||||||
|
left: 0,
|
||||||
|
right: 10,
|
||||||
|
cursor: props.onClick ? 'pointer' : 'default',
|
||||||
|
borderRadius: 3,
|
||||||
|
alignItems: 'center',
|
||||||
|
':hover': {
|
||||||
|
background: props.onClick ? colors.light02 : 'transparent',
|
||||||
|
},
|
||||||
|
'::before': {
|
||||||
|
position: 'relative',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: '500',
|
||||||
|
content: props.number ? `'${props.number}'` : '""',
|
||||||
|
display: 'inline-block',
|
||||||
|
width: 9,
|
||||||
|
height: 9,
|
||||||
|
flexShrink: 0,
|
||||||
|
color: 'rgba(0,0,0,0.4)',
|
||||||
|
lineHeight: '9px',
|
||||||
|
borderRadius: '999em',
|
||||||
|
border: '1px solid rgba(0,0,0,0.2)',
|
||||||
|
backgroundColor: props.threadColor,
|
||||||
|
marginRight: 6,
|
||||||
|
boxShadow: props.selected
|
||||||
|
? `0 0 0 2px ${colors.macOSTitleBarIconSelected}`
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
'::after': {
|
||||||
|
content: props.cut ? '""' : null,
|
||||||
|
position: 'absolute',
|
||||||
|
width: 11,
|
||||||
|
top: -20,
|
||||||
|
left: 0,
|
||||||
|
height: 2,
|
||||||
|
background: colors.white,
|
||||||
|
borderTop: `1px solid ${colors.light30}`,
|
||||||
|
borderBottom: `1px solid ${colors.light30}`,
|
||||||
|
transform: `skewY(-10deg)`,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Time = styled('span')({
|
||||||
|
color: colors.light30,
|
||||||
|
fontWeight: '300',
|
||||||
|
marginRight: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Code = styled(Text)({
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
});
|
||||||
|
|
||||||
|
type TimePoint = {
|
||||||
|
timestamp: number,
|
||||||
|
markerNames: Array<string>,
|
||||||
|
markerKeys: Array<string>,
|
||||||
|
isCut: boolean,
|
||||||
|
positionY: number,
|
||||||
|
color: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {|
|
||||||
|
timePoints: Array<TimePoint>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export default class MarkerTimeline extends Component<Props, State> {
|
||||||
|
static defaultProps = {
|
||||||
|
lineHeight: 22,
|
||||||
|
maxGap: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props: Props) {
|
||||||
|
const sortedMarkers: Array<[number, Array<DataPoint>]> = Array.from(
|
||||||
|
props.points
|
||||||
|
.reduce((acc: Map<number, Array<DataPoint>>, cv: DataPoint) => {
|
||||||
|
const list = acc.get(cv.time);
|
||||||
|
if (list) {
|
||||||
|
list.push(cv);
|
||||||
|
} else {
|
||||||
|
acc.set(cv.time, [cv]);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, (new Map(): Map<number, Array<DataPoint>>))
|
||||||
|
.entries(),
|
||||||
|
).sort((a, b) => a[0] - b[0]);
|
||||||
|
|
||||||
|
const smallestGap = sortedMarkers.reduce((acc, cv, i, arr) => {
|
||||||
|
if (i > 0) {
|
||||||
|
return Math.min(acc, cv[0] - arr[i - 1][0]);
|
||||||
|
} else {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}, Infinity);
|
||||||
|
|
||||||
|
let positionY = 0;
|
||||||
|
const timePoints: Array<TimePoint> = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedMarkers.length; i++) {
|
||||||
|
const [timestamp, points] = sortedMarkers[i];
|
||||||
|
let isCut = false;
|
||||||
|
const color = sortedMarkers[i][1][0].color || colors.white;
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
const relativeTimestamp = timestamp - sortedMarkers[i - 1][0];
|
||||||
|
const gap = (relativeTimestamp / smallestGap) * props.lineHeight;
|
||||||
|
if (gap > props.maxGap) {
|
||||||
|
positionY += props.maxGap;
|
||||||
|
isCut = true;
|
||||||
|
} else {
|
||||||
|
positionY += gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timePoints.push({
|
||||||
|
timestamp,
|
||||||
|
markerNames: points.map(p => p.label),
|
||||||
|
markerKeys: points.map(p => p.key),
|
||||||
|
positionY,
|
||||||
|
isCut,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {timePoints};
|
||||||
|
}
|
||||||
|
|
||||||
|
state: State = {
|
||||||
|
timePoints: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {timePoints} = this.state;
|
||||||
|
const {onClick} = this.props;
|
||||||
|
|
||||||
|
if (!this.props.points || this.props.points.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Markers
|
||||||
|
totalTime={
|
||||||
|
timePoints[timePoints.length - 1].positionY + this.props.lineHeight
|
||||||
|
}>
|
||||||
|
{timePoints.map((p: TimePoint, i: number) => {
|
||||||
|
return (
|
||||||
|
<Point
|
||||||
|
key={i}
|
||||||
|
threadColor={p.color}
|
||||||
|
cut={p.isCut}
|
||||||
|
title={p.markerNames.length > 1 ? p.markerNames.join(', ') : null}
|
||||||
|
positionY={p.positionY}
|
||||||
|
onClick={onClick ? () => onClick(p.markerKeys) : undefined}
|
||||||
|
selected={p.markerKeys.includes(this.props.selected)}
|
||||||
|
number={p.markerNames.length > 1 ? p.markerNames.length : null}>
|
||||||
|
<Time>{p.timestamp}ms</Time>{' '}
|
||||||
|
<Code code>{p.markerNames.join(', ')}</Code>
|
||||||
|
</Point>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Markers>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/ui/components/__tests__/MarkerTimeline.electron.js
Normal file
87
src/ui/components/__tests__/MarkerTimeline.electron.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2018-present Facebook.
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MarkerTimeline from '../MarkerTimeline';
|
||||||
|
|
||||||
|
test('merges points with same timestamp', () => {
|
||||||
|
const points = [
|
||||||
|
{key: 'marker1', label: 'marker1', time: 41},
|
||||||
|
{key: 'marker2', label: 'marker2', time: 41},
|
||||||
|
];
|
||||||
|
|
||||||
|
const {timePoints} = MarkerTimeline.getDerivedStateFromProps({
|
||||||
|
lineHeight: 22,
|
||||||
|
maxGap: 100,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
expect(timePoints[0].markerNames).toContain('marker1');
|
||||||
|
expect(timePoints[0].markerNames).toContain('marker2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sorts points', () => {
|
||||||
|
const {timePoints} = MarkerTimeline.getDerivedStateFromProps({
|
||||||
|
lineHeight: 22,
|
||||||
|
maxGap: 100,
|
||||||
|
points: [
|
||||||
|
{key: 'marker1', label: 'marker1', time: 20},
|
||||||
|
{key: 'marker2', label: 'marker2', time: -50},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(timePoints[0].timestamp).toBe(-50);
|
||||||
|
expect(timePoints[1].timestamp).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles negative timestamps', () => {
|
||||||
|
const points = [{label: 'preStartPoint', key: 'preStartPoint', time: -50}];
|
||||||
|
|
||||||
|
const {timePoints} = MarkerTimeline.getDerivedStateFromProps({
|
||||||
|
lineHeight: 22,
|
||||||
|
maxGap: 100,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
expect(timePoints[0].timestamp).toBe(-50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no points', () => {
|
||||||
|
const {timePoints} = MarkerTimeline.getDerivedStateFromProps({
|
||||||
|
lineHeight: 22,
|
||||||
|
maxGap: 100,
|
||||||
|
points: [],
|
||||||
|
});
|
||||||
|
expect(timePoints).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles single point', () => {
|
||||||
|
const points = [{key: '1', label: 'single point', time: 0}];
|
||||||
|
|
||||||
|
const {timePoints} = MarkerTimeline.getDerivedStateFromProps({
|
||||||
|
lineHeight: 22,
|
||||||
|
maxGap: 100,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
expect(timePoints).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cuts long gaps', () => {
|
||||||
|
const points = [
|
||||||
|
{key: '1', label: 'single point', time: 1},
|
||||||
|
{key: '2', label: 'single point', time: 1000},
|
||||||
|
{key: '3', label: 'single point', time: 1001},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MAX_GAP = 100;
|
||||||
|
|
||||||
|
const {timePoints} = MarkerTimeline.getDerivedStateFromProps({
|
||||||
|
lineHeight: 22,
|
||||||
|
maxGap: MAX_GAP,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(timePoints[0].isCut).toBe(false);
|
||||||
|
expect(timePoints[1].isCut).toBe(true);
|
||||||
|
expect(timePoints[1].positionY).toBe(timePoints[0].positionY + MAX_GAP);
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`handles single point 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"color": "#ffffff",
|
||||||
|
"isCut": false,
|
||||||
|
"markerKeys": Array [
|
||||||
|
"1",
|
||||||
|
],
|
||||||
|
"markerNames": Array [
|
||||||
|
"single point",
|
||||||
|
],
|
||||||
|
"positionY": 0,
|
||||||
|
"timestamp": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`no points 1`] = `Array []`;
|
||||||
@@ -131,6 +131,8 @@ export {default as Heading} from './components/Heading.js';
|
|||||||
// filters
|
// filters
|
||||||
export type {Filter} from './components/filter/types.js';
|
export type {Filter} from './components/filter/types.js';
|
||||||
|
|
||||||
|
export {default as MarkerTimeline} from './components/MarkerTimeline.js';
|
||||||
|
|
||||||
//
|
//
|
||||||
export {
|
export {
|
||||||
SearchBox,
|
SearchBox,
|
||||||
|
|||||||
Reference in New Issue
Block a user