mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-01 20:30:37 +01:00
Compare commits
8 Commits
refactor/a
...
feat/water
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9836976eed | ||
|
|
c4431ba03d | ||
|
|
f5efb25f17 | ||
|
|
8d5e9cd804 | ||
|
|
5f31ea068a | ||
|
|
25e578f288 | ||
|
|
5240236ad3 | ||
|
|
7b0b1798db |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user