groupEventDetails.tsx 9.6 KB

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