groupEventDetails.tsx 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import {Component, Fragment} from 'react';
  2. import {browserHistory, RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as Sentry from '@sentry/react';
  5. import isEqual from 'lodash/isEqual';
  6. import {fetchSentryAppComponents} from 'sentry/actionCreators/sentryAppComponents';
  7. import {Client} from 'sentry/api';
  8. import GroupEventDetailsLoadingError from 'sentry/components/errors/groupEventDetailsLoadingError';
  9. import {withMeta} from 'sentry/components/events/meta/metaProxy';
  10. import GroupSidebar from 'sentry/components/group/sidebar';
  11. import HookOrDefault from 'sentry/components/hookOrDefault';
  12. import * as Layout from 'sentry/components/layouts/thirds';
  13. import LoadingIndicator from 'sentry/components/loadingIndicator';
  14. import MutedBox from 'sentry/components/mutedBox';
  15. import {TransactionProfileIdProvider} from 'sentry/components/profiling/transactionProfileIdProvider';
  16. import ReprocessedBox from 'sentry/components/reprocessedBox';
  17. import ResolutionBox from 'sentry/components/resolutionBox';
  18. import {space} from 'sentry/styles/space';
  19. import {
  20. BaseGroupStatusReprocessing,
  21. Environment,
  22. Group,
  23. GroupActivityReprocess,
  24. Organization,
  25. Project,
  26. } from 'sentry/types';
  27. import {Event} from 'sentry/types/event';
  28. import {defined} from 'sentry/utils';
  29. import fetchSentryAppInstallations from 'sentry/utils/fetchSentryAppInstallations';
  30. import {QuickTraceContext} from 'sentry/utils/performance/quickTrace/quickTraceContext';
  31. import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery';
  32. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  33. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  34. import GroupEventDetailsContent from 'sentry/views/issueDetails/groupEventDetails/groupEventDetailsContent';
  35. import GroupEventHeader from 'sentry/views/issueDetails/groupEventHeader';
  36. import ReprocessingProgress from '../reprocessingProgress';
  37. import {
  38. getEventEnvironment,
  39. getGroupMostRecentActivity,
  40. ReprocessingStatus,
  41. } from '../utils';
  42. const IssuePriorityFeedback = HookOrDefault({
  43. hookName: 'component:issue-priority-feedback',
  44. });
  45. export interface GroupEventDetailsProps
  46. extends RouteComponentProps<{groupId: string; eventId?: string}, {}> {
  47. api: Client;
  48. environments: Environment[];
  49. eventError: boolean;
  50. group: Group;
  51. groupReprocessingStatus: ReprocessingStatus;
  52. loadingEvent: boolean;
  53. onRetry: () => void;
  54. organization: Organization;
  55. project: Project;
  56. event?: Event;
  57. }
  58. type State = {
  59. eventNavLinks: string;
  60. };
  61. class GroupEventDetails extends Component<GroupEventDetailsProps, State> {
  62. state: State = {
  63. eventNavLinks: '',
  64. };
  65. componentDidMount() {
  66. this.fetchData();
  67. }
  68. componentDidUpdate(prevProps: GroupEventDetailsProps) {
  69. const {environments, params, location, organization, project} = this.props;
  70. const environmentsHaveChanged = !isEqual(prevProps.environments, environments);
  71. // If environments are being actively changed and will no longer contain the
  72. // current event's environment, redirect to latest
  73. if (
  74. environmentsHaveChanged &&
  75. prevProps.event &&
  76. params.eventId &&
  77. !['latest', 'oldest'].includes(params.eventId)
  78. ) {
  79. const shouldRedirect =
  80. environments.length > 0 &&
  81. !environments.find(
  82. env => env.name === getEventEnvironment(prevProps.event as Event)
  83. );
  84. if (shouldRedirect) {
  85. browserHistory.replace(
  86. normalizeUrl({
  87. pathname: `/organizations/${organization.slug}/issues/${params.groupId}/`,
  88. query: location.query,
  89. })
  90. );
  91. return;
  92. }
  93. }
  94. if (
  95. prevProps.organization.slug !== organization.slug ||
  96. prevProps.project.slug !== project.slug
  97. ) {
  98. this.fetchData();
  99. }
  100. }
  101. componentWillUnmount() {
  102. this.props.api.clear();
  103. }
  104. fetchData = () => {
  105. const {api, project, organization} = this.props;
  106. const orgSlug = organization.slug;
  107. const projectId = project.id;
  108. fetchSentryAppInstallations(api, orgSlug);
  109. // TODO(marcos): Sometimes PageFiltersStore cannot pick a project.
  110. if (projectId) {
  111. fetchSentryAppComponents(api, orgSlug, projectId);
  112. } else {
  113. Sentry.withScope(scope => {
  114. scope.setExtra('props', this.props);
  115. scope.setExtra('state', this.state);
  116. Sentry.captureMessage('Project ID was not set');
  117. });
  118. }
  119. };
  120. renderContent(eventWithMeta?: Event) {
  121. const {group, project, environments, loadingEvent, onRetry, eventError} = this.props;
  122. if (loadingEvent) {
  123. return <LoadingIndicator />;
  124. }
  125. if (eventError) {
  126. return (
  127. <GroupEventDetailsLoadingError environments={environments} onRetry={onRetry} />
  128. );
  129. }
  130. return (
  131. <GroupEventDetailsContent group={group} event={eventWithMeta} project={project} />
  132. );
  133. }
  134. renderReprocessedBox(
  135. reprocessStatus: ReprocessingStatus,
  136. mostRecentActivity: GroupActivityReprocess
  137. ) {
  138. if (
  139. reprocessStatus !== ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT &&
  140. reprocessStatus !== ReprocessingStatus.REPROCESSED_AND_HAS_EVENT
  141. ) {
  142. return null;
  143. }
  144. const {group, organization} = this.props;
  145. const {count, id: groupId} = group;
  146. const groupCount = Number(count);
  147. return (
  148. <ReprocessedBox
  149. reprocessActivity={mostRecentActivity}
  150. groupCount={groupCount}
  151. groupId={groupId}
  152. orgSlug={organization.slug}
  153. />
  154. );
  155. }
  156. renderGroupStatusBanner() {
  157. if (this.props.group.status === 'ignored') {
  158. return (
  159. <GroupStatusBannerWrapper>
  160. <MutedBox statusDetails={this.props.group.statusDetails} />
  161. </GroupStatusBannerWrapper>
  162. );
  163. }
  164. if (this.props.group.status === 'resolved') {
  165. return (
  166. <GroupStatusBannerWrapper>
  167. <ResolutionBox
  168. statusDetails={this.props.group.statusDetails}
  169. activities={this.props.group.activity}
  170. projectId={this.props.project.id}
  171. />
  172. </GroupStatusBannerWrapper>
  173. );
  174. }
  175. return null;
  176. }
  177. render() {
  178. const {
  179. group,
  180. project,
  181. organization,
  182. environments,
  183. location,
  184. event,
  185. groupReprocessingStatus,
  186. loadingEvent,
  187. eventError,
  188. } = this.props;
  189. const eventWithMeta = withMeta(event);
  190. // Reprocessing
  191. const hasReprocessingV2Feature = organization.features?.includes('reprocessing-v2');
  192. const {activity: activities} = group;
  193. const mostRecentActivity = getGroupMostRecentActivity(activities);
  194. return (
  195. <TransactionProfileIdProvider
  196. projectId={event?.projectID}
  197. transactionId={event?.type === 'transaction' ? event.id : undefined}
  198. timestamp={event?.dateReceived}
  199. >
  200. <VisuallyCompleteWithData
  201. id="IssueDetails-EventBody"
  202. hasData={!loadingEvent && !eventError && defined(eventWithMeta)}
  203. >
  204. <StyledLayoutBody data-test-id="group-event-details">
  205. {hasReprocessingV2Feature &&
  206. groupReprocessingStatus === ReprocessingStatus.REPROCESSING ? (
  207. <ReprocessingProgress
  208. totalEvents={
  209. (mostRecentActivity as GroupActivityReprocess).data.eventCount
  210. }
  211. pendingEvents={
  212. (group.statusDetails as BaseGroupStatusReprocessing['statusDetails'])
  213. .pendingEvents
  214. }
  215. />
  216. ) : (
  217. <Fragment>
  218. <QuickTraceQuery
  219. event={eventWithMeta}
  220. location={location}
  221. orgSlug={organization.slug}
  222. >
  223. {results => {
  224. return (
  225. <StyledLayoutMain>
  226. {this.renderGroupStatusBanner()}
  227. <IssuePriorityFeedback
  228. organization={organization}
  229. group={group}
  230. />
  231. <QuickTraceContext.Provider value={results}>
  232. {eventWithMeta && (
  233. <GroupEventHeader
  234. group={group}
  235. event={eventWithMeta}
  236. project={project}
  237. />
  238. )}
  239. {this.renderReprocessedBox(
  240. groupReprocessingStatus,
  241. mostRecentActivity as GroupActivityReprocess
  242. )}
  243. {this.renderContent(eventWithMeta)}
  244. </QuickTraceContext.Provider>
  245. </StyledLayoutMain>
  246. );
  247. }}
  248. </QuickTraceQuery>
  249. <StyledLayoutSide>
  250. <GroupSidebar
  251. organization={organization}
  252. project={project}
  253. group={group}
  254. event={eventWithMeta}
  255. environments={environments}
  256. />
  257. </StyledLayoutSide>
  258. </Fragment>
  259. )}
  260. </StyledLayoutBody>
  261. </VisuallyCompleteWithData>
  262. </TransactionProfileIdProvider>
  263. );
  264. }
  265. }
  266. const StyledLayoutBody = styled(Layout.Body)`
  267. /* Makes the borders align correctly */
  268. padding: 0 !important;
  269. @media (min-width: ${p => p.theme.breakpoints.large}) {
  270. align-content: stretch;
  271. }
  272. `;
  273. const GroupStatusBannerWrapper = styled('div')`
  274. margin-bottom: ${space(2)};
  275. `;
  276. const StyledLayoutMain = styled(Layout.Main)`
  277. padding-top: ${space(2)};
  278. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  279. padding-top: ${space(1)};
  280. }
  281. @media (min-width: ${p => p.theme.breakpoints.large}) {
  282. border-right: 1px solid ${p => p.theme.border};
  283. padding-right: 0;
  284. }
  285. `;
  286. const StyledLayoutSide = styled(Layout.Side)`
  287. padding: ${space(3)} ${space(2)} ${space(3)};
  288. @media (min-width: ${p => p.theme.breakpoints.large}) {
  289. padding-right: ${space(4)};
  290. padding-left: 0;
  291. }
  292. `;
  293. export default GroupEventDetails;