index.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. // XXX: A lot of the UI for this file will be changed once we use IssueListActions
  2. // We're using GroupList to help us iterate quickly
  3. import type {RouteComponentProps} from 'react-router';
  4. import styled from '@emotion/styled';
  5. import Feature from 'sentry/components/acl/feature';
  6. import {LinkButton} from 'sentry/components/button';
  7. import GroupList from 'sentry/components/issues/groupList';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import Link from 'sentry/components/links/link';
  10. import LoadingError from 'sentry/components/loadingError';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {useApiQuery} from 'sentry/utils/queryClient';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. type RouteParams = {
  17. groupId: string;
  18. };
  19. type Props = RouteComponentProps<RouteParams, {}>;
  20. type RelatedIssuesResponse = {
  21. data: [
  22. {
  23. data: number[];
  24. meta: {
  25. event_id: string;
  26. trace_id: string;
  27. };
  28. type: string;
  29. },
  30. ];
  31. };
  32. function GroupRelatedIssues({params}: Props) {
  33. const {groupId} = params;
  34. const organization = useOrganization();
  35. const orgSlug = organization.slug;
  36. // Fetch the list of related issues
  37. const {
  38. isLoading,
  39. isError,
  40. data: relatedIssues,
  41. refetch,
  42. } = useApiQuery<RelatedIssuesResponse>([`/issues/${groupId}/related-issues/`], {
  43. staleTime: 0,
  44. });
  45. let traceMeta = {
  46. trace_id: '',
  47. event_id: '',
  48. };
  49. const {
  50. same_root_cause: sameRootCauseIssues = [],
  51. trace_connected: traceConnectedIssues = [],
  52. } = (relatedIssues?.data ?? []).reduce(
  53. (mapping, item) => {
  54. if (item.type === 'trace_connected') {
  55. traceMeta = {...item.meta};
  56. }
  57. const issuesList = item.data;
  58. mapping[item.type] = issuesList;
  59. return mapping;
  60. },
  61. {same_root_cause: [], trace_connected: []}
  62. );
  63. return (
  64. <Layout.Body>
  65. <Layout.Main fullWidth>
  66. <HeaderWrapper>
  67. <small>
  68. {t(
  69. 'Related Issues are issues that are related in some way and can be acted on together.'
  70. )}
  71. </small>
  72. </HeaderWrapper>
  73. {isLoading ? (
  74. <LoadingIndicator />
  75. ) : isError ? (
  76. <LoadingError
  77. message={t('Unable to load related issues, please try again later')}
  78. onRetry={refetch}
  79. />
  80. ) : (
  81. <div>
  82. <div>
  83. <HeaderWrapper>
  84. <Title>{t('Issues caused by the same root cause')}</Title>
  85. {sameRootCauseIssues.length > 0 ? (
  86. <div>
  87. <TextButtonWrapper>
  88. <div />
  89. <LinkButton
  90. to={`/organizations/${orgSlug}/issues/?query=issue.id:[${groupId},${sameRootCauseIssues}]`}
  91. size="xs"
  92. >
  93. {t('Open in Issues')}
  94. </LinkButton>
  95. </TextButtonWrapper>
  96. <GroupList
  97. endpointPath={`/organizations/${orgSlug}/issues/`}
  98. orgSlug={orgSlug}
  99. queryParams={{query: `issue.id:[${sameRootCauseIssues}]`}}
  100. query=""
  101. source="related-issues-tab"
  102. canSelectGroups={false}
  103. withChart={false}
  104. />
  105. </div>
  106. ) : (
  107. <small>{t('No same-root-cause related issues were found.')}</small>
  108. )}
  109. </HeaderWrapper>
  110. </div>
  111. <div>
  112. <HeaderWrapper>
  113. <Title>{t('Trace connected issues')}</Title>
  114. {traceConnectedIssues.length > 0 ? (
  115. <div>
  116. <TextButtonWrapper>
  117. <small>
  118. {t('These are the issues belonging to ')}
  119. <Link
  120. to={`/organizations/${orgSlug}/performance/trace/${traceMeta.trace_id}/?node=error-${traceMeta.event_id}`}
  121. >
  122. {t('this trace')}
  123. </Link>
  124. </small>
  125. <LinkButton
  126. to={`/organizations/${orgSlug}/issues/?query=trace:${traceMeta.trace_id}`}
  127. size="xs"
  128. >
  129. {t('Open in Issues')}
  130. </LinkButton>
  131. </TextButtonWrapper>
  132. <GroupList
  133. endpointPath={`/organizations/${orgSlug}/issues/`}
  134. orgSlug={orgSlug}
  135. queryParams={{query: `issue.id:[${traceConnectedIssues}]`}}
  136. query=""
  137. source="related-issues-tab"
  138. canSelectGroups={false}
  139. withChart={false}
  140. />
  141. </div>
  142. ) : (
  143. <small>{t('No trace-connected related issues were found.')}</small>
  144. )}
  145. </HeaderWrapper>
  146. </div>
  147. </div>
  148. )}
  149. </Layout.Main>
  150. </Layout.Body>
  151. );
  152. }
  153. function GroupRelatedIssuesWrapper(props: Props) {
  154. return (
  155. <Feature features={['related-issues']}>
  156. <GroupRelatedIssues {...props} />
  157. </Feature>
  158. );
  159. }
  160. // Export the component without feature flag controls
  161. export {GroupRelatedIssues};
  162. export default GroupRelatedIssuesWrapper;
  163. const Title = styled('h4')`
  164. margin-bottom: ${space(0.75)};
  165. `;
  166. const HeaderWrapper = styled('div')`
  167. margin-bottom: ${space(2)};
  168. small {
  169. color: ${p => p.theme.subText};
  170. }
  171. `;
  172. const TextButtonWrapper = styled('div')`
  173. display: flex;
  174. justify-content: space-between;
  175. margin-bottom: ${space(1)};
  176. `;