row.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import styled from '@emotion/styled';
  2. import memoize from 'lodash/memoize';
  3. import moment from 'moment';
  4. import AsyncComponent from 'app/components/asyncComponent';
  5. import Duration from 'app/components/duration';
  6. import ErrorBoundary from 'app/components/errorBoundary';
  7. import IdBadge from 'app/components/idBadge';
  8. import Link from 'app/components/links/link';
  9. import {PanelItem} from 'app/components/panels';
  10. import TimeSince from 'app/components/timeSince';
  11. import Tooltip from 'app/components/tooltip';
  12. import {IconWarning} from 'app/icons';
  13. import {t, tct} from 'app/locale';
  14. import overflowEllipsis from 'app/styles/overflowEllipsis';
  15. import space from 'app/styles/space';
  16. import {Organization, Project} from 'app/types';
  17. import {getUtcDateString} from 'app/utils/dates';
  18. import getDynamicText from 'app/utils/getDynamicText';
  19. import theme from 'app/utils/theme';
  20. import {alertDetailsLink} from 'app/views/alerts/details';
  21. import {
  22. API_INTERVAL_POINTS_LIMIT,
  23. API_INTERVAL_POINTS_MIN,
  24. } from '../rules/details/constants';
  25. import {Incident, IncidentStats, IncidentStatus} from '../types';
  26. import {getIncidentMetricPreset, isIssueAlert} from '../utils';
  27. import SparkLine from './sparkLine';
  28. import {TableLayout, TitleAndSparkLine} from './styles';
  29. /**
  30. * Retrieve the start/end for showing the graph of the metric
  31. * Will show at least 150 and no more than 10,000 data points
  32. */
  33. export const makeRuleDetailsQuery = (
  34. incident: Incident
  35. ): {start: string; end: string} => {
  36. const {timeWindow} = incident.alertRule;
  37. const timeWindowMillis = timeWindow * 60 * 1000;
  38. const minRange = timeWindowMillis * API_INTERVAL_POINTS_MIN;
  39. const maxRange = timeWindowMillis * API_INTERVAL_POINTS_LIMIT;
  40. const now = moment.utc();
  41. const startDate = moment.utc(incident.dateStarted);
  42. // make a copy of now since we will modify endDate and use now for comparing
  43. const endDate = incident.dateClosed ? moment.utc(incident.dateClosed) : moment(now);
  44. const incidentRange = Math.max(endDate.diff(startDate), 3 * timeWindowMillis);
  45. const range = Math.min(maxRange, Math.max(minRange, incidentRange));
  46. const halfRange = moment.duration(range / 2);
  47. return {
  48. start: getUtcDateString(startDate.subtract(halfRange)),
  49. end: getUtcDateString(moment.min(endDate.add(halfRange), now)),
  50. };
  51. };
  52. type Props = {
  53. incident: Incident;
  54. projects: Project[];
  55. projectsLoaded: boolean;
  56. orgId: string;
  57. filteredStatus: 'open' | 'closed';
  58. organization: Organization;
  59. } & AsyncComponent['props'];
  60. type State = {
  61. stats: IncidentStats;
  62. } & AsyncComponent['state'];
  63. class AlertListRow extends AsyncComponent<Props, State> {
  64. get metricPreset() {
  65. const {incident} = this.props;
  66. return incident ? getIncidentMetricPreset(incident) : undefined;
  67. }
  68. getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
  69. const {orgId, incident, filteredStatus} = this.props;
  70. if (filteredStatus === 'open') {
  71. return [
  72. ['stats', `/organizations/${orgId}/incidents/${incident.identifier}/stats/`],
  73. ];
  74. }
  75. return [];
  76. }
  77. /**
  78. * Memoized function to find a project from a list of projects
  79. */
  80. getProject = memoize((slug: string, projects: Project[]) =>
  81. projects.find(project => project.slug === slug)
  82. );
  83. renderLoading() {
  84. return this.renderBody();
  85. }
  86. renderError() {
  87. return this.renderBody();
  88. }
  89. renderTimeSince(date: string) {
  90. return (
  91. <CreatedResolvedTime>
  92. <TimeSince date={date} />
  93. </CreatedResolvedTime>
  94. );
  95. }
  96. renderStatusIndicator() {
  97. const {status} = this.props.incident;
  98. const isResolved = status === IncidentStatus.CLOSED;
  99. const isWarning = status === IncidentStatus.WARNING;
  100. const color = isResolved ? theme.gray200 : isWarning ? theme.orange300 : theme.red200;
  101. const text = isResolved ? t('Resolved') : isWarning ? t('Warning') : t('Critical');
  102. return (
  103. <Tooltip title={tct('Status: [text]', {text})}>
  104. <StatusIndicator color={color} />
  105. </Tooltip>
  106. );
  107. }
  108. renderBody() {
  109. const {
  110. incident,
  111. orgId,
  112. projectsLoaded,
  113. projects,
  114. filteredStatus,
  115. organization,
  116. } = this.props;
  117. const {error, stats} = this.state;
  118. const started = moment(incident.dateStarted);
  119. const duration = moment
  120. .duration(moment(incident.dateClosed || new Date()).diff(started))
  121. .as('seconds');
  122. const slug = incident.projects[0];
  123. const hasRedesign =
  124. !isIssueAlert(incident.alertRule) &&
  125. organization.features.includes('alert-details-redesign');
  126. const alertLink = hasRedesign
  127. ? {
  128. pathname: alertDetailsLink(organization, incident),
  129. query: {alert: incident.identifier},
  130. }
  131. : {
  132. pathname: `/organizations/${orgId}/alerts/${incident.identifier}/`,
  133. };
  134. return (
  135. <ErrorBoundary>
  136. <IncidentPanelItem>
  137. <TableLayout status={filteredStatus}>
  138. <TitleAndSparkLine status={filteredStatus}>
  139. <Title>
  140. {this.renderStatusIndicator()}
  141. <IncidentLink to={alertLink}>Alert #{incident.id}</IncidentLink>
  142. {incident.title}
  143. </Title>
  144. {filteredStatus === 'open' && (
  145. <SparkLine
  146. error={error && <ErrorLoadingStatsIcon />}
  147. eventStats={stats?.eventStats}
  148. />
  149. )}
  150. </TitleAndSparkLine>
  151. <ProjectBadge
  152. avatarSize={18}
  153. project={!projectsLoaded ? {slug} : this.getProject(slug, projects)}
  154. />
  155. {this.renderTimeSince(incident.dateStarted)}
  156. {filteredStatus === 'closed' && (
  157. <Duration seconds={getDynamicText({value: duration, fixed: 1200})} />
  158. )}
  159. {filteredStatus === 'closed' &&
  160. incident.dateClosed &&
  161. this.renderTimeSince(incident.dateClosed)}
  162. </TableLayout>
  163. </IncidentPanelItem>
  164. </ErrorBoundary>
  165. );
  166. }
  167. }
  168. function ErrorLoadingStatsIcon() {
  169. return (
  170. <Tooltip title={t('Error loading alert stats')}>
  171. <IconWarning />
  172. </Tooltip>
  173. );
  174. }
  175. const CreatedResolvedTime = styled('div')`
  176. ${overflowEllipsis}
  177. line-height: 1.4;
  178. display: flex;
  179. align-items: center;
  180. `;
  181. const ProjectBadge = styled(IdBadge)`
  182. flex-shrink: 0;
  183. `;
  184. const StatusIndicator = styled('div')<{color: string}>`
  185. width: 10px;
  186. height: 12px;
  187. background: ${p => p.color};
  188. display: inline-block;
  189. border-top-right-radius: 40%;
  190. border-bottom-right-radius: 40%;
  191. margin-bottom: -1px;
  192. `;
  193. const Title = styled('span')`
  194. ${overflowEllipsis}
  195. `;
  196. const IncidentLink = styled(Link)`
  197. padding: 0 ${space(1)};
  198. `;
  199. const IncidentPanelItem = styled(PanelItem)`
  200. font-size: ${p => p.theme.fontSizeMedium};
  201. padding: ${space(1.5)} ${space(2)} ${space(1.5)} 0;
  202. `;
  203. export default AlertListRow;