compactIssue.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {bulkUpdate} from 'sentry/actionCreators/group';
  4. import {addLoadingMessage, clearIndicators} from 'sentry/actionCreators/indicator';
  5. import {Client} from 'sentry/api';
  6. import EventOrGroupTitle from 'sentry/components/eventOrGroupTitle';
  7. import ErrorLevel from 'sentry/components/events/errorLevel';
  8. import Link from 'sentry/components/links/link';
  9. import {PanelItem} from 'sentry/components/panels';
  10. import {IconChat, IconMute, IconStar} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import GroupStore from 'sentry/stores/groupStore';
  13. import {space} from 'sentry/styles/space';
  14. import {BaseGroup, Organization} from 'sentry/types';
  15. import {getMessage} from 'sentry/utils/events';
  16. import {Aliases} from 'sentry/utils/theme';
  17. import withApi from 'sentry/utils/withApi';
  18. import withOrganization from 'sentry/utils/withOrganization';
  19. type HeaderProps = {
  20. data: BaseGroup;
  21. organization: Organization;
  22. eventId?: string;
  23. };
  24. function CompactIssueHeader({data, organization, eventId}: HeaderProps) {
  25. const basePath = `/organizations/${organization.slug}/issues/`;
  26. const issueLink = eventId
  27. ? `${basePath}${data.id}/events/${eventId}/?referrer=compact-issue`
  28. : `${basePath}${data.id}/?referrer=compact-issue`;
  29. const commentColor: keyof Aliases =
  30. data.subscriptionDetails && data.subscriptionDetails.reason === 'mentioned'
  31. ? 'success'
  32. : 'textColor';
  33. return (
  34. <Fragment>
  35. <IssueHeaderMetaWrapper>
  36. <StyledErrorLevel size="12px" level={data.level} title={data.level} />
  37. <h3 className="truncate">
  38. <IconLink to={issueLink || ''}>
  39. {data.status === 'ignored' && <IconMute size="xs" />}
  40. {data.isBookmarked && <IconStar isSolid size="xs" />}
  41. <EventOrGroupTitle data={data} />
  42. </IconLink>
  43. </h3>
  44. </IssueHeaderMetaWrapper>
  45. <div className="event-extra">
  46. <span className="project-name">
  47. <strong>{data.project.slug}</strong>
  48. </span>
  49. {data.numComments !== 0 && (
  50. <span>
  51. <IconLink to={`${basePath}${data.id}/activity/`} className="comments">
  52. <IconChat size="xs" color={commentColor} />
  53. <span className="tag-count">{data.numComments}</span>
  54. </IconLink>
  55. </span>
  56. )}
  57. <span className="culprit">{getMessage(data)}</span>
  58. </div>
  59. </Fragment>
  60. );
  61. }
  62. type GroupTypes = ReturnType<typeof GroupStore.get>;
  63. /**
  64. * Type assertion to disambiguate GroupTypes
  65. *
  66. * The GroupCollapseRelease type isn't compatible with BaseGroup
  67. */
  68. function isGroup(maybe: GroupTypes): maybe is BaseGroup {
  69. return (maybe as BaseGroup).status !== undefined;
  70. }
  71. type Props = {
  72. api: Client;
  73. id: string;
  74. organization: Organization;
  75. children?: React.ReactNode;
  76. data?: BaseGroup;
  77. eventId?: string;
  78. };
  79. type State = {
  80. issue?: GroupTypes;
  81. };
  82. class CompactIssue extends Component<Props, State> {
  83. state: State = {
  84. issue: this.props.data || GroupStore.get(this.props.id),
  85. };
  86. UNSAFE_componentWillReceiveProps(nextProps: Props) {
  87. if (nextProps.id !== this.props.id) {
  88. this.setState({
  89. issue: GroupStore.get(this.props.id),
  90. });
  91. }
  92. }
  93. componentWillUnmount() {
  94. this.listener();
  95. }
  96. listener = GroupStore.listen(
  97. (itemIds: Set<string>) => this.onGroupChange(itemIds),
  98. undefined
  99. );
  100. onGroupChange(itemIds: Set<string>) {
  101. if (!itemIds.has(this.props.id)) {
  102. return;
  103. }
  104. const id = this.props.id;
  105. const issue = GroupStore.get(id);
  106. this.setState({
  107. issue,
  108. });
  109. }
  110. onUpdate(data: Record<string, string>) {
  111. const issue = this.state.issue;
  112. if (!issue) {
  113. return;
  114. }
  115. addLoadingMessage(t('Saving changes\u2026'));
  116. bulkUpdate(
  117. this.props.api,
  118. {
  119. orgId: this.props.organization.slug,
  120. projectId: issue.project.slug,
  121. itemIds: [issue.id],
  122. data,
  123. },
  124. {
  125. complete: () => {
  126. clearIndicators();
  127. },
  128. }
  129. );
  130. }
  131. render() {
  132. const issue = this.state.issue;
  133. const {organization} = this.props;
  134. if (!isGroup(issue)) {
  135. return null;
  136. }
  137. let className = 'issue';
  138. if (issue.isBookmarked) {
  139. className += ' isBookmarked';
  140. }
  141. if (issue.hasSeen) {
  142. className += ' hasSeen';
  143. }
  144. if (issue.status === 'resolved') {
  145. className += ' isResolved';
  146. }
  147. if (issue.status === 'ignored') {
  148. className += ' isIgnored';
  149. }
  150. return (
  151. <IssueRow className={className}>
  152. <CompactIssueHeader
  153. data={issue}
  154. organization={organization}
  155. eventId={this.props.eventId}
  156. />
  157. {this.props.children}
  158. </IssueRow>
  159. );
  160. }
  161. }
  162. export default withApi(withOrganization(CompactIssue));
  163. const IssueHeaderMetaWrapper = styled('div')`
  164. display: flex;
  165. align-items: center;
  166. `;
  167. const StyledErrorLevel = styled(ErrorLevel)`
  168. display: block;
  169. margin-right: ${space(1)};
  170. `;
  171. const IconLink = styled(Link)`
  172. & > svg {
  173. margin-right: ${space(0.5)};
  174. }
  175. `;
  176. const IssueRow = styled(PanelItem)`
  177. padding-top: ${space(1.5)};
  178. padding-bottom: ${space(0.75)};
  179. flex-direction: column;
  180. `;