metricHistory.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {css} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import moment from 'moment-timezone';
  4. import CollapsePanel from 'sentry/components/collapsePanel';
  5. import DateTime from 'sentry/components/dateTime';
  6. import Duration from 'sentry/components/duration';
  7. import ErrorBoundary from 'sentry/components/errorBoundary';
  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, tct, tn} from 'sentry/locale';
  12. import space from 'sentry/styles/space';
  13. import {Organization} from 'sentry/types';
  14. import getDynamicText from 'sentry/utils/getDynamicText';
  15. import {AlertRuleThresholdType} from 'sentry/views/alerts/rules/metric/types';
  16. import {Incident, IncidentActivityType, IncidentStatus} from 'sentry/views/alerts/types';
  17. import {alertDetailsLink} from 'sentry/views/alerts/utils';
  18. import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
  19. import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
  20. const COLLAPSE_COUNT = 3;
  21. function getTriggerName(value: string | null) {
  22. if (value === `${IncidentStatus.WARNING}`) {
  23. return t('Warning');
  24. }
  25. if (value === `${IncidentStatus.CRITICAL}`) {
  26. return t('Critical');
  27. }
  28. // Otherwise, activity type is not status change
  29. return '';
  30. }
  31. type MetricAlertActivityProps = {
  32. incident: Incident;
  33. organization: Organization;
  34. };
  35. function MetricAlertActivity({organization, incident}: MetricAlertActivityProps) {
  36. const activities = (incident.activities ?? []).filter(
  37. activity => activity.type === IncidentActivityType.STATUS_CHANGE
  38. );
  39. const criticalActivity = activities.filter(
  40. activity => activity.value === `${IncidentStatus.CRITICAL}`
  41. );
  42. const warningActivity = activities.filter(
  43. activity => activity.value === `${IncidentStatus.WARNING}`
  44. );
  45. const triggeredActivity = criticalActivity.length
  46. ? criticalActivity[0]
  47. : warningActivity[0];
  48. const currentTrigger = getTriggerName(triggeredActivity.value);
  49. const nextActivity = activities.find(
  50. ({previousValue}) => previousValue === triggeredActivity.value
  51. );
  52. const activityDuration = (
  53. nextActivity ? moment(nextActivity.dateCreated) : moment()
  54. ).diff(moment(triggeredActivity.dateCreated), 'milliseconds');
  55. const threshold =
  56. activityDuration !== null &&
  57. tct('[duration]', {
  58. duration: <Duration abbreviation seconds={activityDuration / 1000} />,
  59. });
  60. const warningThreshold = incident.alertRule.triggers
  61. .filter(trigger => trigger.label === 'warning')
  62. .map(trig => trig.alertThreshold);
  63. const criticalThreshold = incident.alertRule.triggers
  64. .filter(trigger => trigger.label === 'critical')
  65. .map(trig => trig.alertThreshold);
  66. return (
  67. <ErrorBoundary>
  68. <Title data-test-id="alert-title">
  69. <StatusIndicator
  70. status={currentTrigger.toLocaleLowerCase()}
  71. tooltipTitle={tct('Status: [level]', {level: currentTrigger})}
  72. />
  73. <Link
  74. to={{
  75. pathname: alertDetailsLink(organization, incident),
  76. query: {alert: incident.identifier},
  77. }}
  78. >
  79. {tct('#[id]', {id: incident.identifier})}
  80. </Link>
  81. </Title>
  82. <Cell>
  83. {tct('[title] [selector] [threshold]', {
  84. title:
  85. AlertWizardAlertNames[getAlertTypeFromAggregateDataset(incident.alertRule)],
  86. selector:
  87. incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
  88. ? 'above'
  89. : 'below',
  90. threshold: currentTrigger === 'Warning' ? warningThreshold : criticalThreshold,
  91. })}
  92. </Cell>
  93. <Cell>
  94. {getDynamicText({
  95. value: threshold,
  96. fixed: '30s',
  97. })}
  98. </Cell>
  99. <StyledDateTime
  100. date={getDynamicText({
  101. value: incident.dateCreated,
  102. fixed: 'Mar 4, 2022 10:44:13 AM UTC',
  103. })}
  104. year
  105. seconds
  106. timeZone
  107. />
  108. </ErrorBoundary>
  109. );
  110. }
  111. type Props = {
  112. organization: Organization;
  113. incidents?: Incident[];
  114. };
  115. function MetricHistory({organization, incidents}: Props) {
  116. const numOfIncidents = (incidents ?? []).length;
  117. return (
  118. <CollapsePanel
  119. items={numOfIncidents}
  120. collapseCount={COLLAPSE_COUNT}
  121. disableBorder={false}
  122. buttonTitle={tn('Hidden Alert', 'Hidden Alerts', numOfIncidents - COLLAPSE_COUNT)}
  123. >
  124. {({isExpanded, showMoreButton}) => (
  125. <div>
  126. <StyledPanelTable
  127. headers={[t('Alert'), t('Reason'), t('Duration'), t('Date Triggered')]}
  128. isEmpty={!numOfIncidents}
  129. emptyMessage={t('No alerts triggered during this time.')}
  130. expanded={numOfIncidents <= COLLAPSE_COUNT || isExpanded}
  131. >
  132. {incidents &&
  133. incidents.map((incident, idx) => {
  134. if (idx >= COLLAPSE_COUNT && !isExpanded) {
  135. return null;
  136. }
  137. return (
  138. <MetricAlertActivity
  139. key={idx}
  140. incident={incident}
  141. organization={organization}
  142. />
  143. );
  144. })}
  145. </StyledPanelTable>
  146. {showMoreButton}
  147. </div>
  148. )}
  149. </CollapsePanel>
  150. );
  151. }
  152. export default MetricHistory;
  153. const StyledPanelTable = styled(PanelTable)<{expanded: boolean; isEmpty: boolean}>`
  154. grid-template-columns: max-content 1fr repeat(2, max-content);
  155. & > div {
  156. padding: ${space(1)} ${space(2)};
  157. }
  158. div:last-of-type {
  159. padding: ${p => p.isEmpty && `48px ${space(1)}`};
  160. }
  161. ${p =>
  162. !p.expanded &&
  163. css`
  164. margin-bottom: 0px;
  165. border-bottom-left-radius: 0px;
  166. border-bottom-right-radius: 0px;
  167. border-bottom: none;
  168. `}
  169. `;
  170. const StyledDateTime = styled(DateTime)`
  171. color: ${p => p.theme.gray300};
  172. font-size: ${p => p.theme.fontSizeMedium};
  173. display: flex;
  174. justify-content: flex-start;
  175. padding: ${space(1)} ${space(2)} !important;
  176. `;
  177. const Title = styled('div')`
  178. display: flex;
  179. align-items: center;
  180. white-space: nowrap;
  181. overflow: hidden;
  182. text-overflow: ellipsis;
  183. width: 100%;
  184. font-size: ${p => p.theme.fontSizeMedium};
  185. padding: ${space(1)};
  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. `;