groupActivityItem.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import {Fragment} from 'react';
  2. import moment from 'moment';
  3. import CommitLink from 'sentry/components/commitLink';
  4. import Duration from 'sentry/components/duration';
  5. import ExternalLink from 'sentry/components/links/externalLink';
  6. import Link from 'sentry/components/links/link';
  7. import PullRequestLink from 'sentry/components/pullRequestLink';
  8. import Version from 'sentry/components/version';
  9. import {t, tct, tn} from 'sentry/locale';
  10. import TeamStore from 'sentry/stores/teamStore';
  11. import {
  12. GroupActivity,
  13. GroupActivityAssigned,
  14. GroupActivitySetIgnored,
  15. GroupActivityType,
  16. Organization,
  17. Project,
  18. User,
  19. } from 'sentry/types';
  20. type Props = {
  21. activity: GroupActivity;
  22. author: React.ReactNode;
  23. orgSlug: Organization['slug'];
  24. projectId: Project['id'];
  25. };
  26. function GroupActivityItem({activity, orgSlug, projectId, author}: Props) {
  27. const issuesLink = `/organizations/${orgSlug}/issues/`;
  28. function getIgnoredMessage(data: GroupActivitySetIgnored['data']) {
  29. if (data.ignoreDuration) {
  30. return tct('[author] ignored this issue for [duration]', {
  31. author,
  32. duration: <Duration seconds={data.ignoreDuration * 60} />,
  33. });
  34. }
  35. if (data.ignoreCount && data.ignoreWindow) {
  36. return tct(
  37. '[author] ignored this issue until it happens [count] time(s) in [duration]',
  38. {
  39. author,
  40. count: data.ignoreCount,
  41. duration: <Duration seconds={data.ignoreWindow * 60} />,
  42. }
  43. );
  44. }
  45. if (data.ignoreCount) {
  46. return tct('[author] ignored this issue until it happens [count] time(s)', {
  47. author,
  48. count: data.ignoreCount,
  49. });
  50. }
  51. if (data.ignoreUserCount && data.ignoreUserWindow) {
  52. return tct(
  53. '[author] ignored this issue until it affects [count] user(s) in [duration]',
  54. {
  55. author,
  56. count: data.ignoreUserCount,
  57. duration: <Duration seconds={data.ignoreUserWindow * 60} />,
  58. }
  59. );
  60. }
  61. if (data.ignoreUserCount) {
  62. return tct('[author] ignored this issue until it affects [count] user(s)', {
  63. author,
  64. count: data.ignoreUserCount,
  65. });
  66. }
  67. return tct('[author] ignored this issue', {author});
  68. }
  69. function getAssignedMessage(data: GroupActivityAssigned['data']) {
  70. let assignee: string | User | undefined = undefined;
  71. if (data.assigneeType === 'team') {
  72. const team = TeamStore.getById(data.assignee);
  73. assignee = team ? team.slug : '<unknown-team>';
  74. return tct('[author] assigned this issue to #[assignee]', {
  75. author,
  76. assignee,
  77. });
  78. }
  79. if (activity.user && data.assignee === activity.user.id) {
  80. return tct('[author] assigned this issue to themselves', {author});
  81. }
  82. if (data.assigneeType === 'user' && data.assigneeEmail) {
  83. return tct('[author] assigned this issue to [assignee]', {
  84. author,
  85. assignee: data.assigneeEmail,
  86. });
  87. }
  88. return tct('[author] assigned this issue to an unknown user', {author});
  89. }
  90. function renderContent() {
  91. switch (activity.type) {
  92. case GroupActivityType.NOTE:
  93. return tct('[author] left a comment', {author});
  94. case GroupActivityType.SET_RESOLVED:
  95. return tct('[author] marked this issue as resolved', {author});
  96. case GroupActivityType.SET_RESOLVED_BY_AGE:
  97. return tct('[author] marked this issue as resolved due to inactivity', {
  98. author,
  99. });
  100. case GroupActivityType.SET_RESOLVED_IN_RELEASE:
  101. const {current_release_version, version} = activity.data;
  102. if (current_release_version) {
  103. return tct(
  104. '[author] marked this issue as resolved in releases greater than [version]',
  105. {
  106. author,
  107. version: (
  108. <Version
  109. version={current_release_version}
  110. projectId={projectId}
  111. tooltipRawVersion
  112. />
  113. ),
  114. }
  115. );
  116. }
  117. return version
  118. ? tct('[author] marked this issue as resolved in [version]', {
  119. author,
  120. version: (
  121. <Version version={version} projectId={projectId} tooltipRawVersion />
  122. ),
  123. })
  124. : tct('[author] marked this issue as resolved in the upcoming release', {
  125. author,
  126. });
  127. case GroupActivityType.SET_RESOLVED_IN_COMMIT:
  128. const deployedReleases = (activity.data.commit?.releases || [])
  129. .filter(r => r.dateReleased !== null)
  130. .sort(
  131. (a, b) => moment(a.dateReleased).valueOf() - moment(b.dateReleased).valueOf()
  132. );
  133. if (deployedReleases.length === 1) {
  134. return tct(
  135. '[author] marked this issue as resolved in [version] [break]' +
  136. 'This commit was released in [release]',
  137. {
  138. author,
  139. version: (
  140. <CommitLink
  141. inline
  142. commitId={activity.data.commit.id}
  143. repository={activity.data.commit.repository}
  144. />
  145. ),
  146. break: <br />,
  147. release: (
  148. <Version
  149. version={deployedReleases[0].version}
  150. projectId={projectId}
  151. tooltipRawVersion
  152. />
  153. ),
  154. }
  155. );
  156. }
  157. if (deployedReleases.length > 1) {
  158. return tct(
  159. '[author] marked this issue as resolved in [version] [break]' +
  160. 'This commit was released in [release] and ' +
  161. (deployedReleases.length - 1) +
  162. ' others',
  163. {
  164. author,
  165. version: (
  166. <CommitLink
  167. inline
  168. commitId={activity.data.commit.id}
  169. repository={activity.data.commit.repository}
  170. />
  171. ),
  172. break: <br />,
  173. release: (
  174. <Version
  175. version={deployedReleases[0].version}
  176. projectId={projectId}
  177. tooltipRawVersion
  178. />
  179. ),
  180. }
  181. );
  182. }
  183. return tct('[author] marked this issue as resolved in [version]', {
  184. author,
  185. version: (
  186. <CommitLink
  187. inline
  188. commitId={activity.data.commit.id}
  189. repository={activity.data.commit.repository}
  190. />
  191. ),
  192. });
  193. case GroupActivityType.SET_RESOLVED_IN_PULL_REQUEST: {
  194. const {data} = activity;
  195. const {pullRequest} = data;
  196. return tct('[author] marked this issue as resolved in [version]', {
  197. author,
  198. version: (
  199. <PullRequestLink
  200. inline
  201. pullRequest={pullRequest}
  202. repository={pullRequest.repository}
  203. />
  204. ),
  205. });
  206. }
  207. case GroupActivityType.SET_UNRESOLVED:
  208. return tct('[author] marked this issue as unresolved', {author});
  209. case GroupActivityType.SET_IGNORED: {
  210. const {data} = activity;
  211. return getIgnoredMessage(data);
  212. }
  213. case GroupActivityType.SET_PUBLIC:
  214. return tct('[author] made this issue public', {author});
  215. case GroupActivityType.SET_PRIVATE:
  216. return tct('[author] made this issue private', {author});
  217. case GroupActivityType.SET_REGRESSION: {
  218. const {data} = activity;
  219. return data.version
  220. ? tct('[author] marked this issue as a regression in [version]', {
  221. author,
  222. version: (
  223. <Version version={data.version} projectId={projectId} tooltipRawVersion />
  224. ),
  225. })
  226. : tct('[author] marked this issue as a regression', {author});
  227. }
  228. case GroupActivityType.CREATE_ISSUE: {
  229. const {data} = activity;
  230. return tct('[author] created an issue on [provider] titled [title]', {
  231. author,
  232. provider: data.provider,
  233. title: <ExternalLink href={data.location}>{data.title}</ExternalLink>,
  234. });
  235. }
  236. case GroupActivityType.UNMERGE_SOURCE: {
  237. const {data} = activity;
  238. const {destination, fingerprints} = data;
  239. return tn(
  240. '%2$s migrated %1$s fingerprint to %3$s',
  241. '%2$s migrated %1$s fingerprints to %3$s',
  242. fingerprints.length,
  243. author,
  244. destination ? (
  245. <Link to={`${issuesLink}${destination.id}`}>{destination.shortId}</Link>
  246. ) : (
  247. t('a group')
  248. )
  249. );
  250. }
  251. case GroupActivityType.UNMERGE_DESTINATION: {
  252. const {data} = activity;
  253. const {source, fingerprints} = data;
  254. return tn(
  255. '%2$s migrated %1$s fingerprint from %3$s',
  256. '%2$s migrated %1$s fingerprints from %3$s',
  257. fingerprints.length,
  258. author,
  259. source ? (
  260. <Link to={`${issuesLink}${source.id}`}>{source.shortId}</Link>
  261. ) : (
  262. t('a group')
  263. )
  264. );
  265. }
  266. case GroupActivityType.FIRST_SEEN:
  267. return tct('[author] first saw this issue', {author});
  268. case GroupActivityType.ASSIGNED: {
  269. const {data} = activity;
  270. return getAssignedMessage(data);
  271. }
  272. case GroupActivityType.UNASSIGNED:
  273. return tct('[author] unassigned this issue', {author});
  274. case GroupActivityType.MERGE:
  275. return tn(
  276. '%2$s merged %1$s issue into this issue',
  277. '%2$s merged %1$s issues into this issue',
  278. activity.data.issues.length,
  279. author
  280. );
  281. case GroupActivityType.REPROCESS: {
  282. const {data} = activity;
  283. const {oldGroupId, eventCount} = data;
  284. return tct('[author] reprocessed the events in this issue. [new-events]', {
  285. author,
  286. ['new-events']: (
  287. <Link
  288. to={`/organizations/${orgSlug}/issues/?query=reprocessing.original_issue_id:${oldGroupId}`}
  289. >
  290. {tn('See %s new event', 'See %s new events', eventCount)}
  291. </Link>
  292. ),
  293. });
  294. }
  295. case GroupActivityType.MARK_REVIEWED: {
  296. return tct('[author] marked this issue as reviewed', {
  297. author,
  298. });
  299. }
  300. default:
  301. return ''; // should never hit (?)
  302. }
  303. }
  304. return <Fragment>{renderContent()}</Fragment>;
  305. }
  306. export default GroupActivityItem;