Skip to content

Commit 4cd7899

Browse files
nsdeschenescodex
andcommitted
feat(metrics): Separate aggregate and equation panels
Render aggregate queries and equations in their own sortable sections on the refreshed metrics tab. This keeps each panel group visually distinct while preserving the drag state needed for section-local reordering. Co-Authored-By: Codex <noreply@openai.com>
1 parent 67443d8 commit 4cd7899

File tree

2 files changed

+157
-37
lines changed

2 files changed

+157
-37
lines changed

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@ import {
1515

1616
import type {DatePageFilterProps} from 'sentry/components/pageFilters/date/datePageFilter';
1717
import {trackAnalytics} from 'sentry/utils/analytics';
18+
import {EQUATION_PREFIX} from 'sentry/utils/discover/fields';
1819
import {MetricsTabContent} from 'sentry/views/explore/metrics/metricsTab';
1920
import {MultiMetricsQueryParamsProvider} from 'sentry/views/explore/metrics/multiMetricsQueryParams';
21+
import {
22+
VisualizeEquation,
23+
VisualizeFunction,
24+
} from 'sentry/views/explore/queryParams/visualize';
2025

2126
jest.mock('sentry/utils/analytics');
2227
const trackAnalyticsMock = jest.mocked(trackAnalytics);
@@ -613,6 +618,65 @@ describe('MetricsTabContent', () => {
613618
expect(screen.getByText('Add Equation')).toBeInTheDocument();
614619
});
615620

621+
it('renders aggregate and equation panels in separate sections in refresh layout', async () => {
622+
const orgWithFeatures = OrganizationFixture({
623+
features: [
624+
'tracemetrics-enabled',
625+
'tracemetrics-equations-in-explore',
626+
'tracemetrics-ui-refresh',
627+
],
628+
});
629+
MockApiClient.addMockResponse({
630+
url: `/organizations/${orgWithFeatures.slug}/events/`,
631+
method: 'GET',
632+
body: {data: []},
633+
});
634+
635+
render(
636+
<ProviderWrapper>
637+
<MetricsTabContent datePageFilterProps={datePageFilterProps} />
638+
</ProviderWrapper>,
639+
{
640+
organization: orgWithFeatures,
641+
initialRouterConfig: {
642+
location: {
643+
pathname: '/organizations/:orgId/explore/metrics/',
644+
query: {
645+
start: '2025-04-10T14%3A37%3A55',
646+
end: '2025-04-10T20%3A04%3A51',
647+
metric: [
648+
JSON.stringify({
649+
metric: {name: 'bar', type: 'distribution'},
650+
query: '',
651+
aggregateFields: [
652+
new VisualizeFunction('p50(value,bar,distribution,-)').serialize(),
653+
],
654+
aggregateSortBys: [],
655+
mode: 'samples',
656+
}),
657+
JSON.stringify({
658+
metric: {name: '', type: ''},
659+
query: '',
660+
aggregateFields: [new VisualizeEquation(EQUATION_PREFIX).serialize()],
661+
aggregateSortBys: [],
662+
mode: 'samples',
663+
}),
664+
],
665+
title: 'Test Title',
666+
},
667+
},
668+
route: '/organizations/:orgId/explore/metrics/',
669+
},
670+
}
671+
);
672+
673+
const aggregateSection = await screen.findByTestId('aggregate-metric-panels');
674+
const equationSection = screen.getByTestId('equation-metric-panels');
675+
676+
expect(within(aggregateSection).getAllByTestId('metric-panel')).toHaveLength(1);
677+
expect(within(equationSection).getAllByTestId('metric-panel')).toHaveLength(1);
678+
});
679+
616680
it('disables both Add Metric and Add Equation buttons when the maximum number of metric queries is reached', async () => {
617681
const metricQueryWithGroupBy = JSON.stringify({
618682
metric: {name: 'bar', type: 'distribution'},

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

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {SortableContext, verticalListSortingStrategy} from '@dnd-kit/sortable';
33
import styled from '@emotion/styled';
44

55
import {Container, Flex, Stack} from '@sentry/scraps/layout';
6+
import {Separator} from '@sentry/scraps/separator';
67

78
import * as Layout from 'sentry/components/layouts/thirds';
89
import type {DatePageFilterProps} from 'sentry/components/pageFilters/date/datePageFilter';
@@ -42,6 +43,7 @@ import {
4243
FilterBarWithSaveAsContainer,
4344
StyledPageFilterBar,
4445
} from 'sentry/views/explore/metrics/styles';
46+
import {isVisualizeEquation} from 'sentry/views/explore/queryParams/visualize';
4547
export const METRICS_CHART_GROUP = 'metrics-charts-group';
4648

4749
type MetricsTabProps = {
@@ -218,8 +220,19 @@ function MetricsTabBodySection() {
218220
isMetricOptionsEmpty,
219221
});
220222
const references = useMetricReferences();
221-
const {sortableItems, sensors, onDragStart, onDragEnd, onDragCancel, isDragging} =
222-
useSortableMetricQueries();
223+
const aggregateMetricQueries = useSortableMetricQueries({
224+
predicate: metricQuery =>
225+
!isVisualizeEquation(metricQuery.queryParams.visualizes[0]!),
226+
});
227+
const equationMetricQueries = useSortableMetricQueries({
228+
predicate: metricQuery => isVisualizeEquation(metricQuery.queryParams.visualizes[0]!),
229+
});
230+
const isDragging =
231+
aggregateMetricQueries.isDragging || equationMetricQueries.isDragging;
232+
const showSectionSeparator =
233+
isDragging &&
234+
aggregateMetricQueries.sortableItems.length > 0 &&
235+
equationMetricQueries.sortableItems.length > 0;
223236

224237
// Cannot add metric queries beyond Z
225238
const isAddMetricDisabled =
@@ -231,41 +244,27 @@ function MetricsTabBodySection() {
231244
<ExploreContentSection>
232245
<Stack>
233246
<WidgetSyncContextProvider groupName={METRICS_CHART_GROUP}>
234-
<DndContext
235-
sensors={sensors}
236-
collisionDetection={closestCenter}
237-
onDragStart={onDragStart}
238-
onDragEnd={onDragEnd}
239-
onDragCancel={onDragCancel}
240-
>
241-
<SortableContext
242-
items={sortableItems}
243-
strategy={verticalListSortingStrategy}
244-
>
245-
{sortableItems.map(({id, metricQuery}, index) => {
246-
return (
247-
<MetricsQueryParamsProvider
248-
key={id}
249-
queryParams={metricQuery.queryParams}
250-
setQueryParams={metricQuery.setQueryParams}
251-
traceMetric={metricQuery.metric}
252-
setTraceMetric={metricQuery.setTraceMetric}
253-
removeMetric={metricQuery.removeMetric}
254-
>
255-
<SortableMetricPanel
256-
sortableId={id}
257-
traceMetric={metricQuery.metric}
258-
queryIndex={index}
259-
queryLabel={metricQuery.label ?? ''}
260-
references={references}
261-
isAnyDragging={isDragging}
262-
canDrag={sortableItems.length > 1}
263-
/>
264-
</MetricsQueryParamsProvider>
265-
);
266-
})}
267-
</SortableContext>
268-
</DndContext>
247+
<SortableMetricPanelSection
248+
dataTestId="aggregate-metric-panels"
249+
sortableQueries={aggregateMetricQueries}
250+
references={references}
251+
isAnyDragging={isDragging}
252+
/>
253+
{showSectionSeparator ? (
254+
<Container paddingBottom="xl">
255+
<Separator
256+
orientation="horizontal"
257+
border="primary"
258+
data-test-id="metric-section-separator"
259+
/>
260+
</Container>
261+
) : null}
262+
<SortableMetricPanelSection
263+
dataTestId="equation-metric-panels"
264+
sortableQueries={equationMetricQueries}
265+
references={references}
266+
isAnyDragging={isDragging}
267+
/>
269268
<Flex gap="sm" direction="row">
270269
<ToolbarVisualizeAddChart
271270
add={addMetricQuery}
@@ -319,6 +318,63 @@ function MetricsTabBodySection() {
319318
);
320319
}
321320

321+
interface SortableMetricPanelSectionProps {
322+
dataTestId: string;
323+
isAnyDragging: boolean;
324+
references: Set<string>;
325+
sortableQueries: ReturnType<typeof useSortableMetricQueries>;
326+
}
327+
328+
function SortableMetricPanelSection({
329+
dataTestId,
330+
sortableQueries,
331+
references,
332+
isAnyDragging,
333+
}: SortableMetricPanelSectionProps) {
334+
const {sortableItems, sensors, onDragStart, onDragEnd, onDragCancel} = sortableQueries;
335+
336+
if (!sortableItems.length) {
337+
return null;
338+
}
339+
340+
return (
341+
<Stack data-test-id={dataTestId}>
342+
<DndContext
343+
sensors={sensors}
344+
collisionDetection={closestCenter}
345+
onDragStart={onDragStart}
346+
onDragEnd={onDragEnd}
347+
onDragCancel={onDragCancel}
348+
>
349+
<SortableContext items={sortableItems} strategy={verticalListSortingStrategy}>
350+
{sortableItems.map(({id, metricQuery, index}) => {
351+
return (
352+
<MetricsQueryParamsProvider
353+
key={id}
354+
queryParams={metricQuery.queryParams}
355+
setQueryParams={metricQuery.setQueryParams}
356+
traceMetric={metricQuery.metric}
357+
setTraceMetric={metricQuery.setTraceMetric}
358+
removeMetric={metricQuery.removeMetric}
359+
>
360+
<SortableMetricPanel
361+
sortableId={id}
362+
traceMetric={metricQuery.metric}
363+
queryIndex={index}
364+
queryLabel={metricQuery.label ?? ''}
365+
references={references}
366+
isAnyDragging={isAnyDragging}
367+
canDrag={sortableItems.length > 1}
368+
/>
369+
</MetricsQueryParamsProvider>
370+
);
371+
})}
372+
</SortableContext>
373+
</DndContext>
374+
</Stack>
375+
);
376+
}
377+
322378
const MetricsQueryBuilderContainer = styled(Container)`
323379
padding: ${p => p.theme.space.xl};
324380
background-color: ${p => p.theme.tokens.background.primary};

0 commit comments

Comments
 (0)