{!isFilterExpanded && (
- <>
+
- >
+
)}
{isDataLoaded && (
- <>
+
{!isFilterExpanded && (
- <>
-
+
+
+
+
+
+
+
+
+ Switch to legacy trace view
+
setIsAnalyticsOpen((prev) => !prev)}
>
@@ -154,15 +174,18 @@ function TraceDetailsHeader({
Analytics
-
- setIsPreviewFieldsOpen(true)}
- />
- >
+ setIsPreviewFieldsOpen(true)}
+ />
+
+
)}
-
+
setIsFilterExpanded(false)}
/>
- {!isFilterExpanded && (
-
- Legacy View
-
- )}
- >
+
)}
diff --git a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceOptionsMenu.tsx b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceOptionsMenu.tsx
index 87d8fc25912..6ba24b468e5 100644
--- a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceOptionsMenu.tsx
+++ b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceOptionsMenu.tsx
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
-import { Ellipsis } from '@signozhq/icons';
+import { Settings2 } from '@signozhq/icons';
import { useTraceStore } from '../stores/traceStore';
@@ -93,7 +93,8 @@ function TraceOptionsMenu({
variant="ghost"
size="icon"
color="secondary"
- prefix={
}
+ aria-label="Trace options"
+ prefix={
}
/>
);
diff --git a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/__tests__/TraceDetailsHeader.test.tsx b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/__tests__/TraceDetailsHeader.test.tsx
index 05dcc3531aa..bdb69d4e28c 100644
--- a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/__tests__/TraceDetailsHeader.test.tsx
+++ b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/__tests__/TraceDetailsHeader.test.tsx
@@ -6,6 +6,7 @@ import TraceDetailsHeader from '../TraceDetailsHeader';
const mockGoBack = jest.fn();
const mockPush = jest.fn();
+const mockReplace = jest.fn();
const mockHasInAppHistory = jest.fn();
jest.mock('lib/history', () => ({
@@ -13,13 +14,47 @@ jest.mock('lib/history', () => ({
default: {
goBack: (): void => mockGoBack(),
push: (path: string): void => mockPush(path),
- replace: jest.fn(),
+ replace: (path: string): void => mockReplace(path),
location: { pathname: '/', search: '' },
listen: (): (() => void) => (): void => undefined,
},
hasInAppHistory: (): boolean => mockHasInAppHistory(),
}));
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: (): { id: string } => ({ id: 'trace-123' }),
+}));
+
+const mockSetLocalStorageKey = jest.fn();
+jest.mock('api/browser/localstorage/set', () => ({
+ __esModule: true,
+ default: (key: string, value: string): void =>
+ mockSetLocalStorageKey(key, value),
+}));
+
+jest.mock(
+ '../../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters',
+ () => ({
+ __esModule: true,
+ default: (): JSX.Element =>
,
+ }),
+);
+
+jest.mock('../../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel', () => ({
+ __esModule: true,
+ default: ({ isOpen }: { isOpen: boolean }): JSX.Element => (
+
+ ),
+}));
+
+jest.mock('components/FieldsSelector', () => ({
+ __esModule: true,
+ default: ({ isOpen }: { isOpen: boolean }): JSX.Element => (
+
+ ),
+}));
+
const baseProps = {
filterMetadata: {
startTime: 0,
@@ -58,3 +93,70 @@ describe('TraceDetailsHeader – back button', () => {
expect(mockGoBack).not.toHaveBeenCalled();
});
});
+
+describe('TraceDetailsHeader – action cluster', () => {
+ beforeEach(() => {
+ mockReplace.mockClear();
+ mockSetLocalStorageKey.mockClear();
+ });
+
+ it('does not render the action buttons while data is still loading', () => {
+ render(
);
+
+ expect(
+ screen.queryByRole('button', { name: /switch to legacy trace view/i }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: /^analytics$/i }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: /trace options/i }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders Legacy View, Analytics, and Settings action buttons once data is loaded', () => {
+ render(
);
+
+ expect(
+ screen.getByRole('button', { name: /switch to legacy trace view/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: /^analytics$/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: /trace options/i }),
+ ).toBeInTheDocument();
+ });
+
+ it('routes to the legacy trace view and persists the preference on click', () => {
+ render(
);
+
+ fireEvent.click(
+ screen.getByRole('button', { name: /switch to legacy trace view/i }),
+ );
+
+ expect(mockSetLocalStorageKey).toHaveBeenCalledWith(
+ 'TRACE_DETAILS_PREFER_OLD_VIEW',
+ 'true',
+ );
+ expect(mockReplace).toHaveBeenCalledTimes(1);
+ expect(mockReplace).toHaveBeenCalledWith(
+ expect.stringContaining('/trace-old/trace-123'),
+ );
+ });
+
+ it('toggles the AnalyticsPanel open state when the Analytics button is clicked', () => {
+ render(
);
+
+ const panel = screen.getByTestId('analytics-panel');
+ expect(panel).toHaveAttribute('data-open', 'false');
+
+ const analyticsBtn = screen.getByRole('button', { name: /^analytics$/i });
+
+ fireEvent.click(analyticsBtn);
+ expect(panel).toHaveAttribute('data-open', 'true');
+
+ fireEvent.click(analyticsBtn);
+ expect(panel).toHaveAttribute('data-open', 'false');
+ });
+});
diff --git a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.module.scss b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.module.scss
index 04a30d81591..3cfdd961f24 100644
--- a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.module.scss
+++ b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.module.scss
@@ -3,12 +3,6 @@
align-items: center;
gap: 12px;
- // QuerySearch child sets `query-builder-search-v2` globally; size it to the
- // search container by reaching into the descendant.
- :global(.query-builder-search-v2) {
- width: 100%;
- }
-
// ToggleGroup children use generated class names; nest the global selectors
// under the local row so they only apply inside this filter row.
:global([class*='toggle-group']) {
@@ -20,8 +14,43 @@
}
}
+// Expanded-mode root: grows to fill .filter wrapper, and lets the search
+// input flex within. In collapsed mode none of these grow — the whole
+// Filters region is content-sized (just the pill + result + toggle).
.isExpanded {
flex: 1;
+
+ .searchInput {
+ flex: 1;
+ }
+
+ .searchAndNav {
+ flex: 1;
+ }
+}
+
+.categoryControls {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.searchInput {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+}
+
+.searchPill {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.searchAndNav {
+ display: flex;
+ align-items: center;
+ min-width: 0;
}
.searchContainer {
@@ -29,6 +58,25 @@
min-width: 0;
}
+.resultActions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.expandedActions {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.highlightControl {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+
.pill {
display: flex;
align-items: center;
@@ -85,14 +133,6 @@
border-radius: 4px;
}
-.collapseBtn {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: none;
-}
-
.highlightErrorsToggle {
display: flex;
align-items: center;
@@ -100,37 +140,3 @@
flex-shrink: 0;
white-space: nowrap;
}
-
-.preNextToggle {
- display: flex;
- flex-shrink: 0;
- gap: 12px;
-}
-
-.preNextCount {
- display: flex;
- align-items: center;
- margin: auto;
- color: var(--l2-foreground);
- font-family: 'Geist Mono';
- font-size: 12px;
- font-weight: 400;
- line-height: 18px;
-}
-
-.filterStatus {
- display: flex;
- align-items: center;
- gap: 6px;
- flex-shrink: 0;
- color: var(--l2-foreground);
- font-family: 'Geist Mono';
- font-size: 12px;
- font-weight: 400;
- line-height: 18px;
-}
-
-.hasError {
- color: var(--destructive);
- cursor: help;
-}
diff --git a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx
index ed4b4a30f73..402ebbad43d 100644
--- a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx
+++ b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx
@@ -1,15 +1,7 @@
import { useCallback, useRef, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
-import {
- ChevronDown,
- ChevronUp,
- Copy,
- Info,
- Loader,
- Search,
- X,
-} from '@signozhq/icons';
+import { ChevronsRight, Copy, Search, X } from '@signozhq/icons';
import { Switch } from '@signozhq/ui/switch';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { toast } from '@signozhq/ui/sonner';
@@ -21,7 +13,6 @@ import {
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
-import { AxiosError } from 'axios';
import cx from 'classnames';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
@@ -42,6 +33,7 @@ import {
SpanCategory,
useSpanCategoryFilter,
} from './hooks/useSpanCategoryFilter';
+import QueryResult from './QueryResult';
import styles from './Filters.module.scss';
@@ -152,6 +144,16 @@ function Filters({
runQuery(expressionRef.current);
}, [runQuery]);
+ const handleClear = useCallback((): void => {
+ setExpression('');
+ expressionRef.current = '';
+ setFilters({ items: [], op: 'AND' });
+ setFilteredSpanIds([]);
+ onFilteredSpansChange?.([], false);
+ setCurrentSearchedIndex(0);
+ setNoData(false);
+ }, [onFilteredSpansChange]);
+
// Expression-based filter hooks
const filterProps = {
expression,
@@ -266,164 +268,167 @@ function Filters({
- )}
- >
- );
+ const hasExpression = expression.trim().length > 0;
+ const hasResults = filteredSpanIds.length > 0;
- // --- COLLAPSED VIEW ---
- if (!isExpanded) {
- const pill = (
- /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
-
- );
+ const handlePrev = useCallback((): void => {
+ handlePrevNext(currentSearchedIndex - 1);
+ setCurrentSearchedIndex((prev) => prev - 1);
+ }, [currentSearchedIndex, handlePrevNext]);
- return (
-
- );
- }
+ const handleNext = useCallback((): void => {
+ handlePrevNext(currentSearchedIndex + 1);
+ setCurrentSearchedIndex((prev) => prev + 1);
+ }, [currentSearchedIndex, handlePrevNext]);
- // --- EXPANDED VIEW ---
- return (
-