metricActivity.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import {Fragment, type ReactElement} from 'react';
  2. import styled from '@emotion/styled';
  3. import moment from 'moment-timezone';
  4. import Duration from 'sentry/components/duration';
  5. import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
  6. import Link from 'sentry/components/links/link';
  7. import {StatusIndicator} from 'sentry/components/statusIndicator';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {ActivationConditionType} from 'sentry/types/alerts';
  11. import type {Organization} from 'sentry/types/organization';
  12. import getDuration from 'sentry/utils/duration/getDuration';
  13. import getDynamicText from 'sentry/utils/getDynamicText';
  14. import {capitalize} from 'sentry/utils/string/capitalize';
  15. import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
  16. import {StyledDateTime} from 'sentry/views/alerts/rules/metric/details/styles';
  17. import {AlertRuleThresholdType} from 'sentry/views/alerts/rules/metric/types';
  18. import type {ActivityType, Incident} from 'sentry/views/alerts/types';
  19. import {IncidentActivityType, IncidentStatus} from 'sentry/views/alerts/types';
  20. import {alertDetailsLink} from 'sentry/views/alerts/utils';
  21. import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
  22. import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
  23. type MetricAlertActivityProps = {
  24. incident: Incident;
  25. organization: Organization;
  26. };
  27. function MetricAlertActivity({organization, incident}: MetricAlertActivityProps) {
  28. // NOTE: while _possible_, we should never expect an incident to _not_ have a status_change activity
  29. const activities: ActivityType[] = (incident.activities ?? []).filter(
  30. activity => activity.type === IncidentActivityType.STATUS_CHANGE
  31. );
  32. const statusValues = [String(IncidentStatus.CRITICAL), String(IncidentStatus.WARNING)];
  33. // TODO: kinda cheating with the forced `!`. Is there a better way to type this?
  34. const latestActivity: ActivityType = activities.find(activity =>
  35. statusValues.includes(String(activity.value))
  36. )!;
  37. const isCritical = Number(latestActivity.value) === IncidentStatus.CRITICAL;
  38. // Find the _final_ most recent activity _after_ our triggered activity
  39. // This exists for the `CLOSED` state (or any state NOT WARNING/CRITICAL)
  40. const finalActivity = activities.find(
  41. activity => activity.previousValue === latestActivity.value
  42. );
  43. const activityDuration = (
  44. finalActivity ? moment(finalActivity.dateCreated) : moment()
  45. ).diff(moment(latestActivity.dateCreated), 'milliseconds');
  46. const triggerLabel = isCritical ? 'critical' : 'warning';
  47. const curentTrigger = incident.alertRule.triggers.find(
  48. trigger => trigger.label === triggerLabel
  49. );
  50. const timeWindow = getDuration(incident.alertRule.timeWindow * 60);
  51. const alertName = capitalize(
  52. AlertWizardAlertNames[getAlertTypeFromAggregateDataset(incident.alertRule)]
  53. );
  54. const project = incident.alertRule.projects[0];
  55. const activation = incident.activation;
  56. let activationBlock: ReactElement | null = null;
  57. // TODO: Split this string check into a separate component
  58. if (activation) {
  59. let condition;
  60. let activator;
  61. switch (activation.conditionType) {
  62. case String(ActivationConditionType.RELEASE_CREATION):
  63. condition = 'Release';
  64. activator = (
  65. <GlobalSelectionLink
  66. to={{
  67. pathname: `/organizations/${
  68. organization.slug
  69. }/releases/${encodeURIComponent(activation.activator)}/`,
  70. query: {project: project},
  71. }}
  72. >
  73. {activation.activator}
  74. </GlobalSelectionLink>
  75. );
  76. break;
  77. case String(ActivationConditionType.DEPLOY_CREATION):
  78. condition = 'Deploy';
  79. activator = activation.activator;
  80. break;
  81. default:
  82. condition = '--';
  83. }
  84. activationBlock = (
  85. <div>
  86. &nbsp;from {condition} {activator}
  87. </div>
  88. );
  89. }
  90. return (
  91. <Fragment>
  92. <Cell>
  93. {latestActivity.value && (
  94. <StatusIndicator
  95. status={isCritical ? 'error' : 'warning'}
  96. tooltipTitle={t('Status: %s', isCritical ? t('Critical') : t('Warning'))}
  97. />
  98. )}
  99. <Link
  100. to={{
  101. pathname: alertDetailsLink(organization, incident),
  102. query: {alert: incident.identifier},
  103. }}
  104. >
  105. #{incident.identifier}
  106. </Link>
  107. </Cell>
  108. <Cell>
  109. {incident.alertRule.comparisonDelta ? (
  110. <Fragment>
  111. {alertName} {curentTrigger?.alertThreshold}%
  112. {t(
  113. ' %s in %s compared to the ',
  114. incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  115. ? t('higher')
  116. : t('lower'),
  117. timeWindow
  118. )}
  119. {COMPARISON_DELTA_OPTIONS.find(
  120. ({value}) => value === incident.alertRule.comparisonDelta
  121. )?.label ?? COMPARISON_DELTA_OPTIONS[0].label}
  122. </Fragment>
  123. ) : (
  124. <Fragment>
  125. {alertName}{' '}
  126. {incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  127. ? t('above')
  128. : t('below')}{' '}
  129. {curentTrigger?.alertThreshold || '_'} {t('within')} {timeWindow}
  130. {activationBlock}
  131. </Fragment>
  132. )}
  133. </Cell>
  134. <Cell>
  135. {activityDuration &&
  136. getDynamicText({
  137. value: <Duration abbreviation seconds={activityDuration / 1000} />,
  138. fixed: '30s',
  139. })}
  140. </Cell>
  141. <Cell>
  142. <StyledDateTime
  143. date={getDynamicText({
  144. value: incident.dateCreated,
  145. fixed: 'Mar 4, 2022 10:44:13 AM UTC',
  146. })}
  147. year
  148. seconds
  149. timeZone
  150. />
  151. </Cell>
  152. </Fragment>
  153. );
  154. }
  155. export default MetricAlertActivity;
  156. const Cell = styled('div')`
  157. display: flex;
  158. align-items: center;
  159. white-space: nowrap;
  160. font-size: ${p => p.theme.fontSizeMedium};
  161. padding: ${space(1)};
  162. `;