metricHistory.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import moment from 'moment-timezone';
  5. import CollapsePanel from 'sentry/components/collapsePanel';
  6. import {DateTime} from 'sentry/components/dateTime';
  7. import Duration from 'sentry/components/duration';
  8. import Link from 'sentry/components/links/link';
  9. import {PanelTable} from 'sentry/components/panels/panelTable';
  10. import {StatusIndicator} from 'sentry/components/statusIndicator';
  11. import {t, tn} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import type {Organization} from 'sentry/types/organization';
  14. import getDuration from 'sentry/utils/duration/getDuration';
  15. import getDynamicText from 'sentry/utils/getDynamicText';
  16. import {capitalize} from 'sentry/utils/string/capitalize';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
  19. import {AlertRuleThresholdType} from 'sentry/views/alerts/rules/metric/types';
  20. import type {ActivityType, Incident} from 'sentry/views/alerts/types';
  21. import {IncidentActivityType, IncidentStatus} from 'sentry/views/alerts/types';
  22. import {alertDetailsLink} from 'sentry/views/alerts/utils';
  23. import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
  24. import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
  25. const COLLAPSE_COUNT = 3;
  26. type MetricAlertActivityProps = {
  27. incident: Incident;
  28. organization: Organization;
  29. };
  30. function MetricAlertActivity({organization, incident}: MetricAlertActivityProps) {
  31. const activities = (incident.activities ?? []).filter(
  32. activity => activity.type === IncidentActivityType.STATUS_CHANGE
  33. );
  34. const criticalActivity = activities.find(
  35. activity => activity.value === `${IncidentStatus.CRITICAL}`
  36. );
  37. const warningActivity = activities.find(
  38. activity => activity.value === `${IncidentStatus.WARNING}`
  39. );
  40. const triggeredActivity: ActivityType = criticalActivity
  41. ? criticalActivity
  42. : warningActivity!;
  43. const isCritical = Number(triggeredActivity.value) === IncidentStatus.CRITICAL;
  44. // Find duration by looking at the difference between the previous and current activity timestamp
  45. const nextActivity = activities.find(
  46. ({previousValue}) => previousValue === triggeredActivity.value
  47. );
  48. const activityDuration = (
  49. nextActivity ? moment(nextActivity.dateCreated) : moment()
  50. ).diff(moment(triggeredActivity.dateCreated), 'milliseconds');
  51. const triggerLabel = isCritical ? 'critical' : 'warning';
  52. const curentTrigger = incident.alertRule.triggers.find(
  53. trigger => trigger.label === triggerLabel
  54. );
  55. const timeWindow = getDuration(incident.alertRule.timeWindow * 60);
  56. const alertName = capitalize(
  57. AlertWizardAlertNames[getAlertTypeFromAggregateDataset(incident.alertRule)]
  58. );
  59. return (
  60. <Fragment>
  61. <Cell>
  62. {triggeredActivity.value && (
  63. <StatusIndicator
  64. status={isCritical ? 'error' : 'warning'}
  65. tooltipTitle={t('Status: %s', isCritical ? t('Critical') : t('Warning'))}
  66. />
  67. )}
  68. <Link
  69. to={{
  70. pathname: alertDetailsLink(organization, incident),
  71. query: {alert: incident.identifier},
  72. }}
  73. >
  74. #{incident.identifier}
  75. </Link>
  76. </Cell>
  77. <Cell>
  78. {/* If an alert rule is a % comparison based detection type */}
  79. {incident.alertRule.detectionType !== 'dynamic' &&
  80. incident.alertRule.comparisonDelta && (
  81. <Fragment>
  82. {alertName} {curentTrigger?.alertThreshold}%
  83. {t(
  84. ' %s in %s compared to the ',
  85. incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  86. ? t('higher')
  87. : t('lower'),
  88. timeWindow
  89. )}
  90. {COMPARISON_DELTA_OPTIONS.find(
  91. ({value}) => value === incident.alertRule.comparisonDelta
  92. )?.label ?? COMPARISON_DELTA_OPTIONS[0]?.label}
  93. </Fragment>
  94. )}
  95. {/* If an alert rule is a static detection type */}
  96. {incident.alertRule.detectionType !== 'dynamic' &&
  97. !incident.alertRule.comparisonDelta && (
  98. <Fragment>
  99. {alertName}{' '}
  100. {incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  101. ? t('above')
  102. : t('below')}{' '}
  103. {curentTrigger?.alertThreshold} {t('in')} {timeWindow}
  104. </Fragment>
  105. )}
  106. {/* If an alert rule is a dynamic detection type */}
  107. {incident.alertRule.detectionType === 'dynamic' && (
  108. <Fragment>
  109. {t('Detected an anomaly in the query for ')}
  110. {alertName}
  111. </Fragment>
  112. )}
  113. </Cell>
  114. <Cell>
  115. {activityDuration &&
  116. getDynamicText({
  117. value: <Duration abbreviation seconds={activityDuration / 1000} />,
  118. fixed: '30s',
  119. })}
  120. </Cell>
  121. <Cell>
  122. <StyledDateTime
  123. date={getDynamicText({
  124. value: incident.dateCreated,
  125. fixed: 'Mar 4, 2022 10:44:13 AM UTC',
  126. })}
  127. year
  128. seconds
  129. timeZone
  130. />
  131. </Cell>
  132. </Fragment>
  133. );
  134. }
  135. type Props = {
  136. incidents?: Incident[];
  137. };
  138. function MetricHistory({incidents}: Props) {
  139. const organization = useOrganization();
  140. const filteredIncidents = (incidents ?? []).filter(
  141. incident => incident.activities?.length
  142. );
  143. const numOfIncidents = filteredIncidents.length;
  144. return (
  145. <CollapsePanel
  146. items={numOfIncidents}
  147. collapseCount={COLLAPSE_COUNT}
  148. disableBorder={false}
  149. buttonTitle={tn('Hidden Alert', 'Hidden Alerts', numOfIncidents - COLLAPSE_COUNT)}
  150. >
  151. {({isExpanded, showMoreButton}) => (
  152. <div>
  153. <StyledPanelTable
  154. headers={[t('Alert'), t('Reason'), t('Duration'), t('Date Triggered')]}
  155. isEmpty={!numOfIncidents}
  156. emptyMessage={t('No alerts triggered during this time.')}
  157. expanded={numOfIncidents <= COLLAPSE_COUNT || isExpanded}
  158. >
  159. {filteredIncidents.map((incident, idx) => {
  160. if (idx >= COLLAPSE_COUNT && !isExpanded) {
  161. return null;
  162. }
  163. return (
  164. <MetricAlertActivity
  165. key={idx}
  166. incident={incident}
  167. organization={organization}
  168. />
  169. );
  170. })}
  171. </StyledPanelTable>
  172. {showMoreButton}
  173. </div>
  174. )}
  175. </CollapsePanel>
  176. );
  177. }
  178. export default MetricHistory;
  179. const StyledPanelTable = styled(PanelTable)<{expanded: boolean; isEmpty: boolean}>`
  180. grid-template-columns: max-content 1fr repeat(2, max-content);
  181. & > div {
  182. padding: ${space(1)} ${space(2)};
  183. }
  184. div:last-of-type {
  185. padding: ${p => p.isEmpty && `48px ${space(1)}`};
  186. }
  187. ${p =>
  188. !p.expanded &&
  189. css`
  190. margin-bottom: 0px;
  191. border-bottom-left-radius: 0px;
  192. border-bottom-right-radius: 0px;
  193. border-bottom: none;
  194. `}
  195. `;
  196. const StyledDateTime = styled(DateTime)`
  197. color: ${p => p.theme.gray300};
  198. `;
  199. const Cell = styled('div')`
  200. display: flex;
  201. align-items: center;
  202. white-space: nowrap;
  203. font-size: ${p => p.theme.fontSizeMedium};
  204. padding: ${space(1)};
  205. `;