index.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {useCallback, useEffect, useState} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import * as qs from 'query-string';
  6. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import LoadingError from 'sentry/components/loadingError';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import Panel from 'sentry/components/panels/panel';
  11. import IssuesReplayCountProvider from 'sentry/components/replays/issuesReplayCountProvider';
  12. import {t} from 'sentry/locale';
  13. import GroupingStore, {SimilarItem} from 'sentry/stores/groupingStore';
  14. import {space} from 'sentry/styles/space';
  15. import {Project} from 'sentry/types';
  16. import {useNavigate} from 'sentry/utils/useNavigate';
  17. import usePrevious from 'sentry/utils/usePrevious';
  18. import List from './list';
  19. type RouteParams = {
  20. groupId: string;
  21. orgId: string;
  22. };
  23. type Props = RouteComponentProps<RouteParams, {}> & {
  24. location: Location;
  25. project: Project;
  26. };
  27. type ItemState = {
  28. filtered: SimilarItem[];
  29. pageLinks: string | null;
  30. similar: SimilarItem[];
  31. };
  32. function SimilarStackTrace({params, location, project}: Props) {
  33. const {orgId, groupId} = params;
  34. const [items, setItems] = useState<ItemState>({
  35. similar: [],
  36. filtered: [],
  37. pageLinks: null,
  38. });
  39. const [status, setStatus] = useState<'loading' | 'error' | 'ready'>('loading');
  40. const navigate = useNavigate();
  41. const prevLocationSearch = usePrevious(location.search);
  42. const hasSimilarityFeature = project.features.includes('similarity-view');
  43. const fetchData = useCallback(() => {
  44. setStatus('loading');
  45. const reqs: Parameters<typeof GroupingStore.onFetch>[0] = [];
  46. if (hasSimilarityFeature) {
  47. reqs.push({
  48. endpoint: `/organizations/${orgId}/issues/${groupId}/similar/?${qs.stringify({
  49. ...location.query,
  50. limit: 50,
  51. })}`,
  52. dataKey: 'similar',
  53. });
  54. }
  55. GroupingStore.onFetch(reqs);
  56. }, [location.query, groupId, orgId, hasSimilarityFeature]);
  57. const onGroupingChange = useCallback(
  58. ({
  59. mergedParent,
  60. similarItems: updatedSimilarItems,
  61. filteredSimilarItems: updatedFilteredSimilarItems,
  62. similarLinks: updatedSimilarLinks,
  63. loading,
  64. error,
  65. }) => {
  66. if (updatedSimilarItems) {
  67. setItems({
  68. similar: updatedSimilarItems,
  69. filtered: updatedFilteredSimilarItems,
  70. pageLinks: updatedSimilarLinks,
  71. });
  72. setStatus(error ? 'error' : loading ? 'loading' : 'ready');
  73. return;
  74. }
  75. if (mergedParent && mergedParent !== groupId) {
  76. // Merge success, since we can't specify target, we need to redirect to new parent
  77. navigate(`/organizations/${orgId}/issues/${mergedParent}/similar/`);
  78. }
  79. },
  80. [navigate, groupId, orgId]
  81. );
  82. useEffect(() => {
  83. fetchData();
  84. }, [fetchData]);
  85. useEffect(() => {
  86. if (prevLocationSearch !== location.search) {
  87. fetchData();
  88. }
  89. }, [fetchData, prevLocationSearch, location.search]);
  90. useEffect(() => {
  91. const unsubscribe = GroupingStore.listen(onGroupingChange, undefined);
  92. return () => {
  93. unsubscribe();
  94. };
  95. }, [onGroupingChange]);
  96. const handleMerge = useCallback(() => {
  97. if (!params) {
  98. return;
  99. }
  100. // You need at least 1 similarItem OR filteredSimilarItems to be able to merge,
  101. // so `firstIssue` should always exist from one of those lists.
  102. //
  103. // Similar issues API currently does not return issues across projects,
  104. // so we can assume that the first issues project slug is the project in
  105. // scope
  106. const [firstIssue] = items.similar.length ? items.similar : items.filtered;
  107. GroupingStore.onMerge({
  108. params,
  109. query: location.query,
  110. projectId: firstIssue.issue.project.slug,
  111. });
  112. }, [params, location.query, items]);
  113. const hasSimilarItems =
  114. hasSimilarityFeature && (items.similar.length > 0 || items.filtered.length > 0);
  115. const groupsIds = items.similar.concat(items.filtered).map(({issue}) => issue.id);
  116. return (
  117. <Layout.Body>
  118. <Layout.Main fullWidth>
  119. <HeaderWrapper>
  120. <Title>{t('Issues with a similar stack trace')}</Title>
  121. <small>
  122. {t(
  123. 'This is an experimental feature. Data may not be immediately available while we process merges.'
  124. )}
  125. </small>
  126. </HeaderWrapper>
  127. {status === 'loading' && <LoadingIndicator />}
  128. {status === 'error' && (
  129. <LoadingError
  130. message={t('Unable to load similar issues, please try again later')}
  131. onRetry={fetchData}
  132. />
  133. )}
  134. {status === 'ready' && !hasSimilarItems && (
  135. <Panel>
  136. <EmptyStateWarning>
  137. <p>{t("There don't seem to be any similar issues.")}</p>
  138. </EmptyStateWarning>
  139. </Panel>
  140. )}
  141. {status === 'ready' && hasSimilarItems && (
  142. <IssuesReplayCountProvider groupIds={groupsIds}>
  143. <List
  144. items={items.similar}
  145. filteredItems={items.filtered}
  146. onMerge={handleMerge}
  147. orgId={orgId}
  148. project={project}
  149. groupId={groupId}
  150. pageLinks={items.pageLinks}
  151. />
  152. </IssuesReplayCountProvider>
  153. )}
  154. </Layout.Main>
  155. </Layout.Body>
  156. );
  157. }
  158. export default SimilarStackTrace;
  159. const Title = styled('h4')`
  160. margin-bottom: ${space(0.75)};
  161. `;
  162. const HeaderWrapper = styled('div')`
  163. margin-bottom: ${space(2)};
  164. small {
  165. color: ${p => p.theme.subText};
  166. }
  167. `;