wizardField.tsx 7.8 KB

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