eventAttachments.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import AttachmentUrl from 'sentry/components/events/attachmentUrl';
  5. import ImageViewer from 'sentry/components/events/attachmentViewers/imageViewer';
  6. import JsonViewer from 'sentry/components/events/attachmentViewers/jsonViewer';
  7. import LogFileViewer from 'sentry/components/events/attachmentViewers/logFileViewer';
  8. import RRWebJsonViewer from 'sentry/components/events/attachmentViewers/rrwebJsonViewer';
  9. import EventAttachmentActions from 'sentry/components/events/eventAttachmentActions';
  10. import {EventDataSection} from 'sentry/components/events/eventDataSection';
  11. import FileSize from 'sentry/components/fileSize';
  12. import {PanelTable} from 'sentry/components/panels';
  13. import {t} from 'sentry/locale';
  14. import {IssueAttachment} from 'sentry/types';
  15. import {Event} from 'sentry/types/event';
  16. import EventAttachmentsCrashReportsNotice from './eventAttachmentsCrashReportsNotice';
  17. type Props = {
  18. attachments: IssueAttachment[];
  19. event: Event;
  20. location: Location;
  21. onDeleteAttachment: (attachmentId: IssueAttachment['id']) => void;
  22. orgId: string;
  23. projectId: string;
  24. };
  25. type State = {
  26. attachmentPreviews: Record<string, boolean>;
  27. expanded: boolean;
  28. };
  29. export class EventAttachments extends Component<Props, State> {
  30. state: State = {
  31. expanded: false,
  32. attachmentPreviews: {},
  33. };
  34. getInlineAttachmentRenderer(attachment: IssueAttachment) {
  35. switch (attachment.mimetype) {
  36. case 'text/plain':
  37. return attachment.size > 0 ? LogFileViewer : undefined;
  38. case 'text/json':
  39. case 'text/x-json':
  40. case 'application/json':
  41. if (attachment.name === 'rrweb.json' || attachment.name.startsWith('rrweb-')) {
  42. return RRWebJsonViewer;
  43. }
  44. return JsonViewer;
  45. case 'image/jpeg':
  46. case 'image/png':
  47. case 'image/gif':
  48. return ImageViewer;
  49. default:
  50. return undefined;
  51. }
  52. }
  53. hasInlineAttachmentRenderer(attachment: IssueAttachment): boolean {
  54. return !!this.getInlineAttachmentRenderer(attachment);
  55. }
  56. attachmentPreviewIsOpen = (attachment: IssueAttachment) => {
  57. return !!this.state.attachmentPreviews[attachment.id];
  58. };
  59. renderInlineAttachment(attachment: IssueAttachment) {
  60. const AttachmentComponent = this.getInlineAttachmentRenderer(attachment);
  61. if (!AttachmentComponent || !this.attachmentPreviewIsOpen(attachment)) {
  62. return null;
  63. }
  64. return (
  65. <AttachmentPreviewWrapper>
  66. <AttachmentComponent
  67. orgId={this.props.orgId}
  68. projectId={this.props.projectId}
  69. eventId={this.props.event.id}
  70. attachment={attachment}
  71. />
  72. </AttachmentPreviewWrapper>
  73. );
  74. }
  75. togglePreview(attachment: IssueAttachment) {
  76. this.setState(({attachmentPreviews}) => ({
  77. attachmentPreviews: {
  78. ...attachmentPreviews,
  79. [attachment.id]: !attachmentPreviews[attachment.id],
  80. },
  81. }));
  82. }
  83. render() {
  84. const {event, projectId, orgId, location, attachments, onDeleteAttachment} =
  85. this.props;
  86. const crashFileStripped = event.metadata.stripped_crash;
  87. if (!attachments.length && !crashFileStripped) {
  88. return null;
  89. }
  90. const title = t('Attachments (%s)', attachments.length);
  91. const lastAttachmentPreviewed =
  92. attachments.length > 0 &&
  93. this.attachmentPreviewIsOpen(attachments[attachments.length - 1]);
  94. return (
  95. <EventDataSection type="attachments" title={title}>
  96. {crashFileStripped && (
  97. <EventAttachmentsCrashReportsNotice
  98. orgSlug={orgId}
  99. projectSlug={projectId}
  100. groupId={event.groupID!}
  101. location={location}
  102. />
  103. )}
  104. {attachments.length > 0 && (
  105. <StyledPanelTable
  106. headers={[
  107. <Name key="name">{t('File Name')}</Name>,
  108. <Size key="size">{t('Size')}</Size>,
  109. t('Actions'),
  110. ]}
  111. >
  112. {attachments.map(attachment => (
  113. <Fragment key={attachment.id}>
  114. <Name>{attachment.name}</Name>
  115. <Size>
  116. <FileSize bytes={attachment.size} />
  117. </Size>
  118. <AttachmentUrl
  119. projectId={projectId}
  120. eventId={event.id}
  121. attachment={attachment}
  122. >
  123. {url => (
  124. <div>
  125. <EventAttachmentActions
  126. url={url}
  127. onDelete={onDeleteAttachment}
  128. onPreview={_attachmentId => this.togglePreview(attachment)}
  129. withPreviewButton
  130. previewIsOpen={this.attachmentPreviewIsOpen(attachment)}
  131. hasPreview={this.hasInlineAttachmentRenderer(attachment)}
  132. attachmentId={attachment.id}
  133. />
  134. </div>
  135. )}
  136. </AttachmentUrl>
  137. {this.renderInlineAttachment(attachment)}
  138. {/* XXX: hack to deal with table grid borders */}
  139. {lastAttachmentPreviewed && (
  140. <Fragment>
  141. <div style={{display: 'none'}} />
  142. <div style={{display: 'none'}} />
  143. </Fragment>
  144. )}
  145. </Fragment>
  146. ))}
  147. </StyledPanelTable>
  148. )}
  149. </EventDataSection>
  150. );
  151. }
  152. }
  153. export default EventAttachments;
  154. const StyledPanelTable = styled(PanelTable)`
  155. grid-template-columns: 1fr auto auto;
  156. `;
  157. const Name = styled('div')`
  158. ${p => p.theme.overflowEllipsis};
  159. `;
  160. const Size = styled('div')`
  161. text-align: right;
  162. `;
  163. const AttachmentPreviewWrapper = styled('div')`
  164. grid-column: auto / span 3;
  165. border: none;
  166. padding: 0;
  167. `;