Skip to content

Commit 8246cf3

Browse files
authored
feat(dashboards-eap): Allow multiple queries for spans dataset (#92399)
- The "Add Filter" button should appear for the spans dataset in the widget builder - A widget should be saveable with multiple filters - When a widget is "Open in Explore"'d, if there's a single query (i.e. one filter) it will direct users to the regular explore page. If there are multiple, we send them to the comparison view Since multiple filters are only allowed for timeseries charts, those are the only types we expect to handle with this method
1 parent 8550400 commit 8246cf3

File tree

5 files changed

+158
-13
lines changed

5 files changed

+158
-13
lines changed

static/app/views/dashboards/utils/getWidgetExploreUrl.spec.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,52 @@ describe('getWidgetExploreUrl', () => {
156156
'&query=%28span.description%3Atest%29%20release%3A%5B%221.0.0%22%2C%222.0.0%22%5D%20'
157157
);
158158
});
159+
160+
it('returns the correct url for multiple queries', () => {
161+
const widget = WidgetFixture({
162+
displayType: DisplayType.LINE,
163+
queries: [
164+
{
165+
fields: [],
166+
aggregates: ['count(span.duration)', 'avg(span.duration)'],
167+
columns: ['span.description'],
168+
conditions: 'is_transaction:true',
169+
orderby: '',
170+
name: '',
171+
},
172+
{
173+
fields: [],
174+
aggregates: ['avg(span.duration)'],
175+
columns: ['span.description'],
176+
conditions: 'is_transaction:false',
177+
orderby: '',
178+
name: '',
179+
},
180+
],
181+
});
182+
183+
const url = getWidgetExploreUrl(widget, undefined, selection, organization);
184+
185+
// Provide a fake base URL to allow parsing the relative URL
186+
const urlObject = new URL(url, 'https://www.example.com');
187+
expect(urlObject.pathname).toBe('/organizations/org-slug/explore/traces/compare/');
188+
189+
expect(urlObject.searchParams.get('interval')).toBe('30m');
190+
expect(urlObject.searchParams.get('title')).toBe('Widget');
191+
192+
const queries = urlObject.searchParams.getAll('queries');
193+
expect(queries).toHaveLength(2);
194+
195+
const query1 = JSON.parse(queries[0]!);
196+
expect(query1.chartType).toBe(1);
197+
expect(query1.fields).toEqual([]);
198+
expect(query1.groupBys).toEqual(['span.description']);
199+
expect(query1.query).toBe('is_transaction:true');
200+
201+
const query2 = JSON.parse(queries[1]!);
202+
expect(query2.chartType).toBe(1);
203+
expect(query2.fields).toEqual([]);
204+
expect(query2.groupBys).toEqual(['span.description']);
205+
expect(query2.query).toBe('is_transaction:false');
206+
});
159207
});

static/app/views/dashboards/utils/getWidgetExploreUrl.tsx

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import {
88
getAggregateAlias,
99
isAggregateFieldOrEquation,
1010
} from 'sentry/utils/discover/fields';
11-
import {decodeBoolean, decodeList, decodeScalar} from 'sentry/utils/queryString';
11+
import {
12+
decodeBoolean,
13+
decodeList,
14+
decodeScalar,
15+
decodeSorts,
16+
} from 'sentry/utils/queryString';
1217
import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
1318
import {DisplayType} from 'sentry/views/dashboards/types';
1419
import {
@@ -18,21 +23,57 @@ import {
1823
getWidgetInterval,
1924
} from 'sentry/views/dashboards/utils';
2025
import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
21-
import {getExploreUrl} from 'sentry/views/explore/utils';
26+
import {getExploreMultiQueryUrl, getExploreUrl} from 'sentry/views/explore/utils';
2227
import {ChartType} from 'sentry/views/insights/common/components/chart';
2328

2429
export function getWidgetExploreUrl(
2530
widget: Widget,
2631
dashboardFilters: DashboardFilters | undefined,
2732
selection: PageFilters,
2833
organization: Organization
34+
) {
35+
if (widget.queries.length > 1) {
36+
return _getWidgetExploreUrlForMultipleQueries(
37+
widget,
38+
dashboardFilters,
39+
selection,
40+
organization
41+
);
42+
}
43+
44+
return _getWidgetExploreUrl(widget, dashboardFilters, selection, organization);
45+
}
46+
47+
function getChartType(displayType: DisplayType) {
48+
let chartType: ChartType = ChartType.LINE;
49+
switch (displayType) {
50+
case DisplayType.BAR:
51+
chartType = ChartType.BAR;
52+
break;
53+
case DisplayType.LINE:
54+
chartType = ChartType.LINE;
55+
break;
56+
case DisplayType.AREA:
57+
chartType = ChartType.AREA;
58+
break;
59+
case DisplayType.TABLE:
60+
case DisplayType.BIG_NUMBER:
61+
break;
62+
default:
63+
break;
64+
}
65+
66+
return chartType;
67+
}
68+
69+
function _getWidgetExploreUrl(
70+
widget: Widget,
71+
dashboardFilters: DashboardFilters | undefined,
72+
selection: PageFilters,
73+
organization: Organization
2974
) {
3075
const eventView = eventViewFromWidget(widget.title, widget.queries[0]!, selection);
31-
const {query: locationQueryParams} = eventView.getResultsViewUrlTarget(
32-
organization,
33-
false,
34-
undefined
35-
);
76+
const locationQueryParams = eventView.generateQueryStringObject();
3677

3778
// Inject the sort field for cases of arbitrary sorting
3879
// The sort is not picked up by eventView unless it is
@@ -50,20 +91,17 @@ export function getWidgetExploreUrl(
5091
),
5192
].slice(0, 3);
5293

94+
const chartType = getChartType(widget.displayType);
5395
let exploreMode: Mode | undefined = undefined;
54-
let chartType: ChartType = ChartType.LINE;
5596
switch (widget.displayType) {
5697
case DisplayType.BAR:
5798
exploreMode = Mode.AGGREGATE;
58-
chartType = ChartType.BAR;
5999
break;
60100
case DisplayType.LINE:
61101
exploreMode = Mode.AGGREGATE;
62-
chartType = ChartType.LINE;
63102
break;
64103
case DisplayType.AREA:
65104
exploreMode = Mode.AGGREGATE;
66-
chartType = ChartType.AREA;
67105
break;
68106
case DisplayType.TABLE:
69107
case DisplayType.BIG_NUMBER:
@@ -158,3 +196,39 @@ function _isSortIncluded(sort: string, yAxes: string[]) {
158196
const rawSort = trimStart(sort, '-');
159197
return yAxes.map(getAggregateAlias).includes(getAggregateAlias(rawSort));
160198
}
199+
200+
function _getWidgetExploreUrlForMultipleQueries(
201+
widget: Widget,
202+
dashboardFilters: DashboardFilters | undefined,
203+
selection: PageFilters,
204+
organization: Organization
205+
): string {
206+
const eventView = eventViewFromWidget(widget.title, widget.queries[0]!, selection);
207+
const locationQueryParams = eventView.generateQueryStringObject();
208+
const datetime = {
209+
end: decodeScalar(locationQueryParams.end) ?? null,
210+
period: decodeScalar(locationQueryParams.statsPeriod) ?? null,
211+
start: decodeScalar(locationQueryParams.start) ?? null,
212+
utc: decodeBoolean(locationQueryParams.utc) ?? null,
213+
};
214+
215+
const currentSelection = {
216+
...selection,
217+
datetime,
218+
};
219+
220+
return getExploreMultiQueryUrl({
221+
organization,
222+
title: widget.title,
223+
selection: currentSelection,
224+
queries: widget.queries.map(query => ({
225+
chartType: getChartType(widget.displayType),
226+
query: applyDashboardFilters(query.conditions, dashboardFilters) ?? '',
227+
sortBys: decodeSorts(query.orderby),
228+
yAxes: query.aggregates,
229+
fields: [],
230+
groupBys: query.columns,
231+
})),
232+
interval: getWidgetInterval(widget, currentSelection.datetime),
233+
});
234+
}

static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.spec.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,28 @@ describe('QueryFilterBuilder', () => {
141141

142142
expect(screen.queryByText('+ Add Filter')).not.toBeInTheDocument();
143143
});
144+
145+
it('allow adding filters for the spans dataset', async () => {
146+
render(
147+
<WidgetBuilderProvider>
148+
<WidgetBuilderQueryFilterBuilder
149+
onQueryConditionChange={() => {}}
150+
validatedWidgetResponse={{} as any}
151+
/>
152+
</WidgetBuilderProvider>,
153+
{
154+
organization,
155+
156+
router: RouterFixture({
157+
location: LocationFixture({
158+
query: {query: [], dataset: WidgetType.SPANS, displayType: DisplayType.LINE},
159+
}),
160+
}),
161+
162+
deprecatedRouterMocks: true,
163+
}
164+
);
165+
166+
expect(await screen.findByText('+ Add Filter')).toBeInTheDocument();
167+
});
144168
});

static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ function WidgetBuilderQueryFilterBuilder({
5959
const canAddSearchConditions =
6060
state.displayType !== DisplayType.TABLE &&
6161
state.displayType !== DisplayType.BIG_NUMBER &&
62-
state.dataset !== WidgetType.SPANS &&
6362
state.query &&
6463
state.query.length < 3;
6564

static/app/views/explore/utils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export function getExploreUrlFromSavedQueryUrl({
143143
});
144144
}
145145

146-
function getExploreMultiQueryUrl({
146+
export function getExploreMultiQueryUrl({
147147
organization,
148148
selection,
149149
interval,

0 commit comments

Comments
 (0)