wizardField.tsx 8.2 KB

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