index.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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 * as Layout from 'sentry/components/layouts/thirds';
  10. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  11. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  12. import {t} from 'sentry/locale';
  13. import {Organization, Project} from 'sentry/types';
  14. import {trackAnalytics} from 'sentry/utils/analytics';
  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<{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, organization} = this.props;
  48. fetchOrgMembers(api, organization.slug);
  49. this.fetchData();
  50. this.trackView();
  51. }
  52. componentDidUpdate(prevProps: Props) {
  53. if (
  54. prevProps.location.search !== this.props.location.search ||
  55. prevProps.organization.slug !== this.props.organization.slug ||
  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. trackAnalytics('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. organization,
  130. params: {ruleId},
  131. location,
  132. } = this.props;
  133. this.setState({isLoading: true, hasError: false});
  134. // Skip loading existing rule
  135. const rulePromise =
  136. ruleId === this.state.rule?.id
  137. ? Promise.resolve(this.state.rule)
  138. : fetchAlertRule(organization.slug, ruleId, {expand: 'latestIncident'});
  139. // Fetch selected incident, if it exists. We need this to set the selected date range
  140. let selectedIncident: Incident | null = null;
  141. if (location.query.alert) {
  142. try {
  143. selectedIncident = await fetchIncident(
  144. api,
  145. organization.slug,
  146. location.query.alert as string
  147. );
  148. } catch {
  149. // TODO: selectedIncident specific error
  150. }
  151. }
  152. const timePeriod = this.getTimePeriod(selectedIncident);
  153. const {start, end} = timePeriod;
  154. try {
  155. const [incidents, rule] = await Promise.all([
  156. fetchIncidentsForRule(organization.slug, ruleId, start, end),
  157. rulePromise,
  158. ]);
  159. this.setState({
  160. incidents,
  161. rule,
  162. selectedIncident,
  163. isLoading: false,
  164. hasError: false,
  165. });
  166. } catch (error) {
  167. this.setState({selectedIncident, isLoading: false, hasError: true, error});
  168. }
  169. };
  170. renderError() {
  171. const {error} = this.state;
  172. return (
  173. <Layout.Page withPadding>
  174. <Alert type="error" showIcon>
  175. {error?.status === 404
  176. ? t('This alert rule could not be found.')
  177. : t('An error occurred while fetching the alert rule.')}
  178. </Alert>
  179. </Layout.Page>
  180. );
  181. }
  182. render() {
  183. const {rule, incidents, hasError, selectedIncident} = this.state;
  184. const {organization, projects, loadingProjects} = this.props;
  185. const timePeriod = this.getTimePeriod(selectedIncident);
  186. if (hasError) {
  187. return this.renderError();
  188. }
  189. const project = projects.find(({slug}) => slug === rule?.projects[0]) as
  190. | Project
  191. | undefined;
  192. const isGlobalSelectionReady = project !== undefined && !loadingProjects;
  193. return (
  194. <PageFiltersContainer
  195. skipLoadLastUsed
  196. skipInitializeUrlParams
  197. shouldForceProject={isGlobalSelectionReady}
  198. forceProject={project}
  199. >
  200. <SentryDocumentTitle title={rule?.name ?? ''} />
  201. <DetailsHeader
  202. hasMetricRuleDetailsError={hasError}
  203. organization={organization}
  204. rule={rule}
  205. project={project}
  206. />
  207. <DetailsBody
  208. {...this.props}
  209. rule={rule}
  210. project={project}
  211. incidents={incidents}
  212. timePeriod={timePeriod}
  213. selectedIncident={selectedIncident}
  214. />
  215. </PageFiltersContainer>
  216. );
  217. }
  218. }
  219. export default withApi(withProjects(MetricAlertDetails));