form.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import {Component, Fragment, PureComponent} from 'react';
  2. import styled from '@emotion/styled';
  3. import {fetchOrgMembers} from 'sentry/actionCreators/members';
  4. import {Client} from 'sentry/api';
  5. import CircleIndicator from 'sentry/components/circleIndicator';
  6. import Field from 'sentry/components/forms/field';
  7. import {IconDiamond} from 'sentry/icons';
  8. import {t, tct} from 'sentry/locale';
  9. import space from 'sentry/styles/space';
  10. import {Config, Organization, Project} from 'sentry/types';
  11. import withApi from 'sentry/utils/withApi';
  12. import withConfig from 'sentry/utils/withConfig';
  13. import {getThresholdUnits} from 'sentry/views/alerts/rules/metric/constants';
  14. import ThresholdControl from 'sentry/views/alerts/rules/metric/triggers/thresholdControl';
  15. import {isSessionAggregate} from '../../../utils';
  16. import {
  17. AlertRuleComparisonType,
  18. AlertRuleThresholdType,
  19. AlertRuleTriggerType,
  20. ThresholdControlValue,
  21. Trigger,
  22. UnsavedMetricRule,
  23. UnsavedTrigger,
  24. } from '../types';
  25. type Props = {
  26. aggregate: UnsavedMetricRule['aggregate'];
  27. api: Client;
  28. comparisonType: AlertRuleComparisonType;
  29. config: Config;
  30. disabled: boolean;
  31. fieldHelp: React.ReactNode;
  32. hasAlertWizardV3: boolean;
  33. isCritical: boolean;
  34. onChange: (trigger: Trigger, changeObj: Partial<Trigger>) => void;
  35. onThresholdPeriodChange: (value: number) => void;
  36. onThresholdTypeChange: (thresholdType: AlertRuleThresholdType) => void;
  37. organization: Organization;
  38. placeholder: string;
  39. projects: Project[];
  40. resolveThreshold: UnsavedMetricRule['resolveThreshold'];
  41. thresholdPeriod: UnsavedMetricRule['thresholdPeriod'];
  42. thresholdType: UnsavedMetricRule['thresholdType'];
  43. trigger: Trigger;
  44. triggerIndex: number;
  45. triggerLabel: React.ReactNode;
  46. /**
  47. * Map of fieldName -> errorMessage
  48. */
  49. error?: {[fieldName: string]: string};
  50. hideControl?: boolean;
  51. };
  52. class TriggerFormItem extends PureComponent<Props> {
  53. /**
  54. * Handler for threshold changes coming from slider or chart.
  55. * Needs to sync state with the form.
  56. */
  57. handleChangeThreshold = (value: ThresholdControlValue) => {
  58. const {onChange, trigger} = this.props;
  59. onChange(
  60. {
  61. ...trigger,
  62. alertThreshold: value.threshold,
  63. },
  64. {alertThreshold: value.threshold}
  65. );
  66. };
  67. render() {
  68. const {
  69. disabled,
  70. error,
  71. trigger,
  72. isCritical,
  73. thresholdType,
  74. thresholdPeriod,
  75. hasAlertWizardV3,
  76. hideControl,
  77. comparisonType,
  78. fieldHelp,
  79. triggerLabel,
  80. placeholder,
  81. onThresholdTypeChange,
  82. onThresholdPeriodChange,
  83. } = this.props;
  84. return (
  85. <StyledField
  86. label={triggerLabel}
  87. help={fieldHelp}
  88. required={isCritical}
  89. error={error && error.alertThreshold}
  90. hasAlertWizardV3={hasAlertWizardV3}
  91. >
  92. <ThresholdControl
  93. disabled={disabled}
  94. disableThresholdType={!isCritical}
  95. type={trigger.label}
  96. thresholdType={thresholdType}
  97. thresholdPeriod={thresholdPeriod}
  98. hideControl={hideControl}
  99. threshold={trigger.alertThreshold}
  100. comparisonType={comparisonType}
  101. placeholder={placeholder}
  102. onChange={this.handleChangeThreshold}
  103. onThresholdTypeChange={onThresholdTypeChange}
  104. onThresholdPeriodChange={onThresholdPeriodChange}
  105. />
  106. </StyledField>
  107. );
  108. }
  109. }
  110. type TriggerFormContainerProps = Omit<
  111. React.ComponentProps<typeof TriggerFormItem>,
  112. | 'onChange'
  113. | 'isCritical'
  114. | 'error'
  115. | 'triggerIndex'
  116. | 'trigger'
  117. | 'fieldHelp'
  118. | 'triggerHelp'
  119. | 'triggerLabel'
  120. | 'placeholder'
  121. > & {
  122. hasAlertWizardV3: boolean;
  123. onChange: (triggerIndex: number, trigger: Trigger, changeObj: Partial<Trigger>) => void;
  124. onResolveThresholdChange: (
  125. resolveThreshold: UnsavedMetricRule['resolveThreshold']
  126. ) => void;
  127. triggers: Trigger[];
  128. errors?: Map<number, {[fieldName: string]: string}>;
  129. };
  130. class TriggerFormContainer extends Component<TriggerFormContainerProps> {
  131. componentDidMount() {
  132. const {api, organization} = this.props;
  133. fetchOrgMembers(api, organization.slug);
  134. }
  135. handleChangeTrigger =
  136. (triggerIndex: number) => (trigger: Trigger, changeObj: Partial<Trigger>) => {
  137. const {onChange} = this.props;
  138. onChange(triggerIndex, trigger, changeObj);
  139. };
  140. handleChangeResolveTrigger = (trigger: Trigger, _: Partial<Trigger>) => {
  141. const {onResolveThresholdChange} = this.props;
  142. onResolveThresholdChange(trigger.alertThreshold);
  143. };
  144. getCriticalThresholdPlaceholder(
  145. aggregate: string,
  146. comparisonType: AlertRuleComparisonType
  147. ) {
  148. if (aggregate.includes('failure_rate')) {
  149. return '0.05';
  150. }
  151. if (isSessionAggregate(aggregate)) {
  152. return '97';
  153. }
  154. if (comparisonType === AlertRuleComparisonType.CHANGE) {
  155. return '100';
  156. }
  157. return '300';
  158. }
  159. getIndicator(type: AlertRuleTriggerType) {
  160. const {hasAlertWizardV3} = this.props;
  161. if (type === AlertRuleTriggerType.CRITICAL) {
  162. return hasAlertWizardV3 ? (
  163. <StyledIconDiamond color="red300" size="sm" />
  164. ) : (
  165. <CriticalIndicator size={12} />
  166. );
  167. }
  168. if (type === AlertRuleTriggerType.WARNING) {
  169. return hasAlertWizardV3 ? (
  170. <StyledIconDiamond color="yellow300" size="sm" />
  171. ) : (
  172. <WarningIndicator size={12} />
  173. );
  174. }
  175. return hasAlertWizardV3 ? (
  176. <StyledIconDiamond color="green300" size="sm" />
  177. ) : (
  178. <ResolvedIndicator size={12} />
  179. );
  180. }
  181. render() {
  182. const {
  183. api,
  184. config,
  185. disabled,
  186. errors,
  187. organization,
  188. triggers,
  189. thresholdType,
  190. thresholdPeriod,
  191. comparisonType,
  192. aggregate,
  193. resolveThreshold,
  194. projects,
  195. hasAlertWizardV3,
  196. onThresholdTypeChange,
  197. onThresholdPeriodChange,
  198. } = this.props;
  199. const resolveTrigger: UnsavedTrigger = {
  200. label: AlertRuleTriggerType.RESOLVE,
  201. alertThreshold: resolveThreshold,
  202. actions: [],
  203. };
  204. const thresholdUnits = getThresholdUnits(aggregate, comparisonType);
  205. return (
  206. <Fragment>
  207. {triggers.map((trigger, index) => {
  208. const isCritical = index === 0;
  209. // eslint-disable-next-line no-use-before-define
  210. return (
  211. <TriggerFormItem
  212. key={index}
  213. api={api}
  214. config={config}
  215. disabled={disabled}
  216. error={errors && errors.get(index)}
  217. trigger={trigger}
  218. thresholdPeriod={thresholdPeriod}
  219. thresholdType={thresholdType}
  220. comparisonType={comparisonType}
  221. aggregate={aggregate}
  222. resolveThreshold={resolveThreshold}
  223. organization={organization}
  224. projects={projects}
  225. triggerIndex={index}
  226. isCritical={isCritical}
  227. hasAlertWizardV3={hasAlertWizardV3}
  228. fieldHelp={
  229. hasAlertWizardV3
  230. ? null
  231. : tct(
  232. 'The threshold[units] that will activate the [severity] status.',
  233. {
  234. severity: isCritical ? t('critical') : t('warning'),
  235. units: thresholdUnits ? ` (${thresholdUnits})` : '',
  236. }
  237. )
  238. }
  239. triggerLabel={
  240. <TriggerLabel>
  241. {this.getIndicator(
  242. isCritical
  243. ? AlertRuleTriggerType.CRITICAL
  244. : AlertRuleTriggerType.WARNING
  245. )}
  246. {isCritical ? t('Critical') : t('Warning')}
  247. </TriggerLabel>
  248. }
  249. placeholder={
  250. isCritical
  251. ? `${this.getCriticalThresholdPlaceholder(aggregate, comparisonType)}${
  252. comparisonType === AlertRuleComparisonType.COUNT
  253. ? thresholdUnits
  254. : ''
  255. }`
  256. : t('None')
  257. }
  258. onChange={this.handleChangeTrigger(index)}
  259. onThresholdTypeChange={onThresholdTypeChange}
  260. onThresholdPeriodChange={onThresholdPeriodChange}
  261. />
  262. );
  263. })}
  264. <TriggerFormItem
  265. api={api}
  266. config={config}
  267. disabled={disabled}
  268. error={errors && errors.get(2)}
  269. trigger={resolveTrigger}
  270. // Flip rule thresholdType to opposite
  271. thresholdPeriod={thresholdPeriod}
  272. thresholdType={+!thresholdType}
  273. comparisonType={comparisonType}
  274. aggregate={aggregate}
  275. resolveThreshold={resolveThreshold}
  276. organization={organization}
  277. projects={projects}
  278. triggerIndex={2}
  279. isCritical={false}
  280. hasAlertWizardV3={hasAlertWizardV3}
  281. fieldHelp={
  282. hasAlertWizardV3
  283. ? null
  284. : tct('The threshold[units] that will activate the resolved status.', {
  285. units: thresholdUnits ? ` (${thresholdUnits})` : '',
  286. })
  287. }
  288. triggerLabel={
  289. <TriggerLabel>
  290. {this.getIndicator(AlertRuleTriggerType.RESOLVE)}
  291. {t('Resolved')}
  292. </TriggerLabel>
  293. }
  294. placeholder={t('Automatic')}
  295. onChange={this.handleChangeResolveTrigger}
  296. onThresholdTypeChange={onThresholdTypeChange}
  297. onThresholdPeriodChange={onThresholdPeriodChange}
  298. />
  299. </Fragment>
  300. );
  301. }
  302. }
  303. const CriticalIndicator = styled(CircleIndicator)`
  304. background: ${p => p.theme.red300};
  305. margin-right: ${space(1)};
  306. `;
  307. const WarningIndicator = styled(CircleIndicator)`
  308. background: ${p => p.theme.yellow300};
  309. margin-right: ${space(1)};
  310. `;
  311. const ResolvedIndicator = styled(CircleIndicator)`
  312. background: ${p => p.theme.green300};
  313. margin-right: ${space(1)};
  314. `;
  315. const TriggerLabel = styled('div')`
  316. display: flex;
  317. flex-direction: row;
  318. align-items: center;
  319. `;
  320. const StyledIconDiamond = styled(IconDiamond)`
  321. margin-right: ${space(0.75)};
  322. `;
  323. const StyledField = styled(Field)<{hasAlertWizardV3: boolean}>`
  324. & > label > div:first-child > span {
  325. display: flex;
  326. flex-direction: row;
  327. }
  328. `;
  329. export default withConfig(withApi(TriggerFormContainer));