diff --git a/static/app/views/explore/spans/hooks/useValidateSpansTab.tsx b/static/app/views/explore/spans/hooks/useValidateSpansTab.tsx index 26b22e52e3b6b9..08d1a1fa0e1a38 100644 --- a/static/app/views/explore/spans/hooks/useValidateSpansTab.tsx +++ b/static/app/views/explore/spans/hooks/useValidateSpansTab.tsx @@ -26,7 +26,7 @@ export function useValidateSpansTab({enabled = true}: UseValidateSpansTabArgs = const groupBys = useQueryParamsGroupBys(); const visualizes = useQueryParamsVisualizes(); - const {data, isLoading} = useQuery({ + const {data, isFetching, isLoading} = useQuery({ ...validateEventParamsOptions({ organization, selection, @@ -49,6 +49,7 @@ export function useValidateSpansTab({enabled = true}: UseValidateSpansTabArgs = return { data, + isFetching, isLoading, }; } diff --git a/static/app/views/explore/tables/columnEditorModal.spec.tsx b/static/app/views/explore/tables/columnEditorModal.spec.tsx index 89d9aca3204c10..f02df2406c55be 100644 --- a/static/app/views/explore/tables/columnEditorModal.spec.tsx +++ b/static/app/views/explore/tables/columnEditorModal.spec.tsx @@ -50,6 +50,24 @@ const booleanTags: TagCollection = { }, }; +const enrichedNumberTags: TagCollection = { + ...numberTags, + 'custom.duration': { + key: 'custom.duration', + name: 'custom.duration', + kind: FieldKind.MEASUREMENT, + }, +}; + +const enrichedBooleanTags: TagCollection = { + ...booleanTags, + 'custom.enabled': { + key: 'custom.enabled', + name: 'custom.enabled', + kind: FieldKind.BOOLEAN, + }, +}; + describe('ColumnEditorModal', () => { it('allows closes modal on apply', async () => { const onClose = jest.fn(); @@ -371,4 +389,32 @@ describe('ColumnEditorModal', () => { expect(columns[1]).toHaveTextContent('id'); expect(columns[1]).toHaveTextContent('string'); }); + + it('renders existing columns with types from supplied tags', async () => { + renderGlobalModal(); + + act(() => { + openModal( + modalProps => ( + {}} + stringTags={stringTags} + numberTags={enrichedNumberTags} + booleanTags={enrichedBooleanTags} + /> + ), + {onClose: jest.fn()} + ); + }); + + expect(await screen.findByRole('button', {name: 'Apply'})).toBeInTheDocument(); + + const columns = screen.getAllByTestId('editor-column'); + expect(columns[0]).toHaveTextContent('custom.duration'); + expect(columns[0]).toHaveTextContent('number'); + expect(columns[1]).toHaveTextContent('custom.enabled'); + expect(columns[1]).toHaveTextContent('boolean'); + }); }); diff --git a/static/app/views/explore/tables/index.tsx b/static/app/views/explore/tables/index.tsx index 57ba58a40638bc..e2f02969028f06 100644 --- a/static/app/views/explore/tables/index.tsx +++ b/static/app/views/explore/tables/index.tsx @@ -1,4 +1,4 @@ -import {Fragment, useEffect} from 'react'; +import {Fragment, useEffect, useMemo} from 'react'; import {FeatureBadge} from '@sentry/scraps/badge'; import {Button} from '@sentry/scraps/button'; @@ -9,8 +9,11 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {IconEdit} from 'sentry/icons/iconEdit'; import {t} from 'sentry/locale'; +import type {TagCollection} from 'sentry/types/group'; import type {Confidence} from 'sentry/types/organization'; +import {FieldKind} from 'sentry/utils/fields'; import {AttributeBreakdownsContent} from 'sentry/views/explore/components/attributeBreakdowns/content'; +import {prettifyAttributeName} from 'sentry/views/explore/components/traceItemAttributes/utils'; import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; import type {AggregatesTableResult} from 'sentry/views/explore/hooks/useExploreAggregatesTable'; import type {SpansTableResult} from 'sentry/views/explore/hooks/useExploreSpansTable'; @@ -24,11 +27,13 @@ import { useSetQueryParamsAggregateFields, useSetQueryParamsFields, } from 'sentry/views/explore/queryParams/context'; +import {useValidateSpansTab} from 'sentry/views/explore/spans/hooks/useValidateSpansTab'; import {AggregateColumnEditorModal} from 'sentry/views/explore/tables/aggregateColumnEditorModal'; import {AggregatesTable} from 'sentry/views/explore/tables/aggregatesTable'; import {ColumnEditorModal} from 'sentry/views/explore/tables/columnEditorModal'; import {SpansTable} from 'sentry/views/explore/tables/spansTable'; import {TracesTable} from 'sentry/views/explore/tables/tracesTable/index'; +import type {EventValidationData} from 'sentry/views/explore/utils/validateEventParamsOptions'; interface BaseExploreTablesProps { confidences: Confidence[]; @@ -58,17 +63,51 @@ export function ExploreTables(props: ExploreTablesProps) { const {attributes: numberTags} = useSpanItemAttributes({}, 'number'); const {attributes: stringTags} = useSpanItemAttributes({}, 'string'); const {attributes: booleanTags} = useSpanItemAttributes({}, 'boolean'); + const {data: validatedColumnsData, isFetching: isValidatingColumns} = + useValidateSpansTab({ + enabled: tab === Tab.SPAN, + }); + const { + validatedBooleanTags, + validatedFields, + validatedNumberTags, + validatedStringTags, + } = useMemo( + () => + getValidatedColumnEditorData({ + booleanTags, + fields, + numberTags, + stringTags, + validatedColumnsData, + }), + [booleanTags, fields, numberTags, stringTags, validatedColumnsData] + ); + + useEffect(() => { + if (tab !== Tab.SPAN || isValidatingColumns) { + return; + } + + const fieldsChanged = + validatedFields.length !== fields.length || + validatedFields.some((field, index) => field !== fields[index]); + + if (fieldsChanged) { + setFields([...validatedFields]); + } + }, [fields, isValidatingColumns, setFields, tab, validatedFields]); const openColumnEditor = () => { openModal( modalProps => ( ), {closeEvents: 'escape-key'} @@ -161,3 +200,64 @@ export function ExploreTables(props: ExploreTablesProps) { ); } + +function getValidatedColumnEditorData({ + booleanTags, + fields, + numberTags, + stringTags, + validatedColumnsData, +}: { + booleanTags: TagCollection; + fields: readonly string[]; + numberTags: TagCollection; + stringTags: TagCollection; + validatedColumnsData?: EventValidationData; +}) { + const validatedBooleanTags = {...booleanTags}; + const validatedNumberTags = {...numberTags}; + const validatedStringTags = {...stringTags}; + const invalidFields = new Set(); + + for (const item of validatedColumnsData?.field ?? []) { + if (!item.name) { + continue; + } + + if (!item.valid) { + invalidFields.add(item.name); + continue; + } + + if (item.attrType === 'boolean') { + validatedBooleanTags[item.name] ??= { + key: item.name, + name: prettifyAttributeName(item.name), + kind: FieldKind.BOOLEAN, + }; + } + + if (item.attrType === 'number') { + validatedNumberTags[item.name] ??= { + key: item.name, + name: prettifyAttributeName(item.name), + kind: FieldKind.MEASUREMENT, + }; + } + + if (item.attrType === 'string') { + validatedStringTags[item.name] ??= { + key: item.name, + name: prettifyAttributeName(item.name), + kind: FieldKind.TAG, + }; + } + } + + return { + validatedBooleanTags, + validatedFields: fields.filter(field => !invalidFields.has(field)), + validatedNumberTags, + validatedStringTags, + }; +}