feat: add support for span hover card in trace details v2 (#8930)

* feat: add support for span hover card in trace details v2

* chore: remove the unnecessary tooltip

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
This commit is contained in:
Shaheer Kochai 2025-09-10 09:01:57 +04:30 committed by GitHub
parent 011b769d4d
commit f91115948a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 364 additions and 148 deletions

View File

@ -0,0 +1,108 @@
.span-hover-card {
width: 206px;
.ant-popover-inner {
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.32) 0%,
rgba(18, 19, 23, 0.36) 98.68%
);
padding: 12px 16px;
border: 1px solid var(--bg-slate-500);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.32) 0%,
rgba(18, 19, 23, 0.36) 98.68%
);
backdrop-filter: blur(20px);
border-radius: 4px;
z-index: -1;
will-change: background-color, backdrop-filter;
}
}
&__title {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-bottom: 0.5rem;
}
&__operation {
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
line-height: 20px;
letter-spacing: 0.48px;
}
&__service {
font-size: 0.875rem;
color: var(--bg-vanilla-400);
font-weight: 400;
}
&__error {
font-size: 0.75rem;
color: var(--bg-cherry-500);
font-weight: 500;
}
&__row {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 174px;
margin-top: 8px;
}
&__label {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
&__value {
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
line-height: 20px;
text-align: right;
}
&__relative-time {
display: flex;
align-items: center;
margin-top: 4px;
gap: 8px;
border-radius: 1px 0 0 1px;
background: linear-gradient(
90deg,
hsla(358, 75%, 59%, 0.2) 0%,
rgba(229, 72, 77, 0) 100%
);
&-icon {
width: 2px;
height: 20px;
flex-shrink: 0;
border-radius: 2px;
background: var(--bg-cherry-500);
}
}
&__relative-text {
color: var(--bg-cherry-300);
font-size: 12px;
line-height: 20px;
}
}

View File

@ -0,0 +1,101 @@
import './SpanHoverCard.styles.scss';
import { Popover, Typography } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { ReactNode } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { toFixed } from 'utils/toFixed';
interface ITraceMetadata {
startTime: number;
endTime: number;
}
interface SpanHoverCardProps {
span: Span;
traceMetadata: ITraceMetadata;
children: ReactNode;
}
function SpanHoverCard({
span,
traceMetadata,
children,
}: SpanHoverCardProps): JSX.Element {
const duration = span.durationNano / 1e6; // Convert nanoseconds to milliseconds
const { time: formattedDuration, timeUnitName } = convertTimeToRelevantUnit(
duration,
);
// Calculate relative start time from trace start
const relativeStartTime = span.timestamp - traceMetadata.startTime;
const {
time: relativeTime,
timeUnitName: relativeTimeUnit,
} = convertTimeToRelevantUnit(relativeStartTime);
// Format absolute start time
const startTimeFormatted = dayjs(span.timestamp).format(
DATE_TIME_FORMATS.SPAN_POPOVER_DATE,
);
const getContent = (): JSX.Element => (
<div className="span-hover-card">
<div className="span-hover-card__row">
<Typography.Text className="span-hover-card__label">
Duration:
</Typography.Text>
<Typography.Text className="span-hover-card__value">
{toFixed(formattedDuration, 2)}
{timeUnitName}
</Typography.Text>
</div>
<div className="span-hover-card__row">
<Typography.Text className="span-hover-card__label">
Events:
</Typography.Text>
<Typography.Text className="span-hover-card__value">
{span.event?.length || 0}
</Typography.Text>
</div>
<div className="span-hover-card__row">
<Typography.Text className="span-hover-card__label">
Start time:
</Typography.Text>
<Typography.Text className="span-hover-card__value">
{startTimeFormatted}
</Typography.Text>
</div>
<div className="span-hover-card__relative-time">
<div className="span-hover-card__relative-time-icon" />
<Typography.Text className="span-hover-card__relative-text">
{toFixed(relativeTime, 2)}
{relativeTimeUnit} after trace start
</Typography.Text>
</div>
</div>
);
return (
<Popover
title={
<div className="span-hover-card__title">
<Typography.Text className="span-hover-card__operation">
{span.name}
</Typography.Text>
</div>
}
content={getContent()}
trigger="hover"
rootClassName="span-hover-card"
autoAdjustOverflow
arrow={false}
>
{children}
</Popover>
);
}
export default SpanHoverCard;

View File

@ -29,6 +29,7 @@ export const DATE_TIME_FORMATS = {
DATE_SHORT: 'MM/DD', DATE_SHORT: 'MM/DD',
YEAR_SHORT: 'YY', YEAR_SHORT: 'YY',
YEAR_MONTH: 'YY-MM', YEAR_MONTH: 'YY-MM',
SPAN_POPOVER_DATE: 'M/D/YY - HH:mm',
// Month name formats // Month name formats
MONTH_DATE_FULL: 'MMMM DD, YYYY', MONTH_DATE_FULL: 'MMMM DD, YYYY',

View File

@ -7,6 +7,7 @@ import { Virtualizer } from '@tanstack/react-virtual';
import { Button, Tooltip, Typography } from 'antd'; import { Button, Tooltip, Typography } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge'; import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import SpanHoverCard from 'components/SpanHoverCard/SpanHoverCard';
import { TableV3 } from 'components/TableV3/TableV3'; import { TableV3 } from 'components/TableV3/TableV3';
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
@ -66,6 +67,7 @@ function SpanOverview({
setSelectedSpan, setSelectedSpan,
handleAddSpanToFunnel, handleAddSpanToFunnel,
selectedSpan, selectedSpan,
traceMetadata,
}: { }: {
span: Span; span: Span;
isSpanCollapsed: boolean; isSpanCollapsed: boolean;
@ -73,6 +75,7 @@ function SpanOverview({
selectedSpan: Span | undefined; selectedSpan: Span | undefined;
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>; setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
handleAddSpanToFunnel: (span: Span) => void; handleAddSpanToFunnel: (span: Span) => void;
traceMetadata: ITraceMetadata;
}): JSX.Element { }): JSX.Element {
const isRootSpan = span.level === 0; const isRootSpan = span.level === 0;
const { hasEditPermission } = useAppContext(); const { hasEditPermission } = useAppContext();
@ -83,6 +86,7 @@ function SpanOverview({
} }
return ( return (
<SpanHoverCard span={span} traceMetadata={traceMetadata}>
<div <div
className={cx( className={cx(
'span-overview', 'span-overview',
@ -186,6 +190,7 @@ function SpanOverview({
</section> </section>
</div> </div>
</div> </div>
</SpanHoverCard>
); );
} }
@ -249,6 +254,7 @@ export function SpanDuration({
}, [leftOffset, width, color]); }, [leftOffset, width, color]);
return ( return (
<SpanHoverCard span={span} traceMetadata={traceMetadata}>
<div <div
className={cx( className={cx(
'span-duration', 'span-duration',
@ -299,14 +305,13 @@ export function SpanDuration({
})} })}
</div> </div>
{hasActionButtons && <SpanLineActionButtons span={span} />} {hasActionButtons && <SpanLineActionButtons span={span} />}
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
<Typography.Text <Typography.Text
className="span-line-text" className="span-line-text"
ellipsis ellipsis
style={textStyle} style={textStyle}
>{`${toFixed(time, 2)} ${timeUnitName}`}</Typography.Text> >{`${toFixed(time, 2)} ${timeUnitName}`}</Typography.Text>
</Tooltip>
</div> </div>
</SpanHoverCard>
); );
} }
@ -341,6 +346,7 @@ function getWaterfallColumns({
selectedSpan={selectedSpan} selectedSpan={selectedSpan}
setSelectedSpan={setSelectedSpan} setSelectedSpan={setSelectedSpan}
handleAddSpanToFunnel={handleAddSpanToFunnel} handleAddSpanToFunnel={handleAddSpanToFunnel}
traceMetadata={traceMetadata}
/> />
), ),
size: 450, size: 450,