import {Fragment, useMemo} from 'react';
import styled from '@emotion/styled';
import color from 'color';
import DateTime from 'sentry/components/dateTime';
import {Tooltip} from 'sentry/components/tooltip';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Event} from 'sentry/types';
import {TraceTimelineTooltip} from 'sentry/views/issueDetails/traceTimeline/traceTimelineTooltip';
import type {TimelineEvent} from './useTraceTimelineEvents';
import {useTraceTimelineEvents} from './useTraceTimelineEvents';
import {getChunkTimeRange, getEventsByColumn} from './utils';
// Adjusting this will change the number of tooltip groups
const PARENT_WIDTH = 12;
// Adjusting subwidth changes how many dots to render
const CHILD_WIDTH = 4;
interface TraceTimelineEventsProps {
event: Event;
width: number;
}
export function TraceTimelineEvents({event, width}: TraceTimelineEventsProps) {
const {startTimestamp, endTimestamp, traceEvents} = useTraceTimelineEvents({event});
let paddedStartTime = startTimestamp;
let paddedEndTime = endTimestamp;
// Duration is 0, pad both sides, this is how we end up with 1 dot in the middle
if (endTimestamp - startTimestamp === 0) {
// If the duration is 0, we need to pad the end time
paddedEndTime = startTimestamp + 1500;
paddedStartTime = startTimestamp - 1500;
}
const durationMs = paddedEndTime - paddedStartTime;
const totalColumns = Math.floor(width / PARENT_WIDTH);
const eventsByColumn = useMemo(
() => getEventsByColumn(traceEvents, durationMs, totalColumns, paddedStartTime),
[durationMs, traceEvents, totalColumns, paddedStartTime]
);
const columnSize = width / totalColumns;
// If the duration is less than 2 minutes, show seconds
const showTimelineSeconds = durationMs < 120 * 1000;
const middleTimestamp = paddedStartTime + Math.floor(durationMs / 2);
const leftMiddleTimestamp = paddedStartTime + Math.floor(durationMs / 4);
const rightMiddleTimestamp = paddedStartTime + Math.floor((durationMs / 4) * 3);
return (
{/* Add padding to the total columns, 1 column of padding on each side */}
{eventsByColumn.map(([column, colEvents]) => {
// Calculate the timestamp range that this column represents
const timeRange = getChunkTimeRange(
paddedStartTime,
column - 1,
durationMs / totalColumns
);
const hasCurrentEvent = colEvents.some(e => e.id === event.id);
return (
);
})}
);
}
/**
* Use grid to create columns that we can place child nodes into.
* Leveraging grid for alignment means we don't need to calculate percent offset
* nor use position:absolute to lay out items.
*
*
* ...
* ...
*
*/
const TimelineColumns = styled('div')`
/* Reset defaults for
*/
list-style: none;
margin: 0;
padding: 0;
/* Layout of the lines */
display: grid;
margin-top: -1px;
height: 0;
`;
const TimestampColumns = styled('div')`
display: flex;
align-items: center;
justify-content: space-between;
margin-top: ${space(1)};
text-align: center;
color: ${p => p.theme.subText};
font-size: ${p => p.theme.fontSizeSmall};
`;
function NodeGroup({
event,
timeRange,
colEvents,
columnSize,
currentEventId,
currentColumn,
}: {
colEvents: TimelineEvent[];
columnSize: number;
currentColumn: number;
currentEventId: string;
event: Event;
timeRange: [number, number];
}) {
const totalSubColumns = Math.floor(columnSize / CHILD_WIDTH);
const {eventsByColumn, columns} = useMemo(() => {
const durationMs = timeRange[1] - timeRange[0];
const eventColumns = getEventsByColumn(
colEvents,
durationMs,
totalSubColumns,
timeRange[0]
);
return {
eventsByColumn: eventColumns,
columns: eventColumns.map(([column]) => column).sort(),
};
}, [colEvents, totalSubColumns, timeRange]);
return (
{eventsByColumn.map(([column, groupEvents]) => {
const isCurrentNode = groupEvents.some(e => e.id === currentEventId);
return (
{isCurrentNode ? (
) : (
groupEvents
.slice(0, 5)
.map(groupEvent =>
'event.type' in groupEvent ? (
) : (
)
)
)}
);
})}
}
overlayStyle={{
padding: `0 !important`,
}}
position="bottom"
isHoverable
skipWrapper
>
1
? `${columns.at(0)} / ${columns.at(-1)}`
: columns.at(0)!,
width: 8 * columns.length,
}}
data-test-id={`trace-timeline-tooltip-${currentColumn}`}
/>
);
}
const EventColumn = styled('div')`
place-items: stretch;
display: grid;
align-items: center;
position: relative;
&:hover {
z-index: ${p => p.theme.zIndex.initial};
}
`;
const IconNode = styled('div')`
position: absolute;
grid-column: 1;
grid-row: 1;
width: 8px;
height: 8px;
border-radius: 50%;
color: ${p => p.theme.white};
box-shadow: ${p => p.theme.dropShadowLight};
user-select: none;
background-color: ${p => color(p.theme.red200).alpha(0.3).string()};
margin-left: -8px;
`;
const PerformanceIconNode = styled(IconNode)`
background-color: unset;
border: 1px solid ${p => p.theme.red300};
`;
const CurrentNodeContainer = styled('div')`
position: absolute;
grid-column: 1;
grid-row: 1;
width: 12px;
height: 12px;
`;
const CurrentNodeRing = styled('div')`
border: 1px solid ${p => p.theme.red300};
height: 24px;
width: 24px;
border-radius: 100%;
position: absolute;
top: -6px;
left: -16px;
animation: pulse 2s ease-out infinite;
@keyframes pulse {
0% {
transform: scale(0.1, 0.1);
opacity: 0;
}
50% {
transform: scale(0.1, 0.1);
opacity: 0;
}
70% {
opacity: 1;
}
100% {
transform: scale(1.2, 1.2);
opacity: 0;
}
}
`;
const CurrentIconNode = styled(IconNode)`
background-color: ${p => p.theme.red300};
width: 12px;
height: 12px;
margin-left: -10px;
`;
const TooltipHelper = styled('span')`
height: 12px;
margin-left: -8px;
margin-top: -6px;
z-index: 1;
`;