Skip to content

feat: use search v2 component for traces #7537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
45a647f
Revert "fix: display same key with multiple data types in filter sugg…
ahmadshaheer Mar 20, 2025
a2cf36b
fix: use query search v2 for traces data source to handle multiple da…
ahmadshaheer Mar 20, 2025
3f4d1f8
fix(QueryBuilderSearchV2): add user typed option if it doesn't exist …
ahmadshaheer Mar 22, 2025
a3eaf32
fix(QueryBuilderSearchV2): increase the height of search dropdown for…
ahmadshaheer Mar 22, 2025
2d24a9e
fix: display span scope selector for trace data source
ahmadshaheer Apr 6, 2025
81bdb0d
chore: remove the span scope selector from qb search v1 and move the …
ahmadshaheer Apr 6, 2025
7be90f4
fix: write test to ensure that we display span scope selector for tra…
ahmadshaheer Apr 6, 2025
7b2dfc5
fix: limit converting -> only to log data source
ahmadshaheer Apr 8, 2025
a79c8c8
fix: don't display empty suggestion if only spaces are typed
ahmadshaheer Apr 8, 2025
58c4d9f
Merge branch 'main' into feat/use-search-v2-component-for-traces
ahmadshaheer Apr 9, 2025
698b8d6
chore: tests for span scope selector
ahmadshaheer Apr 16, 2025
afe591f
chore: qb search flow (key, operator, value) test cases
ahmadshaheer Apr 16, 2025
5e02fb6
refactor: fix the Maximum update depth reached issue while running tests
ahmadshaheer Apr 21, 2025
ac88da8
chore: overall iomprovements to span scope selector tests
ahmadshaheer Apr 21, 2025
a47a27e
Merge branch 'main' into feat/use-search-v2-component-for-traces
ahmadshaheer Apr 21, 2025
9ca0b79
Merge branch 'main' into feat/use-search-v2-component-for-traces
ahmadshaheer Apr 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ export const Query = memo(function Query({
</Col>
)}
<Col flex="1" className="qb-search-container">
{query.dataSource === DataSource.LOGS ? (
{[DataSource.LOGS, DataSource.TRACES].includes(query.dataSource) ? (
<QueryBuilderSearchV2
query={query}
onChange={handleChangeTagFilters}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import { PLACEHOLDER } from './constant';
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
import OptionRenderer from './OptionRenderer';
import OptionRendererForLogs from './OptionRendererForLogs';
import SpanScopeSelector from './SpanScopeSelector';
import { StyledCheckOutlined, TypographyText } from './style';
import {
convertExampleQueriesToOptions,
Expand Down Expand Up @@ -84,11 +83,6 @@ function QueryBuilderSearch({
pathname,
]);

const isTracesExplorerPage = useMemo(
() => pathname === ROUTES.TRACES_EXPLORER,
[pathname],
);

const [isEditingTag, setIsEditingTag] = useState(false);

const {
Expand Down Expand Up @@ -489,7 +483,6 @@ function QueryBuilderSearch({
</Select.Option>
))}
</Select>
{isTracesExplorerPage && <SpanScopeSelector queryName={query.queryName} />}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import './QueryBuilderSearchV2.styles.scss';

import { Typography } from 'antd';
import cx from 'classnames';
import {
ArrowDown,
ArrowUp,
Expand All @@ -25,6 +26,7 @@ interface ICustomDropdownProps {
exampleQueries: TagFilter[];
onChange: (value: TagFilter) => void;
currentFilterItem?: ITag;
isLogsDataSource: boolean;
}

export default function QueryBuilderSearchDropdown(
Expand All @@ -38,11 +40,14 @@ export default function QueryBuilderSearchDropdown(
exampleQueries,
options,
onChange,
isLogsDataSource,
} = props;
const userOs = getUserOperatingSystem();
return (
<>
<div className="content">
<div
className={cx('content', { 'non-logs-data-source': !isLogsDataSource })}
>
{!currentFilterItem?.key ? (
<div className="suggested-filters">Suggested Filters</div>
) : !currentFilterItem?.op ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
.rc-virtual-list-holder {
height: 115px;
}
&.non-logs-data-source {
.rc-virtual-list-holder {
height: 256px;
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
} from '../QueryBuilderSearch/utils';
import { filterByOperatorConfig } from '../utils';
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
import SpanScopeSelector from './SpanScopeSelector';
import Suggestions from './Suggestions';

export interface ITag {
Expand Down Expand Up @@ -294,7 +295,8 @@ function QueryBuilderSearchV2(
if (
isObject(parsedValue) &&
parsedValue?.key &&
parsedValue?.key?.split(' ').length > 1
parsedValue?.key?.split(' ').length > 1 &&
isLogsDataSource
) {
setTags((prev) => [
...prev,
Expand Down Expand Up @@ -409,7 +411,13 @@ function QueryBuilderSearchV2(
}
}
},
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue],
[
currentFilterItem?.key,
currentFilterItem?.op,
currentState,
isLogsDataSource,
searchValue,
],
);

const handleSearch = useCallback((value: string) => {
Expand Down Expand Up @@ -693,12 +701,29 @@ function QueryBuilderSearchV2(
})),
);
} else {
setDropdownOptions(
data?.payload?.attributeKeys?.map((key) => ({
setDropdownOptions([
// Add user typed option if it doesn't exist in the payload
...(tagKey.trim().length > 0 &&
!data?.payload?.attributeKeys?.some((val) => val.key === tagKey)
? [
{
label: tagKey,
value: {
key: tagKey,
dataType: DataTypes.EMPTY,
type: '',
isColumn: false,
isJSON: false,
},
},
]
: []),
// Map existing attribute keys from payload
...(data?.payload?.attributeKeys?.map((key) => ({
label: key.key,
value: key,
})) || [],
);
})) || []),
]);
}
}
if (currentState === DropdownState.OPERATOR) {
Expand Down Expand Up @@ -911,6 +936,11 @@ function QueryBuilderSearchV2(
);
};

const isTracesDataSource = useMemo(
() => query.dataSource === DataSource.TRACES,
[query.dataSource],
);

return (
<div className="query-builder-search-v2">
<Select
Expand Down Expand Up @@ -968,6 +998,7 @@ function QueryBuilderSearchV2(
exampleQueries={suggestionsData?.payload?.example_queries || []}
tags={tags}
currentFilterItem={currentFilterItem}
isLogsDataSource={isLogsDataSource}
/>
)}
>
Expand All @@ -994,6 +1025,7 @@ function QueryBuilderSearchV2(
);
})}
</Select>
{isTracesDataSource && <SpanScopeSelector queryName={query.queryName} />}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ function SpanScopeSelector({ queryName }: SpanScopeSelectorProps): JSX.Element {
<Select
value={selectedScope}
className="span-scope-selector"
data-testid="span-scope-selector"
onChange={handleScopeChange}
options={SELECT_OPTIONS}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/* eslint-disable react/jsx-props-no-spreading */
import {
act,
fireEvent,
render,
RenderResult,
screen,
} from '@testing-library/react';
import {
initialQueriesMap,
initialQueryBuilderFormValues,
} from 'constants/queryBuilder';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { QueryClient, QueryClientProvider } from 'react-query';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';

import QueryBuilderSearchV2 from '../QueryBuilderSearchV2';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});

describe('Span scope selector', () => {
it('should render span scope selector when data source is TRACES', () => {
const { getByTestId } = render(
<QueryClientProvider client={queryClient}>
<QueryBuilderSearchV2
query={{
...initialQueryBuilderFormValues,
dataSource: DataSource.TRACES,
}}
onChange={jest.fn()}
/>
</QueryClientProvider>,
);

expect(getByTestId('span-scope-selector')).toBeInTheDocument();
});

it('should not render span scope selector for non-TRACES data sources', () => {
const { queryByTestId } = render(
<QueryClientProvider client={queryClient}>
<QueryBuilderSearchV2
query={{
...initialQueryBuilderFormValues,
dataSource: DataSource.METRICS,
}}
onChange={jest.fn()}
/>
</QueryClientProvider>,
);

expect(queryByTestId('span-scope-selector')).not.toBeInTheDocument();
});
});

const mockOnChange = jest.fn();
const mockHandleRunQuery = jest.fn();
const defaultProps = {
query: {
...initialQueriesMap.traces.builder.queryData[0],
dataSource: DataSource.TRACES,
queryName: 'traces_query',
},
onChange: mockOnChange,
};

const renderWithContext = (props = {}): RenderResult => {
const mergedProps = { ...defaultProps, ...props };

return render(
<QueryClientProvider client={queryClient}>
<QueryBuilderContext.Provider
value={
{
currentQuery: initialQueriesMap.traces,
handleRunQuery: mockHandleRunQuery,
} as any
}
>
<QueryBuilderSearchV2 {...mergedProps} />
</QueryBuilderContext.Provider>
</QueryClientProvider>,
);
};

const mockAggregateKeysData = {
payload: {
attributeKeys: [
{
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'http.status',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: 'http.status--string--tag--false',
},
],
},
};

jest.mock('hooks/queryBuilder/useGetAggregateKeys', () => ({
useGetAggregateKeys: jest.fn(() => ({
data: mockAggregateKeysData,
isFetching: false,
})),
}));

const mockAggregateValuesData = {
payload: {
stringAttributeValues: ['200', '404', '500'],
numberAttributeValues: [200, 404, 500],
},
};

jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({
useGetAggregateValues: jest.fn(() => ({
data: mockAggregateValuesData,
isFetching: false,
})),
}));

jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));

describe('Suggestion Key -> Operator -> Value Flow', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should complete full flow from key selection to value', async () => {
const { container } = renderWithContext();

// Get the combobox input specifically
const combobox = container.querySelector(
'.query-builder-search-v2 .ant-select-selection-search-input',
) as HTMLInputElement;

// 1. Focus and type to trigger key suggestions
await act(async () => {
fireEvent.focus(combobox);
fireEvent.change(combobox, { target: { value: 'http.' } });
});

// Wait for dropdown to appear
await screen.findByRole('listbox');

// 2. Select a key from suggestions
const statusOption = await screen.findByText('http.status');
await act(async () => {
fireEvent.click(statusOption);
});

// Should show operator suggestions
expect(screen.getByText('=')).toBeInTheDocument();
expect(screen.getByText('!=')).toBeInTheDocument();

// 3. Select an operator
const equalsOption = screen.getByText('=');
await act(async () => {
fireEvent.click(equalsOption);
});

// Should show value suggestions
expect(screen.getByText('200')).toBeInTheDocument();
expect(screen.getByText('404')).toBeInTheDocument();
expect(screen.getByText('500')).toBeInTheDocument();

// 4. Select a value
const valueOption = screen.getByText('200');
await act(async () => {
fireEvent.click(valueOption);
});

// Verify final filter
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: 'http.status' }),
op: '=',
value: '200',
}),
]),
}),
);
});
});
Loading
Loading