import * as React from 'react'; import styled from '@emotion/styled'; import {Location} from 'history'; import {Client} from 'sentry/api'; import ImageViewer from 'sentry/components/events/attachmentViewers/imageViewer'; import JsonViewer from 'sentry/components/events/attachmentViewers/jsonViewer'; import LogFileViewer from 'sentry/components/events/attachmentViewers/logFileViewer'; import RRWebJsonViewer from 'sentry/components/events/attachmentViewers/rrwebJsonViewer'; import EventAttachmentActions from 'sentry/components/events/eventAttachmentActions'; import EventDataSection from 'sentry/components/events/eventDataSection'; import FileSize from 'sentry/components/fileSize'; import {PanelTable} from 'sentry/components/panels'; import {t} from 'sentry/locale'; import overflowEllipsis from 'sentry/styles/overflowEllipsis'; import {IssueAttachment} from 'sentry/types'; import {Event} from 'sentry/types/event'; import AttachmentUrl from 'sentry/utils/attachmentUrl'; import withApi from 'sentry/utils/withApi'; import EventAttachmentsCrashReportsNotice from './eventAttachmentsCrashReportsNotice'; type Props = { api: Client; event: Event; orgId: string; projectId: string; location: Location; attachments: IssueAttachment[]; onDeleteAttachment: (attachmentId: IssueAttachment['id']) => void; }; type State = { attachmentPreviews: Record; expanded: boolean; }; class EventAttachments extends React.Component { state: State = { expanded: false, attachmentPreviews: {}, }; getInlineAttachmentRenderer(attachment: IssueAttachment) { switch (attachment.mimetype) { case 'text/plain': return attachment.size > 0 ? LogFileViewer : undefined; case 'text/json': case 'text/x-json': case 'application/json': if (attachment.name === 'rrweb.json') { return RRWebJsonViewer; } return JsonViewer; case 'image/jpeg': case 'image/png': case 'image/gif': return ImageViewer; default: return undefined; } } hasInlineAttachmentRenderer(attachment: IssueAttachment): boolean { return !!this.getInlineAttachmentRenderer(attachment); } attachmentPreviewIsOpen = (attachment: IssueAttachment) => { return !!this.state.attachmentPreviews[attachment.id]; }; renderInlineAttachment(attachment: IssueAttachment) { const Component = this.getInlineAttachmentRenderer(attachment); if (!Component || !this.attachmentPreviewIsOpen(attachment)) { return null; } return ( ); } togglePreview(attachment: IssueAttachment) { this.setState(({attachmentPreviews}) => ({ attachmentPreviews: { ...attachmentPreviews, [attachment.id]: !attachmentPreviews[attachment.id], }, })); } render() { const {event, projectId, orgId, location, attachments, onDeleteAttachment} = this.props; const crashFileStripped = event.metadata.stripped_crash; if (!attachments.length && !crashFileStripped) { return null; } const title = t('Attachments (%s)', attachments.length); const lastAttachmentPreviewed = attachments.length > 0 && this.attachmentPreviewIsOpen(attachments[attachments.length - 1]); return ( {crashFileStripped && ( )} {attachments.length > 0 && ( {t('File Name')}, {t('Size')}, t('Actions'), ]} > {attachments.map(attachment => ( {attachment.name} {url => (
this.togglePreview(attachment)} withPreviewButton previewIsOpen={this.attachmentPreviewIsOpen(attachment)} hasPreview={this.hasInlineAttachmentRenderer(attachment)} attachmentId={attachment.id} />
)}
{this.renderInlineAttachment(attachment)} {/* XXX: hack to deal with table grid borders */} {lastAttachmentPreviewed && (
)} ))} )} ); } } export default withApi(EventAttachments); const StyledPanelTable = styled(PanelTable)` grid-template-columns: 1fr auto auto; `; const Name = styled('div')` ${overflowEllipsis}; `; const Size = styled('div')` text-align: right; `; const AttachmentPreviewWrapper = styled('div')` grid-column: auto / span 3; border: none; padding: 0; `;