eventAttachments.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import {Client} from 'app/api';
  5. import ImageViewer from 'app/components/events/attachmentViewers/imageViewer';
  6. import JsonViewer from 'app/components/events/attachmentViewers/jsonViewer';
  7. import LogFileViewer from 'app/components/events/attachmentViewers/logFileViewer';
  8. import RRWebJsonViewer from 'app/components/events/attachmentViewers/rrwebJsonViewer';
  9. import EventAttachmentActions from 'app/components/events/eventAttachmentActions';
  10. import EventDataSection from 'app/components/events/eventDataSection';
  11. import FileSize from 'app/components/fileSize';
  12. import {PanelTable} from 'app/components/panels';
  13. import {t} from 'app/locale';
  14. import overflowEllipsis from 'app/styles/overflowEllipsis';
  15. import {EventAttachment} from 'app/types';
  16. import {Event} from 'app/types/event';
  17. import AttachmentUrl from 'app/utils/attachmentUrl';
  18. import withApi from 'app/utils/withApi';
  19. import EventAttachmentsCrashReportsNotice from './eventAttachmentsCrashReportsNotice';
  20. type Props = {
  21. api: Client;
  22. event: Event;
  23. orgId: string;
  24. projectId: string;
  25. location: Location;
  26. };
  27. type State = {
  28. attachmentList: EventAttachment[];
  29. attachmentPreviews: Record<string, boolean>;
  30. expanded: boolean;
  31. };
  32. class EventAttachments extends React.Component<Props, State> {
  33. state: State = {
  34. attachmentList: [],
  35. expanded: false,
  36. attachmentPreviews: {},
  37. };
  38. componentDidMount() {
  39. this.fetchData();
  40. }
  41. componentDidUpdate(prevProps: Props) {
  42. let doFetch = false;
  43. if (!prevProps.event && this.props.event) {
  44. // going from having no event to having an event
  45. doFetch = true;
  46. } else if (this.props.event && this.props.event.id !== prevProps.event.id) {
  47. doFetch = true;
  48. }
  49. if (doFetch) {
  50. this.fetchData();
  51. }
  52. }
  53. // TODO(dcramer): this API request happens twice, and we need a store for it
  54. async fetchData() {
  55. const {event} = this.props;
  56. if (!event) {
  57. return;
  58. }
  59. try {
  60. const data = await this.props.api.requestPromise(
  61. `/projects/${this.props.orgId}/${this.props.projectId}/events/${event.id}/attachments/`
  62. );
  63. this.setState({
  64. attachmentList: data,
  65. });
  66. } catch (_err) {
  67. // TODO: Error-handling
  68. this.setState({
  69. attachmentList: [],
  70. });
  71. }
  72. }
  73. handleDelete = async (deletedAttachmentId: string) => {
  74. this.setState(prevState => ({
  75. attachmentList: prevState.attachmentList.filter(
  76. attachment => attachment.id !== deletedAttachmentId
  77. ),
  78. }));
  79. };
  80. getInlineAttachmentRenderer(attachment: EventAttachment) {
  81. switch (attachment.mimetype) {
  82. case 'text/plain':
  83. return attachment.size > 0 ? LogFileViewer : undefined;
  84. case 'text/json':
  85. case 'text/x-json':
  86. case 'application/json':
  87. if (attachment.name === 'rrweb.json') {
  88. return RRWebJsonViewer;
  89. }
  90. return JsonViewer;
  91. case 'image/jpeg':
  92. case 'image/png':
  93. case 'image/gif':
  94. return ImageViewer;
  95. default:
  96. return undefined;
  97. }
  98. }
  99. hasInlineAttachmentRenderer(attachment: EventAttachment): boolean {
  100. return !!this.getInlineAttachmentRenderer(attachment);
  101. }
  102. attachmentPreviewIsOpen = (attachment: EventAttachment) => {
  103. return !!this.state.attachmentPreviews[attachment.id];
  104. };
  105. renderInlineAttachment(attachment: EventAttachment) {
  106. const Component = this.getInlineAttachmentRenderer(attachment);
  107. if (!Component || !this.attachmentPreviewIsOpen(attachment)) {
  108. return null;
  109. }
  110. return (
  111. <AttachmentPreviewWrapper>
  112. <Component
  113. orgId={this.props.orgId}
  114. projectId={this.props.projectId}
  115. event={this.props.event}
  116. attachment={attachment}
  117. />
  118. </AttachmentPreviewWrapper>
  119. );
  120. }
  121. togglePreview(attachment: EventAttachment) {
  122. this.setState(({attachmentPreviews}) => ({
  123. attachmentPreviews: {
  124. ...attachmentPreviews,
  125. [attachment.id]: !attachmentPreviews[attachment.id],
  126. },
  127. }));
  128. }
  129. render() {
  130. const {event, projectId, orgId, location} = this.props;
  131. const {attachmentList} = this.state;
  132. const crashFileStripped = event.metadata.stripped_crash;
  133. if (!attachmentList.length && !crashFileStripped) {
  134. return null;
  135. }
  136. const title = t('Attachments (%s)', attachmentList.length);
  137. const lastAttachmentPreviewed =
  138. attachmentList.length > 0 &&
  139. this.attachmentPreviewIsOpen(attachmentList[attachmentList.length - 1]);
  140. return (
  141. <EventDataSection type="attachments" title={title}>
  142. {crashFileStripped && (
  143. <EventAttachmentsCrashReportsNotice
  144. orgSlug={orgId}
  145. projectSlug={projectId}
  146. groupId={event.groupID!}
  147. location={location}
  148. />
  149. )}
  150. {attachmentList.length > 0 && (
  151. <StyledPanelTable
  152. headers={[
  153. <Name key="name">{t('File Name')}</Name>,
  154. <Size key="size">{t('Size')}</Size>,
  155. t('Actions'),
  156. ]}
  157. >
  158. {attachmentList.map(attachment => (
  159. <React.Fragment key={attachment.id}>
  160. <Name>{attachment.name}</Name>
  161. <Size>
  162. <FileSize bytes={attachment.size} />
  163. </Size>
  164. <AttachmentUrl
  165. projectId={projectId}
  166. eventId={event.id}
  167. attachment={attachment}
  168. >
  169. {url => (
  170. <div>
  171. <EventAttachmentActions
  172. url={url}
  173. onDelete={this.handleDelete}
  174. onPreview={_attachmentId => this.togglePreview(attachment)}
  175. withPreviewButton
  176. previewIsOpen={this.attachmentPreviewIsOpen(attachment)}
  177. hasPreview={this.hasInlineAttachmentRenderer(attachment)}
  178. attachmentId={attachment.id}
  179. />
  180. </div>
  181. )}
  182. </AttachmentUrl>
  183. {this.renderInlineAttachment(attachment)}
  184. {/* XXX: hack to deal with table grid borders */}
  185. {lastAttachmentPreviewed && (
  186. <React.Fragment>
  187. <div style={{display: 'none'}} />
  188. <div style={{display: 'none'}} />
  189. </React.Fragment>
  190. )}
  191. </React.Fragment>
  192. ))}
  193. </StyledPanelTable>
  194. )}
  195. </EventDataSection>
  196. );
  197. }
  198. }
  199. export default withApi<Props>(EventAttachments);
  200. const StyledPanelTable = styled(PanelTable)`
  201. grid-template-columns: 1fr auto auto;
  202. `;
  203. const Name = styled('div')`
  204. ${overflowEllipsis};
  205. `;
  206. const Size = styled('div')`
  207. text-align: right;
  208. `;
  209. const AttachmentPreviewWrapper = styled('div')`
  210. grid-column: auto / span 3;
  211. border: none;
  212. padding: 0;
  213. `;