diff --git a/static/app/views/dashboards/types.tsx b/static/app/views/dashboards/types.tsx index 35ed36e976d1a5..d59a2dfbfa76d8 100644 --- a/static/app/views/dashboards/types.tsx +++ b/static/app/views/dashboards/types.tsx @@ -80,6 +80,8 @@ export type WidgetQuery = { isHidden?: boolean | null; // Contains the on-demand entries for the widget query. onDemand?: WidgetQueryOnDemand[]; + // Aggregate selected for the Big Number widget builder + selectedAggregate?: number; }; export type Widget = { diff --git a/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/index.tsx b/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/index.tsx index 4ecfccc61a2426..c0160f38194149 100644 --- a/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/index.tsx +++ b/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/index.tsx @@ -14,11 +14,12 @@ interface Props { aggregates: QueryFieldValue[]; dataSet: DataSet; displayType: DisplayType; - onYAxisChange: (newFields: QueryFieldValue[]) => void; + onYAxisChange: (newFields: QueryFieldValue[], newSelectedAggregate?: number) => void; organization: Organization; tags: TagCollection; widgetType: WidgetType; queryErrors?: Record[]; + selectedAggregate?: number; } export function YAxisStep({ @@ -28,6 +29,7 @@ export function YAxisStep({ onYAxisChange, tags, widgetType, + selectedAggregate, }: Props) { return ( ); 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 174931207eb359..0bf9a32f4eaa25 100644 --- a/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector/index.tsx +++ b/static/app/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector/index.tsx @@ -1,7 +1,10 @@ import styled from '@emotion/styled'; +import Feature from 'sentry/components/acl/feature'; import ButtonBar from 'sentry/components/buttonBar'; +import {RadioLineItem} from 'sentry/components/forms/controls/radioGroup'; import FieldGroup from 'sentry/components/forms/fieldGroup'; +import Radio from 'sentry/components/radio'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {TagCollection} from 'sentry/types/group'; @@ -25,11 +28,12 @@ interface Props { /** * Fired when aggregates are added/removed/modified/reordered. */ - onChange: (aggregates: QueryFieldValue[]) => void; + onChange: (aggregates: QueryFieldValue[], selectedAggregate?: number) => void; tags: TagCollection; widgetType: Widget['widgetType']; errors?: Record; noFieldsMessage?: string; + selectedAggregate?: number; } export function YAxisSelector({ @@ -40,20 +44,26 @@ export function YAxisSelector({ onChange, errors, noFieldsMessage, + selectedAggregate, }: Props) { const organization = useOrganization(); const datasetConfig = getDatasetConfig(widgetType); const {customMeasurements} = useCustomMeasurements(); - function handleAddOverlay(event: React.MouseEvent) { + function handleAddFields(event: React.MouseEvent) { event.preventDefault(); const newAggregates = [ ...aggregates, {kind: FieldValueKind.FIELD, field: ''} as QueryFieldValue, ]; - onChange(newAggregates); + if ( + organization.features.includes('dashboards-bignumber-equations') && + displayType === DisplayType.BIG_NUMBER + ) { + onChange(newAggregates, newAggregates.length - 1); + } else onChange(newAggregates); } function handleAddEquation(event: React.MouseEvent) { @@ -63,7 +73,13 @@ export function YAxisSelector({ ...aggregates, {kind: FieldValueKind.EQUATION, field: ''} as QueryFieldValue, ]; - onChange(newAggregates); + if ( + organization.features.includes('dashboards-bignumber-equations') && + displayType === DisplayType.BIG_NUMBER + ) { + const newSelectedAggregate = newAggregates.length - 1; + onChange(newAggregates, newSelectedAggregate); + } else onChange(newAggregates); } function handleRemoveQueryField(event: React.MouseEvent, fieldIndex: number) { @@ -71,7 +87,13 @@ export function YAxisSelector({ const newAggregates = [...aggregates]; newAggregates.splice(fieldIndex, 1); - onChange(newAggregates); + if ( + organization.features.includes('dashboards-bignumber-equations') && + displayType === DisplayType.BIG_NUMBER + ) { + const newSelectedAggregate = newAggregates.length - 1; + onChange(newAggregates, newSelectedAggregate); + } else onChange(newAggregates); } function handleChangeQueryField(value: QueryFieldValue, fieldIndex: number) { @@ -80,13 +102,19 @@ export function YAxisSelector({ onChange(newAggregates); } + function handleSelectField(newSelectedAggregate: number) { + onChange(aggregates, newSelectedAggregate); + } + const fieldError = errors?.find(error => error?.aggregates)?.aggregates; const canDelete = aggregates.length > 1; const hideAddYAxisButtons = - (DisplayType.BIG_NUMBER === displayType && aggregates.length === 1) || ([DisplayType.LINE, DisplayType.AREA, DisplayType.BAR].includes(displayType) && - aggregates.length === 3); + aggregates.length === 3) || + (organization.features.includes('dashboards-bignumber-equations') + ? displayType === DisplayType.BIG_NUMBER && widgetType === WidgetType.RELEASE + : DisplayType.BIG_NUMBER === displayType && aggregates.length === 1); let injectedFunctions: Set = new Set(); @@ -112,6 +140,17 @@ export function YAxisSelector({ {aggregates.map((fieldValue, i) => ( + {aggregates.length > 1 && displayType === DisplayType.BIG_NUMBER && ( + + + handleSelectField(i)} + aria-label={'field' + i} + /> + + + )} - + {datasetConfig.enableEquations && ( )} diff --git a/static/app/views/dashboards/widgetBuilder/utils.tsx b/static/app/views/dashboards/widgetBuilder/utils.tsx index d6b3c3d17094d7..d3ea79b5ce104e 100644 --- a/static/app/views/dashboards/widgetBuilder/utils.tsx +++ b/static/app/views/dashboards/widgetBuilder/utils.tsx @@ -6,7 +6,7 @@ import type {FieldValue} from 'sentry/components/forms/types'; import {t} from 'sentry/locale'; import type {SelectValue} from 'sentry/types/core'; import type {TagCollection} from 'sentry/types/group'; -import type {OrganizationSummary} from 'sentry/types/organization'; +import type {Organization, OrganizationSummary} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; import { aggregateFunctionOutputType, @@ -148,9 +148,11 @@ export function normalizeQueries({ displayType, queries, widgetType, + organization, }: { displayType: DisplayType; queries: Widget['queries']; + organization?: Organization; widgetType?: Widget['widgetType']; }): Widget['queries'] { const isTimeseriesChart = getIsTimeseriesChart(displayType); @@ -294,16 +296,25 @@ export function normalizeQueries({ } if (DisplayType.BIG_NUMBER === displayType) { - // For world map chart, cap fields of the queries to only one field. - queries = queries.map(query => { - return { - ...query, - fields: query.aggregates.slice(0, 1), - aggregates: query.aggregates.slice(0, 1), - orderby: '', - columns: [], - }; - }); + if (organization?.features.includes('dashboards-bignumber-equations')) { + queries = queries.map(query => { + return { + ...query, + orderby: '', + columns: [], + }; + }); + } else { + queries = queries.map(query => { + return { + ...query, + fields: query.aggregates.slice(0, 1), + aggregates: query.aggregates.slice(0, 1), + orderby: '', + columns: [], + }; + }); + } } return queries; diff --git a/static/app/views/dashboards/widgetBuilder/widgetBuilder.spec.tsx b/static/app/views/dashboards/widgetBuilder/widgetBuilder.spec.tsx index 06f8c624bcb033..b74da2c2eaa355 100644 --- a/static/app/views/dashboards/widgetBuilder/widgetBuilder.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/widgetBuilder.spec.tsx @@ -601,6 +601,39 @@ describe('WidgetBuilder', function () { expect(handleSave).toHaveBeenCalledTimes(1); }); + it('can add additional fields and equation for Big Number with selection', async function () { + renderTestComponent({ + query: { + displayType: DisplayType.BIG_NUMBER, + }, + orgFeatures: [...defaultOrgFeatures, 'dashboards-bignumber-equations'], + }); + + // Add new field + await userEvent.click(screen.getByLabelText('Add Field')); + expect(screen.getByText('(Required)')).toBeInTheDocument(); + await selectEvent.select(screen.getByText('(Required)'), ['count_unique(…)']); + expect(screen.getByRole('radio', {name: 'field1'})).toBeChecked(); + + // Add another new field + await userEvent.click(screen.getByLabelText('Add Field')); + expect(screen.getByText('(Required)')).toBeInTheDocument(); + await selectEvent.select(screen.getByText('(Required)'), ['eps()']); + expect(screen.getByRole('radio', {name: 'field2'})).toBeChecked(); + + // Add an equation + await userEvent.click(screen.getByLabelText('Add an Equation')); + expect(screen.getByPlaceholderText('Equation')).toBeInTheDocument(); + expect(screen.getByRole('radio', {name: 'field3'})).toBeChecked(); + await userEvent.click(screen.getByPlaceholderText('Equation')); + await userEvent.paste('eps() + 100'); + + // Check if right value is displayed from equation + await userEvent.click(screen.getByPlaceholderText('Equation')); + await userEvent.paste('2 * 100'); + expect(screen.getByText('200')).toBeInTheDocument(); + }); + it('can add equation fields', async function () { const handleSave = jest.fn(); diff --git a/static/app/views/dashboards/widgetBuilder/widgetBuilder.tsx b/static/app/views/dashboards/widgetBuilder/widgetBuilder.tsx index 880857eb56f48a..88b755d09c9749 100644 --- a/static/app/views/dashboards/widgetBuilder/widgetBuilder.tsx +++ b/static/app/views/dashboards/widgetBuilder/widgetBuilder.tsx @@ -327,6 +327,7 @@ function WidgetBuilder({ displayType: newDisplayType, queries: widgetFromDashboard.queries, widgetType: widgetFromDashboard.widgetType ?? defaultWidgetType, + organization: organization, }).map(query => ({ ...query, // Use the last aggregate because that's where the y-axis is stored @@ -339,6 +340,7 @@ function WidgetBuilder({ displayType: newDisplayType, queries: widgetFromDashboard.queries, widgetType: widgetFromDashboard.widgetType ?? defaultWidgetType, + organization: organization, }); } @@ -437,6 +439,7 @@ function WidgetBuilder({ displayType: newDisplayType, queries: [{...getDatasetConfig(defaultWidgetType).defaultWidgetQuery}], widgetType: defaultWidgetType, + organization: organization, }) ); set(newState, 'dataSet', defaultDataset); @@ -448,6 +451,7 @@ function WidgetBuilder({ displayType: newDisplayType, queries: prevState.queries, widgetType: DATA_SET_TO_WIDGET_TYPE[prevState.dataSet], + organization: organization, }); if (newDisplayType === DisplayType.TOP_N) { @@ -675,7 +679,10 @@ function WidgetBuilder({ return handleColumnFieldChange; } - function handleYAxisChange(newFields: QueryFieldValue[]) { + function handleYAxisChange( + newFields: QueryFieldValue[], + newSelectedAggregate?: number + ) { const fieldStrings = newFields.map(generateFieldAsString); const newState = cloneDeep(state); @@ -704,6 +711,12 @@ function WidgetBuilder({ return newQuery; }); + if ( + organization.features.includes('dashboards-bignumber-equations') && + defined(newSelectedAggregate) + ) + newQueries[0].selectedAggregate = newSelectedAggregate; + set(newState, 'queries', newQueries); set(newState, 'userHasModified', true); @@ -1242,10 +1255,13 @@ function WidgetBuilder({ displayType={state.displayType} widgetType={widgetType} queryErrors={state.errors?.queries} - onYAxisChange={newFields => { - handleYAxisChange(newFields); + onYAxisChange={(newFields, newSelectedField) => { + handleYAxisChange(newFields, newSelectedField); }} aggregates={explodedAggregates} + selectedAggregate={ + state.queries[0].selectedAggregate + } tags={tags} organization={organization} /> diff --git a/static/app/views/dashboards/widgetBuilder/widgetLibrary/index.tsx b/static/app/views/dashboards/widgetBuilder/widgetLibrary/index.tsx index 50b9333f1464c3..4d1c71f78e2f82 100644 --- a/static/app/views/dashboards/widgetBuilder/widgetLibrary/index.tsx +++ b/static/app/views/dashboards/widgetBuilder/widgetLibrary/index.tsx @@ -67,6 +67,7 @@ export function WidgetLibrary({ displayType, queries: widget.queries, widgetType: widget.widgetType, + organization: organization, }); const newWidget = { diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index c19cadd74ed0ec..ce39c92378d2ba 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -32,6 +32,7 @@ import type { } from 'sentry/types/echarts'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; +import {defined} from 'sentry/utils'; import { axisLabelFormatter, axisLabelFormatterUsingAggregateOutputType, @@ -206,7 +207,15 @@ class WidgetCardChart extends Component { const tableMeta = {...result.meta}; const fields = Object.keys(tableMeta); - const field = fields[0]; + let field = fields[0]; + + if ( + organization.features.includes('dashboards-bignumber-equations') && + defined(widget.queries[0].selectedAggregate) + ) { + const index = widget.queries[0].selectedAggregate; + field = widget.queries[0].aggregates[index]; + } // Change tableMeta for the field from integer to string since we will be rendering with toLocaleString const shouldExpandInteger = !!expandNumbers && tableMeta[field] === 'integer';