body.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import {Component, Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import moment from 'moment';
  6. import {Client} from 'sentry/api';
  7. import Alert from 'sentry/components/alert';
  8. import {getInterval} from 'sentry/components/charts/utils';
  9. import Duration from 'sentry/components/duration';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import {ChangeData} from 'sentry/components/organizations/timeRangeSelector';
  12. import PageTimeRangeSelector from 'sentry/components/pageTimeRangeSelector';
  13. import {Panel, PanelBody} from 'sentry/components/panels';
  14. import Placeholder from 'sentry/components/placeholder';
  15. import {t, tct} from 'sentry/locale';
  16. import space from 'sentry/styles/space';
  17. import {Organization, Project} from 'sentry/types';
  18. import MetricHistory from 'sentry/views/alerts/rules/metric/details/metricHistory';
  19. import {Dataset, MetricRule, TimePeriod} from 'sentry/views/alerts/rules/metric/types';
  20. import {extractEventTypeFilterFromRule} from 'sentry/views/alerts/rules/metric/utils/getEventTypeFilter';
  21. import {AlertRuleStatus, Incident} from '../../../types';
  22. import {isCrashFreeAlert} from '../utils/isCrashFreeAlert';
  23. import {
  24. API_INTERVAL_POINTS_LIMIT,
  25. SELECTOR_RELATIVE_PERIODS,
  26. TIME_WINDOWS,
  27. TimePeriodType,
  28. } from './constants';
  29. import MetricChart from './metricChart';
  30. import RelatedIssues from './relatedIssues';
  31. import RelatedTransactions from './relatedTransactions';
  32. import Sidebar from './sidebar';
  33. type Props = {
  34. api: Client;
  35. location: Location;
  36. organization: Organization;
  37. timePeriod: TimePeriodType;
  38. incidents?: Incident[];
  39. project?: Project;
  40. rule?: MetricRule;
  41. selectedIncident?: Incident | null;
  42. } & RouteComponentProps<{orgId: string}, {}>;
  43. export default class DetailsBody extends Component<Props> {
  44. getTimeWindow(): React.ReactNode {
  45. const {rule} = this.props;
  46. if (!rule) {
  47. return '';
  48. }
  49. const {timeWindow} = rule;
  50. return tct('[window]', {
  51. window: <Duration seconds={timeWindow * 60} />,
  52. });
  53. }
  54. getInterval() {
  55. const {
  56. timePeriod: {start, end},
  57. rule,
  58. } = this.props;
  59. const startDate = moment.utc(start);
  60. const endDate = moment.utc(end);
  61. const timeWindow = rule?.timeWindow;
  62. const startEndDifferenceMs = endDate.diff(startDate);
  63. if (
  64. timeWindow &&
  65. (startEndDifferenceMs < API_INTERVAL_POINTS_LIMIT * timeWindow * 60 * 1000 ||
  66. // Special case 7 days * 1m interval over the api limit
  67. startEndDifferenceMs === TIME_WINDOWS[TimePeriod.SEVEN_DAYS])
  68. ) {
  69. return `${timeWindow}m`;
  70. }
  71. return getInterval({start, end}, 'high');
  72. }
  73. getFilter() {
  74. const {rule} = this.props;
  75. const {dataset, query} = rule ?? {};
  76. if (!rule) {
  77. return null;
  78. }
  79. const eventType = isCrashFreeAlert(dataset)
  80. ? null
  81. : extractEventTypeFilterFromRule(rule);
  82. return [eventType, query].join(' ').split(' ');
  83. }
  84. handleTimePeriodChange = (datetime: ChangeData) => {
  85. const {start, end, relative} = datetime;
  86. if (start && end) {
  87. return this.props.router.push({
  88. ...this.props.location,
  89. query: {
  90. start: moment(start).utc().format(),
  91. end: moment(end).utc().format(),
  92. },
  93. });
  94. }
  95. return this.props.router.push({
  96. ...this.props.location,
  97. query: {
  98. period: relative,
  99. },
  100. });
  101. };
  102. renderLoading() {
  103. return (
  104. <Layout.Body>
  105. <Layout.Main>
  106. <Placeholder height="38px" />
  107. <ChartPanel>
  108. <PanelBody withPadding>
  109. <Placeholder height="200px" />
  110. </PanelBody>
  111. </ChartPanel>
  112. </Layout.Main>
  113. <Layout.Side>
  114. <Placeholder height="200px" />
  115. </Layout.Side>
  116. </Layout.Body>
  117. );
  118. }
  119. render() {
  120. const {
  121. api,
  122. project,
  123. rule,
  124. incidents,
  125. location,
  126. organization,
  127. timePeriod,
  128. selectedIncident,
  129. params: {orgId},
  130. } = this.props;
  131. if (!rule || !project) {
  132. return this.renderLoading();
  133. }
  134. const {query, dataset} = rule;
  135. const queryWithTypeFilter = `${query} ${extractEventTypeFilterFromRule(rule)}`.trim();
  136. const relativeOptions = {
  137. ...SELECTOR_RELATIVE_PERIODS,
  138. ...(rule.timeWindow > 1 ? {[TimePeriod.FOURTEEN_DAYS]: t('Last 14 days')} : {}),
  139. };
  140. return (
  141. <Fragment>
  142. {selectedIncident &&
  143. selectedIncident.alertRule.status === AlertRuleStatus.SNAPSHOT && (
  144. <StyledLayoutBody>
  145. <StyledAlert type="warning" showIcon>
  146. {t(
  147. 'Alert Rule settings have been updated since this alert was triggered.'
  148. )}
  149. </StyledAlert>
  150. </StyledLayoutBody>
  151. )}
  152. <Layout.Body>
  153. <Layout.Main>
  154. <StyledPageTimeRangeSelector
  155. organization={organization}
  156. relative={timePeriod.period ?? ''}
  157. start={(timePeriod.custom && timePeriod.start) || null}
  158. end={(timePeriod.custom && timePeriod.end) || null}
  159. utc={null}
  160. onUpdate={this.handleTimePeriodChange}
  161. relativeOptions={relativeOptions}
  162. showAbsolute={false}
  163. />
  164. <MetricChart
  165. api={api}
  166. rule={rule}
  167. incidents={incidents}
  168. timePeriod={timePeriod}
  169. selectedIncident={selectedIncident}
  170. organization={organization}
  171. project={project}
  172. interval={this.getInterval()}
  173. query={isCrashFreeAlert(dataset) ? query : queryWithTypeFilter}
  174. filter={this.getFilter()}
  175. orgId={orgId}
  176. />
  177. <DetailWrapper>
  178. <ActivityWrapper>
  179. <MetricHistory organization={organization} incidents={incidents} />
  180. {[Dataset.METRICS, Dataset.SESSIONS, Dataset.ERRORS].includes(
  181. dataset
  182. ) && (
  183. <RelatedIssues
  184. organization={organization}
  185. rule={rule}
  186. projects={[project]}
  187. timePeriod={timePeriod}
  188. query={
  189. dataset === Dataset.ERRORS
  190. ? queryWithTypeFilter
  191. : isCrashFreeAlert(dataset)
  192. ? `${query} error.unhandled:true`
  193. : undefined
  194. }
  195. />
  196. )}
  197. {dataset === Dataset.TRANSACTIONS && (
  198. <RelatedTransactions
  199. organization={organization}
  200. location={location}
  201. rule={rule}
  202. projects={[project]}
  203. timePeriod={timePeriod}
  204. filter={extractEventTypeFilterFromRule(rule)}
  205. />
  206. )}
  207. </ActivityWrapper>
  208. </DetailWrapper>
  209. </Layout.Main>
  210. <Layout.Side>
  211. <Sidebar rule={rule} />
  212. </Layout.Side>
  213. </Layout.Body>
  214. </Fragment>
  215. );
  216. }
  217. }
  218. const DetailWrapper = styled('div')`
  219. display: flex;
  220. flex: 1;
  221. @media (max-width: ${p => p.theme.breakpoints.small}) {
  222. flex-direction: column-reverse;
  223. }
  224. `;
  225. const StyledLayoutBody = styled(Layout.Body)`
  226. flex-grow: 0;
  227. padding-bottom: 0 !important;
  228. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  229. grid-template-columns: auto;
  230. }
  231. `;
  232. const StyledAlert = styled(Alert)`
  233. margin: 0;
  234. `;
  235. const ActivityWrapper = styled('div')`
  236. display: flex;
  237. flex: 1;
  238. flex-direction: column;
  239. width: 100%;
  240. `;
  241. const ChartPanel = styled(Panel)`
  242. margin-top: ${space(2)};
  243. `;
  244. const StyledPageTimeRangeSelector = styled(PageTimeRangeSelector)`
  245. margin-bottom: ${space(2)};
  246. `;