diff --git a/static/app/views/explore/components/typeBadge.tsx b/static/app/views/explore/components/typeBadge.tsx new file mode 100644 index 00000000000000..d42d82fdc4344c --- /dev/null +++ b/static/app/views/explore/components/typeBadge.tsx @@ -0,0 +1,18 @@ +import BadgeTag from 'sentry/components/badge/tag'; +import {t} from 'sentry/locale'; +import type {Tag} from 'sentry/types/group'; +import {FieldKind} from 'sentry/utils/fields'; + +interface TypeBadgeProps { + tag?: Tag; +} + +export function TypeBadge({tag}: TypeBadgeProps) { + if (tag?.kind === FieldKind.MEASUREMENT) { + return {t('number')}; + } + if (tag?.kind === FieldKind.TAG) { + return {t('string')}; + } + return null; +} diff --git a/static/app/views/explore/contexts/spanTagsContext.tsx b/static/app/views/explore/contexts/spanTagsContext.tsx index 87392760546754..234d8d13c2b829 100644 --- a/static/app/views/explore/contexts/spanTagsContext.tsx +++ b/static/app/views/explore/contexts/spanTagsContext.tsx @@ -4,9 +4,11 @@ import {createContext, useContext, useMemo} from 'react'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import type {Tag, TagCollection} from 'sentry/types/group'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; +import {FieldKind} from 'sentry/utils/fields'; import {useApiQuery} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; +import {SpanIndexedField} from 'sentry/views/insights/types'; import { useSpanFieldStaticTags, useSpanFieldSupportedTags, @@ -25,6 +27,21 @@ interface SpanTagsProviderProps { } export function SpanTagsProvider({children, dataset}: SpanTagsProviderProps) { + const numericSpanFields: Set = useMemo(() => { + return new Set([ + SpanIndexedField.SPAN_DURATION, + SpanIndexedField.SPAN_SELF_TIME, + SpanIndexedField.INP, + SpanIndexedField.INP_SCORE, + SpanIndexedField.INP_SCORE_WEIGHT, + SpanIndexedField.TOTAL_SCORE, + SpanIndexedField.CACHE_ITEM_SIZE, + SpanIndexedField.MESSAGING_MESSAGE_BODY_SIZE, + SpanIndexedField.MESSAGING_MESSAGE_RECEIVE_LATENCY, + SpanIndexedField.MESSAGING_MESSAGE_RETRY_COUNT, + ]); + }, []); + const supportedTags = useSpanFieldSupportedTags(); const numberTags: TagCollection = useTypedSpanTags({ @@ -44,8 +61,15 @@ export function SpanTagsProvider({children, dataset}: SpanTagsProviderProps) { return {}; } - return numberTags; - }, [dataset, numberTags]); + return { + ...numberTags, + ...Object.fromEntries( + Object.entries(staticTags) + .filter(([key, _]) => numericSpanFields.has(key)) + .map(([key, tag]) => [key, {...tag, kind: FieldKind.MEASUREMENT}]) + ), + }; + }, [dataset, numberTags, numericSpanFields, staticTags]); const allStringTags = useMemo(() => { if (dataset === DiscoverDatasets.SPANS_INDEXED) { @@ -54,9 +78,13 @@ export function SpanTagsProvider({children, dataset}: SpanTagsProviderProps) { return { ...stringTags, - ...staticTags, + ...Object.fromEntries( + Object.entries(staticTags) + .filter(([key, _]) => !numericSpanFields.has(key)) + .map(([key, tag]) => [key, {...tag, kind: FieldKind.TAG}]) + ), }; - }, [dataset, supportedTags, stringTags, staticTags]); + }, [dataset, supportedTags, stringTags, staticTags, numericSpanFields]); const tags = { number: allNumberTags, @@ -66,7 +94,7 @@ export function SpanTagsProvider({children, dataset}: SpanTagsProviderProps) { return {children}; } -export const useSpanTags = (type?: 'number' | 'string') => { +export function useSpanTags(type?: 'number' | 'string') { const typedTags = useContext(SpanTagsContext); if (typedTags === undefined) { @@ -77,7 +105,14 @@ export const useSpanTags = (type?: 'number' | 'string') => { return typedTags.number; } return typedTags.string; -}; +} + +export function useSpanTag(key: string) { + const numberTags = useSpanTags('number'); + const stringTags = useSpanTags('string'); + + return stringTags[key] ?? numberTags[key] ?? null; +} function useTypedSpanTags({ enabled, @@ -123,14 +158,17 @@ function useTypedSpanTags({ continue; } - allTags[tag.key] = { - key: tag.key, - name: tag.name, + const key = type === 'number' ? `tags[${tag.key},number]` : tag.key; + + allTags[key] = { + key, + name: tag.key, + kind: type === 'number' ? FieldKind.MEASUREMENT : FieldKind.TAG, }; } return allTags; - }, [result]); + }, [result, type]); return tags; } diff --git a/static/app/views/explore/hooks/useResultsMode.spec.tsx b/static/app/views/explore/hooks/useResultsMode.spec.tsx index 40c05b69fec7bc..21e1019041131b 100644 --- a/static/app/views/explore/hooks/useResultsMode.spec.tsx +++ b/static/app/views/explore/hooks/useResultsMode.spec.tsx @@ -40,7 +40,7 @@ describe('useResultMode', function () { expect(resultMode).toEqual('samples'); // default expect(sampleFields).toEqual([ 'project', - 'id', + 'span_id', 'span.op', 'span.description', 'span.duration', @@ -57,7 +57,7 @@ describe('useResultMode', function () { expect(sampleFields).toEqual([ 'project', - 'id', + 'span_id', 'span.op', 'span.description', 'span.duration', diff --git a/static/app/views/explore/hooks/useSampleFields.spec.tsx b/static/app/views/explore/hooks/useSampleFields.spec.tsx index bfb1b47f24a833..20fdd9d3231894 100644 --- a/static/app/views/explore/hooks/useSampleFields.spec.tsx +++ b/static/app/views/explore/hooks/useSampleFields.spec.tsx @@ -15,7 +15,7 @@ describe('useSampleFields', function () { expect(sampleFields).toEqual([ 'project', - 'id', + 'span_id', 'span.op', 'span.description', 'span.duration', @@ -28,7 +28,7 @@ describe('useSampleFields', function () { act(() => setSampleFields([])); expect(sampleFields).toEqual([ 'project', - 'id', + 'span_id', 'span.op', 'span.description', 'span.duration', diff --git a/static/app/views/explore/hooks/useSampleFields.tsx b/static/app/views/explore/hooks/useSampleFields.tsx index b904b9cb67f59a..0f36e1c2d6fd87 100644 --- a/static/app/views/explore/hooks/useSampleFields.tsx +++ b/static/app/views/explore/hooks/useSampleFields.tsx @@ -31,7 +31,14 @@ function useSampleFieldsImpl({ return fields; } - return ['project', 'id', 'span.op', 'span.description', 'span.duration', 'timestamp']; + return [ + 'project', + 'span_id', + 'span.op', + 'span.description', + 'span.duration', + 'timestamp', + ]; }, [location.query.field]); const setSampleFields = useCallback( diff --git a/static/app/views/explore/tables/columnEditorModal.spec.tsx b/static/app/views/explore/tables/columnEditorModal.spec.tsx index 0a57350f14a0fd..976d33940b43f1 100644 --- a/static/app/views/explore/tables/columnEditorModal.spec.tsx +++ b/static/app/views/explore/tables/columnEditorModal.spec.tsx @@ -1,20 +1,28 @@ import {act, renderGlobalModal, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {openModal} from 'sentry/actionCreators/modal'; +import type {TagCollection} from 'sentry/types/group'; import {ColumnEditorModal} from 'sentry/views/explore/tables/columnEditorModal'; -const tagOptions = { +const stringTags: TagCollection = { id: { key: 'id', - name: 'ID', + name: 'id', }, project: { key: 'project', - name: 'Project', + name: 'project', }, 'span.op': { key: 'span.op', - name: 'Span OP', + name: 'span.op', + }, +}; + +const numberTags: TagCollection = { + 'span.duration': { + key: 'span.duration', + name: 'span.duration', }, }; @@ -36,7 +44,8 @@ describe('ColumnEditorModal', function () { {...modalProps} columns={['id', 'project']} onColumnsChange={() => {}} - tags={tagOptions} + stringTags={stringTags} + numberTags={numberTags} /> ), {onClose} @@ -60,7 +69,8 @@ describe('ColumnEditorModal', function () { {...modalProps} columns={['id', 'project']} onColumnsChange={onColumnsChange} - tags={tagOptions} + stringTags={stringTags} + numberTags={numberTags} /> ), {onClose: jest.fn()} @@ -98,7 +108,8 @@ describe('ColumnEditorModal', function () { {...modalProps} columns={['id', 'project']} onColumnsChange={onColumnsChange} - tags={tagOptions} + stringTags={stringTags} + numberTags={numberTags} /> ), {onClose: jest.fn()} @@ -117,14 +128,14 @@ describe('ColumnEditorModal', function () { expect(column).toHaveTextContent(columns2[i]); }); - const options = ['id', 'project', 'span.op']; + const options = ['id', 'project', 'span.duration', 'span.op']; await userEvent.click(screen.getByRole('button', {name: 'None'})); const columnOptions = await screen.findAllByRole('option'); columnOptions.forEach((option, i) => { expect(option).toHaveTextContent(options[i]); }); - await userEvent.click(columnOptions[2]); + await userEvent.click(columnOptions[3]); const columns3 = ['id', 'project', 'span.op']; screen.getAllByTestId('editor-column').forEach((column, i) => { expect(column).toHaveTextContent(columns3[i]); @@ -146,7 +157,8 @@ describe('ColumnEditorModal', function () { {...modalProps} columns={['id', 'project']} onColumnsChange={onColumnsChange} - tags={tagOptions} + stringTags={stringTags} + numberTags={numberTags} /> ), {onClose: jest.fn()} @@ -158,14 +170,14 @@ describe('ColumnEditorModal', function () { expect(column).toHaveTextContent(columns1[i]); }); - const options = ['id', 'project', 'span.op']; + const options = ['id', 'project', 'span.duration', 'span.op']; await userEvent.click(screen.getByRole('button', {name: 'project'})); const columnOptions = await screen.findAllByRole('option'); columnOptions.forEach((option, i) => { expect(option).toHaveTextContent(options[i]); }); - await userEvent.click(columnOptions[2]); + await userEvent.click(columnOptions[3]); const columns2 = ['id', 'span.op']; screen.getAllByTestId('editor-column').forEach((column, i) => { expect(column).toHaveTextContent(columns2[i]); diff --git a/static/app/views/explore/tables/columnEditorModal.tsx b/static/app/views/explore/tables/columnEditorModal.tsx index 82b16775775b1a..f115bf28497aaa 100644 --- a/static/app/views/explore/tables/columnEditorModal.tsx +++ b/static/app/views/explore/tables/columnEditorModal.tsx @@ -30,6 +30,7 @@ import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {TagCollection} from 'sentry/types/group'; import {defined} from 'sentry/utils'; +import {TypeBadge} from 'sentry/views/explore/components/typeBadge'; type Column = { column: string | undefined; @@ -38,8 +39,9 @@ type Column = { interface ColumnEditorModalProps extends ModalRenderProps { columns: string[]; + numberTags: TagCollection; onColumnsChange: (fields: string[]) => void; - tags: TagCollection; + stringTags: TagCollection; } export function ColumnEditorModal({ @@ -49,7 +51,8 @@ export function ColumnEditorModal({ closeModal, columns, onColumnsChange, - tags, + numberTags, + stringTags, }: ColumnEditorModalProps) { const [editableColumns, setEditableColumns] = useState( // falsey ids are not draggable in dndkit @@ -58,14 +61,38 @@ export function ColumnEditorModal({ const [nextId, setNextId] = useState(columns.length + 1); - const tagOptions = useMemo(() => { - return Object.values(tags).map(tag => { - return { - label: tag.key, - value: tag.key, - }; + const tags: SelectOption[] = useMemo(() => { + const allTags = [ + ...Object.values(stringTags).map(tag => { + return { + label: tag.name, + value: tag.key, + textValue: tag.name, + trailingItems: , + }; + }), + ...Object.values(numberTags).map(tag => { + return { + label: tag.name, + value: tag.key, + textValue: tag.name, + trailingItems: , + }; + }), + ]; + allTags.sort((a, b) => { + if (a.label < b.label) { + return -1; + } + + if (a.label > b.label) { + return 1; + } + + return 0; }); - }, [tags]); + return allTags; + }, [stringTags, numberTags]); function handleApply() { onColumnsChange(editableColumns.map(({column}) => column).filter(defined)); @@ -114,7 +141,7 @@ export function ColumnEditorModal({ onColumnChange={updateColumnAtIndex} onColumnDelete={deleteColumnAtIndex} onColumnSwap={swapColumnsAtIndex} - tags={tagOptions} + tags={tags} /> @@ -224,6 +251,30 @@ function ColumnEditorRow({ } } + // The compact select component uses the option label to render the current + // selection. This overrides it to render in a trailing item showing the type. + const label = useMemo(() => { + if (defined(column.column)) { + const tag = tags.find(option => option.value === column.column); + if (defined(tag)) { + return ( + + {tag.label} + {tag.trailingItems && + (typeof tag.trailingItems === 'function' + ? tag.trailingItems({ + disabled: false, + isFocused: false, + isSelected: false, + }) + : tag.trailingItems)} + + ); + } + } + return {column.column ?? t('None')}; + }, [column.column, tags]); + return ( p.theme.overflowEllipsis} + text-align: left; + line-height: normal; + display: flex; + justify-content: space-between; +`; diff --git a/static/app/views/explore/tables/index.tsx b/static/app/views/explore/tables/index.tsx index 6cd8c71c62d4ce..afb925131ff7a6 100644 --- a/static/app/views/explore/tables/index.tsx +++ b/static/app/views/explore/tables/index.tsx @@ -42,7 +42,8 @@ function ExploreAggregatesTable() { function ExploreSamplesTable() { const [tab, setTab] = useState(Tab.SPAN); const [fields, setFields] = useSampleFields(); - const tags = useSpanTags(); + const numberTags = useSpanTags('number'); + const stringTags = useSpanTags('string'); const openColumnEditor = useCallback(() => { openModal( @@ -51,12 +52,13 @@ function ExploreSamplesTable() { {...modalProps} columns={fields} onColumnsChange={setFields} - tags={tags} + stringTags={stringTags} + numberTags={numberTags} /> ), {closeEvents: 'escape-key'} ); - }, [fields, setFields, tags]); + }, [fields, setFields, stringTags, numberTags]); return ( diff --git a/static/app/views/explore/tables/spansTable.tsx b/static/app/views/explore/tables/spansTable.tsx index f88b8a64e81680..22048fcddcaa51 100644 --- a/static/app/views/explore/tables/spansTable.tsx +++ b/static/app/views/explore/tables/spansTable.tsx @@ -25,6 +25,7 @@ import { TableStatus, useTableStyles, } from 'sentry/views/explore/components/table'; +import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext'; import {useDataset} from 'sentry/views/explore/hooks/useDataset'; import {useSampleFields} from 'sentry/views/explore/hooks/useSampleFields'; import {useSorts} from 'sentry/views/explore/hooks/useSorts'; @@ -82,16 +83,22 @@ export function SpansTable({}: SpansTableProps) { const meta = result.meta ?? {}; + const numberTags = useSpanTags('number'); + const stringTags = useSpanTags('string'); + return ( - {fields.map((field, i) => ( - - {field} - - ))} + {fields.map((field, i) => { + const tag = stringTags[field] ?? numberTags[field] ?? null; + return ( + + {tag?.name ?? field} + + ); + })} diff --git a/static/app/views/explore/toolbar/index.spec.tsx b/static/app/views/explore/toolbar/index.spec.tsx index 827dadffe111aa..d02a817f553494 100644 --- a/static/app/views/explore/toolbar/index.spec.tsx +++ b/static/app/views/explore/toolbar/index.spec.tsx @@ -54,7 +54,7 @@ describe('ExploreToolbar', function () { expect(sampleFields).toEqual([ 'project', - 'id', + 'span_id', 'span.op', 'span.description', 'span.duration', @@ -81,7 +81,7 @@ describe('ExploreToolbar', function () { expect(sampleFields).toEqual([ 'project', - 'id', + 'span_id', 'span.op', 'span.description', 'span.duration', @@ -188,7 +188,7 @@ describe('ExploreToolbar', function () { // check the default field options const fields = [ 'project', - 'id', + 'span_id', 'span.op', 'span.description', 'span.duration', diff --git a/static/app/views/explore/toolbar/toolbarGroupBy.tsx b/static/app/views/explore/toolbar/toolbarGroupBy.tsx index c8a15b6832151c..646b3f6242b6ff 100644 --- a/static/app/views/explore/toolbar/toolbarGroupBy.tsx +++ b/static/app/views/explore/toolbar/toolbarGroupBy.tsx @@ -40,6 +40,7 @@ export function ToolbarGroupBy({disabled}: ToolbarGroupByProps) { return { label: groupBy, value: groupBy, + textValue: groupBy, }; }); @@ -47,12 +48,13 @@ export function ToolbarGroupBy({disabled}: ToolbarGroupByProps) { return { label: tagKey, value: tagKey, + textValue: tagKey, }; }); return [ // hard code in an empty option - {label: t('None'), value: ''}, + {label: t('None'), value: '', textValue: t('none')}, ...unknownOptions, ...knownOptions, ]; diff --git a/static/app/views/explore/toolbar/toolbarSortBy.tsx b/static/app/views/explore/toolbar/toolbarSortBy.tsx index 83d90a191e3163..f5dc7636185e6c 100644 --- a/static/app/views/explore/toolbar/toolbarSortBy.tsx +++ b/static/app/views/explore/toolbar/toolbarSortBy.tsx @@ -4,6 +4,8 @@ import type {SelectOption} from 'sentry/components/compactSelect'; import {CompactSelect} from 'sentry/components/compactSelect'; import {t} from 'sentry/locale'; import type {Sort} from 'sentry/utils/discover/fields'; +import {TypeBadge} from 'sentry/views/explore/components/typeBadge'; +import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext'; import type {Field} from 'sentry/views/explore/hooks/useSampleFields'; import {ToolbarHeader, ToolbarLabel, ToolbarRow, ToolbarSection} from './styles'; @@ -15,14 +17,20 @@ interface ToolbarSortByProps { } export function ToolbarSortBy({fields, setSorts, sorts}: ToolbarSortByProps) { + const numberTags = useSpanTags('number'); + const stringTags = useSpanTags('string'); + const fieldOptions: SelectOption[] = useMemo(() => { return fields.map(field => { + const tag = stringTags[field] ?? numberTags[field] ?? null; return { - label: field, - value: field, + label: tag?.name ?? field, + value: tag?.key ?? field, + textValue: tag?.name ?? field, + trailingItems: , }; }); - }, [fields]); + }, [fields, numberTags, stringTags]); const setSortField = useCallback( (i: number, {value}: SelectOption) => { @@ -43,10 +51,12 @@ export function ToolbarSortBy({fields, setSorts, sorts}: ToolbarSortByProps) { { label: 'Desc', value: 'desc', + textValue: t('Descending'), }, { label: 'Asc', value: 'asc', + textValue: t('Ascending'), }, ]; }, []); diff --git a/static/app/views/explore/toolbar/toolbarVisualize.tsx b/static/app/views/explore/toolbar/toolbarVisualize.tsx index a0d1c1a6ba3c7f..a3f8ea6c38df6d 100644 --- a/static/app/views/explore/toolbar/toolbarVisualize.tsx +++ b/static/app/views/explore/toolbar/toolbarVisualize.tsx @@ -46,6 +46,7 @@ export function ToolbarVisualize({}: ToolbarVisualizeProps) { return { label: field, value: field, + textValue: field, }; }); @@ -54,6 +55,7 @@ export function ToolbarVisualize({}: ToolbarVisualizeProps) { return { label: aggregate, value: aggregate, + textValue: aggregate, }; });