Skip to content

Commit 9fb8494

Browse files
authored
feat(dashboards): Add to Dashboard from Explore (#81064)
Adds a button to the chart in explore so we can click "Add to Dashboard" and use Explore as a jump off point for a dashboard widget. The button has the following functionality: - Takes the first 3 series from the chart and opens up the add to dashboard modal - The filter should transfer over - When switching to table view, the table in dashboards gets filled with the columns from the samples table if added from sample mode - When added from aggregate mode, the table view gets the group bys followed by the aggregates Note: The group by doesn't currently get filled in for timeseries charts. I will follow up with another PR to make group by work.
1 parent cf7a490 commit 9fb8494

File tree

5 files changed

+372
-1
lines changed

5 files changed

+372
-1
lines changed

static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {FieldValueKind} from 'sentry/views/discover/table/types';
2121
import {AddButton} from './addButton';
2222
import {DeleteButton} from './deleteButton';
2323

24+
export const MAX_NUM_Y_AXES = 3;
25+
2426
interface Props {
2527
aggregates: QueryFieldValue[];
2628
displayType: DisplayType;
@@ -107,7 +109,7 @@ export function YAxisSelector({
107109

108110
const hideAddYAxisButtons =
109111
([DisplayType.LINE, DisplayType.AREA, DisplayType.BAR].includes(displayType) &&
110-
aggregates.length === 3) ||
112+
aggregates.length === MAX_NUM_Y_AXES) ||
111113
(displayType === DisplayType.BIG_NUMBER && widgetType === WidgetType.RELEASE);
112114

113115
let injectedFunctions: Set<string> = new Set();

static/app/views/discover/utils.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@ export function constructAddQueryToDashboardLink({
823823
defaultTitle,
824824
displayType: displayType === DisplayType.TOP_N ? DisplayType.AREA : displayType,
825825
dataset: widgetType,
826+
field: eventView.getFields(),
826827
limit:
827828
displayType === DisplayType.TOP_N
828829
? Number(eventView.topEvents) || TOP_N

static/app/views/explore/charts/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import usePageFilters from 'sentry/utils/usePageFilters';
2323
import useProjects from 'sentry/utils/useProjects';
2424
import {formatVersion} from 'sentry/utils/versions/formatVersion';
2525
import {Dataset} from 'sentry/views/alerts/rules/metric/types';
26+
import {AddToDashboardButton} from 'sentry/views/explore/components/addToDashboardButton';
2627
import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval';
2728
import {useDataset} from 'sentry/views/explore/hooks/useDataset';
2829
import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
@@ -273,6 +274,9 @@ export function ExploreCharts({query, setError}: ExploreChartsProps) {
273274
/>
274275
</Tooltip>
275276
</Feature>
277+
<Feature features="organizations:dashboards-eap">
278+
<AddToDashboardButton visualizeIndex={index} />
279+
</Feature>
276280
</ChartHeader>
277281
<Chart
278282
height={CHART_HEIGHT}
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
2+
3+
import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
4+
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
5+
import {AddToDashboardButton} from 'sentry/views/explore/components/addToDashboardButton';
6+
import {useResultMode} from 'sentry/views/explore/hooks/useResultsMode';
7+
import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
8+
import {ChartType} from 'sentry/views/insights/common/components/chart';
9+
10+
jest.mock('sentry/actionCreators/modal');
11+
jest.mock('sentry/views/explore/hooks/useVisualizes');
12+
jest.mock('sentry/views/explore/hooks/useResultsMode');
13+
14+
describe('AddToDashboardButton', () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
18+
jest.mocked(useVisualizes).mockReturnValue([
19+
[
20+
{
21+
yAxes: ['avg(span.duration)'],
22+
chartType: ChartType.LINE,
23+
label: 'Custom Explore Widget',
24+
},
25+
],
26+
jest.fn(),
27+
]);
28+
29+
jest.mocked(useResultMode).mockReturnValue(['samples', jest.fn()]);
30+
});
31+
32+
it('renders', async () => {
33+
render(<AddToDashboardButton visualizeIndex={0} />);
34+
await userEvent.hover(screen.getByLabelText('Add to Dashboard'));
35+
expect(await screen.findByText('Add to Dashboard')).toBeInTheDocument();
36+
});
37+
38+
it('opens the dashboard modal with the correct query for samples mode', async () => {
39+
render(<AddToDashboardButton visualizeIndex={0} />);
40+
await userEvent.click(screen.getByLabelText('Add to Dashboard'));
41+
42+
// The table columns are encoded as the fields for the defaultWidgetQuery
43+
expect(openAddToDashboardModal).toHaveBeenCalledWith(
44+
expect.objectContaining({
45+
// For Add + Stay on Page
46+
widget: {
47+
title: 'Custom Explore Widget',
48+
displayType: DisplayType.LINE,
49+
interval: undefined,
50+
limit: undefined,
51+
widgetType: WidgetType.SPANS,
52+
queries: [
53+
{
54+
aggregates: ['avg(span.duration)'],
55+
columns: [],
56+
fields: ['avg(span.duration)'],
57+
conditions: '',
58+
orderby: '-timestamp',
59+
name: '',
60+
},
61+
],
62+
},
63+
64+
// For Open in Widget Builder
65+
widgetAsQueryParams: expect.objectContaining({
66+
dataset: WidgetType.SPANS,
67+
defaultTableColumns: [
68+
'span_id',
69+
'project',
70+
'span.op',
71+
'span.description',
72+
'span.duration',
73+
'timestamp',
74+
],
75+
defaultTitle: 'Custom Explore Widget',
76+
defaultWidgetQuery:
77+
'name=&aggregates=avg(span.duration)&columns=&fields=avg(span.duration)&conditions=&orderby=-timestamp',
78+
displayType: DisplayType.LINE,
79+
field: [
80+
'span_id',
81+
'project',
82+
'span.op',
83+
'span.description',
84+
'span.duration',
85+
'timestamp',
86+
],
87+
}),
88+
})
89+
);
90+
});
91+
92+
it('opens the dashboard modal with the correct query based on the visualize index', async () => {
93+
// Mock a second visualize object
94+
jest.mocked(useVisualizes).mockReturnValue([
95+
[
96+
{
97+
yAxes: ['avg(span.duration)'],
98+
chartType: ChartType.LINE,
99+
label: 'Custom Explore Widget',
100+
},
101+
{
102+
yAxes: ['max(span.duration)'],
103+
chartType: ChartType.LINE,
104+
label: 'Custom Explore Widget',
105+
},
106+
],
107+
jest.fn(),
108+
]);
109+
110+
render(<AddToDashboardButton visualizeIndex={1} />);
111+
await userEvent.click(screen.getByLabelText('Add to Dashboard'));
112+
113+
// The group by and the yAxes are encoded as the fields for the defaultTableQuery
114+
expect(openAddToDashboardModal).toHaveBeenCalledWith(
115+
expect.objectContaining({
116+
// For Add + Stay on Page
117+
widget: {
118+
title: 'Custom Explore Widget',
119+
displayType: DisplayType.LINE,
120+
interval: undefined,
121+
limit: undefined,
122+
widgetType: WidgetType.SPANS,
123+
queries: [
124+
{
125+
aggregates: ['max(span.duration)'],
126+
columns: [],
127+
fields: ['max(span.duration)'],
128+
conditions: '',
129+
orderby: '-timestamp',
130+
name: '',
131+
},
132+
],
133+
},
134+
135+
// For Open in Widget Builder
136+
widgetAsQueryParams: expect.objectContaining({
137+
dataset: WidgetType.SPANS,
138+
defaultTableColumns: [
139+
'span_id',
140+
'project',
141+
'span.op',
142+
'span.description',
143+
'span.duration',
144+
'timestamp',
145+
],
146+
defaultTitle: 'Custom Explore Widget',
147+
defaultWidgetQuery:
148+
'name=&aggregates=max(span.duration)&columns=&fields=max(span.duration)&conditions=&orderby=-timestamp',
149+
displayType: DisplayType.LINE,
150+
field: [
151+
'span_id',
152+
'project',
153+
'span.op',
154+
'span.description',
155+
'span.duration',
156+
'timestamp',
157+
],
158+
}),
159+
})
160+
);
161+
});
162+
163+
it('uses the yAxes for the aggregate mode', async () => {
164+
jest.mocked(useResultMode).mockReturnValue(['aggregate', jest.fn()]);
165+
166+
render(<AddToDashboardButton visualizeIndex={0} />);
167+
await userEvent.click(screen.getByLabelText('Add to Dashboard'));
168+
169+
expect(openAddToDashboardModal).toHaveBeenCalledWith(
170+
expect.objectContaining({
171+
// For Add + Stay on Page
172+
widget: {
173+
title: 'Custom Explore Widget',
174+
displayType: DisplayType.LINE,
175+
interval: undefined,
176+
limit: undefined,
177+
widgetType: WidgetType.SPANS,
178+
queries: [
179+
{
180+
aggregates: ['avg(span.duration)'],
181+
columns: [],
182+
fields: ['avg(span.duration)'],
183+
conditions: '',
184+
orderby: '-avg(span.duration)',
185+
name: '',
186+
},
187+
],
188+
},
189+
190+
// For Open in Widget Builder
191+
widgetAsQueryParams: expect.objectContaining({
192+
dataset: WidgetType.SPANS,
193+
defaultTableColumns: ['avg(span.duration)'],
194+
defaultTitle: 'Custom Explore Widget',
195+
defaultWidgetQuery:
196+
'name=&aggregates=avg(span.duration)&columns=&fields=avg(span.duration)&conditions=&orderby=-avg(span.duration)',
197+
displayType: DisplayType.LINE,
198+
field: ['avg(span.duration)'],
199+
}),
200+
})
201+
);
202+
});
203+
204+
it('takes the first 3 yAxes', async () => {
205+
jest.mocked(useResultMode).mockReturnValue(['aggregate', jest.fn()]);
206+
jest.mocked(useVisualizes).mockReturnValue([
207+
[
208+
{
209+
yAxes: [
210+
'avg(span.duration)',
211+
'max(span.duration)',
212+
'min(span.duration)',
213+
'p90(span.duration)',
214+
],
215+
chartType: ChartType.LINE,
216+
label: 'Custom Explore Widget',
217+
},
218+
],
219+
jest.fn(),
220+
]);
221+
222+
render(<AddToDashboardButton visualizeIndex={0} />);
223+
await userEvent.click(screen.getByLabelText('Add to Dashboard'));
224+
225+
expect(openAddToDashboardModal).toHaveBeenCalledWith(
226+
expect.objectContaining({
227+
// For Add + Stay on Page
228+
widget: {
229+
title: 'Custom Explore Widget',
230+
displayType: DisplayType.LINE,
231+
interval: undefined,
232+
limit: undefined,
233+
widgetType: WidgetType.SPANS,
234+
queries: [
235+
{
236+
aggregates: [
237+
'avg(span.duration)',
238+
'max(span.duration)',
239+
'min(span.duration)',
240+
],
241+
columns: [],
242+
fields: ['avg(span.duration)', 'max(span.duration)', 'min(span.duration)'],
243+
conditions: '',
244+
orderby: '-avg(span.duration)',
245+
name: '',
246+
},
247+
],
248+
},
249+
250+
// For Open in Widget Builder
251+
widgetAsQueryParams: expect.objectContaining({
252+
dataset: WidgetType.SPANS,
253+
defaultTableColumns: [
254+
'avg(span.duration)',
255+
'max(span.duration)',
256+
'min(span.duration)',
257+
],
258+
defaultTitle: 'Custom Explore Widget',
259+
defaultWidgetQuery:
260+
'name=&aggregates=avg(span.duration)%2Cmax(span.duration)%2Cmin(span.duration)&columns=&fields=avg(span.duration)%2Cmax(span.duration)%2Cmin(span.duration)&conditions=&orderby=-avg(span.duration)',
261+
displayType: DisplayType.LINE,
262+
field: ['avg(span.duration)', 'max(span.duration)', 'min(span.duration)'],
263+
}),
264+
})
265+
);
266+
});
267+
});

0 commit comments

Comments
 (0)