groupActivity.tsx 7.9 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 * as Layout from 'sentry/components/layouts/thirds';
  19. import LoadingIndicator from 'sentry/components/loadingIndicator';
  20. import ReprocessedBox from 'sentry/components/reprocessedBox';
  21. import {DEFAULT_ERROR_JSON} from 'sentry/constants';
  22. import {t} from 'sentry/locale';
  23. import ConfigStore from 'sentry/stores/configStore';
  24. import space from 'sentry/styles/space';
  25. import {
  26. Group,
  27. GroupActivityAssigned,
  28. GroupActivityReprocess,
  29. GroupActivityType,
  30. Organization,
  31. User,
  32. } from 'sentry/types';
  33. import {uniqueId} from 'sentry/utils/guid';
  34. import Teams from 'sentry/utils/teams';
  35. import withApi from 'sentry/utils/withApi';
  36. import withOrganization from 'sentry/utils/withOrganization';
  37. import GroupActivityItem from './groupActivityItem';
  38. import {
  39. getGroupMostRecentActivity,
  40. getGroupReprocessingStatus,
  41. ReprocessingStatus,
  42. } from './utils';
  43. type Props = {
  44. api: Client;
  45. group: Group;
  46. organization: Organization;
  47. } & RouteComponentProps<{orgId: string}, {}>;
  48. type State = {
  49. createBusy: boolean;
  50. error: boolean;
  51. errorJSON: CreateError | null;
  52. inputId: string;
  53. };
  54. class GroupActivity extends Component<Props, State> {
  55. // TODO(dcramer): only re-render on group/activity change
  56. state: State = {
  57. createBusy: false,
  58. error: false,
  59. errorJSON: null,
  60. inputId: uniqueId(),
  61. };
  62. handleNoteDelete = async ({modelId, text: oldText}) => {
  63. const {api, group} = this.props;
  64. addLoadingMessage(t('Removing comment...'));
  65. try {
  66. await deleteNote(api, group, modelId, oldText);
  67. clearIndicators();
  68. } catch (_err) {
  69. addErrorMessage(t('Failed to delete comment'));
  70. }
  71. };
  72. /**
  73. * Note: This is nearly the same logic as `app/views/alerts/details/activity`
  74. * This can be abstracted a bit if we create more objects that can have activities
  75. */
  76. handleNoteCreate = async note => {
  77. const {api, group} = this.props;
  78. this.setState({
  79. createBusy: true,
  80. });
  81. addLoadingMessage(t('Posting comment...'));
  82. try {
  83. await createNote(api, group, note);
  84. this.setState({
  85. createBusy: false,
  86. // This is used as a `key` to Note Input so that after successful post
  87. // we reset the value of the input
  88. inputId: uniqueId(),
  89. });
  90. clearIndicators();
  91. } catch (error) {
  92. this.setState({
  93. createBusy: false,
  94. error: true,
  95. errorJSON: error.responseJSON || DEFAULT_ERROR_JSON,
  96. });
  97. addErrorMessage(t('Unable to post comment'));
  98. }
  99. };
  100. handleNoteUpdate = async (note, {modelId, text: oldText}) => {
  101. const {api, group} = this.props;
  102. addLoadingMessage(t('Updating comment...'));
  103. try {
  104. await updateNote(api, group, note, modelId, oldText);
  105. clearIndicators();
  106. } catch (error) {
  107. this.setState({
  108. error: true,
  109. errorJSON: error.responseJSON || DEFAULT_ERROR_JSON,
  110. });
  111. addErrorMessage(t('Unable to update comment'));
  112. }
  113. };
  114. render() {
  115. const {group, organization} = this.props;
  116. const {activity: activities, count, id: groupId} = group;
  117. const groupCount = Number(count);
  118. const mostRecentActivity = getGroupMostRecentActivity(activities);
  119. const reprocessingStatus = getGroupReprocessingStatus(group, mostRecentActivity);
  120. const me = ConfigStore.get('user');
  121. const projectSlugs = group && group.project ? [group.project.slug] : [];
  122. const noteProps = {
  123. minHeight: 140,
  124. group,
  125. projectSlugs,
  126. placeholder: t(
  127. 'Add details or updates to this event. \nTag users with @, or teams with #'
  128. ),
  129. };
  130. return (
  131. <Fragment>
  132. {(reprocessingStatus === ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT ||
  133. reprocessingStatus === ReprocessingStatus.REPROCESSED_AND_HAS_EVENT) && (
  134. <StyledReprocessedBox
  135. reprocessActivity={mostRecentActivity as GroupActivityReprocess}
  136. groupCount={groupCount}
  137. orgSlug={organization.slug}
  138. groupId={groupId}
  139. />
  140. )}
  141. <Layout.Body>
  142. <Layout.Main>
  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. </Layout.Main>
  216. </Layout.Body>
  217. </Fragment>
  218. );
  219. }
  220. }
  221. export {GroupActivity};
  222. export default withApi(withOrganization(GroupActivity));
  223. const StyledReprocessedBox = styled(ReprocessedBox)`
  224. margin: -${space(3)} -${space(4)} ${space(4)} -${space(4)};
  225. z-index: 1;
  226. `;