metricHistory.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import capitalize from 'lodash/capitalize';
  5. import moment from 'moment-timezone';
  6. import CollapsePanel from 'sentry/components/collapsePanel';
  7. import DateTime from 'sentry/components/dateTime';
  8. import Duration from 'sentry/components/duration';
  9. import Link from 'sentry/components/links/link';
  10. import PanelTable from 'sentry/components/panels/panelTable';
  11. import StatusIndicator from 'sentry/components/statusIndicator';
  12. import {t, tn} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {Organization} from 'sentry/types';
  15. import {getDuration} from 'sentry/utils/formatters';
  16. import getDynamicText from 'sentry/utils/getDynamicText';
  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} from 'sentry/views/alerts/types';
  21. import {Incident, 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. {incident.alertRule.comparisonDelta ? (
  79. <Fragment>
  80. {alertName} {curentTrigger?.alertThreshold}%
  81. {t(
  82. ' %s in %s compared to the ',
  83. incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  84. ? t('higher')
  85. : t('lower'),
  86. timeWindow
  87. )}
  88. {COMPARISON_DELTA_OPTIONS.find(
  89. ({value}) => value === incident.alertRule.comparisonDelta
  90. )?.label ?? COMPARISON_DELTA_OPTIONS[0].label}
  91. </Fragment>
  92. ) : (
  93. <Fragment>
  94. {alertName}{' '}
  95. {incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  96. ? t('above')
  97. : t('below')}{' '}
  98. {curentTrigger?.alertThreshold} {t('in')} {timeWindow}
  99. </Fragment>
  100. )}
  101. </Cell>
  102. <Cell>
  103. {activityDuration &&
  104. getDynamicText({
  105. value: <Duration abbreviation seconds={activityDuration / 1000} />,
  106. fixed: '30s',
  107. })}
  108. </Cell>
  109. <Cell>
  110. <StyledDateTime
  111. date={getDynamicText({
  112. value: incident.dateCreated,
  113. fixed: 'Mar 4, 2022 10:44:13 AM UTC',
  114. })}
  115. year
  116. seconds
  117. timeZone
  118. />
  119. </Cell>
  120. </Fragment>
  121. );
  122. }
  123. type Props = {
  124. incidents?: Incident[];
  125. };
  126. function MetricHistory({incidents}: Props) {
  127. const organization = useOrganization();
  128. const filteredIncidents = (incidents ?? []).filter(
  129. incident => incident.activities?.length
  130. );
  131. const numOfIncidents = filteredIncidents.length;
  132. return (
  133. <CollapsePanel
  134. items={numOfIncidents}
  135. collapseCount={COLLAPSE_COUNT}
  136. disableBorder={false}
  137. buttonTitle={tn('Hidden Alert', 'Hidden Alerts', numOfIncidents - COLLAPSE_COUNT)}
  138. >
  139. {({isExpanded, showMoreButton}) => (
  140. <div>
  141. <StyledPanelTable
  142. headers={[t('Alert'), t('Reason'), t('Duration'), t('Date Triggered')]}
  143. isEmpty={!numOfIncidents}
  144. emptyMessage={t('No alerts triggered during this time.')}
  145. expanded={numOfIncidents <= COLLAPSE_COUNT || isExpanded}
  146. >
  147. {filteredIncidents.map((incident, idx) => {
  148. if (idx >= COLLAPSE_COUNT && !isExpanded) {
  149. return null;
  150. }
  151. return (
  152. <MetricAlertActivity
  153. key={idx}
  154. incident={incident}
  155. organization={organization}
  156. />
  157. );
  158. })}
  159. </StyledPanelTable>
  160. {showMoreButton}
  161. </div>
  162. )}
  163. </CollapsePanel>
  164. );
  165. }
  166. export default MetricHistory;
  167. const StyledPanelTable = styled(PanelTable)<{expanded: boolean; isEmpty: boolean}>`
  168. grid-template-columns: max-content 1fr repeat(2, max-content);
  169. & > div {
  170. padding: ${space(1)} ${space(2)};
  171. }
  172. div:last-of-type {
  173. padding: ${p => p.isEmpty && `48px ${space(1)}`};
  174. }
  175. ${p =>
  176. !p.expanded &&
  177. css`
  178. margin-bottom: 0px;
  179. border-bottom-left-radius: 0px;
  180. border-bottom-right-radius: 0px;
  181. border-bottom: none;
  182. `}
  183. `;
  184. const StyledDateTime = styled(DateTime)`
  185. color: ${p => p.theme.gray300};
  186. `;
  187. const Cell = styled('div')`
  188. display: flex;
  189. align-items: center;
  190. white-space: nowrap;
  191. font-size: ${p => p.theme.fontSizeMedium};
  192. padding: ${space(1)};
  193. `;