index.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import {Component} from 'react';
  2. import {browserHistory, RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import * as qs from 'query-string';
  6. import Alert from 'sentry/components/alert';
  7. import Button from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import * as Layout from 'sentry/components/layouts/thirds';
  10. import LoadingError from 'sentry/components/loadingError';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  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 {callIfFunction} from 'sentry/utils/callIfFunction';
  17. import List from './list';
  18. type RouteParams = {
  19. groupId: string;
  20. orgId: string;
  21. };
  22. type Props = RouteComponentProps<RouteParams, {}> & {
  23. location: Location;
  24. project: Project;
  25. };
  26. type State = {
  27. error: boolean;
  28. filteredSimilarItems: SimilarItem[];
  29. loading: boolean;
  30. similarItems: SimilarItem[];
  31. similarLinks: string | null;
  32. v2: boolean;
  33. };
  34. class SimilarStackTrace extends Component<Props, State> {
  35. state: State = {
  36. similarItems: [],
  37. filteredSimilarItems: [],
  38. similarLinks: null,
  39. loading: true,
  40. error: false,
  41. v2: false,
  42. };
  43. componentDidMount() {
  44. this.fetchData();
  45. }
  46. componentWillReceiveProps(nextProps: Props) {
  47. if (
  48. nextProps.params.groupId !== this.props.params.groupId ||
  49. nextProps.location.search !== this.props.location.search
  50. ) {
  51. this.fetchData();
  52. }
  53. }
  54. componentWillUnmount() {
  55. callIfFunction(this.listener);
  56. }
  57. onGroupingChange = ({
  58. mergedParent,
  59. similarItems,
  60. similarLinks,
  61. filteredSimilarItems,
  62. loading,
  63. error,
  64. }) => {
  65. if (similarItems) {
  66. this.setState({
  67. similarItems,
  68. similarLinks,
  69. filteredSimilarItems,
  70. loading: loading ?? false,
  71. error: error ?? false,
  72. });
  73. return;
  74. }
  75. if (!mergedParent) {
  76. return;
  77. }
  78. if (mergedParent !== this.props.params.groupId) {
  79. const {params} = this.props;
  80. // Merge success, since we can't specify target, we need to redirect to new parent
  81. browserHistory.push(
  82. `/organizations/${params.orgId}/issues/${mergedParent}/similar/`
  83. );
  84. return;
  85. }
  86. return;
  87. };
  88. listener = GroupingStore.listen(this.onGroupingChange, undefined);
  89. fetchData() {
  90. const {params, location} = this.props;
  91. this.setState({loading: true, error: false});
  92. const reqs: Parameters<typeof GroupingStore.onFetch>[0] = [];
  93. if (this.hasSimilarityFeature()) {
  94. const version = this.state.v2 ? '2' : '1';
  95. reqs.push({
  96. endpoint: `/issues/${params.groupId}/similar/?${qs.stringify({
  97. ...location.query,
  98. limit: 50,
  99. version,
  100. })}`,
  101. dataKey: 'similar',
  102. });
  103. }
  104. GroupingStore.onFetch(reqs);
  105. }
  106. handleMerge = () => {
  107. const {params, location} = this.props;
  108. const query = location.query;
  109. if (!params) {
  110. return;
  111. }
  112. // You need at least 1 similarItem OR filteredSimilarItems to be able to merge,
  113. // so `firstIssue` should always exist from one of those lists.
  114. //
  115. // Similar issues API currently does not return issues across projects,
  116. // so we can assume that the first issues project slug is the project in
  117. // scope
  118. const [firstIssue] = this.state.similarItems.length
  119. ? this.state.similarItems
  120. : this.state.filteredSimilarItems;
  121. GroupingStore.onMerge({
  122. params,
  123. query,
  124. projectId: firstIssue.issue.project.slug,
  125. });
  126. };
  127. hasSimilarityV2Feature() {
  128. return this.props.project.features.includes('similarity-view-v2');
  129. }
  130. hasSimilarityFeature() {
  131. return this.props.project.features.includes('similarity-view');
  132. }
  133. toggleSimilarityVersion = () => {
  134. this.setState(prevState => ({v2: !prevState.v2}), this.fetchData);
  135. };
  136. render() {
  137. const {params, project} = this.props;
  138. const {orgId, groupId} = params;
  139. const {similarItems, filteredSimilarItems, loading, error, v2, similarLinks} =
  140. this.state;
  141. const hasV2 = this.hasSimilarityV2Feature();
  142. const isLoading = loading;
  143. const isError = error && !isLoading;
  144. const isLoadedSuccessfully = !isError && !isLoading;
  145. const hasSimilarItems =
  146. this.hasSimilarityFeature() &&
  147. (similarItems.length > 0 || filteredSimilarItems.length > 0) &&
  148. isLoadedSuccessfully;
  149. return (
  150. <Layout.Body>
  151. <Layout.Main fullWidth>
  152. <Alert type="warning">
  153. {t(
  154. 'This is an experimental feature. Data may not be immediately available while we process merges.'
  155. )}
  156. </Alert>
  157. <HeaderWrapper>
  158. <Title>{t('Issues with a similar stack trace')}</Title>
  159. {hasV2 && (
  160. <ButtonBar merged active={v2 ? 'new' : 'old'}>
  161. <Button barId="old" size="sm" onClick={this.toggleSimilarityVersion}>
  162. {t('Old Algorithm')}
  163. </Button>
  164. <Button barId="new" size="sm" onClick={this.toggleSimilarityVersion}>
  165. {t('New Algorithm')}
  166. </Button>
  167. </ButtonBar>
  168. )}
  169. </HeaderWrapper>
  170. {isLoading && <LoadingIndicator />}
  171. {isError && (
  172. <LoadingError
  173. message={t('Unable to load similar issues, please try again later')}
  174. onRetry={this.fetchData}
  175. />
  176. )}
  177. {hasSimilarItems && (
  178. <List
  179. items={similarItems}
  180. filteredItems={filteredSimilarItems}
  181. onMerge={this.handleMerge}
  182. orgId={orgId}
  183. project={project}
  184. groupId={groupId}
  185. pageLinks={similarLinks}
  186. v2={v2}
  187. />
  188. )}
  189. </Layout.Main>
  190. </Layout.Body>
  191. );
  192. }
  193. }
  194. export default SimilarStackTrace;
  195. const Title = styled('h4')`
  196. margin-bottom: 0;
  197. `;
  198. const HeaderWrapper = styled('div')`
  199. display: flex;
  200. align-items: center;
  201. justify-content: space-between;
  202. margin-bottom: ${space(2)};
  203. `;