mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
288 lines
8.3 KiB
TypeScript
288 lines
8.3 KiB
TypeScript
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 (
|
|
<Fragment>
|
|
{/* Add padding to the total columns, 1 column of padding on each side */}
|
|
<TimelineColumns style={{gridTemplateColumns: `repeat(${totalColumns + 2}, 1fr)`}}>
|
|
{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 (
|
|
<EventColumn
|
|
key={`${column}-${hasCurrentEvent ? 'current-event' : 'regular'}`}
|
|
// Add 1 to the column to account for the padding
|
|
style={{gridColumn: Math.floor(column) + 1, width: columnSize}}
|
|
>
|
|
<NodeGroup
|
|
event={event}
|
|
colEvents={colEvents}
|
|
columnSize={columnSize}
|
|
timeRange={timeRange}
|
|
currentEventId={event.id}
|
|
currentColumn={column}
|
|
/>
|
|
</EventColumn>
|
|
);
|
|
})}
|
|
</TimelineColumns>
|
|
<TimestampColumns>
|
|
<DateTime date={paddedStartTime} seconds={showTimelineSeconds} timeOnly />
|
|
<DateTime date={leftMiddleTimestamp} seconds={showTimelineSeconds} timeOnly />
|
|
<DateTime date={middleTimestamp} seconds={showTimelineSeconds} timeOnly />
|
|
<DateTime date={rightMiddleTimestamp} seconds={showTimelineSeconds} timeOnly />
|
|
<DateTime date={paddedEndTime} seconds={showTimelineSeconds} timeOnly />
|
|
</TimestampColumns>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* <Columns>
|
|
* <Col>...</Col>
|
|
* <Col>...</Col>
|
|
* </Columns>
|
|
*/
|
|
const TimelineColumns = styled('div')`
|
|
/* Reset defaults for <ul> */
|
|
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<number>(([column]) => column).sort(),
|
|
};
|
|
}, [colEvents, totalSubColumns, timeRange]);
|
|
|
|
return (
|
|
<Fragment>
|
|
<TimelineColumns style={{gridTemplateColumns: `repeat(${totalSubColumns}, 1fr)`}}>
|
|
{eventsByColumn.map(([column, groupEvents]) => {
|
|
const isCurrentNode = groupEvents.some(e => e.id === currentEventId);
|
|
return (
|
|
<EventColumn key={`${column}-currrent-event`} style={{gridColumn: column}}>
|
|
{isCurrentNode ? (
|
|
<CurrentNodeContainer aria-label={t('Current Event')}>
|
|
<CurrentNodeRing />
|
|
<CurrentIconNode />
|
|
</CurrentNodeContainer>
|
|
) : (
|
|
groupEvents
|
|
.slice(0, 5)
|
|
.map(groupEvent =>
|
|
'event.type' in groupEvent ? (
|
|
<IconNode key={groupEvent.id} />
|
|
) : (
|
|
<PerformanceIconNode key={groupEvent.id} />
|
|
)
|
|
)
|
|
)}
|
|
</EventColumn>
|
|
);
|
|
})}
|
|
</TimelineColumns>
|
|
<TimelineColumns style={{gridTemplateColumns: `repeat(${totalSubColumns}, 1fr)`}}>
|
|
<Tooltip
|
|
title={<TraceTimelineTooltip event={event} timelineEvents={colEvents} />}
|
|
overlayStyle={{
|
|
padding: `0 !important`,
|
|
}}
|
|
position="bottom"
|
|
isHoverable
|
|
skipWrapper
|
|
>
|
|
<TooltipHelper
|
|
style={{
|
|
gridColumn:
|
|
columns.length > 1
|
|
? `${columns.at(0)} / ${columns.at(-1)}`
|
|
: columns.at(0)!,
|
|
width: 8 * columns.length,
|
|
}}
|
|
data-test-id={`trace-timeline-tooltip-${currentColumn}`}
|
|
/>
|
|
</Tooltip>
|
|
</TimelineColumns>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
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.0;
|
|
}
|
|
50% {
|
|
transform: scale(0.1, 0.1);
|
|
opacity: 0.0;
|
|
}
|
|
70% {
|
|
opacity: 1.0;
|
|
}
|
|
100% {
|
|
transform: scale(1.2, 1.2);
|
|
opacity: 0.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;
|
|
`;
|