eventAttachments.tsx 5.9 KB

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