wizardField.tsx 6.8 KB

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