fix: remove isRoot and isEntrypoint from the list of selectable columns in the columns menu in traces explorer and traces list panel in dashboard (#9629)

* fix: hide isRoot and isEntryPoint options from columns options

* test: add tests to ensure isRoot and isEntryPoint are hidden in column options

* refactor: improve the columns exclusion logic + update test
This commit is contained in:
Shaheer Kochai
2025-12-02 20:50:04 +04:30
committed by GitHub
parent 646f359f33
commit 3df426625a
6 changed files with 312 additions and 5 deletions

View File

@@ -1,12 +1,17 @@
import { Checkbox, Empty } from 'antd';
import { AxiosResponse } from 'axios';
import Spinner from 'components/Spinner';
import { EXCLUDED_COLUMNS } from 'container/OptionsMenu/constants';
import { QueryKeySuggestionsResponseProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
type ExplorerAttributeColumnsProps = {
isLoading: boolean;
data: any;
data: AxiosResponse<QueryKeySuggestionsResponseProps> | undefined;
searchText: string;
isAttributeKeySelected: (key: string) => boolean;
handleCheckboxChange: (key: string) => void;
dataSource: DataSource;
};
function ExplorerAttributeColumns({
@@ -15,6 +20,7 @@ function ExplorerAttributeColumns({
searchText,
isAttributeKeySelected,
handleCheckboxChange,
dataSource,
}: ExplorerAttributeColumnsProps): JSX.Element {
if (isLoading) {
return (
@@ -27,8 +33,10 @@ function ExplorerAttributeColumns({
const filteredAttributeKeys =
Object.values(data?.data?.data?.keys || {})
?.flat()
?.filter((attributeKey: any) =>
attributeKey.name.toLowerCase().includes(searchText.toLowerCase()),
?.filter(
(attributeKey) =>
attributeKey.name.toLowerCase().includes(searchText.toLowerCase()) &&
!EXCLUDED_COLUMNS[dataSource].includes(attributeKey.name),
) || [];
if (filteredAttributeKeys.length === 0) {
return (

View File

@@ -183,6 +183,7 @@ function ExplorerColumnsRenderer({
searchText={searchText}
isAttributeKeySelected={isAttributeKeySelected}
handleCheckboxChange={handleCheckboxChange}
dataSource={initialDataSource}
/>
),
},

View File

@@ -450,4 +450,58 @@ describe('ExplorerColumnsRenderer', () => {
}
});
});
it('does not show isRoot or isEntryPoint in add column dropdown (traces, dashboard table panel)', async () => {
(useQueryBuilder as jest.Mock).mockReturnValue({
currentQuery: {
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
},
],
},
},
});
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{ name: 'isRoot', dataType: 'bool', type: '' },
{ name: 'isEntryPoint', dataType: 'bool', type: '' },
{ name: 'duration', dataType: 'number', type: '' },
{ name: 'serviceName', dataType: 'string', type: '' },
],
},
},
},
},
isLoading: false,
isError: false,
});
render(
<Wrapper>
<ExplorerColumnsRenderer
selectedLogFields={[]}
setSelectedLogFields={mockSetSelectedLogFields}
selectedTracesFields={[]}
setSelectedTracesFields={mockSetSelectedTracesFields}
/>
</Wrapper>,
);
await userEvent.click(screen.getByTestId('add-columns-button'));
// Visible columns should appear
expect(screen.getByText('duration')).toBeInTheDocument();
expect(screen.getByText('serviceName')).toBeInTheDocument();
// Hidden columns should NOT appear
expect(screen.queryByText('isRoot')).not.toBeInTheDocument();
expect(screen.queryByText('isEntryPoint')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,235 @@
import { renderHook } from '@testing-library/react';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useQueries } from 'react-query';
import { DataSource } from 'types/common/queryBuilder';
import useOptionsMenu from '../useOptionsMenu';
// Mock all dependencies
jest.mock('hooks/useNotifications');
jest.mock('providers/preferences/context/PreferenceContextProvider');
jest.mock('hooks/useUrlQueryData');
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueries: jest.fn(),
}));
describe('useOptionsMenu', () => {
const mockNotifications = { error: jest.fn(), success: jest.fn() };
const mockUpdateColumns = jest.fn();
const mockUpdateFormatting = jest.fn();
const mockRedirectWithQuery = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useNotifications as jest.Mock).mockReturnValue({
notifications: mockNotifications,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
traces: {
preferences: {
columns: [],
formatting: {
format: 'raw',
maxLines: 2,
fontSize: 'small',
},
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
logs: {
preferences: {
columns: [],
formatting: {
format: 'raw',
maxLines: 2,
fontSize: 'small',
},
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
});
(useUrlQueryData as jest.Mock).mockReturnValue({
query: null,
redirectWithQuery: mockRedirectWithQuery,
});
(useQueries as jest.Mock).mockReturnValue([]);
});
it('does not show isRoot or isEntryPoint in column options when dataSource is TRACES', () => {
// Mock the query key suggestions to return data including isRoot and isEntryPoint
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'isRoot',
signal: 'traces',
fieldDataType: 'bool',
fieldContext: '',
},
{
name: 'isEntryPoint',
signal: 'traces',
fieldDataType: 'bool',
fieldContext: '',
},
{
name: 'duration',
signal: 'traces',
fieldDataType: 'float64',
fieldContext: '',
},
{
name: 'serviceName',
signal: 'traces',
fieldDataType: 'string',
fieldContext: '',
},
],
},
},
},
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
const columnOptions = result.current.config.addColumn?.options ?? [];
const optionNames = columnOptions.map((option) => option.label);
// isRoot and isEntryPoint should NOT be in the options
expect(optionNames).not.toContain('isRoot');
expect(optionNames).not.toContain('body');
expect(optionNames).not.toContain('isEntryPoint');
// Other attributes should be present
expect(optionNames).toContain('duration');
expect(optionNames).toContain('serviceName');
});
it('does not show body in column options when dataSource is METRICS', () => {
// Mock the query key suggestions to return data including body
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'body',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'status',
signal: 'metrics',
fieldDataType: 'int64',
fieldContext: '',
},
{
name: 'value',
signal: 'metrics',
fieldDataType: 'float64',
fieldContext: '',
},
],
},
},
},
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.METRICS,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
const columnOptions = result.current.config.addColumn?.options ?? [];
const optionNames = columnOptions.map((option) => option.label);
// body should NOT be in the options
expect(optionNames).not.toContain('body');
// Other attributes should be present
expect(optionNames).toContain('status');
expect(optionNames).toContain('value');
});
it('does not show body in column options when dataSource is LOGS', () => {
// Mock the query key suggestions to return data including body
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'body',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'level',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'timestamp',
signal: 'logs',
fieldDataType: 'int64',
fieldContext: '',
},
],
},
},
},
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
const columnOptions = result.current.config.addColumn?.options ?? [];
const optionNames = columnOptions.map((option) => option.label);
// body should be in the options
expect(optionNames).toContain('body');
// Other attributes should be present
expect(optionNames).toContain('level');
expect(optionNames).toContain('timestamp');
});
});

View File

@@ -1,4 +1,5 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { DataSource } from 'types/common/queryBuilder';
import { FontSize, OptionsQuery } from './types';
@@ -11,6 +12,12 @@ export const defaultOptionsQuery: OptionsQuery = {
fontSize: FontSize.SMALL,
};
export const EXCLUDED_COLUMNS: Record<DataSource, string[]> = {
[DataSource.TRACES]: ['body', 'isRoot', 'isEntryPoint'],
[DataSource.METRICS]: ['body'],
[DataSource.LOGS]: [],
};
export const defaultLogsSelectedColumns: TelemetryFieldKey[] = [
{
name: 'timestamp',

View File

@@ -27,6 +27,7 @@ import {
defaultLogsSelectedColumns,
defaultOptionsQuery,
defaultTraceSelectedColumns,
EXCLUDED_COLUMNS,
URL_OPTIONS,
} from './constants';
import {
@@ -267,8 +268,9 @@ const useOptionsMenu = ({
const optionsFromAttributeKeys = useMemo(() => {
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
if (dataSource !== DataSource.LOGS) {
return item.name !== 'body';
const exclusions = EXCLUDED_COLUMNS[dataSource];
if (exclusions) {
return !exclusions.includes(item.name);
}
return true;
});