mergedItem.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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. source="merged-item"
  123. />
  124. </EventDetails>
  125. )}
  126. </MergedEventList>
  127. )}
  128. </MergedGroup>
  129. );
  130. }
  131. }
  132. const MergedGroup = styled('div')<{busy: boolean}>`
  133. ${p => p.busy && 'opacity: 0.2'};
  134. `;
  135. const ActionWrapper = styled('div')`
  136. display: grid;
  137. grid-auto-flow: column;
  138. align-items: center;
  139. gap: ${space(1)};
  140. /* Can't use styled components for this because of broad selector */
  141. input[type='checkbox'] {
  142. margin: 0;
  143. }
  144. `;
  145. const Controls = styled('div')<{expanded: boolean}>`
  146. display: flex;
  147. justify-content: space-between;
  148. border-top: 1px solid ${p => p.theme.innerBorder};
  149. background-color: ${p => p.theme.backgroundSecondary};
  150. padding: ${space(0.5)} ${space(1)};
  151. ${p => p.expanded && `border-bottom: 1px solid ${p.theme.innerBorder}`};
  152. ${MergedGroup} {
  153. &:first-child & {
  154. border-top: none;
  155. }
  156. &:last-child & {
  157. border-top: none;
  158. border-bottom: 1px solid ${p => p.theme.innerBorder};
  159. }
  160. }
  161. `;
  162. const FingerprintLabel = styled('label')`
  163. font-family: ${p => p.theme.text.familyMono};
  164. ${Controls} & {
  165. font-weight: 400;
  166. margin: 0;
  167. }
  168. `;
  169. const Collapse = styled('span')`
  170. cursor: pointer;
  171. `;
  172. const MergedEventList = styled('div')`
  173. overflow: hidden;
  174. border: none;
  175. background-color: ${p => p.theme.background};
  176. `;
  177. const EventDetails = styled('div')`
  178. display: flex;
  179. justify-content: space-between;
  180. .event-list & {
  181. padding: ${space(1)};
  182. }
  183. `;
  184. export default MergedItem;