diff --git a/static/app/views/issueDetails/groupMerged/index.spec.tsx b/static/app/views/issueDetails/groupMerged/index.spec.tsx index f317f53ceb3f03..af234fd6dbb47e 100644 --- a/static/app/views/issueDetails/groupMerged/index.spec.tsx +++ b/static/app/views/issueDetails/groupMerged/index.spec.tsx @@ -78,5 +78,8 @@ describe('Issues -> Merged View', function () { expect(title.parentElement).toHaveTextContent( 'Fingerprints included in this issue (2)' ); + + const links = await screen.findAllByRole('button', {name: 'View latest event'}); + expect(links).toHaveLength(mockData.merged.length); }); }); diff --git a/static/app/views/issueDetails/groupMerged/mergedItem.tsx b/static/app/views/issueDetails/groupMerged/mergedItem.tsx index 7b6a4f5a84329d..8b35348aabf49d 100644 --- a/static/app/views/issueDetails/groupMerged/mergedItem.tsx +++ b/static/app/views/issueDetails/groupMerged/mergedItem.tsx @@ -1,44 +1,37 @@ -import {Component} from 'react'; +import {useEffect, useState} from 'react'; import styled from '@emotion/styled'; -import {Button} from 'sentry/components/button'; +import {Button, LinkButton} from 'sentry/components/button'; import Checkbox from 'sentry/components/checkbox'; +import {Flex} from 'sentry/components/container/flex'; import EventOrGroupHeader from 'sentry/components/eventOrGroupHeader'; import {Tooltip} from 'sentry/components/tooltip'; -import {IconChevron} from 'sentry/icons'; +import {IconChevron, IconLink} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Fingerprint} from 'sentry/stores/groupingStore'; import GroupingStore from 'sentry/stores/groupingStore'; import {space} from 'sentry/styles/space'; -import type {Organization} from 'sentry/types/organization'; +import {useLocation} from 'sentry/utils/useLocation'; +import useOrganization from 'sentry/utils/useOrganization'; +import {createIssueLink} from 'sentry/views/issueList/utils'; -type Props = { +interface Props { fingerprint: Fingerprint; - organization: Organization; totalFingerprint: number; -}; - -type State = { - busy: boolean; - checked: boolean; - collapsed: boolean; -}; - -class MergedItem extends Component { - state: State = { - collapsed: false, - checked: false, - busy: false, - }; +} - listener = GroupingStore.listen(data => this.onGroupChange(data), undefined); +export function MergedItem({fingerprint, totalFingerprint}: Props) { + const organization = useOrganization(); + const location = useLocation(); + const [busy, setBusy] = useState(false); + const [collapsed, setCollapsed] = useState(false); + const [checked, setChecked] = useState(false); - onGroupChange = ({unmergeState}) => { + function onGroupChange({unmergeState}) { if (!unmergeState) { return; } - const {fingerprint} = this.props; const stateForId = unmergeState.has(fingerprint.id) ? unmergeState.get(fingerprint.id) : undefined; @@ -48,42 +41,37 @@ class MergedItem extends Component { } Object.keys(stateForId).forEach(key => { - if (stateForId[key] === this.state[key]) { - return; + if (key === 'collapsed') { + setCollapsed(Boolean(stateForId[key])); + } else if (key === 'checked') { + setChecked(Boolean(stateForId[key])); + } else if (key === 'busy') { + setBusy(Boolean(stateForId[key])); } - - this.setState(prevState => ({...prevState, [key]: stateForId[key]})); }); - }; + } - handleToggleEvents = () => { - const {fingerprint} = this.props; + function handleToggleEvents() { GroupingStore.onToggleCollapseFingerprint(fingerprint.id); - }; - - // Disable default behavior of toggling checkbox - handleLabelClick(event: React.MouseEvent) { - event.preventDefault(); } - handleToggle = () => { - const {fingerprint} = this.props; + function handleToggle() { const {latestEvent} = fingerprint; - if (this.state.busy) { + if (busy) { return; } // clicking anywhere in the row will toggle the checkbox GroupingStore.onToggleUnmerge([fingerprint.id, latestEvent.id]); - }; + } - handleCheckClick() { + function handleCheckClick() { // noop because of react warning about being a controlled input without `onChange` // we handle change via row click } - renderFingerprint(id: string, label?: string) { + function renderFingerprint(id: string, label?: string) { if (!label) { return id; } @@ -95,55 +83,77 @@ class MergedItem extends Component { ); } - render() { - const {fingerprint, organization, totalFingerprint} = this.props; - const {latestEvent, id, label} = fingerprint; - const {collapsed, busy, checked} = this.state; - const checkboxDisabled = busy || totalFingerprint === 1; - - // `latestEvent` can be null if last event w/ fingerprint is not within retention period - return ( - - - - - { + const teardown = GroupingStore.listen(data => onGroupChange(data), undefined); + return () => { + teardown(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const {latestEvent, id, label} = fingerprint; + const checkboxDisabled = busy || totalFingerprint === 1; + + const issueLink = latestEvent + ? createIssueLink({ + organization, + location, + data: latestEvent, + eventId: latestEvent.id, + referrer: 'merged-item', + }) + : null; + + // `latestEvent` can be null if last event w/ fingerprint is not within retention period + return ( + + + + + + + {renderFingerprint(id, label)} + + +