compactIssue.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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. projectId: string;
  23. eventId?: string;
  24. };
  25. function CompactIssueHeader({data, organization, projectId, eventId}: HeaderProps) {
  26. const basePath = `/organizations/${organization.slug}/issues/`;
  27. const issueLink = eventId
  28. ? `/organizations/${organization.slug}/projects/${projectId}/events/${eventId}/?referrer=compact-issue`
  29. : `${basePath}${data.id}/?referrer=compact-issue`;
  30. const commentColor: keyof Aliases =
  31. data.subscriptionDetails && data.subscriptionDetails.reason === 'mentioned'
  32. ? 'success'
  33. : 'textColor';
  34. return (
  35. <Fragment>
  36. <IssueHeaderMetaWrapper>
  37. <StyledErrorLevel size="12px" level={data.level} title={data.level} />
  38. <h3 className="truncate">
  39. <IconLink to={issueLink || ''}>
  40. {data.status === 'ignored' && <IconMute size="xs" />}
  41. {data.isBookmarked && <IconStar isSolid size="xs" />}
  42. <EventOrGroupTitle data={data} />
  43. </IconLink>
  44. </h3>
  45. </IssueHeaderMetaWrapper>
  46. <div className="event-extra">
  47. <span className="project-name">
  48. <strong>{data.project.slug}</strong>
  49. </span>
  50. {data.numComments !== 0 && (
  51. <span>
  52. <IconLink to={`${basePath}${data.id}/activity/`} className="comments">
  53. <IconChat size="xs" color={commentColor} />
  54. <span className="tag-count">{data.numComments}</span>
  55. </IconLink>
  56. </span>
  57. )}
  58. <span className="culprit">{getMessage(data)}</span>
  59. </div>
  60. </Fragment>
  61. );
  62. }
  63. type GroupTypes = ReturnType<typeof GroupStore.get>;
  64. /**
  65. * Type assertion to disambiguate GroupTypes
  66. *
  67. * The GroupCollapseRelease type isn't compatible with BaseGroup
  68. */
  69. function isGroup(maybe: GroupTypes): maybe is BaseGroup {
  70. return (maybe as BaseGroup).status !== undefined;
  71. }
  72. type Props = {
  73. api: Client;
  74. id: string;
  75. organization: Organization;
  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. 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. projectId={issue.project.slug}
  156. eventId={this.props.eventId}
  157. />
  158. {this.props.children}
  159. </IssueRow>
  160. );
  161. }
  162. }
  163. export default withApi(withOrganization(CompactIssue));
  164. const IssueHeaderMetaWrapper = styled('div')`
  165. display: flex;
  166. align-items: center;
  167. `;
  168. const StyledErrorLevel = styled(ErrorLevel)`
  169. display: block;
  170. margin-right: ${space(1)};
  171. `;
  172. const IconLink = styled(Link)`
  173. & > svg {
  174. margin-right: ${space(0.5)};
  175. }
  176. `;
  177. const IssueRow = styled(PanelItem)`
  178. padding-top: ${space(1.5)};
  179. padding-bottom: ${space(0.75)};
  180. flex-direction: column;
  181. `;