metricField.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import Button from 'sentry/components/button';
  5. import FormField from 'sentry/components/forms/formField';
  6. import FormModel from 'sentry/components/forms/model';
  7. import Tooltip from 'sentry/components/tooltip';
  8. import {t, tct} from 'sentry/locale';
  9. import {Organization} from 'sentry/types';
  10. import {
  11. Aggregation,
  12. AGGREGATIONS,
  13. ColumnType,
  14. explodeFieldString,
  15. FIELDS,
  16. generateFieldAsString,
  17. } from 'sentry/utils/discover/fields';
  18. import {
  19. AlertType,
  20. hideParameterSelectorSet,
  21. hidePrimarySelectorSet,
  22. } from 'sentry/views/alerts/wizard/options';
  23. import {QueryField} from 'sentry/views/eventsV2/table/queryField';
  24. import {FieldValueKind} from 'sentry/views/eventsV2/table/types';
  25. import {generateFieldOptions} from 'sentry/views/eventsV2/utils';
  26. import {
  27. errorFieldConfig,
  28. getWizardAlertFieldConfig,
  29. OptionConfig,
  30. transactionFieldConfig,
  31. } from './constants';
  32. import {PRESET_AGGREGATES} from './presets';
  33. import {Dataset} from './types';
  34. type Props = Omit<FormField['props'], 'children'> & {
  35. organization: Organization;
  36. alertType?: AlertType;
  37. /**
  38. * Optionally set a width for each column of selector
  39. */
  40. columnWidth?: number;
  41. inFieldLabels?: boolean;
  42. };
  43. const getFieldOptionConfig = ({
  44. dataset,
  45. alertType,
  46. }: {
  47. dataset: Dataset;
  48. alertType?: AlertType;
  49. }) => {
  50. let config: OptionConfig;
  51. let hidePrimarySelector = false;
  52. let hideParameterSelector = false;
  53. if (alertType) {
  54. config = getWizardAlertFieldConfig(alertType, dataset);
  55. hidePrimarySelector = hidePrimarySelectorSet.has(alertType);
  56. hideParameterSelector = hideParameterSelectorSet.has(alertType);
  57. } else {
  58. config = dataset === Dataset.ERRORS ? errorFieldConfig : transactionFieldConfig;
  59. }
  60. const aggregations = Object.fromEntries<Aggregation>(
  61. config.aggregations.map(key => {
  62. // TODO(scttcper): Temporary hack for default value while we handle the translation of user
  63. if (key === 'count_unique') {
  64. const agg = AGGREGATIONS[key] as Aggregation;
  65. agg.getFieldOverrides = () => {
  66. return {defaultValue: 'tags[sentry:user]'};
  67. };
  68. return [key, agg];
  69. }
  70. return [key, AGGREGATIONS[key]];
  71. })
  72. );
  73. const fields = Object.fromEntries<ColumnType>(
  74. config.fields.map(key => {
  75. // XXX(epurkhiser): Temporary hack while we handle the translation of user ->
  76. // tags[sentry:user].
  77. if (key === 'user') {
  78. return ['tags[sentry:user]', 'string'];
  79. }
  80. return [key, FIELDS[key]];
  81. })
  82. );
  83. const {measurementKeys} = config;
  84. return {
  85. fieldOptionsConfig: {aggregations, fields, measurementKeys},
  86. hidePrimarySelector,
  87. hideParameterSelector,
  88. };
  89. };
  90. const help = ({name, model}: {model: FormModel; name: string}) => {
  91. const aggregate = model.getValue(name) as string;
  92. const presets = PRESET_AGGREGATES.filter(preset =>
  93. preset.validDataset.includes(model.getValue('dataset') as Dataset)
  94. )
  95. .map(preset => ({...preset, selected: preset.match.test(aggregate)}))
  96. .map((preset, i, list) => (
  97. <Fragment key={preset.name}>
  98. <Tooltip title={t('This preset is selected')} disabled={!preset.selected}>
  99. <PresetButton
  100. type="button"
  101. onClick={() => model.setValue(name, preset.default)}
  102. disabled={preset.selected}
  103. >
  104. {preset.name}
  105. </PresetButton>
  106. </Tooltip>
  107. {i + 1 < list.length && ', '}
  108. </Fragment>
  109. ));
  110. return tct(
  111. 'Choose an aggregate function. Not sure what to select, try a preset: [presets]',
  112. {presets}
  113. );
  114. };
  115. const MetricField = ({
  116. organization,
  117. columnWidth,
  118. inFieldLabels,
  119. alertType,
  120. ...props
  121. }: Props) => (
  122. <FormField help={help} {...props}>
  123. {({onChange, value, model, disabled}) => {
  124. const dataset = model.getValue('dataset');
  125. const {fieldOptionsConfig, hidePrimarySelector, hideParameterSelector} =
  126. getFieldOptionConfig({
  127. dataset: dataset as Dataset,
  128. alertType,
  129. });
  130. const fieldOptions = generateFieldOptions({organization, ...fieldOptionsConfig});
  131. const fieldValue = explodeFieldString(value ?? '');
  132. const fieldKey =
  133. fieldValue?.kind === FieldValueKind.FUNCTION
  134. ? `function:${fieldValue.function[0]}`
  135. : '';
  136. const selectedField = fieldOptions[fieldKey]?.value;
  137. const numParameters: number =
  138. selectedField?.kind === FieldValueKind.FUNCTION
  139. ? selectedField.meta.parameters.length
  140. : 0;
  141. const parameterColumns =
  142. numParameters - (hideParameterSelector ? 1 : 0) - (hidePrimarySelector ? 1 : 0);
  143. return (
  144. <Fragment>
  145. <StyledQueryField
  146. filterPrimaryOptions={option => option.value.kind === FieldValueKind.FUNCTION}
  147. fieldOptions={fieldOptions}
  148. fieldValue={fieldValue}
  149. onChange={v => onChange(generateFieldAsString(v), {})}
  150. columnWidth={columnWidth}
  151. gridColumns={parameterColumns + 1}
  152. inFieldLabels={inFieldLabels}
  153. shouldRenderTag={false}
  154. disabled={disabled}
  155. hideParameterSelector={hideParameterSelector}
  156. hidePrimarySelector={hidePrimarySelector}
  157. />
  158. </Fragment>
  159. );
  160. }}
  161. </FormField>
  162. );
  163. const StyledQueryField = styled(QueryField)<{gridColumns: number; columnWidth?: number}>`
  164. ${p =>
  165. p.columnWidth &&
  166. css`
  167. width: ${p.gridColumns * p.columnWidth}px;
  168. `}
  169. `;
  170. const PresetButton = styled(Button)<{disabled: boolean}>`
  171. ${p =>
  172. p.disabled &&
  173. css`
  174. color: ${p.theme.textColor};
  175. &:hover,
  176. &:focus {
  177. color: ${p.theme.textColor};
  178. }
  179. `}
  180. `;
  181. PresetButton.defaultProps = {
  182. priority: 'link',
  183. borderless: true,
  184. };
  185. export default MetricField;