Compare commits

...

8 Commits

7 changed files with 137 additions and 99 deletions

View File

@@ -0,0 +1,25 @@
.container {
// Gutter matches the header/subHeader 16px; bottom gap before the panels.
padding: 0 16px 12px;
}
.title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
}
.link {
display: inline-flex;
align-items: center;
gap: 4px;
color: inherit;
font-weight: 500;
white-space: nowrap;
&:hover {
opacity: 0.85;
}
}

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { Callout } from '@signozhq/ui/callout';
import { ArrowUpRight } from '@signozhq/icons';
import styles from './MissingSpansBanner.module.scss';
const MISSING_SPANS_DOCS_URL =
'https://signoz.io/docs/userguide/traces/#missing-spans';
function MissingSpansBanner(): JSX.Element | null {
// Session-only dismissal — not persisted, so the banner returns on reload.
const [isDismissed, setIsDismissed] = useState(false);
if (isDismissed) {
return null;
}
// Wrapper owns the gutter: Callout is width:100%, so putting the gutter as a
// margin on it would overflow the parent by the margin width. Pad instead.
return (
<div className={styles.container}>
<Callout
type="info"
size="small"
showIcon
action="dismissible"
onClick={(): void => setIsDismissed(true)}
testId="missing-spans-banner"
title={
<span className={styles.title}>
This trace has missing spans
<a
className={styles.link}
href={MISSING_SPANS_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
>
Learn More <ArrowUpRight size={14} />
</a>
</span>
}
/>
</div>
);
}
export default MissingSpansBanner;

View File

@@ -31,6 +31,7 @@ import { useTraceDetailLogEvent } from '../hooks/useTraceDetailLogEvent';
import { useTraceStore } from '../stores/traceStore';
import AnalyticsPanel from '../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel';
import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters';
import MissingSpansBanner from './MissingSpansBanner';
import TraceOptionsMenu from './TraceOptionsMenu';
import styles from './TraceDetailsHeader.module.scss';
@@ -48,6 +49,7 @@ export interface TraceMetadataForHeader {
rootServiceName: string;
rootServiceEntryPoint: string;
rootSpanStatusCode: string;
hasMissingSpans: boolean;
}
interface TraceDetailsHeaderProps {
@@ -229,6 +231,8 @@ function TraceDetailsHeader({
</div>
)}
{traceMetadata?.hasMissingSpans && <MissingSpansBanner />}
<FieldsSelector
isOpen={isPreviewFieldsOpen}
title="Preview fields"

View File

@@ -41,6 +41,7 @@
:global(.ant-collapse-header) {
border-top: 1px solid var(--l2-border);
border-bottom: 1px solid var(--l2-border);
border-radius: 0 !important;
}
:global(.ant-collapse-content) {
@@ -98,6 +99,13 @@
flex-direction: column;
overflow: hidden;
// The flamegraph's ResizableBox above renders a 1px resize handle at its
// bottom edge; drop the header's own top border so the two don't stack
// into a double border at the flamegraph/waterfall juncture.
:global(.ant-collapse-header) {
border-top: none;
}
:global(.ant-collapse-item) {
flex: 1;
display: flex;

View File

@@ -49,59 +49,6 @@
flex-direction: column;
}
.missingSpans {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
margin: 16px;
padding: 12px;
border-radius: 4px;
background: rgba(69, 104, 220, 0.1);
}
.leftInfo {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
.text {
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
.rightInfo {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
&:hover {
background-color: unset;
color: var(--bg-robin-200);
}
}
.splitPanel {
flex: 1;
min-height: 0;
@@ -216,10 +163,12 @@
.treeRow:hover,
.treeRow.hoveredSpan {
border-radius: 4px;
// Left end of the row band — round only the outer (left) corners so the
// highlight joins the status + timeline segments into one continuous band.
border-radius: 4px 0 0 4px;
background: color-mix(
in srgb,
var(--l3-background) 20%,
var(--l3-background) 60%,
transparent
) !important;
@@ -262,20 +211,22 @@
--badge-border-width: 0px;
&.hoveredSpan {
border-radius: 4px;
// Middle segment of the row band — square so it butts up against the
// name and timeline segments (no rounded corner at the badge column).
border-radius: 0;
background: color-mix(
in srgb,
var(--l3-background) 20%,
var(--l3-background) 60%,
transparent
) !important;
}
&.isInterested,
&.isSelectedNonMatching {
border-radius: 4px;
border-radius: 0;
background: color-mix(
in srgb,
var(--l3-background) 40%,
var(--l3-background) 80%,
transparent
) !important;
}
@@ -309,20 +260,21 @@
&:hover,
&.hoveredSpan {
border-radius: 4px;
// Right end of the row band — round only the outer (right) corners.
border-radius: 0 4px 4px 0;
background: color-mix(
in srgb,
var(--l3-background) 20%,
var(--l3-background) 60%,
transparent
) !important;
}
&:has(.isInterested),
&:has(.isSelectedNonMatching) {
border-radius: 4px;
border-radius: 0 4px 4px 0;
background: color-mix(
in srgb,
var(--l3-background) 40%,
var(--l3-background) 80%,
transparent
) !important;
}
@@ -345,10 +297,11 @@
&.isInterested,
&.isSelectedNonMatching {
border-radius: 4px;
// Left end of the row band — outer (left) corners only.
border-radius: 4px 0 0 4px;
background: color-mix(
in srgb,
var(--l3-background) 40%,
var(--l3-background) 80%,
transparent
) !important;
}
@@ -471,7 +424,7 @@
padding-left: 8px;
flex-shrink: 0;
height: 100%;
background: linear-gradient(to left, var(--l1-background) 60%, transparent);
background: linear-gradient(to left, var(--l2-background) 40%, transparent);
z-index: 2;
opacity: 0;
pointer-events: none;
@@ -599,6 +552,18 @@
opacity: 0.15;
}
// A dimmed span must still show the full-opacity hover state when hovered.
// These win over `.isDimmed` on specificity so brightness is restored across
// the whole row (name column, status cell, and timeline bar) on hover.
.treeRow:hover .isDimmed,
.treeRow.hoveredSpan .isDimmed,
.timelineRow:hover .isDimmed,
.timelineRow.hoveredSpan .isDimmed,
.statusCell:hover.isDimmed,
.statusCell.hoveredSpan.isDimmed {
opacity: 1;
}
.isHighlighted {
opacity: 1;
}

View File

@@ -33,14 +33,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { colorToRgb } from 'lib/uPlotLib/utils/generateColor';
import {
ArrowUpRight,
ChevronDown,
ChevronRight,
CircleAlert,
Link,
ListPlus,
} from '@signozhq/icons';
import { ChevronDown, ChevronRight, Link, ListPlus } from '@signozhq/icons';
import { useTraceStore } from 'pages/TraceDetailsV3/stores/traceStore';
import { resolveSpanColor } from 'pages/TraceDetailsV3/utils';
import { useBoundaryPagination } from 'pages/TraceDetailsV3/TraceWaterfall/hooks/useBoundaryPagination';
@@ -854,28 +847,6 @@ function Success(props: ISuccessProps): JSX.Element {
return (
<div className={styles.root}>
{traceMetadata.hasMissingSpans && (
<div className={styles.missingSpans}>
<section className={styles.leftInfo}>
<CircleAlert size={14} />
<span className={styles.text}>This trace has missing spans</span>
</section>
<Button
variant="ghost"
color="secondary"
className={styles.rightInfo}
suffix={<ArrowUpRight size={14} />}
onClick={(): WindowProxy | null =>
window.open(
'https://signoz.io/docs/userguide/traces/#missing-spans',
'_blank',
)
}
>
Learn More
</Button>
</div>
)}
{isFetching && <div className={styles.loadingBar} />}
<div className={styles.splitPanel} ref={scrollContainerRef}>
{/* Sticky header row */}
@@ -994,8 +965,8 @@ function Success(props: ISuccessProps): JSX.Element {
transform: `translateY(${virtualRow.start}px)`,
}}
data-span-id={span.span_id}
onMouseEnter={(): void => handleRowMouseEnter(span.span_id)}
onMouseLeave={handleRowMouseLeave}
onMouseEnter={(): void => applyHoverClass(span.span_id)}
onMouseLeave={(): void => applyHoverClass(null)}
onClick={(): void => handleSpanClick(span)}
>
{span.response_status_code && (

View File

@@ -1,7 +1,12 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ChartNoAxesGantt, TriangleAlert } from '@signozhq/icons';
import {
ChartNoAxesGantt,
ChevronDown,
ChevronRight,
TriangleAlert,
} from '@signozhq/icons';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { Collapse } from 'antd';
@@ -34,6 +39,16 @@ import cx from 'classnames';
import styles from './TraceDetailsV3.module.scss';
// Lucide chevrons for the flame/waterfall accordion headers, matching the
// span-tree chevrons in the waterfall.
function renderPanelExpandIcon({
isActive,
}: {
isActive?: boolean;
}): JSX.Element {
return isActive ? <ChevronDown size={14} /> : <ChevronRight size={14} />;
}
function TraceDetailsV3(): JSX.Element {
const { id: traceId } = useParams<TraceDetailV3URLProps>();
const urlQuery = useUrlQuery();
@@ -329,6 +344,7 @@ function TraceDetailsV3(): JSX.Element {
rootServiceName: payload.rootServiceName,
rootServiceEntryPoint: payload.rootServiceEntryPoint,
rootSpanStatusCode: rootSpan?.response_status_code || '',
hasMissingSpans: payload.hasMissingSpans || false,
};
}, [traceData?.payload]);
@@ -388,6 +404,7 @@ function TraceDetailsV3(): JSX.Element {
activeKey={activeKeys.filter((k) => k === 'flame')}
onChange={(): void => handleCollapseChange('flame')}
size="small"
expandIcon={renderPanelExpandIcon}
className={styles.flameCollapse}
items={[
{
@@ -442,6 +459,7 @@ function TraceDetailsV3(): JSX.Element {
activeKey={activeKeys.filter((k) => k === 'waterfall')}
onChange={(): void => handleCollapseChange('waterfall')}
size="small"
expandIcon={renderPanelExpandIcon}
className={cx(styles.waterfallCollapse, {
[styles.isDocked]: isWaterfallDocked,
})}