Files
flipper/desktop/flipper-plugin/src/ui/MarkerTimeline.tsx
Luke De Feo 2d217575bb improve marker timeline
Summary:
1. removed background hover
2. made can select actually work
3. disable user selection
4. remove decimal points from ms

Reviewed By: lblasa

Differential Revision: D48270181

fbshipit-source-id: 724efedb5cad7f93c80ed5dd7f49efe4c434c324
2023-08-21 04:24:16 -07:00

245 lines
5.7 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 from 'react';
import {Component} from 'react';
import styled from '@emotion/styled';
import {theme} from './theme';
import {Layout} from './Layout';
import {Typography} from 'antd';
type DataPoint = {
time: number;
color?: string;
label: string;
key: string;
};
type Props = {
onClick?: (keys: Array<string>) => void;
selected?: string | null | undefined;
points: DataPoint[];
lineHeight: number;
maxGap: number;
};
type MouseEventHandler = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => void;
const Markers = styled.div<{totalTime: number}>((props) => ({
position: 'relative',
margin: 10,
height: props.totalTime,
'::before': {
content: '""',
width: 1,
borderLeft: `1px dotted ${theme.disabledColor}`,
position: 'absolute',
top: 5,
bottom: 20,
left: 5,
},
}));
Markers.displayName = 'MarkerTimeline:Markers';
const Point = styled(Layout.Horizontal)<{
positionY: number;
onClick: MouseEventHandler | undefined;
number: number | undefined;
threadColor: string;
selected: boolean;
cut: boolean;
}>((props) => ({
position: 'absolute',
top: props.positionY,
left: 0,
right: 10,
cursor: props.onClick ? 'pointer' : 'default',
borderRadius: 3,
alignItems: 'baseline',
lineHeight: '16px',
':hover': {
backgroundColor: theme.backgroundWash,
'> span': {
whiteSpace: 'initial',
},
},
'::before': {
position: 'relative',
textAlign: 'center',
fontSize: 8,
fontWeight: 500,
content: props.number ? `'${props.number}'` : '""',
display: 'inline-block',
width: 9,
height: 9,
flexShrink: 0,
color: theme.textColorSecondary,
lineHeight: '9px',
borderRadius: '999em',
border: theme.dividerColor,
backgroundColor: props.threadColor,
marginRight: 6,
marginTop: 3,
zIndex: 3,
boxShadow: props.selected
? `0 0 0 4px ${theme.selectionBackgroundColor}`
: undefined,
},
'::after': {
content: props.cut ? '""' : undefined,
position: 'absolute',
width: 11,
top: -20,
left: 0,
height: 2,
background: theme.backgroundDefault,
borderTop: `1px solid ${theme.dividerColor}`,
borderBottom: `1px solid ${theme.dividerColor}`,
transform: `skewY(-10deg)`,
},
}));
Point.displayName = 'MakerTimeline:Point';
const Time = styled.span({
color: theme.textColorSecondary,
fontWeight: 300,
marginRight: 4,
});
Time.displayName = 'MakerTimeline:Time';
const Name = styled(Typography.Text)({
overflow: 'hidden',
opacity: 0.8,
textOverflow: 'ellipsis',
marginTop: -1,
marginLeft: theme.space.tiny,
fontFamily: 'monospace',
});
Name.displayName = 'MakerTimeline:Name';
type TimePoint = {
timestamp: number;
markerNames: Array<string>;
markerKeys: Array<string>;
isCut: boolean;
positionY: number;
color: string;
};
type State = {
timePoints: Array<TimePoint>;
};
export class MarkerTimeline extends Component<Props, State> {
static defaultProps = {
lineHeight: 22,
maxGap: 100,
};
static getDerivedStateFromProps(props: Props) {
const sortedMarkers: [number, DataPoint[]][] = Array.from(
props.points
.reduce((acc: Map<number, DataPoint[]>, cv: DataPoint) => {
const list = acc.get(cv.time);
if (list) {
list.push(cv);
} else {
acc.set(cv.time, [cv]);
}
return acc;
}, new Map())
.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 || theme.backgroundDefault;
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}
positionY={p.positionY}
onClick={onClick ? () => onClick(p.markerKeys) : undefined}
selected={
this.props.selected
? p.markerKeys.includes(this.props.selected)
: false
}
number={
p.markerNames.length > 1 ? p.markerNames.length : undefined
}>
<Time>{p.timestamp.toFixed(0)}ms</Time>{' '}
<Name>{p.markerNames.join(', ')}</Name>
</Point>
);
})}
</Markers>
);
}
}