index.tsx 6.1 KB

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