diff --git a/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector/index.tsx b/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector/index.tsx index f860de7be2913f..7758e59702da47 100644 --- a/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector/index.tsx +++ b/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector/index.tsx @@ -21,6 +21,8 @@ import {FieldValueKind} from 'sentry/views/discover/table/types'; import {AddButton} from './addButton'; import {DeleteButton} from './deleteButton'; +export const MAX_NUM_Y_AXES = 3; + interface Props { aggregates: QueryFieldValue[]; displayType: DisplayType; @@ -107,7 +109,7 @@ export function YAxisSelector({ const hideAddYAxisButtons = ([DisplayType.LINE, DisplayType.AREA, DisplayType.BAR].includes(displayType) && - aggregates.length === 3) || + aggregates.length === MAX_NUM_Y_AXES) || (displayType === DisplayType.BIG_NUMBER && widgetType === WidgetType.RELEASE); let injectedFunctions: Set = new Set(); diff --git a/static/app/views/discover/utils.tsx b/static/app/views/discover/utils.tsx index 76cdf83d1f93a5..5d47f82828d50e 100644 --- a/static/app/views/discover/utils.tsx +++ b/static/app/views/discover/utils.tsx @@ -823,6 +823,7 @@ export function constructAddQueryToDashboardLink({ defaultTitle, displayType: displayType === DisplayType.TOP_N ? DisplayType.AREA : displayType, dataset: widgetType, + field: eventView.getFields(), limit: displayType === DisplayType.TOP_N ? Number(eventView.topEvents) || TOP_N diff --git a/static/app/views/explore/charts/index.tsx b/static/app/views/explore/charts/index.tsx index e705a0937476cd..3adafeeb16f32a 100644 --- a/static/app/views/explore/charts/index.tsx +++ b/static/app/views/explore/charts/index.tsx @@ -23,6 +23,7 @@ import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; import {formatVersion} from 'sentry/utils/versions/formatVersion'; import {Dataset} from 'sentry/views/alerts/rules/metric/types'; +import {AddToDashboardButton} from 'sentry/views/explore/components/addToDashboardButton'; import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval'; import {useDataset} from 'sentry/views/explore/hooks/useDataset'; import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes'; @@ -273,6 +274,9 @@ export function ExploreCharts({query, setError}: ExploreChartsProps) { /> + + + { + beforeEach(() => { + jest.clearAllMocks(); + + jest.mocked(useVisualizes).mockReturnValue([ + [ + { + yAxes: ['avg(span.duration)'], + chartType: ChartType.LINE, + label: 'Custom Explore Widget', + }, + ], + jest.fn(), + ]); + + jest.mocked(useResultMode).mockReturnValue(['samples', jest.fn()]); + }); + + it('renders', async () => { + render(); + await userEvent.hover(screen.getByLabelText('Add to Dashboard')); + expect(await screen.findByText('Add to Dashboard')).toBeInTheDocument(); + }); + + it('opens the dashboard modal with the correct query for samples mode', async () => { + render(); + await userEvent.click(screen.getByLabelText('Add to Dashboard')); + + // The table columns are encoded as the fields for the defaultWidgetQuery + expect(openAddToDashboardModal).toHaveBeenCalledWith( + expect.objectContaining({ + // For Add + Stay on Page + widget: { + title: 'Custom Explore Widget', + displayType: DisplayType.LINE, + interval: undefined, + limit: undefined, + widgetType: WidgetType.SPANS, + queries: [ + { + aggregates: ['avg(span.duration)'], + columns: [], + fields: ['avg(span.duration)'], + conditions: '', + orderby: '-timestamp', + name: '', + }, + ], + }, + + // For Open in Widget Builder + widgetAsQueryParams: expect.objectContaining({ + dataset: WidgetType.SPANS, + defaultTableColumns: [ + 'id', + 'project', + 'span.op', + 'span.description', + 'span.duration', + 'timestamp', + ], + defaultTitle: 'Custom Explore Widget', + defaultWidgetQuery: + 'name=&aggregates=avg(span.duration)&columns=&fields=avg(span.duration)&conditions=&orderby=-timestamp', + displayType: DisplayType.LINE, + field: [ + 'id', + 'project', + 'span.op', + 'span.description', + 'span.duration', + 'timestamp', + ], + }), + }) + ); + }); + + it('opens the dashboard modal with the correct query based on the visualize index', async () => { + // Mock a second visualize object + jest.mocked(useVisualizes).mockReturnValue([ + [ + { + yAxes: ['avg(span.duration)'], + chartType: ChartType.LINE, + label: 'Custom Explore Widget', + }, + { + yAxes: ['max(span.duration)'], + chartType: ChartType.LINE, + label: 'Custom Explore Widget', + }, + ], + jest.fn(), + ]); + + render(); + await userEvent.click(screen.getByLabelText('Add to Dashboard')); + + // The group by and the yAxes are encoded as the fields for the defaultTableQuery + expect(openAddToDashboardModal).toHaveBeenCalledWith( + expect.objectContaining({ + // For Add + Stay on Page + widget: { + title: 'Custom Explore Widget', + displayType: DisplayType.LINE, + interval: undefined, + limit: undefined, + widgetType: WidgetType.SPANS, + queries: [ + { + aggregates: ['max(span.duration)'], + columns: [], + fields: ['max(span.duration)'], + conditions: '', + orderby: '-timestamp', + name: '', + }, + ], + }, + + // For Open in Widget Builder + widgetAsQueryParams: expect.objectContaining({ + dataset: WidgetType.SPANS, + defaultTableColumns: [ + 'id', + 'project', + 'span.op', + 'span.description', + 'span.duration', + 'timestamp', + ], + defaultTitle: 'Custom Explore Widget', + defaultWidgetQuery: + 'name=&aggregates=max(span.duration)&columns=&fields=max(span.duration)&conditions=&orderby=-timestamp', + displayType: DisplayType.LINE, + field: [ + 'id', + 'project', + 'span.op', + 'span.description', + 'span.duration', + 'timestamp', + ], + }), + }) + ); + }); + + it('uses the yAxes for the aggregate mode', async () => { + jest.mocked(useResultMode).mockReturnValue(['aggregate', jest.fn()]); + + render(); + await userEvent.click(screen.getByLabelText('Add to Dashboard')); + + expect(openAddToDashboardModal).toHaveBeenCalledWith( + expect.objectContaining({ + // For Add + Stay on Page + widget: { + title: 'Custom Explore Widget', + displayType: DisplayType.LINE, + interval: undefined, + limit: undefined, + widgetType: WidgetType.SPANS, + queries: [ + { + aggregates: ['avg(span.duration)'], + columns: [], + fields: ['avg(span.duration)'], + conditions: '', + orderby: '-avg(span.duration)', + name: '', + }, + ], + }, + + // For Open in Widget Builder + widgetAsQueryParams: expect.objectContaining({ + dataset: WidgetType.SPANS, + defaultTableColumns: ['avg(span.duration)'], + defaultTitle: 'Custom Explore Widget', + defaultWidgetQuery: + 'name=&aggregates=avg(span.duration)&columns=&fields=avg(span.duration)&conditions=&orderby=-avg(span.duration)', + displayType: DisplayType.LINE, + field: ['avg(span.duration)'], + }), + }) + ); + }); + + it('takes the first 3 yAxes', async () => { + jest.mocked(useResultMode).mockReturnValue(['aggregate', jest.fn()]); + jest.mocked(useVisualizes).mockReturnValue([ + [ + { + yAxes: [ + 'avg(span.duration)', + 'max(span.duration)', + 'min(span.duration)', + 'p90(span.duration)', + ], + chartType: ChartType.LINE, + label: 'Custom Explore Widget', + }, + ], + jest.fn(), + ]); + + render(); + await userEvent.click(screen.getByLabelText('Add to Dashboard')); + + expect(openAddToDashboardModal).toHaveBeenCalledWith( + expect.objectContaining({ + // For Add + Stay on Page + widget: { + title: 'Custom Explore Widget', + displayType: DisplayType.LINE, + interval: undefined, + limit: undefined, + widgetType: WidgetType.SPANS, + queries: [ + { + aggregates: [ + 'avg(span.duration)', + 'max(span.duration)', + 'min(span.duration)', + ], + columns: [], + fields: ['avg(span.duration)', 'max(span.duration)', 'min(span.duration)'], + conditions: '', + orderby: '-avg(span.duration)', + name: '', + }, + ], + }, + + // For Open in Widget Builder + widgetAsQueryParams: expect.objectContaining({ + dataset: WidgetType.SPANS, + defaultTableColumns: [ + 'avg(span.duration)', + 'max(span.duration)', + 'min(span.duration)', + ], + defaultTitle: 'Custom Explore Widget', + defaultWidgetQuery: + '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)', + displayType: DisplayType.LINE, + field: ['avg(span.duration)', 'max(span.duration)', 'min(span.duration)'], + }), + }) + ); + }); +}); diff --git a/static/app/views/explore/components/addToDashboardButton.tsx b/static/app/views/explore/components/addToDashboardButton.tsx new file mode 100644 index 00000000000000..e3daff8b368e58 --- /dev/null +++ b/static/app/views/explore/components/addToDashboardButton.tsx @@ -0,0 +1,97 @@ +import {useCallback, useMemo} from 'react'; + +import {Button} from 'sentry/components/button'; +import {Tooltip} from 'sentry/components/tooltip'; +import {IconDashboard} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import type {NewQuery} from 'sentry/types/organization'; +import EventView from 'sentry/utils/discover/eventView'; +import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {useLocation} from 'sentry/utils/useLocation'; +import useOrganization from 'sentry/utils/useOrganization'; +import usePageFilters from 'sentry/utils/usePageFilters'; +import useRouter from 'sentry/utils/useRouter'; +import {WidgetType} from 'sentry/views/dashboards/types'; +import {MAX_NUM_Y_AXES} from 'sentry/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector'; +import {handleAddQueryToDashboard} from 'sentry/views/discover/utils'; +import {useDataset} from 'sentry/views/explore/hooks/useDataset'; +import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys'; +import {useResultMode} from 'sentry/views/explore/hooks/useResultsMode'; +import {useSampleFields} from 'sentry/views/explore/hooks/useSampleFields'; +import {useSorts} from 'sentry/views/explore/hooks/useSorts'; +import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery'; +import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes'; +import {formatSort} from 'sentry/views/explore/tables/aggregatesTable'; + +interface AddToDashboardButtonProps { + visualizeIndex: number; +} + +export function AddToDashboardButton({visualizeIndex}: AddToDashboardButtonProps) { + const location = useLocation(); + const router = useRouter(); + const {selection} = usePageFilters(); + const organization = useOrganization(); + + const [resultMode] = useResultMode(); + const [dataset] = useDataset(); + const {groupBys} = useGroupBys(); + const [visualizes] = useVisualizes(); + const [sampleFields] = useSampleFields(); + const yAxes = useMemo( + () => visualizes[visualizeIndex].yAxes.slice(0, MAX_NUM_Y_AXES), + [visualizes, visualizeIndex] + ); + const fields = useMemo(() => { + if (resultMode === 'samples') { + return sampleFields.filter(Boolean); + } + + return [...groupBys, ...yAxes].filter(Boolean); + }, [groupBys, resultMode, sampleFields, yAxes]); + const [sorts] = useSorts({fields}); + const [query] = useUserQuery(); + + const discoverQuery: NewQuery = useMemo(() => { + const search = new MutableSearch(query); + + return { + name: t('Custom Explore Widget'), + fields, + orderby: sorts.map(formatSort), + query: search.formatString(), + version: 2, + dataset, + yAxis: yAxes, + }; + }, [dataset, fields, sorts, query, yAxes]); + + const eventView = useMemo(() => { + const newEventView = EventView.fromNewQueryWithPageFilters(discoverQuery, selection); + newEventView.dataset = dataset; + return newEventView; + }, [discoverQuery, selection, dataset]); + + const handleAddToDashboard = useCallback(() => { + handleAddQueryToDashboard({ + organization, + location, + eventView, + router, + yAxis: eventView.yAxis, + widgetType: WidgetType.SPANS, + }); + }, [organization, location, eventView, router]); + + return ( + +