groupEventDetails.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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 statusDetails={group.statusDetails} />
  138. ) : (
  139. <MutedBox statusDetails={group.statusDetails} />
  140. )}
  141. </GroupStatusBannerWrapper>
  142. );
  143. }
  144. if (group.status === 'resolved') {
  145. return (
  146. <GroupStatusBannerWrapper>
  147. <ResolutionBox
  148. statusDetails={group.statusDetails}
  149. activities={group.activity}
  150. projectId={project.id}
  151. />
  152. </GroupStatusBannerWrapper>
  153. );
  154. }
  155. return null;
  156. };
  157. const renderReprocessedBox = () => {
  158. if (
  159. groupReprocessingStatus !== ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT &&
  160. groupReprocessingStatus !== ReprocessingStatus.REPROCESSED_AND_HAS_EVENT
  161. ) {
  162. return null;
  163. }
  164. const {count, id: groupId} = group;
  165. const groupCount = Number(count);
  166. return (
  167. <ReprocessedBox
  168. reprocessActivity={mostRecentActivity as GroupActivityReprocess}
  169. groupCount={groupCount}
  170. groupId={groupId}
  171. orgSlug={organization.slug}
  172. />
  173. );
  174. };
  175. const renderContent = () => {
  176. if (loadingEvent) {
  177. return <LoadingIndicator />;
  178. }
  179. if (eventError) {
  180. return (
  181. <GroupEventDetailsLoadingError environments={environments} onRetry={onRetry} />
  182. );
  183. }
  184. return (
  185. <GroupEventDetailsContent group={group} event={eventWithMeta} project={project} />
  186. );
  187. };
  188. return (
  189. <TransactionProfileIdProvider
  190. projectId={event?.projectID}
  191. transactionId={event?.type === 'transaction' ? event.id : undefined}
  192. timestamp={event?.dateReceived}
  193. >
  194. <VisuallyCompleteWithData
  195. id="IssueDetails-EventBody"
  196. hasData={!loadingEvent && !eventError && defined(eventWithMeta)}
  197. isLoading={loadingEvent}
  198. >
  199. <StyledLayoutBody data-test-id="group-event-details">
  200. {hasReprocessingV2Feature &&
  201. groupReprocessingStatus === ReprocessingStatus.REPROCESSING ? (
  202. <ReprocessingProgress
  203. totalEvents={(mostRecentActivity as GroupActivityReprocess).data.eventCount}
  204. pendingEvents={
  205. (group.statusDetails as BaseGroupStatusReprocessing['statusDetails'])
  206. .pendingEvents
  207. }
  208. />
  209. ) : (
  210. <Fragment>
  211. <QuickTraceQuery
  212. event={eventWithMeta}
  213. location={location}
  214. orgSlug={organization.slug}
  215. >
  216. {results => {
  217. return (
  218. <StyledLayoutMain>
  219. {renderGroupStatusBanner()}
  220. <IssuePriorityFeedback organization={organization} group={group} />
  221. <QuickTraceContext.Provider value={results}>
  222. {eventWithMeta && (
  223. <GroupEventHeader
  224. group={group}
  225. event={eventWithMeta}
  226. project={project}
  227. />
  228. )}
  229. {renderReprocessedBox()}
  230. {renderContent()}
  231. </QuickTraceContext.Provider>
  232. </StyledLayoutMain>
  233. );
  234. }}
  235. </QuickTraceQuery>
  236. <StyledLayoutSide>
  237. <GroupSidebar
  238. organization={organization}
  239. project={project}
  240. group={group}
  241. event={eventWithMeta}
  242. environments={environments}
  243. />
  244. </StyledLayoutSide>
  245. </Fragment>
  246. )}
  247. </StyledLayoutBody>
  248. </VisuallyCompleteWithData>
  249. </TransactionProfileIdProvider>
  250. );
  251. }
  252. const StyledLayoutBody = styled(Layout.Body)`
  253. /* Makes the borders align correctly */
  254. padding: 0 !important;
  255. @media (min-width: ${p => p.theme.breakpoints.large}) {
  256. align-content: stretch;
  257. }
  258. `;
  259. const GroupStatusBannerWrapper = styled('div')`
  260. margin-bottom: ${space(2)};
  261. `;
  262. const StyledLayoutMain = styled(Layout.Main)`
  263. padding-top: ${space(2)};
  264. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  265. padding-top: ${space(1)};
  266. }
  267. @media (min-width: ${p => p.theme.breakpoints.large}) {
  268. border-right: 1px solid ${p => p.theme.border};
  269. padding-right: 0;
  270. }
  271. `;
  272. const StyledLayoutSide = styled(Layout.Side)`
  273. padding: ${space(3)} ${space(2)} ${space(3)};
  274. @media (min-width: ${p => p.theme.breakpoints.large}) {
  275. padding-right: ${space(4)};
  276. padding-left: 0;
  277. }
  278. `;
  279. export default GroupEventDetails;