import {Component} from 'react'; import {browserHistory, RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import {Location} from 'history'; import * as qs from 'query-string'; import Alert from 'sentry/components/alert'; import Button from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import * as Layout from 'sentry/components/layouts/thirds'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; import GroupingStore, {SimilarItem} from 'sentry/stores/groupingStore'; import space from 'sentry/styles/space'; import {Project} from 'sentry/types'; import List from './list'; type RouteParams = { groupId: string; orgId: string; }; type Props = RouteComponentProps & { location: Location; project: Project; }; type State = { error: boolean; filteredSimilarItems: SimilarItem[]; loading: boolean; similarItems: SimilarItem[]; similarLinks: string | null; v2: boolean; }; class SimilarStackTrace extends Component { state: State = { similarItems: [], filteredSimilarItems: [], similarLinks: null, loading: true, error: false, v2: false, }; componentDidMount() { this.fetchData(); } componentWillReceiveProps(nextProps: Props) { if ( nextProps.params.groupId !== this.props.params.groupId || nextProps.location.search !== this.props.location.search ) { this.fetchData(); } } componentWillUnmount() { this.listener?.(); } onGroupingChange = ({ mergedParent, similarItems, similarLinks, filteredSimilarItems, loading, error, }) => { if (similarItems) { this.setState({ similarItems, similarLinks, filteredSimilarItems, loading: loading ?? false, error: error ?? false, }); return; } if (!mergedParent) { return; } if (mergedParent !== this.props.params.groupId) { const {params} = this.props; // Merge success, since we can't specify target, we need to redirect to new parent browserHistory.push( `/organizations/${params.orgId}/issues/${mergedParent}/similar/` ); return; } return; }; listener = GroupingStore.listen(this.onGroupingChange, undefined); fetchData() { const {params, location} = this.props; this.setState({loading: true, error: false}); const reqs: Parameters[0] = []; if (this.hasSimilarityFeature()) { const version = this.state.v2 ? '2' : '1'; reqs.push({ endpoint: `/issues/${params.groupId}/similar/?${qs.stringify({ ...location.query, limit: 50, version, })}`, dataKey: 'similar', }); } GroupingStore.onFetch(reqs); } handleMerge = () => { const {params, location} = this.props; const query = location.query; if (!params) { return; } // You need at least 1 similarItem OR filteredSimilarItems to be able to merge, // so `firstIssue` should always exist from one of those lists. // // Similar issues API currently does not return issues across projects, // so we can assume that the first issues project slug is the project in // scope const [firstIssue] = this.state.similarItems.length ? this.state.similarItems : this.state.filteredSimilarItems; GroupingStore.onMerge({ params, query, projectId: firstIssue.issue.project.slug, }); }; hasSimilarityV2Feature() { return this.props.project.features.includes('similarity-view-v2'); } hasSimilarityFeature() { return this.props.project.features.includes('similarity-view'); } toggleSimilarityVersion = () => { this.setState(prevState => ({v2: !prevState.v2}), this.fetchData); }; render() { const {params, project} = this.props; const {orgId, groupId} = params; const {similarItems, filteredSimilarItems, loading, error, v2, similarLinks} = this.state; const hasV2 = this.hasSimilarityV2Feature(); const isLoading = loading; const isError = error && !isLoading; const isLoadedSuccessfully = !isError && !isLoading; const hasSimilarItems = this.hasSimilarityFeature() && (similarItems.length > 0 || filteredSimilarItems.length > 0) && isLoadedSuccessfully; return ( {t( 'This is an experimental feature. Data may not be immediately available while we process merges.' )} {t('Issues with a similar stack trace')} {hasV2 && ( )} {isLoading && } {isError && ( )} {hasSimilarItems && ( )} ); } } export default SimilarStackTrace; const Title = styled('h4')` margin-bottom: 0; `; const HeaderWrapper = styled('div')` display: flex; align-items: center; justify-content: space-between; margin-bottom: ${space(2)}; `;