Skip to content

Commit 33f3f5a

Browse files
authored
feat(dashboards): Add Release Health fields to widget modal (#31661)
Filter to Release Health Metrics Fields and tags when the Metrics dataset selector is chosen. There's a lot of logic that's required to show the right fields, the QueryFields & WidgetQueryFields components are getting particularly bloated, but holding off on any refactors till we move logic over to the full page widget builder.
1 parent c7dfc56 commit 33f3f5a

File tree

9 files changed

+348
-41
lines changed

9 files changed

+348
-41
lines changed

static/app/components/dashboards/widgetQueriesForm.tsx

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from 'sentry/utils/discover/fields';
2020
import {Widget, WidgetQuery, WidgetType} from 'sentry/views/dashboardsV2/types';
2121
import {generateFieldOptions} from 'sentry/views/eventsV2/utils';
22+
import MetricsSearchBar from 'sentry/views/performance/metricsSearchBar';
2223
import Input from 'sentry/views/settings/components/forms/controls/input';
2324
import Field from 'sentry/views/settings/components/forms/field';
2425

@@ -52,6 +53,7 @@ type Props = {
5253
queries: WidgetQuery[];
5354
selection: PageFilters;
5455
errors?: Array<Record<string, any>>;
56+
widgetType?: Widget['widgetType'];
5557
};
5658

5759
/**
@@ -82,10 +84,62 @@ class WidgetQueriesForm extends React.Component<Props> {
8284
return errors.find(queryError => queryError && queryError[key]);
8385
}
8486

87+
renderSearchBar(widgetQuery: WidgetQuery, queryIndex: number) {
88+
const {organization, selection, widgetType} = this.props;
89+
90+
return widgetType === WidgetType.METRICS ? (
91+
<StyledMetricsSearchBar
92+
searchSource="widget_builder"
93+
orgSlug={organization.slug}
94+
query={widgetQuery.conditions}
95+
onSearch={field => {
96+
// SearchBar will call handlers for both onSearch and onBlur
97+
// when selecting a value from the autocomplete dropdown. This can
98+
// cause state issues for the search bar in our use case. To prevent
99+
// this, we set a timer in our onSearch handler to block our onBlur
100+
// handler from firing if it is within 200ms, ie from clicking an
101+
// autocomplete value.
102+
this.blurTimeout = window.setTimeout(() => {
103+
this.blurTimeout = null;
104+
}, 200);
105+
return this.handleFieldChange(queryIndex, 'conditions')(field);
106+
}}
107+
maxQueryLength={MAX_QUERY_LENGTH}
108+
projectIds={selection.projects}
109+
/>
110+
) : (
111+
<StyledSearchBar
112+
searchSource="widget_builder"
113+
organization={organization}
114+
projectIds={selection.projects}
115+
query={widgetQuery.conditions}
116+
fields={[]}
117+
onSearch={field => {
118+
// SearchBar will call handlers for both onSearch and onBlur
119+
// when selecting a value from the autocomplete dropdown. This can
120+
// cause state issues for the search bar in our use case. To prevent
121+
// this, we set a timer in our onSearch handler to block our onBlur
122+
// handler from firing if it is within 200ms, ie from clicking an
123+
// autocomplete value.
124+
this.blurTimeout = window.setTimeout(() => {
125+
this.blurTimeout = null;
126+
}, 200);
127+
return this.handleFieldChange(queryIndex, 'conditions')(field);
128+
}}
129+
onBlur={field => {
130+
if (!this.blurTimeout) {
131+
this.handleFieldChange(queryIndex, 'conditions')(field);
132+
}
133+
}}
134+
useFormWrapper={false}
135+
maxQueryLength={MAX_QUERY_LENGTH}
136+
/>
137+
);
138+
}
139+
85140
render() {
86141
const {
87142
organization,
88-
selection,
89143
errors,
90144
queries,
91145
canAddSearchConditions,
@@ -94,6 +148,7 @@ class WidgetQueriesForm extends React.Component<Props> {
94148
displayType,
95149
fieldOptions,
96150
onChange,
151+
widgetType = WidgetType.DISCOVER,
97152
} = this.props;
98153

99154
const hideLegendAlias = ['table', 'world_map', 'big_number'].includes(displayType);
@@ -113,32 +168,7 @@ class WidgetQueriesForm extends React.Component<Props> {
113168
error={errors?.[queryIndex].conditions}
114169
>
115170
<SearchConditionsWrapper>
116-
<StyledSearchBar
117-
searchSource="widget_builder"
118-
organization={organization}
119-
projectIds={selection.projects}
120-
query={widgetQuery.conditions}
121-
fields={[]}
122-
onSearch={field => {
123-
// SearchBar will call handlers for both onSearch and onBlur
124-
// when selecting a value from the autocomplete dropdown. This can
125-
// cause state issues for the search bar in our use case. To prevent
126-
// this, we set a timer in our onSearch handler to block our onBlur
127-
// handler from firing if it is within 200ms, ie from clicking an
128-
// autocomplete value.
129-
this.blurTimeout = window.setTimeout(() => {
130-
this.blurTimeout = null;
131-
}, 200);
132-
return this.handleFieldChange(queryIndex, 'conditions')(field);
133-
}}
134-
onBlur={field => {
135-
if (!this.blurTimeout) {
136-
this.handleFieldChange(queryIndex, 'conditions')(field);
137-
}
138-
}}
139-
useFormWrapper={false}
140-
maxQueryLength={MAX_QUERY_LENGTH}
141-
/>
171+
{this.renderSearchBar(widgetQuery, queryIndex)}
142172
{!hideLegendAlias && (
143173
<LegendAliasInput
144174
type="text"
@@ -181,7 +211,7 @@ class WidgetQueriesForm extends React.Component<Props> {
181211
</Button>
182212
)}
183213
<WidgetQueryFields
184-
widgetType={WidgetType.DISCOVER}
214+
widgetType={widgetType}
185215
displayType={displayType}
186216
fieldOptions={fieldOptions}
187217
errors={this.getFirstQueryError('fields')}
@@ -216,7 +246,7 @@ class WidgetQueriesForm extends React.Component<Props> {
216246
});
217247
}}
218248
/>
219-
{['table', 'top_n'].includes(displayType) && (
249+
{['table', 'top_n'].includes(displayType) && widgetType !== WidgetType.METRICS && (
220250
<Field
221251
label={t('Sort by')}
222252
inline={false}
@@ -257,6 +287,10 @@ const StyledSearchBar = styled(SearchBar)`
257287
flex-grow: 1;
258288
`;
259289

290+
const StyledMetricsSearchBar = styled(MetricsSearchBar)`
291+
flex-grow: 1;
292+
`;
293+
260294
const LegendAliasInput = styled(Input)`
261295
width: 33%;
262296
`;

static/app/components/dashboards/widgetQueryFields.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
isLegalYAxisType,
1212
QueryFieldValue,
1313
} from 'sentry/utils/discover/fields';
14-
import {Widget} from 'sentry/views/dashboardsV2/types';
14+
import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboardsV2/types';
15+
import {generateMetricsWidgetFieldOptions} from 'sentry/views/dashboardsV2/widgetBuilder/metricWidget/fields';
1516
import ColumnEditCollection from 'sentry/views/eventsV2/table/columnEditCollection';
1617
import {QueryField} from 'sentry/views/eventsV2/table/queryField';
1718
import {FieldValueKind} from 'sentry/views/eventsV2/table/types';
@@ -51,6 +52,8 @@ function WidgetQueryFields({
5152
onChange,
5253
style,
5354
}: Props) {
55+
const isMetricWidget = widgetType === WidgetType.METRICS;
56+
5457
// Handle new fields being added.
5558
function handleAdd(event: React.MouseEvent) {
5659
event.preventDefault();
@@ -121,6 +124,16 @@ function WidgetQueryFields({
121124
}
122125
}
123126

127+
if (
128+
widgetType === WidgetType.METRICS &&
129+
(displayType === DisplayType.TABLE || displayType === DisplayType.TOP_N)
130+
) {
131+
return (
132+
option.value.kind === FieldValueKind.FUNCTION ||
133+
option.value.kind === FieldValueKind.TAG
134+
);
135+
}
136+
124137
return option.value.kind === FieldValueKind.FUNCTION;
125138
};
126139

@@ -135,6 +148,10 @@ function WidgetQueryFields({
135148
return true;
136149
}
137150

151+
if (isMetricWidget) {
152+
return true;
153+
}
154+
138155
const functionName = fieldValue.function[0];
139156
const primaryOutput = aggregateFunctionOutputType(
140157
functionName as string,
@@ -176,6 +193,7 @@ function WidgetQueryFields({
176193
onChange={handleColumnChange}
177194
fieldOptions={fieldOptions}
178195
organization={organization}
196+
filterPrimaryOptions={isMetricWidget ? filterPrimaryOptions : undefined}
179197
source={widgetType}
180198
/>
181199
</Field>
@@ -203,6 +221,8 @@ function WidgetQueryFields({
203221
onChange={handleTopNColumnChange}
204222
fieldOptions={fieldOptions}
205223
organization={organization}
224+
filterPrimaryOptions={isMetricWidget ? filterPrimaryOptions : undefined}
225+
source={widgetType}
206226
/>
207227
</Field>
208228
<Field
@@ -218,7 +238,11 @@ function WidgetQueryFields({
218238
<QueryFieldWrapper key={`${fieldValue}:0`}>
219239
<QueryField
220240
fieldValue={fieldValue}
221-
fieldOptions={generateFieldOptions({organization})}
241+
fieldOptions={
242+
isMetricWidget
243+
? generateMetricsWidgetFieldOptions()
244+
: generateFieldOptions({organization})
245+
}
222246
onChange={value => handleTopNChangeField(value)}
223247
filterPrimaryOptions={filterPrimaryOptions}
224248
filterAggregateParameters={filterAggregateParameters(fieldValue)}

static/app/components/modals/addDashboardWidgetModal.tsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ import {t, tct} from 'sentry/locale';
2121
import space from 'sentry/styles/space';
2222
import {
2323
DateString,
24+
MetricTag,
2425
Organization,
2526
PageFilters,
2627
SelectValue,
2728
TagCollection,
2829
} from 'sentry/types';
2930
import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
31+
import handleXhrErrorResponse from 'sentry/utils/handleXhrErrorResponse';
3032
import Measurements from 'sentry/utils/measurements/measurements';
3133
import {SessionMetric} from 'sentry/utils/metrics/fields';
3234
import {SPAN_OP_BREAKDOWN_FIELDS} from 'sentry/utils/performance/spanOperationBreakdowns/constants';
@@ -50,6 +52,10 @@ import {
5052
normalizeQueries,
5153
} from 'sentry/views/dashboardsV2/widgetBuilder/eventWidget/utils';
5254
import {generateIssueWidgetFieldOptions} from 'sentry/views/dashboardsV2/widgetBuilder/issueWidget/utils';
55+
import {
56+
generateMetricsWidgetFieldOptions,
57+
METRICS_FIELDS,
58+
} from 'sentry/views/dashboardsV2/widgetBuilder/metricWidget/fields';
5359
import WidgetCard from 'sentry/views/dashboardsV2/widgetCard';
5460
import {WidgetTemplate} from 'sentry/views/dashboardsV2/widgetLibrary/data';
5561
import {generateFieldOptions} from 'sentry/views/eventsV2/utils';
@@ -99,6 +105,7 @@ type State = {
99105
displayType: Widget['displayType'];
100106
interval: Widget['interval'];
101107
loading: boolean;
108+
metricTags: MetricTag[];
102109
queries: Widget['queries'];
103110
title: string;
104111
userHasModified: boolean;
@@ -123,7 +130,7 @@ const newIssueQuery = {
123130

124131
const newMetricsQuery = {
125132
name: '',
126-
fields: [SessionMetric.SENTRY_SESSIONS_SESSION],
133+
fields: [`sum(${SessionMetric.SENTRY_SESSIONS_SESSION})`],
127134
conditions: '',
128135
orderby: '',
129136
};
@@ -155,6 +162,7 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
155162
errors: undefined,
156163
loading: !!this.omitDashboardProp,
157164
dashboards: [],
165+
metricTags: [],
158166
userHasModified: false,
159167
widgetType: WidgetType.DISCOVER,
160168
};
@@ -169,6 +177,7 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
169177
errors: undefined,
170178
loading: false,
171179
dashboards: [],
180+
metricTags: [],
172181
userHasModified: false,
173182
widgetType: widget.widgetType ?? WidgetType.DISCOVER,
174183
};
@@ -178,6 +187,9 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
178187
if (this.omitDashboardProp) {
179188
this.fetchDashboards();
180189
}
190+
if (this.props.organization.features.includes('dashboards-metrics')) {
191+
this.fetchMetricsTags();
192+
}
181193
}
182194

183195
get omitDashboardProp() {
@@ -512,6 +524,29 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
512524
this.setState({loading: false});
513525
}
514526

527+
async fetchMetricsTags() {
528+
const {api, organization, selection} = this.props;
529+
const promise: Promise<MetricTag[]> = api.requestPromise(
530+
`/organizations/${organization.slug}/metrics/tags/`,
531+
{
532+
query: {
533+
project: !selection.projects.length ? undefined : selection.projects,
534+
},
535+
}
536+
);
537+
538+
try {
539+
const metricTags = await promise;
540+
this.setState({
541+
metricTags,
542+
});
543+
} catch (error) {
544+
const errorResponse = error?.responseJSON ?? t('Unable to fetch metric tags');
545+
addErrorMessage(errorResponse);
546+
handleXhrErrorResponse(errorResponse)(error);
547+
}
548+
}
549+
515550
handleDashboardChange(option: SelectValue<string>) {
516551
this.setState({selectedDashboard: option});
517552
}
@@ -583,6 +618,10 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
583618
: selection;
584619

585620
const issueWidgetFieldOptions = generateIssueWidgetFieldOptions();
621+
const metricsWidgetFieldOptions = generateMetricsWidgetFieldOptions(
622+
METRICS_FIELDS,
623+
Object.values(state.metricTags).map(({key}) => key)
624+
);
586625
const fieldOptions = (measurementKeys: string[]) =>
587626
generateFieldOptions({
588627
organization,
@@ -625,7 +664,23 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
625664
);
626665

627666
case WidgetType.METRICS:
628-
return null;
667+
return (
668+
<WidgetQueriesForm
669+
organization={organization}
670+
selection={querySelection}
671+
displayType={state.displayType}
672+
widgetType={state.widgetType}
673+
queries={state.queries}
674+
errors={errors?.queries}
675+
fieldOptions={metricsWidgetFieldOptions}
676+
onChange={(queryIndex: number, widgetQuery: WidgetQuery) =>
677+
this.handleQueryChange(widgetQuery, queryIndex)
678+
}
679+
canAddSearchConditions={this.canAddSearchConditions()}
680+
handleAddSearchConditions={this.handleAddSearchConditions}
681+
handleDeleteQuery={this.handleQueryRemove}
682+
/>
683+
);
629684

630685
case WidgetType.DISCOVER:
631686
default:
@@ -641,6 +696,7 @@ class AddDashboardWidgetModal extends React.Component<Props, State> {
641696
selection={querySelection}
642697
fieldOptions={amendedFieldOptions}
643698
displayType={state.displayType}
699+
widgetType={state.widgetType}
644700
queries={state.queries}
645701
errors={errors?.queries}
646702
onChange={(queryIndex: number, widgetQuery: WidgetQuery) =>

static/app/utils/discover/fields.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import isEqual from 'lodash/isEqual';
33
import {RELEASE_ADOPTION_STAGES} from 'sentry/constants';
44
import {Organization, SelectValue} from 'sentry/types';
55
import {assert} from 'sentry/types/utils';
6+
import {MetricsColumnType} from 'sentry/views/dashboardsV2/widgetBuilder/metricWidget/fields';
67

78
import {METRIC_TO_COLUMN_TYPE} from '../metrics/fields';
89

@@ -36,7 +37,10 @@ export type ParsedFunction = {
3637

3738
type ValidateColumnValueFunction = ({name: string, dataType: ColumnType}) => boolean;
3839

39-
export type ValidateColumnTypes = ColumnType[] | ValidateColumnValueFunction;
40+
export type ValidateColumnTypes =
41+
| ColumnType[]
42+
| MetricsColumnType[]
43+
| ValidateColumnValueFunction;
4044

4145
export type AggregateParameter =
4246
| {

0 commit comments

Comments
 (0)