index.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import {Fragment} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {LinkButton} from 'sentry/components/button';
  5. import GroupList from 'sentry/components/issues/groupList';
  6. import Link from 'sentry/components/links/link';
  7. import LoadingError from 'sentry/components/loadingError';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {useApiQuery} from 'sentry/utils/queryClient';
  12. import useOrganization from 'sentry/utils/useOrganization';
  13. type RouteParams = {
  14. groupId: string;
  15. };
  16. type Props = RouteComponentProps<RouteParams, {}>;
  17. type RelatedIssuesResponse = {
  18. data: number[];
  19. meta: {
  20. event_id: string;
  21. trace_id: string;
  22. };
  23. type: string;
  24. };
  25. interface RelatedIssuesSectionProps {
  26. groupId: string;
  27. orgSlug: string;
  28. relationType: string;
  29. }
  30. function GroupRelatedIssues({params}: Props) {
  31. const {groupId} = params;
  32. const organization = useOrganization();
  33. const orgSlug = organization.slug;
  34. return (
  35. <Fragment>
  36. <RelatedIssuesSection
  37. groupId={groupId}
  38. orgSlug={orgSlug}
  39. relationType="same_root_cause"
  40. />
  41. <RelatedIssuesSection
  42. groupId={groupId}
  43. orgSlug={orgSlug}
  44. relationType="trace_connected"
  45. />
  46. </Fragment>
  47. );
  48. }
  49. function RelatedIssuesSection({
  50. groupId,
  51. orgSlug,
  52. relationType,
  53. }: RelatedIssuesSectionProps) {
  54. // Fetch the list of related issues
  55. const {
  56. isLoading,
  57. isError,
  58. data: relatedIssues,
  59. refetch,
  60. } = useApiQuery<RelatedIssuesResponse>(
  61. [`/issues/${groupId}/related-issues/?type=${relationType}`],
  62. {
  63. staleTime: 0,
  64. }
  65. );
  66. const traceMeta = relationType === 'trace_connected' ? relatedIssues?.meta : undefined;
  67. const issues = relatedIssues?.data ?? [];
  68. const query = `issue.id:[${issues}]`;
  69. // project=-1 allows ensuring that the query will show issues from any projects for the org
  70. // This is important for traces since issues can be for any project in the org
  71. const baseUrl = `/organizations/${orgSlug}/issues/?project=-1`;
  72. let title: React.ReactNode = null;
  73. let linkToTrace: React.ReactNode = null;
  74. let openIssuesButton: React.ReactNode = null;
  75. if (relationType === 'trace_connected' && traceMeta) {
  76. title = t('Issues in the same trace');
  77. linkToTrace = (
  78. <small>
  79. {t('These issues were all found within ')}
  80. <Link
  81. to={`/organizations/${orgSlug}/performance/trace/${traceMeta.trace_id}/?node=error-${traceMeta.event_id}`}
  82. >
  83. {t('this trace')}
  84. </Link>
  85. .
  86. </small>
  87. );
  88. openIssuesButton = (
  89. <LinkButton
  90. to={`${baseUrl}&query=trace:${traceMeta.trace_id}`}
  91. size="xs"
  92. analyticsEventName="Clicked Open Issues from trace-connected related issues"
  93. analyticsEventKey="similar_issues.trace_connected_issues_clicked_open_issues"
  94. >
  95. {t('Open in Issues')}
  96. </LinkButton>
  97. );
  98. } else {
  99. title = t('Issues caused by the same root cause');
  100. openIssuesButton = (
  101. <LinkButton
  102. to={`${baseUrl}&query=issue.id:[${groupId},${issues}]`}
  103. size="xs"
  104. analyticsEventName="Clicked Open Issues from same-root related issues"
  105. analyticsEventKey="similar_issues.same_root_cause_clicked_open_issues"
  106. >
  107. {t('Open in Issues')}
  108. </LinkButton>
  109. );
  110. }
  111. return (
  112. <HeaderWrapper>
  113. {isLoading ? (
  114. <LoadingIndicator />
  115. ) : isError ? (
  116. <LoadingError
  117. message={t('Unable to load related issues, please try again later')}
  118. onRetry={refetch}
  119. />
  120. ) : issues.length > 0 ? (
  121. <Fragment>
  122. <Title>{title}</Title>
  123. <TextButtonWrapper>
  124. {linkToTrace}
  125. {openIssuesButton}
  126. </TextButtonWrapper>
  127. <GroupList
  128. orgSlug={orgSlug}
  129. queryParams={{query: query}}
  130. source="similar-issues-tab"
  131. canSelectGroups={false}
  132. withChart={false}
  133. />
  134. </Fragment>
  135. ) : null}
  136. </HeaderWrapper>
  137. );
  138. }
  139. // Export the component without feature flag controls
  140. export {GroupRelatedIssues};
  141. const Title = styled('h4')`
  142. margin-bottom: ${space(0.75)};
  143. `;
  144. const HeaderWrapper = styled('div')`
  145. margin-bottom: ${space(2)};
  146. small {
  147. color: ${p => p.theme.subText};
  148. }
  149. `;
  150. const TextButtonWrapper = styled('div')`
  151. display: flex;
  152. justify-content: space-between;
  153. margin-bottom: ${space(1)};
  154. `;