wizardField.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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 {hasDdmAlertsSupport} 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. ...(hasDdmAlertsSupport(organization)
  104. ? [
  105. {
  106. label: AlertWizardAlertNames.custom_metrics,
  107. value: 'custom_transactions' as const,
  108. },
  109. ]
  110. : []),
  111. ],
  112. },
  113. {
  114. label: hasDdmAlertsSupport(organization) ? t('METRICS') : t('CUSTOM'),
  115. options: [
  116. {
  117. label: AlertWizardAlertNames.custom_metrics,
  118. value: hasDdmAlertsSupport(organization)
  119. ? 'custom_metrics'
  120. : 'custom_transactions',
  121. },
  122. ],
  123. },
  124. ];
  125. return (
  126. <FormField {...fieldProps}>
  127. {({onChange, model, disabled}) => {
  128. const aggregate = model.getValue('aggregate');
  129. const dataset: Dataset = model.getValue('dataset');
  130. const selectedTemplate: AlertType = alertType || 'custom_metrics';
  131. const {fieldOptionsConfig, hidePrimarySelector, hideParameterSelector} =
  132. getFieldOptionConfig({
  133. dataset: dataset as Dataset,
  134. alertType,
  135. });
  136. const fieldOptions = generateFieldOptions({organization, ...fieldOptionsConfig});
  137. const fieldValue = explodeFieldString(aggregate ?? '');
  138. const fieldKey =
  139. fieldValue?.kind === FieldValueKind.FUNCTION
  140. ? `function:${fieldValue.function[0]}`
  141. : '';
  142. const selectedField = fieldOptions[fieldKey]?.value;
  143. const numParameters: number =
  144. selectedField?.kind === FieldValueKind.FUNCTION
  145. ? selectedField.meta.parameters.length
  146. : 0;
  147. const gridColumns =
  148. 1 +
  149. numParameters -
  150. (hideParameterSelector ? 1 : 0) -
  151. (hidePrimarySelector ? 1 : 0);
  152. return (
  153. <Container hideGap={gridColumns < 1}>
  154. <SelectControl
  155. value={selectedTemplate}
  156. options={menuOptions}
  157. disabled={disabled}
  158. onChange={(option: MenuOption) => {
  159. const template = AlertWizardRuleTemplates[option.value];
  160. model.setValue('aggregate', template.aggregate);
  161. model.setValue('dataset', template.dataset);
  162. model.setValue('eventTypes', [template.eventTypes]);
  163. // Keep alertType last
  164. model.setValue('alertType', option.value);
  165. }}
  166. />
  167. {hasDdmAlertsSupport(organization) && alertType === 'custom_metrics' ? (
  168. <MriField
  169. project={project}
  170. aggregate={aggregate}
  171. onChange={newAggregate => onChange(newAggregate, {})}
  172. />
  173. ) : (
  174. <StyledQueryField
  175. filterPrimaryOptions={option =>
  176. option.value.kind === FieldValueKind.FUNCTION
  177. }
  178. fieldOptions={fieldOptions}
  179. fieldValue={fieldValue}
  180. onChange={v => onChange(generateFieldAsString(v), {})}
  181. columnWidth={columnWidth}
  182. gridColumns={gridColumns}
  183. inFieldLabels={inFieldLabels}
  184. shouldRenderTag={false}
  185. disabled={disabled}
  186. hideParameterSelector={hideParameterSelector}
  187. hidePrimarySelector={hidePrimarySelector}
  188. />
  189. )}
  190. </Container>
  191. );
  192. }}
  193. </FormField>
  194. );
  195. }
  196. const Container = styled('div')<{hideGap: boolean}>`
  197. display: grid;
  198. grid-template-columns: 1fr auto;
  199. gap: ${p => (p.hideGap ? space(0) : space(1))};
  200. `;
  201. const StyledQueryField = styled(QueryField)<{gridColumns: number; columnWidth?: number}>`
  202. ${p =>
  203. p.columnWidth &&
  204. css`
  205. width: ${p.gridColumns * p.columnWidth}px;
  206. `}
  207. `;