groupActivity.tsx 6.8 KB

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