mergedItem.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import Checkbox from 'sentry/components/checkbox';
  4. import EventOrGroupHeader from 'sentry/components/eventOrGroupHeader';
  5. import Tooltip from 'sentry/components/tooltip';
  6. import {IconChevron} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import GroupingStore, {Fingerprint} from 'sentry/stores/groupingStore';
  9. import space from 'sentry/styles/space';
  10. import {Organization} from 'sentry/types';
  11. type Props = {
  12. fingerprint: Fingerprint;
  13. organization: Organization;
  14. totalFingerprint: number;
  15. };
  16. type State = {
  17. busy: boolean;
  18. checked: boolean;
  19. collapsed: boolean;
  20. };
  21. class MergedItem extends Component<Props, State> {
  22. state: State = {
  23. collapsed: false,
  24. checked: false,
  25. busy: false,
  26. };
  27. listener = GroupingStore.listen(data => this.onGroupChange(data), undefined);
  28. onGroupChange = ({unmergeState}) => {
  29. if (!unmergeState) {
  30. return;
  31. }
  32. const {fingerprint} = this.props;
  33. const stateForId = unmergeState.has(fingerprint.id)
  34. ? unmergeState.get(fingerprint.id)
  35. : undefined;
  36. if (!stateForId) {
  37. return;
  38. }
  39. Object.keys(stateForId).forEach(key => {
  40. if (stateForId[key] === this.state[key]) {
  41. return;
  42. }
  43. this.setState(prevState => ({...prevState, [key]: stateForId[key]}));
  44. });
  45. };
  46. handleToggleEvents = () => {
  47. const {fingerprint} = this.props;
  48. GroupingStore.onToggleCollapseFingerprint(fingerprint.id);
  49. };
  50. // Disable default behavior of toggling checkbox
  51. handleLabelClick(event: React.MouseEvent) {
  52. event.preventDefault();
  53. }
  54. handleToggle = () => {
  55. const {fingerprint} = this.props;
  56. const {latestEvent} = fingerprint;
  57. if (this.state.busy) {
  58. return;
  59. }
  60. // clicking anywhere in the row will toggle the checkbox
  61. GroupingStore.onToggleUnmerge([fingerprint.id, latestEvent.id]);
  62. };
  63. handleCheckClick() {
  64. // noop because of react warning about being a controlled input without `onChange`
  65. // we handle change via row click
  66. }
  67. renderFingerprint(id: string, label?: string) {
  68. if (!label) {
  69. return id;
  70. }
  71. return (
  72. <Tooltip title={id}>
  73. <code>{label}</code>
  74. </Tooltip>
  75. );
  76. }
  77. render() {
  78. const {fingerprint, organization, totalFingerprint} = this.props;
  79. const {latestEvent, id, label} = fingerprint;
  80. const {collapsed, busy, checked} = this.state;
  81. const checkboxDisabled = busy || totalFingerprint === 1;
  82. // `latestEvent` can be null if last event w/ fingerprint is not within retention period
  83. return (
  84. <MergedGroup busy={busy}>
  85. <Controls expanded={!collapsed}>
  86. <ActionWrapper onClick={this.handleToggle}>
  87. <Tooltip
  88. disabled={!checkboxDisabled}
  89. title={
  90. checkboxDisabled && totalFingerprint === 1
  91. ? t('To check, the list must contain 2 or more items')
  92. : undefined
  93. }
  94. >
  95. <Checkbox
  96. id={id}
  97. value={id}
  98. checked={checked}
  99. disabled={checkboxDisabled}
  100. onChange={this.handleCheckClick}
  101. />
  102. </Tooltip>
  103. <FingerprintLabel onClick={this.handleLabelClick} htmlFor={id}>
  104. {this.renderFingerprint(id, label)}
  105. </FingerprintLabel>
  106. </ActionWrapper>
  107. <div>
  108. <Collapse onClick={this.handleToggleEvents}>
  109. <IconChevron direction={collapsed ? 'down' : 'up'} size="xs" />
  110. </Collapse>
  111. </div>
  112. </Controls>
  113. {!collapsed && (
  114. <MergedEventList className="event-list">
  115. {latestEvent && (
  116. <EventDetails className="event-details">
  117. <EventOrGroupHeader
  118. data={latestEvent}
  119. organization={organization}
  120. hideIcons
  121. hideLevel
  122. />
  123. </EventDetails>
  124. )}
  125. </MergedEventList>
  126. )}
  127. </MergedGroup>
  128. );
  129. }
  130. }
  131. const MergedGroup = styled('div')<{busy: boolean}>`
  132. ${p => p.busy && 'opacity: 0.2'};
  133. `;
  134. const ActionWrapper = styled('div')`
  135. display: grid;
  136. grid-auto-flow: column;
  137. align-items: center;
  138. gap: ${space(1)};
  139. /* Can't use styled components for this because of broad selector */
  140. input[type='checkbox'] {
  141. margin: 0;
  142. }
  143. `;
  144. const Controls = styled('div')<{expanded: boolean}>`
  145. display: flex;
  146. justify-content: space-between;
  147. border-top: 1px solid ${p => p.theme.innerBorder};
  148. background-color: ${p => p.theme.backgroundSecondary};
  149. padding: ${space(0.5)} ${space(1)};
  150. ${p => p.expanded && `border-bottom: 1px solid ${p.theme.innerBorder}`};
  151. ${MergedGroup} {
  152. &:first-child & {
  153. border-top: none;
  154. }
  155. &:last-child & {
  156. border-top: none;
  157. border-bottom: 1px solid ${p => p.theme.innerBorder};
  158. }
  159. }
  160. `;
  161. const FingerprintLabel = styled('label')`
  162. font-family: ${p => p.theme.text.familyMono};
  163. ${Controls} & {
  164. font-weight: 400;
  165. margin: 0;
  166. }
  167. `;
  168. const Collapse = styled('span')`
  169. cursor: pointer;
  170. `;
  171. const MergedEventList = styled('div')`
  172. overflow: hidden;
  173. border: none;
  174. background-color: ${p => p.theme.background};
  175. `;
  176. const EventDetails = styled('div')`
  177. display: flex;
  178. justify-content: space-between;
  179. .event-list & {
  180. padding: ${space(1)};
  181. }
  182. `;
  183. export default MergedItem;