index.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import {Component, Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import {Location} from 'history';
  4. import moment from 'moment';
  5. import {fetchOrgMembers} from 'sentry/actionCreators/members';
  6. import {Client, ResponseMeta} from 'sentry/api';
  7. import Alert from 'sentry/components/alert';
  8. import DateTime from 'sentry/components/dateTime';
  9. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  10. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  11. import {t} from 'sentry/locale';
  12. import {PageContent} from 'sentry/styles/organization';
  13. import {Organization, Project} from 'sentry/types';
  14. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  15. import {getUtcDateString} from 'sentry/utils/dates';
  16. import withApi from 'sentry/utils/withApi';
  17. import withProjects from 'sentry/utils/withProjects';
  18. import {MetricRule, TimePeriod} from 'sentry/views/alerts/rules/metric/types';
  19. import type {Incident} from 'sentry/views/alerts/types';
  20. import {
  21. fetchAlertRule,
  22. fetchIncident,
  23. fetchIncidentsForRule,
  24. } from 'sentry/views/alerts/utils/apiCalls';
  25. import DetailsBody from './body';
  26. import {TIME_OPTIONS, TIME_WINDOWS, TimePeriodType} from './constants';
  27. import DetailsHeader from './header';
  28. import {buildMetricGraphDateRange} from './utils';
  29. interface Props extends RouteComponentProps<{orgId: string; ruleId: string}, {}> {
  30. api: Client;
  31. location: Location;
  32. organization: Organization;
  33. projects: Project[];
  34. loadingProjects?: boolean;
  35. }
  36. interface State {
  37. error: ResponseMeta | null;
  38. hasError: boolean;
  39. isLoading: boolean;
  40. selectedIncident: Incident | null;
  41. incidents?: Incident[];
  42. rule?: MetricRule;
  43. }
  44. class MetricAlertDetails extends Component<Props, State> {
  45. state: State = {isLoading: false, hasError: false, error: null, selectedIncident: null};
  46. componentDidMount() {
  47. const {api, params} = this.props;
  48. fetchOrgMembers(api, params.orgId);
  49. this.fetchData();
  50. this.trackView();
  51. }
  52. componentDidUpdate(prevProps: Props) {
  53. if (
  54. prevProps.location.search !== this.props.location.search ||
  55. prevProps.params.orgId !== this.props.params.orgId ||
  56. prevProps.params.ruleId !== this.props.params.ruleId
  57. ) {
  58. this.fetchData();
  59. this.trackView();
  60. }
  61. }
  62. trackView() {
  63. const {params, organization, location} = this.props;
  64. trackAdvancedAnalyticsEvent('alert_rule_details.viewed', {
  65. organization,
  66. rule_id: parseInt(params.ruleId, 10),
  67. alert: (location.query.alert as string) ?? '',
  68. has_chartcuterie: organization.features
  69. .includes('metric-alert-chartcuterie')
  70. .toString(),
  71. });
  72. }
  73. getTimePeriod(selectedIncident: Incident | null): TimePeriodType {
  74. const {location} = this.props;
  75. const period = (location.query.period as string) ?? TimePeriod.SEVEN_DAYS;
  76. if (location.query.start && location.query.end) {
  77. return {
  78. start: location.query.start as string,
  79. end: location.query.end as string,
  80. period,
  81. usingPeriod: false,
  82. label: t('Custom time'),
  83. display: (
  84. <Fragment>
  85. <DateTime date={moment.utc(location.query.start)} />
  86. {' — '}
  87. <DateTime date={moment.utc(location.query.end)} />
  88. </Fragment>
  89. ),
  90. custom: true,
  91. };
  92. }
  93. if (location.query.alert && selectedIncident) {
  94. const {start, end} = buildMetricGraphDateRange(selectedIncident);
  95. return {
  96. start,
  97. end,
  98. period,
  99. usingPeriod: false,
  100. label: t('Custom time'),
  101. display: (
  102. <Fragment>
  103. <DateTime date={moment.utc(start)} />
  104. {' — '}
  105. <DateTime date={moment.utc(end)} />
  106. </Fragment>
  107. ),
  108. custom: true,
  109. };
  110. }
  111. const timeOption =
  112. TIME_OPTIONS.find(item => item.value === period) ?? TIME_OPTIONS[1];
  113. const start = getUtcDateString(
  114. moment(moment.utc().diff(TIME_WINDOWS[timeOption.value]))
  115. );
  116. const end = getUtcDateString(moment.utc());
  117. return {
  118. start,
  119. end,
  120. period,
  121. usingPeriod: true,
  122. label: timeOption.label as string,
  123. display: timeOption.label as string,
  124. };
  125. }
  126. fetchData = async () => {
  127. const {
  128. api,
  129. params: {orgId, ruleId},
  130. location,
  131. } = this.props;
  132. this.setState({isLoading: true, hasError: false});
  133. // Skip loading existing rule
  134. const rulePromise =
  135. ruleId === this.state.rule?.id
  136. ? Promise.resolve(this.state.rule)
  137. : fetchAlertRule(orgId, ruleId, {expand: 'latestIncident'});
  138. // Fetch selected incident, if it exists. We need this to set the selected date range
  139. let selectedIncident: Incident | null = null;
  140. if (location.query.alert) {
  141. try {
  142. selectedIncident = await fetchIncident(
  143. api,
  144. orgId,
  145. location.query.alert as string
  146. );
  147. } catch {
  148. // TODO: selectedIncident specific error
  149. }
  150. }
  151. const timePeriod = this.getTimePeriod(selectedIncident);
  152. const {start, end} = timePeriod;
  153. try {
  154. const [incidents, rule] = await Promise.all([
  155. fetchIncidentsForRule(orgId, ruleId, start, end),
  156. rulePromise,
  157. ]);
  158. this.setState({
  159. incidents,
  160. rule,
  161. selectedIncident,
  162. isLoading: false,
  163. hasError: false,
  164. });
  165. } catch (error) {
  166. this.setState({selectedIncident, isLoading: false, hasError: true, error});
  167. }
  168. };
  169. renderError() {
  170. const {error} = this.state;
  171. return (
  172. <PageContent>
  173. <Alert type="error" showIcon>
  174. {error?.status === 404
  175. ? t('This alert rule could not be found.')
  176. : t('An error occurred while fetching the alert rule.')}
  177. </Alert>
  178. </PageContent>
  179. );
  180. }
  181. render() {
  182. const {rule, incidents, hasError, selectedIncident} = this.state;
  183. const {params, projects, loadingProjects} = this.props;
  184. const timePeriod = this.getTimePeriod(selectedIncident);
  185. if (hasError) {
  186. return this.renderError();
  187. }
  188. const project = projects.find(({slug}) => slug === rule?.projects[0]) as
  189. | Project
  190. | undefined;
  191. const isGlobalSelectionReady = project !== undefined && !loadingProjects;
  192. return (
  193. <PageFiltersContainer
  194. skipLoadLastUsed
  195. skipInitializeUrlParams
  196. shouldForceProject={isGlobalSelectionReady}
  197. forceProject={project}
  198. >
  199. <SentryDocumentTitle title={rule?.name ?? ''} />
  200. <DetailsHeader
  201. hasMetricRuleDetailsError={hasError}
  202. params={params}
  203. rule={rule}
  204. project={project}
  205. />
  206. <DetailsBody
  207. {...this.props}
  208. rule={rule}
  209. project={project}
  210. incidents={incidents}
  211. timePeriod={timePeriod}
  212. selectedIncident={selectedIncident}
  213. />
  214. </PageFiltersContainer>
  215. );
  216. }
  217. }
  218. export default withApi(withProjects(MetricAlertDetails));