compactIssue.tsx 5.3 KB

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