Compare commits

...

10 Commits

Author SHA1 Message Date
aks07
9836976eed fix(trace-waterfall): soften row-action button gradient 2026-07-01 18:57:18 +05:30
aks07
c4431ba03d fix(trace-waterfall): strengthen selected/hover highlight into one continuous band 2026-07-01 18:57:18 +05:30
aks07
f5efb25f17 fix(trace-waterfall): keep hovered rows at full opacity when dimmed 2026-07-01 18:57:18 +05:30
aks07
8d5e9cd804 feat(trace): use Lucide chevrons for accordion expand icons 2026-07-01 17:20:30 +05:30
aks07
5f31ea068a feat(trace): move missing-spans banner to header using Callout 2026-07-01 17:20:16 +05:30
aks07
25e578f288 fix(trace-waterfall): scope span hover-card to the span-name column 2026-07-01 17:19:29 +05:30
aks07
5240236ad3 fix(trace): square accordion header corners 2026-07-01 17:19:20 +05:30
aks07
7b0b1798db fix(trace): remove double border at flamegraph/waterfall juncture 2026-07-01 17:19:00 +05:30
Abhi kumar
13812fac62 fix(dashboard): pie panel collapses multi-column ClickHouse query to a single slice (#11919)
* fix(dashboard): pie panel collapses multi-column clickhouse scalar to one slice

A pie panel backed by a ClickHouse query with several aggregations
(e.g. `count() AS col1, sum() AS col2`) rendered a single slice labelled
with the query name and only the first value column's value; the other
value columns were silently dropped.

Root cause: the scalar response carries every value column in the scalar
table, but PiePanelWrapper read the legacy `data.result` time-series field
instead. For a scalar that field collapses to a single series that keeps
only the first value column, so the pie never saw the rest. This is the
pie counterpart of the table collapse fixed in #11794.

Fix: when the scalar table has more than one value column, build pie
slices from the scalar table under `newResult` (the same source the table
and value panels already use) — one slice per value column, group-by
columns become the label. Single-aggregation and grouped pies keep the
existing series path unchanged. Frontend-only, V1.

* fix: formatter datetime

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-07-01 05:15:47 +00:00
Vinicius Lourenço
df77b8d125 fix(settings): ensure scroll on tiny screens (#11916)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-06-30 18:45:47 +00:00
15 changed files with 488 additions and 134 deletions

View File

@@ -3,7 +3,6 @@
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
border-radius: 4px;
}

View File

@@ -680,6 +680,13 @@ describe('formatUniversalUnit', () => {
});
describe('Datetime', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2026-01-01T00:00:00Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('formats datetime units', () => {
expect(formatUniversalUnit(900, UniversalYAxisUnit.DATETIME_FROM_NOW)).toBe(
'56 years ago',

View File

@@ -1,9 +1,14 @@
@use '../../styles/scrollbar' as *;
.members-settings-page {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4) var(--padding-2) var(--padding-6) var(--padding-4);
height: 100%;
overflow-y: auto;
@include custom-scrollbar;
}
.members-settings {

View File

@@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Group } from '@visx/group';
import { Pie } from '@visx/shape';
@@ -8,12 +8,10 @@ import { themeColors } from 'constants/theme';
import { getPieChartClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import { useIsDarkMode } from 'hooks/useDarkMode';
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { isNaN } from 'lodash-es';
import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu';
import { PanelWrapperProps, TooltipData } from './panelWrapper.types';
import { preparePieChartData } from './preparePieChartData';
import { lightenColor, tooltipStyles } from './utils';
import './PiePanelWrapper.styles.scss';
@@ -44,37 +42,15 @@ function PiePanelWrapper({
detectBounds: true,
});
const panelData = queryResponse.data?.payload?.data?.result || [];
const isDarkMode = useIsDarkMode();
let pieChartData: {
label: string;
value: string;
color: string;
record: any;
}[] = [].concat(
...(panelData
.map((d) => {
const label = getLabelName(d.metric, d.queryName || '', d.legend || '');
return {
label,
value: d?.values?.[0]?.[1],
record: d,
color:
widget?.customLegendColors?.[label] ||
generateColor(
label,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
),
};
})
.filter((d) => d !== undefined) as never[]),
);
pieChartData = pieChartData.filter(
(arc) =>
arc.value && !isNaN(parseFloat(arc.value)) && parseFloat(arc.value) > 0,
const pieChartData = useMemo(
() =>
preparePieChartData(queryResponse.data?.payload, {
customLegendColors: widget?.customLegendColors,
colorMap: isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
}),
[queryResponse.data?.payload, widget?.customLegendColors, isDarkMode],
);
let size = 0;

View File

@@ -0,0 +1,185 @@
import { themeColors } from 'constants/theme';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
import { preparePieChartData } from '../preparePieChartData';
const options = { colorMap: themeColors.chartcolors };
/**
* Mirrors a query-range payload: the (possibly collapsed) time-series `result`
* plus the scalar table nested under `newResult` (as getQueryResults produces it).
*/
function makePayload(
result: QueryData[],
tables: QueryDataV3[],
): MetricRangePayloadProps {
return {
data: {
result,
resultType: 'scalar',
newResult: { data: { result: tables, resultType: 'scalar' } },
},
} as MetricRangePayloadProps;
}
function tableEntry(
columns: NonNullable<QueryDataV3['table']>['columns'],
rows: NonNullable<QueryDataV3['table']>['rows'],
overrides: Partial<QueryDataV3> = {},
): QueryDataV3 {
return {
queryName: 'A',
legend: '',
series: null,
list: null,
table: { columns, rows },
...overrides,
} as QueryDataV3;
}
describe('preparePieChartData', () => {
it('renders a slice per value column for a multi-column ClickHouse scalar', () => {
// SELECT count() AS col1, sum(value) AS col2 — the backend collapses the
// time-series result onto col1; the full data lives in the scalar table.
const payload = makePayload(
[
{
metric: {},
queryName: 'A',
legend: '',
values: [[0, '23399927']],
} as QueryData,
],
[
tableEntry(
[
{ name: 'col1', queryName: 'A', isValueColumn: true, id: 'col1' },
{ name: 'col2', queryName: 'A', isValueColumn: true, id: 'col2' },
],
[{ data: { col1: 23399927, col2: 588691297 } }],
),
],
);
const slices = preparePieChartData(payload, options);
expect(slices).toHaveLength(2);
expect(slices.map((s) => [s.label, s.value])).toStrictEqual([
['col1', '23399927'],
['col2', '588691297'],
]);
});
it('prefixes the group when multiple value columns are grouped', () => {
const payload = makePayload(
[],
[
tableEntry(
[
{ name: 'env', queryName: 'A', isValueColumn: false, id: 'env' },
{ name: 'col1', queryName: 'A', isValueColumn: true, id: 'col1' },
{ name: 'col2', queryName: 'A', isValueColumn: true, id: 'col2' },
],
[{ data: { env: 'prod', col1: 10, col2: 20 } }],
),
],
);
const slices = preparePieChartData(payload, options);
expect(slices.map((s) => s.label)).toStrictEqual([
'prod · col1',
'prod · col2',
]);
expect(slices[0].record.metric).toStrictEqual({ env: 'prod' });
});
it('drops non-positive and non-numeric values', () => {
const payload = makePayload(
[],
[
tableEntry(
[
{ name: 'col1', queryName: 'A', isValueColumn: true, id: 'col1' },
{ name: 'col2', queryName: 'A', isValueColumn: true, id: 'col2' },
{ name: 'col3', queryName: 'A', isValueColumn: true, id: 'col3' },
],
[{ data: { col1: 5, col2: 0, col3: 'n/a' } }],
),
],
);
const slices = preparePieChartData(payload, options);
expect(slices.map((s) => s.label)).toStrictEqual(['col1']);
});
it('keeps the series path for a single value column (grouped panel)', () => {
// One value column → the time-series result is authoritative (one slice per
// group), so existing behaviour is preserved.
const payload = makePayload(
[
{
metric: { 'service.name': 'adservice' },
queryName: 'A',
legend: 'adservice',
values: [[0, '100']],
} as QueryData,
{
metric: { 'service.name': 'cartservice' },
queryName: 'A',
legend: 'cartservice',
values: [[0, '200']],
} as QueryData,
],
[
tableEntry(
[
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
{ name: 'count', queryName: 'A', isValueColumn: true, id: 'A' },
],
[
{ data: { 'service.name': 'adservice', A: 100 } },
{ data: { 'service.name': 'cartservice', A: 200 } },
],
),
],
);
const slices = preparePieChartData(payload, options);
expect(slices.map((s) => [s.label, s.value])).toStrictEqual([
['adservice', '100'],
['cartservice', '200'],
]);
});
it('uses the legacy series result when there is no scalar table', () => {
const payload = makePayload(
[
{
metric: { 'service.name': 'adservice' },
queryName: 'A',
legend: '{{service.name}}',
values: [[1000, '42']],
} as QueryData,
],
[],
);
const slices = preparePieChartData(payload, options);
expect(slices).toHaveLength(1);
expect(slices[0].value).toBe('42');
});
it('returns no slices for an empty payload', () => {
expect(preparePieChartData(undefined, options)).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,144 @@
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { isNaN } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData, QueryDataV3 } from 'types/api/widgets/getQuery';
export interface PieChartSlice {
label: string;
value: string;
color: string;
record: {
queryName: string;
legend?: string;
/** Group-by labels, used for drilldown; absent when the slice has no group. */
metric?: QueryData['metric'];
};
}
interface PreparePieChartDataOptions {
customLegendColors?: Record<string, string>;
colorMap: Record<string, string>;
}
const colorFor = (
label: string,
{ customLegendColors, colorMap }: PreparePieChartDataOptions,
): string => customLegendColors?.[label] || generateColor(label, colorMap);
const isPositive = (value: string): boolean =>
!!value && !isNaN(parseFloat(value)) && parseFloat(value) > 0;
/**
* Time-series result: one slice per series, value = first datapoint. This is the
* original pie behaviour — kept verbatim (same label/value/colour/record) so
* single-value and grouped panels are unaffected.
*/
function slicesFromSeries(
result: QueryData[],
options: PreparePieChartDataOptions,
): PieChartSlice[] {
return result
.filter((d) => d?.values?.[0]?.[1] !== undefined)
.map((d) => {
const label = getLabelName(d.metric, d.queryName || '', d.legend || '');
return {
label,
value: d.values[0][1],
color: colorFor(label, options),
record: d,
};
});
}
/**
* V5 scalar table: one slice per (row × value column). With more than one value
* column the column name keeps the slices distinct, so a ClickHouse query like
* `count() AS col1, sum() AS col2` renders a slice per column instead of
* collapsing onto the first; group-by columns become the slice label.
*/
function slicesFromTables(
tables: QueryDataV3[],
options: PreparePieChartDataOptions,
): PieChartSlice[] {
const slices: PieChartSlice[] = [];
tables.forEach((entry) => {
const { table } = entry;
if (!table?.columns?.length || !table?.rows?.length) {
return;
}
const valueColumns = table.columns.filter((column) => column.isValueColumn);
if (valueColumns.length === 0) {
return;
}
const labelColumns = table.columns.filter((column) => !column.isValueColumn);
const hasMultipleValueColumns = valueColumns.length > 1;
table.rows.forEach((row) => {
const groupLabel = labelColumns
.map((column) => row.data[column.id || column.name])
.filter((part) => part != null)
.map(String)
.join(', ');
// Drilldown filters by group-by labels; leave it undefined when there
// are none (e.g. a ClickHouse query) so no filterless menu is offered.
const metric = labelColumns.length
? labelColumns.reduce<Record<string, string>>((acc, column) => {
acc[column.name] = String(row.data[column.id || column.name]);
return acc;
}, {})
: undefined;
valueColumns.forEach((column) => {
let label: string;
if (hasMultipleValueColumns) {
label = groupLabel ? `${groupLabel} · ${column.name}` : column.name;
} else {
label = groupLabel || entry.legend || entry.queryName || '';
}
slices.push({
label,
value: String(row.data[column.id || column.name]),
color: colorFor(label, options),
record: { queryName: entry.queryName, legend: entry.legend, metric },
});
});
});
});
return slices;
}
/**
* Builds pie slices from a query-range payload, dropping non-positive/non-numeric
* values.
*
* A scalar response with several value columns (e.g. a ClickHouse
* `count() AS col1, sum() AS col2`) collapses to a single series in
* `data.result` — only the first value column survives. The full data is kept in
* the scalar table under `newResult`, so in that case slices are built from the
* table (one per value column). Otherwise the legacy time-series result is used,
* preserving existing behaviour for single-value and grouped panels.
*/
export function preparePieChartData(
payload: MetricRangePayloadProps | undefined,
options: PreparePieChartDataOptions,
): PieChartSlice[] {
const tables = (payload?.data?.newResult?.data?.result || []).filter(
(entry) => entry?.table?.rows?.length,
);
const hasMultipleValueColumns = tables.some(
(entry) =>
(entry.table?.columns || []).filter((column) => column.isValueColumn)
.length > 1,
);
const slices = hasMultipleValueColumns
? slicesFromTables(tables, options)
: slicesFromSeries(payload?.data?.result || [], options);
return slices.filter((slice) => isPositive(slice.value));
}

View File

@@ -1,7 +1,6 @@
.rolesListingTable {
margin-top: 12px;
border-radius: 4px;
overflow: hidden;
}
.scrollContainer {

View File

@@ -40,6 +40,7 @@
.rolesSettingsContent {
padding: 0 16px;
padding-bottom: 16px;
}
.rolesSettingsToolbar {

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,
})}