groupActivity.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import {Component, Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import uniq from 'lodash/uniq';
  5. import {createNote, deleteNote, updateNote} from 'sentry/actionCreators/group';
  6. import {
  7. addErrorMessage,
  8. addLoadingMessage,
  9. clearIndicators,
  10. } from 'sentry/actionCreators/indicator';
  11. import {Client} from 'sentry/api';
  12. import ActivityAuthor from 'sentry/components/activity/author';
  13. import ActivityItem from 'sentry/components/activity/item';
  14. import Note from 'sentry/components/activity/note';
  15. import NoteInputWithStorage from 'sentry/components/activity/note/inputWithStorage';
  16. import {CreateError} from 'sentry/components/activity/note/types';
  17. import ErrorBoundary from 'sentry/components/errorBoundary';
  18. import LoadingIndicator from 'sentry/components/loadingIndicator';
  19. import ReprocessedBox from 'sentry/components/reprocessedBox';
  20. import {DEFAULT_ERROR_JSON} from 'sentry/constants';
  21. import {t} from 'sentry/locale';
  22. import ConfigStore from 'sentry/stores/configStore';
  23. import space from 'sentry/styles/space';
  24. import {
  25. Group,
  26. GroupActivityAssigned,
  27. GroupActivityReprocess,
  28. GroupActivityType,
  29. Organization,
  30. User,
  31. } from 'sentry/types';
  32. import {uniqueId} from 'sentry/utils/guid';
  33. import Teams from 'sentry/utils/teams';
  34. import withApi from 'sentry/utils/withApi';
  35. import withOrganization from 'sentry/utils/withOrganization';
  36. import GroupActivityItem from './groupActivityItem';
  37. import {
  38. getGroupMostRecentActivity,
  39. getGroupReprocessingStatus,
  40. ReprocessingStatus,
  41. } from './utils';
  42. type Props = {
  43. api: Client;
  44. organization: Organization;
  45. group: Group;
  46. } & RouteComponentProps<{orgId: string}, {}>;
  47. type State = {
  48. createBusy: boolean;
  49. error: boolean;
  50. errorJSON: CreateError | null;
  51. inputId: string;
  52. };
  53. class GroupActivity extends Component<Props, State> {
  54. // TODO(dcramer): only re-render on group/activity change
  55. state: State = {
  56. createBusy: false,
  57. error: false,
  58. errorJSON: null,
  59. inputId: uniqueId(),
  60. };
  61. handleNoteDelete = async ({modelId, text: oldText}) => {
  62. const {api, group} = this.props;
  63. addLoadingMessage(t('Removing comment...'));
  64. try {
  65. await deleteNote(api, group, modelId, oldText);
  66. clearIndicators();
  67. } catch (_err) {
  68. addErrorMessage(t('Failed to delete comment'));
  69. }
  70. };
  71. /**
  72. * Note: This is nearly the same logic as `app/views/alerts/details/activity`
  73. * This can be abstracted a bit if we create more objects that can have activities
  74. */
  75. handleNoteCreate = async note => {
  76. const {api, group} = this.props;
  77. this.setState({
  78. createBusy: true,
  79. });
  80. addLoadingMessage(t('Posting comment...'));
  81. try {
  82. await createNote(api, group, note);
  83. this.setState({
  84. createBusy: false,
  85. // This is used as a `key` to Note Input so that after successful post
  86. // we reset the value of the input
  87. inputId: uniqueId(),
  88. });
  89. clearIndicators();
  90. } catch (error) {
  91. this.setState({
  92. createBusy: false,
  93. error: true,
  94. errorJSON: error.responseJSON || DEFAULT_ERROR_JSON,
  95. });
  96. addErrorMessage(t('Unable to post comment'));
  97. }
  98. };
  99. handleNoteUpdate = async (note, {modelId, text: oldText}) => {
  100. const {api, group} = this.props;
  101. addLoadingMessage(t('Updating comment...'));
  102. try {
  103. await updateNote(api, group, note, modelId, oldText);
  104. clearIndicators();
  105. } catch (error) {
  106. this.setState({
  107. error: true,
  108. errorJSON: error.responseJSON || DEFAULT_ERROR_JSON,
  109. });
  110. addErrorMessage(t('Unable to update comment'));
  111. }
  112. };
  113. render() {
  114. const {group, organization} = this.props;
  115. const {activity: activities, count, id: groupId} = group;
  116. const groupCount = Number(count);
  117. const mostRecentActivity = getGroupMostRecentActivity(activities);
  118. const reprocessingStatus = getGroupReprocessingStatus(group, mostRecentActivity);
  119. const me = ConfigStore.get('user');
  120. const projectSlugs = group && group.project ? [group.project.slug] : [];
  121. const noteProps = {
  122. minHeight: 140,
  123. group,
  124. projectSlugs,
  125. placeholder: t(
  126. 'Add details or updates to this event. \nTag users with @, or teams with #'
  127. ),
  128. };
  129. return (
  130. <Fragment>
  131. {(reprocessingStatus === ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT ||
  132. reprocessingStatus === ReprocessingStatus.REPROCESSED_AND_HAS_EVENT) && (
  133. <StyledReprocessedBox
  134. reprocessActivity={mostRecentActivity as GroupActivityReprocess}
  135. groupCount={groupCount}
  136. orgSlug={organization.slug}
  137. groupId={groupId}
  138. />
  139. )}
  140. <div className="row">
  141. <div className="col-md-9">
  142. <div>
  143. <ActivityItem author={{type: 'user', user: me}}>
  144. {() => (
  145. <NoteInputWithStorage
  146. key={this.state.inputId}
  147. storageKey="groupinput:latest"
  148. itemKey={group.id}
  149. onCreate={this.handleNoteCreate}
  150. busy={this.state.createBusy}
  151. error={this.state.error}
  152. errorJSON={this.state.errorJSON}
  153. {...noteProps}
  154. />
  155. )}
  156. </ActivityItem>
  157. <Teams
  158. ids={uniq(
  159. group.activity
  160. .filter(
  161. (item): item is GroupActivityAssigned =>
  162. item.type === GroupActivityType.ASSIGNED &&
  163. item.data.assigneeType === 'team' &&
  164. item.data.assignee?.length > 0
  165. )
  166. .map(item => item.data.assignee)
  167. )}
  168. >
  169. {({initiallyLoaded}) =>
  170. initiallyLoaded ? (
  171. group.activity.map(item => {
  172. const authorName = item.user ? item.user.name : 'Sentry';
  173. if (item.type === GroupActivityType.NOTE) {
  174. return (
  175. <ErrorBoundary mini key={`note-${item.id}`}>
  176. <Note
  177. showTime={false}
  178. text={item.data.text}
  179. modelId={item.id}
  180. user={item.user as User}
  181. dateCreated={item.dateCreated}
  182. authorName={authorName}
  183. onDelete={this.handleNoteDelete}
  184. onUpdate={this.handleNoteUpdate}
  185. {...noteProps}
  186. />
  187. </ErrorBoundary>
  188. );
  189. }
  190. return (
  191. <ErrorBoundary mini key={`item-${item.id}`}>
  192. <ActivityItem
  193. author={{
  194. type: item.user ? 'user' : 'system',
  195. user: item.user ?? undefined,
  196. }}
  197. date={item.dateCreated}
  198. header={
  199. <GroupActivityItem
  200. author={<ActivityAuthor>{authorName}</ActivityAuthor>}
  201. activity={item}
  202. orgSlug={this.props.params.orgId}
  203. projectId={group.project.id}
  204. />
  205. }
  206. />
  207. </ErrorBoundary>
  208. );
  209. })
  210. ) : (
  211. <LoadingIndicator />
  212. )
  213. }
  214. </Teams>
  215. </div>
  216. </div>
  217. </div>
  218. </Fragment>
  219. );
  220. }
  221. }
  222. export {GroupActivity};
  223. export default withApi(withOrganization(GroupActivity));
  224. const StyledReprocessedBox = styled(ReprocessedBox)`
  225. margin: -${space(3)} -${space(4)} ${space(4)} -${space(4)};
  226. z-index: 1;
  227. `;