Compare commits

..

1 Commits

Author SHA1 Message Date
Vinícius Lourenço
c710e41d55 fix(tanstack-table): reset page to 1 when change page size 2026-05-18 11:07:02 -03:00
37 changed files with 143 additions and 94 deletions

View File

@@ -10,13 +10,6 @@ export default defineConfig({
signoz: {
input: {
target: '../docs/api/openapi.yml',
// Perses' `common.JSONRef` (used by `DashboardGridItem.content`) has a
// field tagged `json:"$ref"`, so our spec contains a property literally
// named `$ref`.
// Orval v8's validator (`@scalar/openapi-parser`) treats every `$ref` key
// as a JSON Reference and aborts with `INVALID_REFERENCE` when the value isn't a URI string.
// Safe to disable: yes, the spec is generated by `cmd/openapi.go` and gated by backend CI, not hand-edited.
unsafeDisableValidation: true,
},
output: {
target: './src/api/generated/services',

View File

@@ -144,18 +144,18 @@ const routes: AppRoutes[] = [
// /trace-old serves V3 (URL-only access). Flip the two `component`
// values back to release V3.
{
path: ROUTES.TRACE_DETAIL_OLD,
path: ROUTES.TRACE_DETAIL,
exact: true,
component: TraceDetail,
isPrivate: true,
key: 'TRACE_DETAIL_OLD',
key: 'TRACE_DETAIL',
},
{
path: ROUTES.TRACE_DETAIL,
path: ROUTES.TRACE_DETAIL_OLD,
exact: true,
component: TraceDetailV3,
isPrivate: true,
key: 'TRACE_DETAIL',
key: 'TRACE_DETAIL_OLD',
},
{
path: ROUTES.SETTINGS,

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
export interface AlertmanagertypesChannelDTO {
/**

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -3,6 +3,7 @@
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
* OpenAPI spec version: 0.0.1
*/
import { useMutation, useQuery } from 'react-query';
import type {

View File

@@ -625,6 +625,7 @@ function TanStackTableInner<TData>(
defaultValue="10"
onChange={(value): void => {
setLimit(+value);
setPage(1);
pagination.onLimitChange?.(+value);
}}
items={paginationPageSizeItems}

View File

@@ -401,6 +401,59 @@ describe('TanStackTableView Integration', () => {
expect(onLimitChange).toHaveBeenCalledWith(20);
});
});
it('resets page to 1 when limit changes', async () => {
const user = userEvent.setup();
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderTanStackTable({
props: {
pagination: { total: 100, defaultPage: 1, defaultLimit: 10 },
enableQueryParams: true,
},
onUrlUpdate,
});
await waitFor(() => {
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
// Navigate to page 2
const nav = screen.getByRole('navigation');
const page2 = Array.from(nav.querySelectorAll('button')).find(
(btn) => btn.textContent?.trim() === '2',
);
if (!page2) {
throw new Error('Page 2 button not found in pagination');
}
await user.click(page2);
await waitFor(() => {
const lastPage = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('page'))
.filter(Boolean)
.pop();
expect(lastPage).toBe('2');
});
// Change page size
const comboboxTrigger = document.querySelector(
'button[aria-haspopup="dialog"]',
) as HTMLElement;
await user.click(comboboxTrigger);
const option20 = await screen.findByRole('option', { name: '20' });
await user.click(option20);
// Verify page reset to 1
await waitFor(() => {
const lastPage = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('page'))
.filter(Boolean)
.pop();
expect(lastPage).toBe('1');
});
});
});
describe('sorting', () => {

View File

@@ -189,7 +189,7 @@ describe('Tooltip utils', () => {
];
}
it('builds tooltip content sorted by value descending with isActive flag set correctly', () => {
it('builds tooltip content in series-index order with isActive flag set correctly', () => {
const data: AlignedData = [[0], [10], [20], [30]];
const series = createSeriesConfig();
const dataIndexes = [null, 0, 0, 0];
@@ -206,21 +206,21 @@ describe('Tooltip utils', () => {
});
expect(result).toHaveLength(2);
// Sorted by value descending: B (20) before A (10)
// Series are returned in series-index order (A=index 1 before B=index 2)
expect(result[0]).toMatchObject<Partial<TooltipContentItem>>({
label: 'B',
value: 20,
tooltipValue: 'formatted-20',
color: 'color-2',
isActive: true,
});
expect(result[1]).toMatchObject<Partial<TooltipContentItem>>({
label: 'A',
value: 10,
tooltipValue: 'formatted-10',
color: '#ff0000',
isActive: false,
});
expect(result[1]).toMatchObject<Partial<TooltipContentItem>>({
label: 'B',
value: 20,
tooltipValue: 'formatted-20',
color: 'color-2',
isActive: true,
});
});
it('skips series with null data index or non-finite values', () => {
@@ -274,7 +274,7 @@ describe('Tooltip utils', () => {
expect(result[1].value).toBe(30);
});
it('returns items sorted by value descending', () => {
it('returns items in series-index order', () => {
// Series values in non-sorted order: 3, 1, 4, 2
const data: AlignedData = [[0], [3], [1], [4], [2]];
const series: Series[] = [
@@ -297,7 +297,7 @@ describe('Tooltip utils', () => {
decimalPrecision,
});
expect(result.map((item) => item.value)).toStrictEqual([4, 3, 2, 1]);
expect(result.map((item) => item.value)).toStrictEqual([3, 1, 4, 2]);
});
});
});

View File

@@ -142,7 +142,5 @@ export function buildTooltipContent({
}
}
items.sort((a, b) => b.value - a.value);
return items;
}

View File

@@ -473,7 +473,6 @@ export const SpanDuration = memo(function SpanDuration({
const columnDefHelper = createColumnHelper<SpanV3>();
const ROW_HEIGHT = 28;
const WATERFALL_BOTTOM_PADDING = 24;
const DEFAULT_SIDEBAR_WIDTH = 450;
const MIN_SIDEBAR_WIDTH = 240;
const MAX_SIDEBAR_WIDTH = 900;
@@ -741,69 +740,53 @@ function Success(props: ISuccessProps): JSX.Element {
);
}, [spans, sidebarWidth]);
// Scroll a span to viewport center if it isn't already visible. Shared by
// the two effects below — one keyed on interestedSpanId (chevron, boundary
// pagination, deep-link to unloaded), the other on selectedSpan (in-window
// URL navigation that doesn't mutate interestedSpanId).
const scrollSpanIntoView = useCallback(
(span: SpanV3, spansList: SpanV3[]): void => {
if (!virtualizerRef.current) {
return;
}
const idx = spansList.findIndex((s) => s.span_id === span.span_id);
if (idx === -1) {
return;
}
const scrollEl = scrollContainerRef.current;
const scrollTop = scrollEl?.scrollTop ?? 0;
const viewportHeight = scrollEl?.clientHeight ?? 0;
const viewportStartIdx = Math.floor(scrollTop / ROW_HEIGHT);
const viewportEndIdx =
Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) - 1;
const isOnScreen =
viewportHeight > 0 && idx >= viewportStartIdx && idx <= viewportEndIdx;
if (isOnScreen) {
return;
}
setTimeout(() => {
virtualizerRef.current?.scrollToIndex(idx, {
align: 'center',
behavior: 'auto',
});
const sidebarScrollEl = scrollContainerRef.current?.querySelector(
'.resizable-box__content',
);
if (sidebarScrollEl) {
const targetScrollLeft = Math.max(0, span.level * CONNECTOR_WIDTH - 40);
(sidebarScrollEl as HTMLElement).scrollLeft = targetScrollLeft;
}
}, 100);
},
[],
);
// Scroll to the interested span only when it isn't already on screen.
// Covers every entry point uniformly: deep-link, flamegraph click,
// filter prev/next, browser back/forward all scroll only if needed;
// waterfall row clicks and chevron expand/collapse don't yank the viewport
// because the affected row is by definition already visible.
useEffect(() => {
if (interestedSpanId.spanId !== '') {
if (interestedSpanId.spanId !== '' && virtualizerRef.current) {
const idx = spans.findIndex(
(span) => span.span_id === interestedSpanId.spanId,
);
if (idx !== -1) {
scrollSpanIntoView(spans[idx], spans);
const visible = virtualizerRef.current.getVirtualItems();
const isOnScreen =
visible.length > 0 &&
idx >= visible[0].index &&
idx <= visible[visible.length - 1].index;
if (!isOnScreen) {
setTimeout(() => {
virtualizerRef.current?.scrollToIndex(idx, {
align: 'center',
behavior: 'auto',
});
// Auto-scroll sidebar horizontally to show the span name
const span = spans[idx];
const sidebarScrollEl = scrollContainerRef.current?.querySelector(
'.resizable-box__content',
);
if (sidebarScrollEl) {
const targetScrollLeft = Math.max(0, span.level * CONNECTOR_WIDTH - 40);
sidebarScrollEl.scrollLeft = targetScrollLeft;
}
}, 400);
}
setSelectedSpan(spans[idx]);
}
} else {
setSelectedSpan((prev) => prev ?? spans[0]);
setSelectedSpan((prev) => {
if (!prev) {
return spans[0];
}
return prev;
});
}
}, [interestedSpanId, setSelectedSpan, spans, scrollSpanIntoView]);
// Covers URL-driven navigation to an already-loaded span (flamegraph /
// filter / browser back) that the interestedSpanId-keyed effect doesn't see.
useEffect(() => {
if (selectedSpan) {
scrollSpanIntoView(selectedSpan, spans);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSpan, scrollSpanIntoView]);
}, [interestedSpanId, setSelectedSpan, spans]);
const virtualItems = virtualizer.getVirtualItems();
const leftRows = leftTable.getRowModel().rows;
@@ -863,7 +846,7 @@ function Success(props: ISuccessProps): JSX.Element {
<div
className={styles.splitBody}
style={{
minHeight: virtualizer.getTotalSize() + WATERFALL_BOTTOM_PADDING,
minHeight: virtualizer.getTotalSize(),
height: '100%',
}}
>

View File

@@ -74,21 +74,17 @@ function TraceDetailsV3(): JSX.Element {
onClose: handleSpanDetailsClose,
});
const allSpansRef = useRef<SpanV3[]>([]);
// Refetch only when the URL target isn't already loaded. Keeps row clicks
// and other in-window URL navigation from triggering a backend window slide.
useEffect(() => {
const spanId = urlQuery.get('spanId') || '';
// Only update interestedSpanId when a new span is selected,
// not when it's cleared (panel close) — avoids unnecessary API refetch
if (!spanId) {
return;
}
const idx = allSpansRef.current.findIndex((s) => s.span_id === spanId);
if (idx !== -1) {
setSelectedSpan(allSpansRef.current[idx]);
return;
}
setInterestedSpanId({ spanId, isUncollapsed: true });
setInterestedSpanId({
spanId,
isUncollapsed: true,
});
}, [urlQuery]);
// Hardcoded for now — fetch aggregations for all 3 candidate color-by fields
@@ -149,10 +145,6 @@ function TraceDetailsV3(): JSX.Element {
};
}
useEffect(() => {
allSpansRef.current = allSpans;
}, [allSpans]);
// Frontend mode: expand all parents by default when full data arrives
useEffect(() => {
if (isFullDataLoaded && allSpans.length > 0) {