ruleRow.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {Component, Fragment} from 'react';
  2. import {Link, RouteComponentProps} from 'react-router';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {t, tct} from 'app/locale';
  6. import space from 'app/styles/space';
  7. import {IssueAlertRule} from 'app/types/alerts';
  8. import {getDisplayName} from 'app/utils/environment';
  9. import recreateRoute from 'app/utils/recreateRoute';
  10. import {
  11. AlertRuleThresholdType,
  12. SavedIncidentRule,
  13. } from 'app/views/settings/incidentRules/types';
  14. function isIssueAlert(data: IssueAlertRule | SavedIncidentRule): data is IssueAlertRule {
  15. return !data.hasOwnProperty('triggers');
  16. }
  17. type Props = {
  18. data: IssueAlertRule | SavedIncidentRule;
  19. type: 'issue' | 'metric';
  20. // Is the alert rule editable?
  21. canEdit?: boolean;
  22. } & Pick<
  23. RouteComponentProps<{orgId: string; projectId: string}, {}>,
  24. 'params' | 'routes' | 'location'
  25. >;
  26. type State = {
  27. loading: boolean;
  28. error: boolean;
  29. };
  30. class RuleRow extends Component<Props, State> {
  31. state: State = {loading: false, error: false};
  32. renderIssueRule(data: IssueAlertRule) {
  33. const {params, routes, location, canEdit} = this.props;
  34. const editLink = recreateRoute(`rules/${data.id}/`, {
  35. params,
  36. routes,
  37. location,
  38. });
  39. const environmentName = data.environment
  40. ? getDisplayName({name: data.environment})
  41. : t('All Environments');
  42. return (
  43. <Fragment>
  44. <RuleType>{t('Issue')}</RuleType>
  45. <div>
  46. {canEdit ? <RuleName to={editLink}>{data.name}</RuleName> : data.name}
  47. <RuleDescription>
  48. {t('Environment')}: {environmentName}
  49. </RuleDescription>
  50. </div>
  51. <ConditionsWithHeader>
  52. <MatchTypeHeader>
  53. {tct('[matchType] of the following:', {
  54. matchType: data.actionMatch,
  55. })}
  56. </MatchTypeHeader>
  57. {data.conditions.length !== 0 && (
  58. <Conditions>
  59. {data.conditions.map((condition, i) => (
  60. <div key={i}>{condition.name}</div>
  61. ))}
  62. </Conditions>
  63. )}
  64. </ConditionsWithHeader>
  65. <Actions>
  66. {data.actions.map((action, i) => (
  67. <Action key={i}>{action.name}</Action>
  68. ))}
  69. </Actions>
  70. </Fragment>
  71. );
  72. }
  73. renderMetricRule(data: SavedIncidentRule) {
  74. const {params, routes, location, canEdit} = this.props;
  75. const editLink = recreateRoute(`metric-rules/${data.id}/`, {
  76. params,
  77. routes,
  78. location,
  79. });
  80. const numberOfTriggers = data.triggers.length;
  81. return (
  82. <Fragment>
  83. <RuleType rowSpans={numberOfTriggers}>{t('Metric')}</RuleType>
  84. <RuleNameAndDescription rowSpans={numberOfTriggers}>
  85. {canEdit ? <RuleName to={editLink}>{data.name}</RuleName> : data.name}
  86. <RuleDescription />
  87. </RuleNameAndDescription>
  88. {numberOfTriggers !== 0 &&
  89. data.triggers.map((trigger, i) => {
  90. const hideBorder = i !== numberOfTriggers - 1;
  91. return (
  92. <Fragment key={i}>
  93. <Trigger key={`trigger-${i}`} hideBorder={hideBorder}>
  94. <StatusBadge>{trigger.label}</StatusBadge>
  95. <TriggerDescription>
  96. {data.aggregate}{' '}
  97. {data.thresholdType === AlertRuleThresholdType.ABOVE
  98. ? t('above')
  99. : t('below')}{' '}
  100. {trigger.alertThreshold}/{data.timeWindow}
  101. {t('min')}
  102. </TriggerDescription>
  103. </Trigger>
  104. <Actions key={`actions-${i}`} hideBorder={hideBorder}>
  105. {trigger.actions?.length
  106. ? trigger.actions.map((action, j) => (
  107. <Action key={j}>{action.desc}</Action>
  108. ))
  109. : t('None')}
  110. </Actions>
  111. </Fragment>
  112. );
  113. })}
  114. </Fragment>
  115. );
  116. }
  117. render() {
  118. const {data} = this.props;
  119. return isIssueAlert(data) ? this.renderIssueRule(data) : this.renderMetricRule(data);
  120. }
  121. }
  122. export default RuleRow;
  123. type RowSpansProp = {
  124. rowSpans?: number;
  125. };
  126. type HasBorderProp = {
  127. hideBorder?: boolean;
  128. };
  129. const RuleType = styled('div')<RowSpansProp>`
  130. color: ${p => p.theme.subText};
  131. font-size: ${p => p.theme.fontSizeSmall};
  132. font-weight: bold;
  133. text-transform: uppercase;
  134. ${p => p.rowSpans && `grid-row: auto / span ${p.rowSpans}`};
  135. `;
  136. const RuleNameAndDescription = styled('div')<RowSpansProp>`
  137. ${p => p.rowSpans && `grid-row: auto / span ${p.rowSpans}`};
  138. `;
  139. const RuleName = styled(Link)`
  140. font-weight: bold;
  141. `;
  142. const listingCss = css`
  143. display: grid;
  144. grid-gap: ${space(1)};
  145. `;
  146. const Conditions = styled('div')`
  147. ${listingCss};
  148. `;
  149. const Actions = styled('div')<HasBorderProp>`
  150. font-size: ${p => p.theme.fontSizeSmall};
  151. ${listingCss};
  152. ${p => p.hideBorder && `border-bottom: none`};
  153. `;
  154. const Action = styled('div')`
  155. line-height: 14px;
  156. `;
  157. const ConditionsWithHeader = styled('div')`
  158. font-size: ${p => p.theme.fontSizeSmall};
  159. `;
  160. const MatchTypeHeader = styled('div')`
  161. font-weight: bold;
  162. text-transform: uppercase;
  163. color: ${p => p.theme.gray300};
  164. margin-bottom: ${space(1)};
  165. `;
  166. const RuleDescription = styled('div')`
  167. font-size: ${p => p.theme.fontSizeSmall};
  168. margin: ${space(0.5)} 0;
  169. white-space: nowrap;
  170. `;
  171. const Trigger = styled('div')<HasBorderProp>`
  172. display: flex;
  173. align-items: flex-start;
  174. font-size: ${p => p.theme.fontSizeSmall};
  175. ${p => p.hideBorder && `border-bottom: none`};
  176. `;
  177. const TriggerDescription = styled('div')`
  178. white-space: nowrap;
  179. `;
  180. const StatusBadge = styled('div')`
  181. background-color: ${p => p.theme.gray200};
  182. color: ${p => p.theme.textColor};
  183. text-transform: uppercase;
  184. padding: ${space(0.25)} ${space(0.5)};
  185. font-weight: 600;
  186. margin-right: ${space(0.5)};
  187. border-radius: ${p => p.theme.borderRadius};
  188. font-size: ${p => p.theme.fontSizeRelativeSmall};
  189. `;