mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-01 12:20:38 +01:00
Compare commits
5 Commits
feat/noz-e
...
feat/trace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7991c321ff | ||
|
|
3385776cce | ||
|
|
f5286d69f6 | ||
|
|
bcbac9a15c | ||
|
|
43038c59b5 |
@@ -0,0 +1,9 @@
|
||||
// A single metadata cell: non-interactive, so keep the default arrow cursor.
|
||||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import styles from './EntityMetadataItem.module.scss';
|
||||
|
||||
interface EntityMetadataItemProps {
|
||||
tooltip: string;
|
||||
icon?: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function EntityMetadataItem({
|
||||
tooltip,
|
||||
icon,
|
||||
children,
|
||||
}: EntityMetadataItemProps): JSX.Element {
|
||||
return (
|
||||
<TooltipSimple title={tooltip}>
|
||||
<span className={styles.item}>
|
||||
{icon}
|
||||
<Typography.Text as="span">{children}</Typography.Text>
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
);
|
||||
}
|
||||
|
||||
EntityMetadataItem.defaultProps = {
|
||||
icon: null,
|
||||
};
|
||||
|
||||
export default EntityMetadataItem;
|
||||
@@ -0,0 +1,10 @@
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 16px;
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--l2-foreground);
|
||||
// Keep Typography.Text at the row's size (it reads this var, not inherited font-size).
|
||||
--typography-font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { CalendarClock, Server, Timer } from '@signozhq/icons';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import cx from 'classnames';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
|
||||
|
||||
import EntityMetadataItem from './EntityMetadataItem';
|
||||
|
||||
import styles from './EntityMetadataRow.module.scss';
|
||||
|
||||
interface EntityMetadataRowProps {
|
||||
entity: 'trace' | 'span';
|
||||
className?: string;
|
||||
service?: { name: string; entryPoint?: string };
|
||||
durationMs?: number;
|
||||
execTimePercent?: number;
|
||||
timestamp?: string;
|
||||
statusCode?: string | number;
|
||||
}
|
||||
|
||||
const ICON_SIZE = 14;
|
||||
|
||||
// Shared metadata row for the trace-details header and the span summary. Each
|
||||
// node renders only when its data is provided; hovering shows a tooltip naming
|
||||
// the value and keeps the default cursor. Interactive bits (e.g. linked spans)
|
||||
// are intentionally kept out of here and rendered by the caller.
|
||||
function EntityMetadataRow({
|
||||
entity,
|
||||
className,
|
||||
service,
|
||||
durationMs,
|
||||
execTimePercent,
|
||||
timestamp,
|
||||
statusCode,
|
||||
}: EntityMetadataRowProps): JSX.Element {
|
||||
const entityLabel = entity === 'trace' ? 'Trace' : 'Span';
|
||||
const durationTooltip =
|
||||
entity === 'trace' ? 'Trace Duration' : 'Span Duration';
|
||||
// Single source of duration formatting so both rows label units identically.
|
||||
const duration =
|
||||
durationMs != null
|
||||
? getYAxisFormattedValue(`${durationMs}`, 'ms')
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.row, className)}>
|
||||
{service && (
|
||||
<EntityMetadataItem
|
||||
tooltip="Root service and entry-point span"
|
||||
icon={<Server size={ICON_SIZE} />}
|
||||
>
|
||||
{service.name}
|
||||
{service.entryPoint && (
|
||||
<>
|
||||
{' — '}
|
||||
<Badge color="secondary" variant="outline">
|
||||
{service.entryPoint}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
</EntityMetadataItem>
|
||||
)}
|
||||
|
||||
{duration && (
|
||||
<EntityMetadataItem
|
||||
tooltip={durationTooltip}
|
||||
icon={<Timer size={ICON_SIZE} />}
|
||||
>
|
||||
{duration}
|
||||
{execTimePercent != null && (
|
||||
<>
|
||||
{' — '}
|
||||
<strong>{execTimePercent.toFixed(2)}%</strong>
|
||||
{' of total exec time'}
|
||||
</>
|
||||
)}
|
||||
</EntityMetadataItem>
|
||||
)}
|
||||
|
||||
{timestamp && (
|
||||
<EntityMetadataItem
|
||||
tooltip={`${entityLabel} start time`}
|
||||
icon={<CalendarClock size={ICON_SIZE} />}
|
||||
>
|
||||
{timestamp}
|
||||
</EntityMetadataItem>
|
||||
)}
|
||||
|
||||
{statusCode && (
|
||||
<EntityMetadataItem tooltip="Root span status code">
|
||||
<HttpStatusBadge statusCode={statusCode} />
|
||||
</EntityMetadataItem>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EntityMetadataRow.defaultProps = {
|
||||
className: undefined,
|
||||
service: undefined,
|
||||
durationMs: undefined,
|
||||
execTimePercent: undefined,
|
||||
timestamp: undefined,
|
||||
statusCode: undefined,
|
||||
};
|
||||
|
||||
export default EntityMetadataRow;
|
||||
@@ -1,4 +1,4 @@
|
||||
.root {
|
||||
.panel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -9,13 +9,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
.panelBody {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: none;
|
||||
background: var(--l1-background);
|
||||
font-size: 14px;
|
||||
@@ -33,14 +34,17 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Single scroll: when the summary sits above (non-docked modes) it scrolls away
|
||||
// and the tab section pins to the top; tab content scrolls inside `.tabsScroll`.
|
||||
.tabsSection {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-height: 100%;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
flex: 0 0 auto;
|
||||
height: 100%;
|
||||
|
||||
// TabsRoot — direct child of tabs-section
|
||||
> div {
|
||||
@@ -75,79 +79,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.spanRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.spanInfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.spanInfoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.highlightedOptions {
|
||||
padding: 8px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
|
||||
// KeyValueLabel uses a global `.key-value-label` root; constrain it
|
||||
// inside the two-column grid so values can ellipsize cleanly.
|
||||
:global(.key-value-label) {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.serviceDot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.statusMessageBadge {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.traceId {
|
||||
color: var(--accent-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.traceIdCopy {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
// Tooltip is rendered in a portal but the SpanDetailsPanel can be docked as a
|
||||
// FloatingPanel (z-index 999), which would otherwise sit on top of the default
|
||||
// tooltip (z-index 50). Bump the tooltip above the panel.
|
||||
|
||||
@@ -5,20 +5,11 @@ import {
|
||||
TabsRoot,
|
||||
TabsTrigger,
|
||||
} from '@signozhq/ui/tabs';
|
||||
import {
|
||||
Bookmark,
|
||||
CalendarClock,
|
||||
ChartColumnBig,
|
||||
Link2,
|
||||
List,
|
||||
ScrollText,
|
||||
Timer,
|
||||
} from '@signozhq/icons';
|
||||
import { Bookmark, ChartColumnBig, List, ScrollText } from '@signozhq/icons';
|
||||
import { Skeleton } from 'antd';
|
||||
import { DetailsHeader, DetailsPanelDrawer } from 'components/DetailsPanel';
|
||||
import { HeaderAction } from 'components/DetailsPanel/DetailsHeader/DetailsHeader';
|
||||
import { DetailsPanelState } from 'components/DetailsPanel/types';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
@@ -41,14 +32,13 @@ import {
|
||||
} from 'pages/TraceDetailsV3/utils';
|
||||
import { DataViewer } from 'periscope/components/DataViewer';
|
||||
import { FloatingPanel } from 'periscope/components/FloatingPanel';
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { getLeafKeyFromPath } from 'periscope/components/PrettyView/utils';
|
||||
import { useMeasure } from 'react-use';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { HIGHLIGHTED_OPTIONS } from './config';
|
||||
import {
|
||||
// KEY_ATTRIBUTE_KEYS, // uncomment when key attributes section is re-enabled
|
||||
SpanDetailVariant,
|
||||
@@ -57,17 +47,10 @@ import {
|
||||
import DockModeSwitcher from './DockModeSwitcher';
|
||||
import { useSpanAttributeActions } from './hooks/useSpanAttributeActions';
|
||||
import { useTracePinnedFields } from './hooks/useTracePinnedFields';
|
||||
import {
|
||||
LinkedSpansPanel,
|
||||
LinkedSpansToggle,
|
||||
useLinkedSpans,
|
||||
} from './LinkedSpans/LinkedSpans';
|
||||
import SpanPercentileBadge from './SpanPercentile/SpanPercentileBadge';
|
||||
import SpanPercentilePanel from './SpanPercentile/SpanPercentilePanel';
|
||||
import useSpanPercentile from './SpanPercentile/useSpanPercentile';
|
||||
import Events from './Events/Events';
|
||||
import SpanLogs from './SpanLogs/SpanLogs';
|
||||
import { useSpanContextLogs } from './SpanLogs/useSpanContextLogs';
|
||||
import SpanSummary from './SpanSummary';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
|
||||
@@ -80,6 +63,10 @@ interface SpanDetailsPanelProps {
|
||||
traceEndTime?: number;
|
||||
}
|
||||
|
||||
// At/above this panel width the summary moves inside the Overview tab (bottom
|
||||
// dock, or a floating/right panel widened to match). ~right-dock max width.
|
||||
const WIDE_PANEL_BREAKPOINT = 720;
|
||||
|
||||
function SpanDetailsContent({
|
||||
selectedSpan,
|
||||
traceStartTime,
|
||||
@@ -90,6 +77,7 @@ function SpanDetailsContent({
|
||||
traceEndTime?: number;
|
||||
}): JSX.Element {
|
||||
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
|
||||
const [bodyRef, { width: bodyWidth }] = useMeasure<HTMLDivElement>();
|
||||
const spanAttributeActions = useSpanAttributeActions();
|
||||
const logTraceEvent = useTraceDetailLogEvent('v3', selectedSpan.trace_id);
|
||||
const handleTabChange = useCallback(
|
||||
@@ -101,8 +89,6 @@ function SpanDetailsContent({
|
||||
},
|
||||
[logTraceEvent, selectedSpan.span_id],
|
||||
);
|
||||
const percentile = useSpanPercentile(selectedSpan);
|
||||
const linkedSpans = useLinkedSpans((selectedSpan as any).references);
|
||||
|
||||
// One-time conversion of any V2-format value still living in the
|
||||
// `span_details_pinned_attributes` user pref into V3 nested-path format.
|
||||
@@ -281,113 +267,20 @@ function SpanDetailsContent({
|
||||
// .map((key) => ({ key, value: allAttrs[key] }));
|
||||
// }, [selectedSpan]);
|
||||
|
||||
// Width-driven: when the panel is wide, the summary moves inside the Overview
|
||||
// tab; when narrow it stays above the tabs.
|
||||
const isWide = bodyWidth >= WIDE_PANEL_BREAKPOINT;
|
||||
const summary = (
|
||||
<SpanSummary
|
||||
selectedSpan={selectedSpan}
|
||||
traceStartTime={traceStartTime}
|
||||
traceEndTime={traceEndTime}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.detailsSection}>
|
||||
<div className={styles.spanRow}>
|
||||
<KeyValueLabel
|
||||
badgeKey="Span name"
|
||||
badgeValue={selectedSpan.name}
|
||||
maxCharacters={50}
|
||||
/>
|
||||
<SpanPercentileBadge
|
||||
loading={percentile.loading}
|
||||
percentileValue={percentile.percentileValue}
|
||||
duration={percentile.duration}
|
||||
spanPercentileData={percentile.spanPercentileData}
|
||||
isOpen={percentile.isOpen}
|
||||
toggleOpen={percentile.toggleOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SpanPercentilePanel selectedSpan={selectedSpan} percentile={percentile} />
|
||||
|
||||
{/* Span info: exec time + start time */}
|
||||
<div className={styles.spanInfo}>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<Timer size={14} />
|
||||
<span>
|
||||
{getYAxisFormattedValue(`${selectedSpan.duration_nano / 1000000}`, 'ms')}
|
||||
{traceStartTime && traceEndTime && traceEndTime > traceStartTime && (
|
||||
<>
|
||||
{' — '}
|
||||
<strong>
|
||||
{(
|
||||
(selectedSpan.duration_nano * 100) /
|
||||
((traceEndTime - traceStartTime) * 1e6)
|
||||
).toFixed(2)}
|
||||
%
|
||||
</strong>
|
||||
{' of total exec time'}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<CalendarClock size={14} />
|
||||
<span>
|
||||
{dayjs(selectedSpan.timestamp).format('HH:mm:ss — MMM D, YYYY')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.spanInfoItem}>
|
||||
<Link2 size={14} />
|
||||
<LinkedSpansToggle
|
||||
count={linkedSpans.count}
|
||||
isOpen={linkedSpans.isOpen}
|
||||
toggleOpen={linkedSpans.toggleOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LinkedSpansPanel
|
||||
linkedSpans={linkedSpans.linkedSpans}
|
||||
isOpen={linkedSpans.isOpen}
|
||||
/>
|
||||
|
||||
{/* Step 6: HighlightedOptions */}
|
||||
<div className={styles.highlightedOptions}>
|
||||
{HIGHLIGHTED_OPTIONS.map((option) => {
|
||||
const rendered = option.render(selectedSpan);
|
||||
if (!rendered) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<KeyValueLabel
|
||||
key={option.key}
|
||||
badgeKey={option.label}
|
||||
badgeValue={rendered}
|
||||
direction="column"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Step 7: KeyAttributes — commented out, pinning in PrettyView covers this.
|
||||
{keyAttributes.length > 0 && (
|
||||
<div className="span-details-panel__key-attributes">
|
||||
<div className="span-details-panel__key-attributes-label">
|
||||
KEY ATTRIBUTES
|
||||
</div>
|
||||
<div className="span-details-panel__key-attributes-chips">
|
||||
{keyAttributes.map(({ key, value }) => (
|
||||
<ActionMenu
|
||||
key={key}
|
||||
items={buildKeyAttrMenu(key, value)}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<div>
|
||||
<KeyValueLabel badgeKey={key} badgeValue={value} />
|
||||
</div>
|
||||
</ActionMenu>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
*/}
|
||||
|
||||
{/* Step 8: MiniTraceContext */}
|
||||
</div>
|
||||
<div className={styles.panelBody} ref={bodyRef}>
|
||||
{!isWide && <div className={styles.detailsSection}>{summary}</div>}
|
||||
|
||||
<div className={styles.tabsSection}>
|
||||
{/* Step 9: ContentTabs */}
|
||||
@@ -411,6 +304,7 @@ function SpanDetailsContent({
|
||||
|
||||
<div className={styles.tabsScroll}>
|
||||
<TabsContent value="overview">
|
||||
{isWide && summary}
|
||||
<DataViewer
|
||||
data={spanDisplayData}
|
||||
drawerKey="trace-details"
|
||||
@@ -535,7 +429,7 @@ function SpanDetailsPanel({
|
||||
traceEndTime={traceEndTime}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.panelBody}>
|
||||
<Skeleton active paragraph={{ rows: 6 }} title={{ width: '60%' }} />
|
||||
</div>
|
||||
)}
|
||||
@@ -546,7 +440,7 @@ function SpanDetailsPanel({
|
||||
variant === SpanDetailVariant.DOCKED ||
|
||||
variant === SpanDetailVariant.DOCKED_RIGHT
|
||||
) {
|
||||
return <div className={styles.root}>{content}</div>;
|
||||
return <div className={styles.panel}>{content}</div>;
|
||||
}
|
||||
|
||||
if (variant === SpanDetailVariant.DRAWER) {
|
||||
@@ -554,7 +448,7 @@ function SpanDetailsPanel({
|
||||
<DetailsPanelDrawer
|
||||
isOpen={panelState.isOpen}
|
||||
onClose={panelState.close}
|
||||
className={styles.root}
|
||||
className={styles.panel}
|
||||
>
|
||||
{content}
|
||||
</DetailsPanelDrawer>
|
||||
@@ -564,7 +458,7 @@ function SpanDetailsPanel({
|
||||
return (
|
||||
<FloatingPanel
|
||||
isOpen={panelState.isOpen}
|
||||
className={styles.root}
|
||||
className={styles.panel}
|
||||
width={PANEL_WIDTH}
|
||||
minWidth={480}
|
||||
height={window.innerHeight - PANEL_MARGIN_TOP - PANEL_MARGIN_BOTTOM}
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
filter: drop-shadow(2px 4px 16px rgba(0, 0, 0, 0.2));
|
||||
backdrop-filter: blur(20px);
|
||||
margin: 8px 16px;
|
||||
margin: 8px 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -16,7 +17,7 @@
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -162,20 +163,20 @@
|
||||
|
||||
.resourceSelector {
|
||||
overflow: hidden;
|
||||
width: calc(100% + 16px);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: -8px;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.resourceSelectorHeader {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.resourceSelectorInput {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
.spanRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// Metadata and the linked-spans line share one column so every line has the
|
||||
// same vertical gap and line-height as the rows inside MetadataRow.
|
||||
.spanMetaGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
--typography-line-height: var(--line-height-20);
|
||||
}
|
||||
|
||||
.spanInfoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: var(--periscope-font-size-base);
|
||||
line-height: var(--line-height-20);
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.highlightedOptions {
|
||||
padding: 8px 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
|
||||
// Constrain KeyValueLabel inside the grid so values can ellipsize cleanly.
|
||||
:global(.key-value-label) {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.serviceDot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Badges stay fit-content so short values shrink and long ones truncate.
|
||||
.serviceBadge,
|
||||
.statusMessageBadge {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Truncating text inside a badge (service name, status message).
|
||||
.badgeEllipsisText {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Link2 } from '@signozhq/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import EntityMetadataRow from '../EntityMetadata/EntityMetadataRow';
|
||||
import { HIGHLIGHTED_OPTIONS } from './config';
|
||||
import {
|
||||
LinkedSpansPanel,
|
||||
LinkedSpansToggle,
|
||||
useLinkedSpans,
|
||||
} from './LinkedSpans/LinkedSpans';
|
||||
import SpanPercentileBadge from './SpanPercentile/SpanPercentileBadge';
|
||||
import SpanPercentilePanel from './SpanPercentile/SpanPercentilePanel';
|
||||
import useSpanPercentile from './SpanPercentile/useSpanPercentile';
|
||||
|
||||
import styles from './SpanSummary.module.scss';
|
||||
|
||||
interface SpanSummaryProps {
|
||||
selectedSpan: SpanV3;
|
||||
traceStartTime?: number;
|
||||
traceEndTime?: number;
|
||||
}
|
||||
|
||||
// Summary block shown above (narrow) / beside (wide) the tabs: span name +
|
||||
// percentile, exec time / timestamp / linked spans, and the highlighted options.
|
||||
function SpanSummary({
|
||||
selectedSpan,
|
||||
traceStartTime,
|
||||
traceEndTime,
|
||||
}: SpanSummaryProps): JSX.Element {
|
||||
const percentile = useSpanPercentile(selectedSpan);
|
||||
const linkedSpans = useLinkedSpans((selectedSpan as any).references);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.spanRow}>
|
||||
<KeyValueLabel
|
||||
badgeKey="Span name"
|
||||
badgeValue={selectedSpan.name}
|
||||
maxCharacters={50}
|
||||
/>
|
||||
<SpanPercentileBadge
|
||||
loading={percentile.loading}
|
||||
percentileValue={percentile.percentileValue}
|
||||
duration={percentile.duration}
|
||||
spanPercentileData={percentile.spanPercentileData}
|
||||
isOpen={percentile.isOpen}
|
||||
toggleOpen={percentile.toggleOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SpanPercentilePanel selectedSpan={selectedSpan} percentile={percentile} />
|
||||
|
||||
<div className={styles.spanMetaGroup}>
|
||||
<EntityMetadataRow
|
||||
entity="span"
|
||||
durationMs={selectedSpan.duration_nano / 1000000}
|
||||
execTimePercent={
|
||||
traceStartTime && traceEndTime && traceEndTime > traceStartTime
|
||||
? (selectedSpan.duration_nano * 100) /
|
||||
((traceEndTime - traceStartTime) * 1e6)
|
||||
: undefined
|
||||
}
|
||||
timestamp={dayjs(selectedSpan.timestamp).format('HH:mm:ss — MMM D, YYYY')}
|
||||
/>
|
||||
|
||||
<div className={styles.spanInfoItem}>
|
||||
<Link2 size={14} />
|
||||
<LinkedSpansToggle
|
||||
count={linkedSpans.count}
|
||||
isOpen={linkedSpans.isOpen}
|
||||
toggleOpen={linkedSpans.toggleOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LinkedSpansPanel
|
||||
linkedSpans={linkedSpans.linkedSpans}
|
||||
isOpen={linkedSpans.isOpen}
|
||||
/>
|
||||
|
||||
<div className={styles.highlightedOptions}>
|
||||
{HIGHLIGHTED_OPTIONS.map((option) => {
|
||||
const rendered = option.render(selectedSpan);
|
||||
if (!rendered) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<KeyValueLabel
|
||||
key={option.key}
|
||||
badgeKey={option.label}
|
||||
badgeValue={rendered}
|
||||
direction="column"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpanSummary;
|
||||
@@ -0,0 +1,18 @@
|
||||
.traceId {
|
||||
color: var(--accent-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.traceIdCopy {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { toast } from '@signozhq/ui/sonner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
import styles from './TraceIdField.module.scss';
|
||||
|
||||
interface TraceIdFieldProps {
|
||||
span: SpanV3;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import ExpandableValue from 'periscope/components/ExpandableValue';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
import styles from './SpanSummary.module.scss';
|
||||
import { TraceIdField } from './TraceIdField';
|
||||
|
||||
interface HighlightedOption {
|
||||
@@ -18,9 +18,11 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
|
||||
label: 'SERVICE',
|
||||
render: (span): ReactNode | null =>
|
||||
span['service.name'] ? (
|
||||
<Badge color="vanilla">
|
||||
<Badge color="vanilla" className={styles.serviceBadge}>
|
||||
<span className={styles.serviceDot} />
|
||||
{span['service.name']}
|
||||
<span className={styles.badgeEllipsisText} title={span['service.name']}>
|
||||
{span['service.name']}
|
||||
</span>
|
||||
</Badge>
|
||||
) : null,
|
||||
},
|
||||
@@ -50,12 +52,8 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
|
||||
render: (span): ReactNode | null =>
|
||||
span.status_message ? (
|
||||
<ExpandableValue value={span.status_message} title="Status message">
|
||||
<Badge
|
||||
color="vanilla"
|
||||
textEllipsis="end"
|
||||
className={styles.statusMessageBadge}
|
||||
>
|
||||
{span.status_message}
|
||||
<Badge color="vanilla" className={styles.statusMessageBadge}>
|
||||
<span className={styles.badgeEllipsisText}>{span.status_message}</span>
|
||||
</Badge>
|
||||
</ExpandableValue>
|
||||
) : null,
|
||||
|
||||
@@ -61,25 +61,6 @@
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.subItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.entryPointBadge {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
:global(.ant-skeleton-input) {
|
||||
width: 160px !important;
|
||||
|
||||
@@ -10,18 +10,10 @@ import {
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import FieldsSelector from 'components/FieldsSelector';
|
||||
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import history, { hasInAppHistory } from 'lib/history';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CalendarClock,
|
||||
ChartPie,
|
||||
Server,
|
||||
Timer,
|
||||
} from '@signozhq/icons';
|
||||
import { ArrowLeft, ChartPie } from '@signozhq/icons';
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { TraceDetailV3URLProps } from 'types/api/trace/getTraceV3';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -29,6 +21,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { TraceDetailEventKeys, TraceDetailEvents } from '../events';
|
||||
import { useTraceDetailLogEvent } from '../hooks/useTraceDetailLogEvent';
|
||||
import { useTraceStore } from '../stores/traceStore';
|
||||
import EntityMetadataRow from '../EntityMetadata/EntityMetadataRow';
|
||||
import AnalyticsPanel from '../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel';
|
||||
import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters';
|
||||
import TraceOptionsMenu from './TraceOptionsMenu';
|
||||
@@ -122,8 +115,6 @@ function TraceDetailsHeader({
|
||||
const durationMs = traceMetadata
|
||||
? traceMetadata.endTimestampMillis - traceMetadata.startTimestampMillis
|
||||
: 0;
|
||||
const { time: formattedDuration, timeUnitName } =
|
||||
convertTimeToRelevantUnit(durationMs);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
@@ -200,29 +191,18 @@ function TraceDetailsHeader({
|
||||
{showTraceDetails && (
|
||||
<div className={styles.subHeader}>
|
||||
{traceMetadata ? (
|
||||
<>
|
||||
<span className={styles.subItem}>
|
||||
<Server size={13} />
|
||||
{traceMetadata.rootServiceName}
|
||||
<span className={styles.separator}>—</span>
|
||||
<span className={styles.entryPointBadge}>
|
||||
{traceMetadata.rootServiceEntryPoint}
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.subItem}>
|
||||
<Timer size={13} />
|
||||
{parseFloat(formattedDuration.toFixed(2))} {timeUnitName}
|
||||
</span>
|
||||
<span className={styles.subItem}>
|
||||
<CalendarClock size={13} />
|
||||
{dayjs(traceMetadata.startTimestampMillis).format(
|
||||
DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS,
|
||||
)}
|
||||
</span>
|
||||
{traceMetadata.rootSpanStatusCode && (
|
||||
<HttpStatusBadge statusCode={traceMetadata.rootSpanStatusCode} />
|
||||
<EntityMetadataRow
|
||||
entity="trace"
|
||||
service={{
|
||||
name: traceMetadata.rootServiceName,
|
||||
entryPoint: traceMetadata.rootServiceEntryPoint,
|
||||
}}
|
||||
durationMs={durationMs}
|
||||
timestamp={dayjs(traceMetadata.startTimestampMillis).format(
|
||||
DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS,
|
||||
)}
|
||||
</>
|
||||
statusCode={traceMetadata.rootSpanStatusCode}
|
||||
/>
|
||||
) : (
|
||||
<DetailsLoader />
|
||||
)}
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
.rightDock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid var(--l2-border);
|
||||
min-width: 0;
|
||||
border-top: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
// Shared Ant Collapse chrome reset for both the flamegraph and waterfall
|
||||
|
||||
Reference in New Issue
Block a user