eventAttachments.tsx 5.9 KB

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