wizardField.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {css} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import SelectControl from 'sentry/components/forms/controls/selectControl';
  4. import FormField, {FormFieldProps} from 'sentry/components/forms/formField';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import {Organization, Project} from 'sentry/types';
  8. import {explodeFieldString, generateFieldAsString} from 'sentry/utils/discover/fields';
  9. import {hasDDMFeature} from 'sentry/utils/metrics/features';
  10. import MriField from 'sentry/views/alerts/rules/metric/mriField';
  11. import {Dataset} from 'sentry/views/alerts/rules/metric/types';
  12. import {
  13. AlertType,
  14. AlertWizardAlertNames,
  15. AlertWizardRuleTemplates,
  16. } from 'sentry/views/alerts/wizard/options';
  17. import {QueryField} from 'sentry/views/discover/table/queryField';
  18. import {FieldValueKind} from 'sentry/views/discover/table/types';
  19. import {generateFieldOptions} from 'sentry/views/discover/utils';
  20. import {getFieldOptionConfig} from './metricField';
  21. type MenuOption = {label: string; value: AlertType};
  22. type GroupedMenuOption = {label: string; options: Array<MenuOption>};
  23. type Props = Omit<FormFieldProps, 'children'> & {
  24. organization: Organization;
  25. project: Project;
  26. alertType?: AlertType;
  27. /**
  28. * Optionally set a width for each column of selector
  29. */
  30. columnWidth?: number;
  31. inFieldLabels?: boolean;
  32. };
  33. export default function WizardField({
  34. organization,
  35. columnWidth,
  36. inFieldLabels,
  37. alertType,
  38. project,
  39. ...fieldProps
  40. }: Props) {
  41. const menuOptions: GroupedMenuOption[] = [
  42. {
  43. label: t('ERRORS'),
  44. options: [
  45. {
  46. label: AlertWizardAlertNames.num_errors,
  47. value: 'num_errors',
  48. },
  49. {
  50. label: AlertWizardAlertNames.users_experiencing_errors,
  51. value: 'users_experiencing_errors',
  52. },
  53. ],
  54. },
  55. ...((organization.features.includes('crash-rate-alerts')
  56. ? [
  57. {
  58. label: t('SESSIONS'),
  59. options: [
  60. {
  61. label: AlertWizardAlertNames.crash_free_sessions,
  62. value: 'crash_free_sessions',
  63. },
  64. {
  65. label: AlertWizardAlertNames.crash_free_users,
  66. value: 'crash_free_users',
  67. },
  68. ],
  69. },
  70. ]
  71. : []) as GroupedMenuOption[]),
  72. {
  73. label: t('PERFORMANCE'),
  74. options: [
  75. {
  76. label: AlertWizardAlertNames.throughput,
  77. value: 'throughput',
  78. },
  79. {
  80. label: AlertWizardAlertNames.trans_duration,
  81. value: 'trans_duration',
  82. },
  83. {
  84. label: AlertWizardAlertNames.apdex,
  85. value: 'apdex',
  86. },
  87. {
  88. label: AlertWizardAlertNames.failure_rate,
  89. value: 'failure_rate',
  90. },
  91. {
  92. label: AlertWizardAlertNames.lcp,
  93. value: 'lcp',
  94. },
  95. {
  96. label: AlertWizardAlertNames.fid,
  97. value: 'fid',
  98. },
  99. {
  100. label: AlertWizardAlertNames.cls,
  101. value: 'cls',
  102. },
  103. ...(hasDDMFeature(organization)
  104. ? [
  105. {
  106. label: AlertWizardAlertNames.custom_transactions,
  107. value: 'custom_transactions' as const,
  108. },
  109. ]
  110. : []),
  111. ],
  112. },
  113. {
  114. label: hasDDMFeature(organization) ? t('METRICS') : t('CUSTOM'),
  115. options: [
  116. hasDDMFeature(organization)
  117. ? {
  118. label: AlertWizardAlertNames.custom_metrics,
  119. value: 'custom_metrics',
  120. }
  121. : {
  122. label: AlertWizardAlertNames.custom_transactions,
  123. value: 'custom_transactions',
  124. },
  125. ],
  126. },
  127. ];
  128. return (
  129. <FormField {...fieldProps}>
  130. {({onChange, model, disabled}) => {
  131. const aggregate = model.getValue('aggregate');
  132. const dataset: Dataset = model.getValue('dataset');
  133. const selectedTemplate: AlertType = alertType || 'custom_metrics';
  134. const {fieldOptionsConfig, hidePrimarySelector, hideParameterSelector} =
  135. getFieldOptionConfig({
  136. dataset: dataset as Dataset,
  137. alertType,
  138. });
  139. const fieldOptions = generateFieldOptions({organization, ...fieldOptionsConfig});
  140. const fieldValue = explodeFieldString(aggregate ?? '');
  141. const fieldKey =
  142. fieldValue?.kind === FieldValueKind.FUNCTION
  143. ? `function:${fieldValue.function[0]}`
  144. : '';
  145. const selectedField = fieldOptions[fieldKey]?.value;
  146. const numParameters: number =
  147. selectedField?.kind === FieldValueKind.FUNCTION
  148. ? selectedField.meta.parameters.length
  149. : 0;
  150. const gridColumns =
  151. 1 +
  152. numParameters -
  153. (hideParameterSelector ? 1 : 0) -
  154. (hidePrimarySelector ? 1 : 0);
  155. return (
  156. <Container hideGap={gridColumns < 1}>
  157. <SelectControl
  158. value={selectedTemplate}
  159. options={menuOptions}
  160. disabled={disabled}
  161. onChange={(option: MenuOption) => {
  162. const template = AlertWizardRuleTemplates[option.value];
  163. model.setValue('aggregate', template.aggregate);
  164. model.setValue('dataset', template.dataset);
  165. model.setValue('eventTypes', [template.eventTypes]);
  166. // Keep alertType last
  167. model.setValue('alertType', option.value);
  168. }}
  169. />
  170. {hasDDMFeature(organization) && alertType === 'custom_metrics' ? (
  171. <MriField
  172. project={project}
  173. aggregate={aggregate}
  174. onChange={newAggregate => onChange(newAggregate, {})}
  175. />
  176. ) : (
  177. <StyledQueryField
  178. filterPrimaryOptions={option =>
  179. option.value.kind === FieldValueKind.FUNCTION
  180. }
  181. fieldOptions={fieldOptions}
  182. fieldValue={fieldValue}
  183. onChange={v => onChange(generateFieldAsString(v), {})}
  184. columnWidth={columnWidth}
  185. gridColumns={gridColumns}
  186. inFieldLabels={inFieldLabels}
  187. shouldRenderTag={false}
  188. disabled={disabled}
  189. hideParameterSelector={hideParameterSelector}
  190. hidePrimarySelector={hidePrimarySelector}
  191. />
  192. )}
  193. </Container>
  194. );
  195. }}
  196. </FormField>
  197. );
  198. }
  199. const Container = styled('div')<{hideGap: boolean}>`
  200. display: grid;
  201. grid-template-columns: 1fr auto;
  202. gap: ${p => (p.hideGap ? space(0) : space(1))};
  203. `;
  204. const StyledQueryField = styled(QueryField)<{gridColumns: number; columnWidth?: number}>`
  205. ${p =>
  206. p.columnWidth &&
  207. css`
  208. width: ${p.gridColumns * p.columnWidth}px;
  209. `}
  210. `;