Skip to content

Commit 43b99a2

Browse files
committed
feat(tracemetrics): Add column sorting to samples table
Wire up click-to-sort on metrics samples table headers and persist sort state in the URL. The aggregates table already had sorting; this brings the samples table to parity.
1 parent 5402dcb commit 43b99a2

File tree

4 files changed

+107
-3
lines changed

4 files changed

+107
-3
lines changed

static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableHeader.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {ReactNode} from 'react';
33
import {Tooltip} from '@sentry/scraps/tooltip';
44

55
import {t} from 'sentry/locale';
6+
import type {Sort} from 'sentry/utils/discover/fields';
67
import {
78
StyledSimpleTableHeader,
89
StyledSimpleTableHeaderCell,
@@ -13,7 +14,15 @@ import {
1314
type SampleTableColumnKey,
1415
} from 'sentry/views/explore/metrics/types';
1516
import {getMetricTableColumnType} from 'sentry/views/explore/metrics/utils';
16-
import {useQueryParamsSortBys} from 'sentry/views/explore/queryParams/context';
17+
import {
18+
useQueryParamsSortBys,
19+
useSetQueryParamsSortBys,
20+
} from 'sentry/views/explore/queryParams/context';
21+
22+
const NON_SORTABLE_COLUMNS = new Set<SampleTableColumnKey>([
23+
VirtualTableSampleColumnKey.EXPAND_ROW,
24+
VirtualTableSampleColumnKey.PROJECT_BADGE,
25+
]);
1726

1827
interface MetricsSamplesTableHeaderProps {
1928
columns: SampleTableColumnKey[];
@@ -25,6 +34,7 @@ export function MetricsSamplesTableHeader({
2534
embedded,
2635
}: MetricsSamplesTableHeaderProps) {
2736
const sorts = useQueryParamsSortBys();
37+
const setSorts = useSetQueryParamsSortBys();
2838

2939
return (
3040
<StyledSimpleTableHeader>
@@ -37,6 +47,7 @@ export function MetricsSamplesTableHeader({
3747
field={field}
3848
index={i}
3949
sort={sorts.find(s => s.field === field)?.kind}
50+
setSorts={setSorts}
4051
embedded={embedded}
4152
>
4253
{label}
@@ -52,23 +63,32 @@ function FieldHeaderCellWrapper({
5263
children,
5364
index,
5465
sort,
66+
setSorts,
5567
embedded = false,
5668
}: {
5769
children: ReactNode;
5870
field: SampleTableColumnKey;
5971
index: number;
72+
setSorts: (sorts: Sort[]) => void;
6073
embedded?: boolean;
6174
sort?: 'asc' | 'desc';
6275
}) {
6376
const columnType = getMetricTableColumnType(field);
6477
const label = getFieldLabel(field);
6578
const hasPadding = field !== VirtualTableSampleColumnKey.EXPAND_ROW;
79+
const canSort = !NON_SORTABLE_COLUMNS.has(field);
80+
81+
function handleSortClick() {
82+
const kind = sort === 'desc' ? 'asc' : 'desc';
83+
setSorts([{field, kind}]);
84+
}
6685

6786
if (columnType === 'metric_value') {
6887
return (
6988
<StyledSimpleTableHeaderCell
7089
key={index}
7190
sort={sort}
91+
handleSortClick={canSort ? handleSortClick : undefined}
7292
style={{
7393
justifyContent: 'flex-end',
7494
paddingRight: 'calc(12px + 15px)', // 12px is the padding of the cell, 15px is the width of the scrollbar.
@@ -86,6 +106,7 @@ function FieldHeaderCellWrapper({
86106
<StyledSimpleTableHeaderCell
87107
key={index}
88108
sort={sort}
109+
handleSortClick={canSort ? handleSortClick : undefined}
89110
noPadding={!hasPadding}
90111
embedded={embedded}
91112
>

static/app/views/explore/metrics/metricQuery.spec.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,62 @@ describe('decodeMetricsQueryParams', () => {
112112
expect(decoded?.queryParams.aggregateFields).toEqual(
113113
original.queryParams.aggregateFields
114114
);
115+
expect(decoded?.queryParams.sortBys).toEqual(original.queryParams.sortBys);
116+
});
117+
118+
it('round-trips custom sortBys through encode/decode', () => {
119+
const original = {
120+
metric: {name: 'test_metric', type: 'counter'},
121+
queryParams: new ReadableQueryParams({
122+
extrapolate: true,
123+
mode: Mode.SAMPLES,
124+
query: '',
125+
cursor: '',
126+
fields: ['id', 'timestamp'],
127+
sortBys: [{field: 'value', kind: 'asc' as const}],
128+
aggregateCursor: '',
129+
aggregateFields: [new VisualizeFunction('sum(value,test_metric,counter,-)')],
130+
aggregateSortBys: [
131+
{field: 'sum(value,test_metric,counter,-)', kind: 'desc' as const},
132+
],
133+
}),
134+
};
135+
136+
const encoded = encodeMetricQueryParams(original);
137+
const decoded = decodeMetricsQueryParams(encoded);
138+
139+
expect(decoded).not.toBeNull();
140+
expect(decoded?.queryParams.sortBys).toEqual([{field: 'value', kind: 'asc'}]);
141+
});
142+
143+
it('falls back to default sortBys when missing from JSON', () => {
144+
const json = JSON.stringify({
145+
metric: {name: 'test_metric', type: 'counter'},
146+
query: '',
147+
aggregateFields: [{yAxes: ['sum(value,test_metric,counter,-)']}],
148+
aggregateSortBys: [],
149+
mode: 'samples',
150+
});
151+
152+
const result = decodeMetricsQueryParams(json);
153+
154+
expect(result).not.toBeNull();
155+
expect(result?.queryParams.sortBys).toEqual([{field: 'timestamp', kind: 'desc'}]);
156+
});
157+
158+
it('falls back to default sortBys when format is invalid', () => {
159+
const json = JSON.stringify({
160+
metric: {name: 'test_metric', type: 'counter'},
161+
query: '',
162+
aggregateFields: [{yAxes: ['sum(value,test_metric,counter,-)']}],
163+
aggregateSortBys: [],
164+
sortBys: [{field: 'value', kind: 'invalid'}],
165+
mode: 'samples',
166+
});
167+
168+
const result = decodeMetricsQueryParams(json);
169+
170+
expect(result).not.toBeNull();
171+
expect(result?.queryParams.sortBys).toEqual([{field: 'timestamp', kind: 'desc'}]);
115172
});
116173
});

static/app/views/explore/metrics/metricQuery.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export function decodeMetricsQueryParams(value: string): BaseMetricQuery | null
6767
const groupBys = parseGroupBys(json.aggregateFields);
6868
const aggregateFields = [...visualizes, ...groupBys];
6969
const aggregateSortBys = parseAggregateSortBys(json.aggregateSortBys, aggregateFields);
70+
const fields = defaultFields();
71+
const sortBys = parseSortBys(json.sortBys, fields);
7072

7173
return {
7274
metric,
@@ -76,8 +78,8 @@ export function decodeMetricsQueryParams(value: string): BaseMetricQuery | null
7678
query,
7779

7880
cursor: '',
79-
fields: defaultFields(),
80-
sortBys: defaultSortBys(defaultFields()),
81+
fields,
82+
sortBys,
8183

8284
aggregateCursor: '',
8385
aggregateFields,
@@ -99,6 +101,7 @@ export function encodeMetricQueryParams(metricQuery: BaseMetricQuery): string {
99101
return field;
100102
}),
101103
aggregateSortBys: metricQuery.queryParams.aggregateSortBys,
104+
sortBys: metricQuery.queryParams.sortBys,
102105
mode: metricQuery.queryParams.mode,
103106
});
104107
}
@@ -219,3 +222,25 @@ function parseAggregateSortBys(
219222

220223
return value;
221224
}
225+
226+
function parseSortBys(value: unknown, fields: string[]): Sort[] {
227+
if (!Array.isArray(value) || value.length === 0) {
228+
return defaultSortBys(fields);
229+
}
230+
231+
const isValid = value.every(
232+
(v: unknown) =>
233+
v !== null &&
234+
typeof v === 'object' &&
235+
'field' in v &&
236+
typeof v.field === 'string' &&
237+
'kind' in v &&
238+
(v.kind === 'asc' || v.kind === 'desc')
239+
);
240+
241+
if (!isValid) {
242+
return defaultSortBys(fields);
243+
}
244+
245+
return value as Sort[];
246+
}

static/app/views/explore/metrics/metricsQueryParams.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export function MetricsQueryParamsProvider({
6262
query: getUpdatedValue(writableQueryParams.query, defaultQuery),
6363
aggregateFields: writableQueryParams.aggregateFields,
6464
aggregateSortBys: writableQueryParams.aggregateSortBys,
65+
sortBys: writableQueryParams.sortBys,
6566
mode: writableQueryParams.mode,
6667
});
6768

0 commit comments

Comments
 (0)