import {Component} from 'react'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; import Checkbox from 'sentry/components/checkbox'; import EventOrGroupHeader from 'sentry/components/eventOrGroupHeader'; import {Tooltip} from 'sentry/components/tooltip'; import {IconChevron} 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'; type Props = { fingerprint: Fingerprint; organization: Organization; totalFingerprint: number; }; type State = { busy: boolean; checked: boolean; collapsed: boolean; }; class MergedItem extends Component<Props, State> { state: State = { collapsed: false, checked: false, busy: false, }; listener = GroupingStore.listen(data => this.onGroupChange(data), undefined); onGroupChange = ({unmergeState}) => { if (!unmergeState) { return; } const {fingerprint} = this.props; const stateForId = unmergeState.has(fingerprint.id) ? unmergeState.get(fingerprint.id) : undefined; if (!stateForId) { return; } Object.keys(stateForId).forEach(key => { if (stateForId[key] === this.state[key]) { return; } this.setState(prevState => ({...prevState, [key]: stateForId[key]})); }); }; handleToggleEvents = () => { const {fingerprint} = this.props; GroupingStore.onToggleCollapseFingerprint(fingerprint.id); }; // Disable default behavior of toggling checkbox handleLabelClick(event: React.MouseEvent) { event.preventDefault(); } handleToggle = () => { const {fingerprint} = this.props; const {latestEvent} = fingerprint; if (this.state.busy) { return; } // clicking anywhere in the row will toggle the checkbox GroupingStore.onToggleUnmerge([fingerprint.id, latestEvent.id]); }; handleCheckClick() { // noop because of react warning about being a controlled input without `onChange` // we handle change via row click } renderFingerprint(id: string, label?: string) { if (!label) { return id; } return ( <Tooltip title={id}> <code>{label}</code> </Tooltip> ); } 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 ( <MergedGroup busy={busy}> <Controls expanded={!collapsed}> <FingerprintLabel onClick={this.handleToggle}> <Tooltip containerDisplayMode="flex" disabled={!checkboxDisabled} title={ checkboxDisabled && totalFingerprint === 1 ? t('To check, the list must contain 2 or more items') : undefined } > <Checkbox value={id} checked={checked} disabled={checkboxDisabled} onChange={this.handleCheckClick} size="xs" /> </Tooltip> {this.renderFingerprint(id, label)} </FingerprintLabel> <Button aria-label={ collapsed ? t('Show %s fingerprints', id) : t('Collapse %s fingerprints', id) } size="zero" borderless icon={<IconChevron direction={collapsed ? 'down' : 'up'} size="xs" />} onClick={this.handleToggleEvents} /> </Controls> {!collapsed && ( <MergedEventList className="event-list"> {latestEvent && ( <EventDetails className="event-details"> <EventOrGroupHeader data={latestEvent} organization={organization} hideIcons hideLevel source="merged-item" /> </EventDetails> )} </MergedEventList> )} </MergedGroup> ); } } const MergedGroup = styled('div')<{busy: boolean}>` ${p => p.busy && 'opacity: 0.2'}; `; const Controls = styled('div')<{expanded: boolean}>` display: flex; justify-content: space-between; background-color: ${p => p.theme.backgroundSecondary}; ${p => p.expanded && `border-bottom: 1px solid ${p.theme.innerBorder}`}; padding: ${space(0.5)} ${space(1)}; ${MergedGroup}:not(:first-child) & { border-top: 1px solid ${p => p.theme.innerBorder}; } ${MergedGroup}:last-child & { ${p => !p.expanded && `border-bottom: none`}; ${p => !p.expanded && `border-radius: 0 0 ${p.theme.borderRadius} ${p.theme.borderRadius}`}; } `; const FingerprintLabel = styled('label')` display: flex; align-items: center; gap: ${space(1)}; font-family: ${p => p.theme.text.familyMono}; line-height: 1; font-weight: 400; margin: 0; `; const MergedEventList = styled('div')` overflow: hidden; border: none; background-color: ${p => p.theme.background}; `; const EventDetails = styled('div')` display: flex; justify-content: space-between; .event-list & { padding: ${space(1)}; } `; export default MergedItem;