|
@@ -1,6 +1,5 @@
|
|
|
-import {Component, Fragment} from 'react';
|
|
|
+import {Fragment, useState} from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
-import {Location} from 'history';
|
|
|
|
|
|
import AttachmentUrl from 'sentry/components/events/attachmentUrl';
|
|
|
import ImageViewer from 'sentry/components/events/attachmentViewers/imageViewer';
|
|
@@ -14,160 +13,176 @@ import {PanelTable} from 'sentry/components/panels';
|
|
|
import {t} from 'sentry/locale';
|
|
|
import {IssueAttachment} from 'sentry/types';
|
|
|
import {Event} from 'sentry/types/event';
|
|
|
+import {useLocation} from 'sentry/utils/useLocation';
|
|
|
+import useOrganization from 'sentry/utils/useOrganization';
|
|
|
|
|
|
import EventAttachmentsCrashReportsNotice from './eventAttachmentsCrashReportsNotice';
|
|
|
|
|
|
-type Props = {
|
|
|
+type EventAttachmentsProps = {
|
|
|
attachments: IssueAttachment[];
|
|
|
event: Event;
|
|
|
- location: Location;
|
|
|
onDeleteAttachment: (attachmentId: IssueAttachment['id']) => void;
|
|
|
- orgId: string;
|
|
|
projectSlug: string;
|
|
|
};
|
|
|
|
|
|
-type State = {
|
|
|
- attachmentPreviews: Record<string, boolean>;
|
|
|
- expanded: boolean;
|
|
|
-};
|
|
|
+type AttachmentPreviewOpenMap = Record<string, boolean>;
|
|
|
|
|
|
-export class EventAttachments extends Component<Props, State> {
|
|
|
- state: State = {
|
|
|
- expanded: false,
|
|
|
- attachmentPreviews: {},
|
|
|
- };
|
|
|
+interface InlineAttachmentsProps
|
|
|
+ extends Pick<EventAttachmentsProps, 'event' | 'projectSlug'> {
|
|
|
+ attachment: IssueAttachment;
|
|
|
+ attachmentPreviews: AttachmentPreviewOpenMap;
|
|
|
+}
|
|
|
|
|
|
- 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' || attachment.name.startsWith('rrweb-')) {
|
|
|
- return RRWebJsonViewer;
|
|
|
- }
|
|
|
- return JsonViewer;
|
|
|
- case 'image/jpeg':
|
|
|
- case 'image/png':
|
|
|
- case 'image/gif':
|
|
|
- return ImageViewer;
|
|
|
- default:
|
|
|
- return undefined;
|
|
|
- }
|
|
|
+const 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' || attachment.name.startsWith('rrweb-')) {
|
|
|
+ return RRWebJsonViewer;
|
|
|
+ }
|
|
|
+ return JsonViewer;
|
|
|
+ case 'image/jpeg':
|
|
|
+ case 'image/png':
|
|
|
+ case 'image/gif':
|
|
|
+ return ImageViewer;
|
|
|
+ default:
|
|
|
+ return undefined;
|
|
|
}
|
|
|
+};
|
|
|
+
|
|
|
+const hasInlineAttachmentRenderer = (attachment: IssueAttachment): boolean => {
|
|
|
+ return !!getInlineAttachmentRenderer(attachment);
|
|
|
+};
|
|
|
+
|
|
|
+const attachmentPreviewIsOpen = (
|
|
|
+ attachmentPreviews: Record<string, boolean>,
|
|
|
+ attachment: IssueAttachment
|
|
|
+) => {
|
|
|
+ return attachmentPreviews[attachment.id] === true;
|
|
|
+};
|
|
|
|
|
|
- hasInlineAttachmentRenderer(attachment: IssueAttachment): boolean {
|
|
|
- return !!this.getInlineAttachmentRenderer(attachment);
|
|
|
+const InlineEventAttachment = ({
|
|
|
+ attachmentPreviews,
|
|
|
+ attachment,
|
|
|
+ projectSlug,
|
|
|
+ event,
|
|
|
+}: InlineAttachmentsProps) => {
|
|
|
+ const organization = useOrganization();
|
|
|
+ const AttachmentComponent = getInlineAttachmentRenderer(attachment);
|
|
|
+
|
|
|
+ if (!AttachmentComponent || !attachmentPreviewIsOpen(attachmentPreviews, attachment)) {
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
- attachmentPreviewIsOpen = (attachment: IssueAttachment) => {
|
|
|
- return !!this.state.attachmentPreviews[attachment.id];
|
|
|
- };
|
|
|
+ return (
|
|
|
+ <AttachmentPreviewWrapper>
|
|
|
+ <AttachmentComponent
|
|
|
+ orgId={organization.slug}
|
|
|
+ projectSlug={projectSlug}
|
|
|
+ eventId={event.id}
|
|
|
+ attachment={attachment}
|
|
|
+ />
|
|
|
+ </AttachmentPreviewWrapper>
|
|
|
+ );
|
|
|
+};
|
|
|
|
|
|
- renderInlineAttachment(attachment: IssueAttachment) {
|
|
|
- const AttachmentComponent = this.getInlineAttachmentRenderer(attachment);
|
|
|
- if (!AttachmentComponent || !this.attachmentPreviewIsOpen(attachment)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- return (
|
|
|
- <AttachmentPreviewWrapper>
|
|
|
- <AttachmentComponent
|
|
|
- orgId={this.props.orgId}
|
|
|
- projectSlug={this.props.projectSlug}
|
|
|
- eventId={this.props.event.id}
|
|
|
- attachment={attachment}
|
|
|
- />
|
|
|
- </AttachmentPreviewWrapper>
|
|
|
- );
|
|
|
+export const EventAttachments = ({
|
|
|
+ attachments,
|
|
|
+ event,
|
|
|
+ onDeleteAttachment,
|
|
|
+ projectSlug,
|
|
|
+}: EventAttachmentsProps) => {
|
|
|
+ const organization = useOrganization();
|
|
|
+ const location = useLocation();
|
|
|
+ const [attachmentPreviews, setAttachmentPreviews] = useState<AttachmentPreviewOpenMap>(
|
|
|
+ {}
|
|
|
+ );
|
|
|
+ const crashFileStripped = event.metadata.stripped_crash;
|
|
|
+
|
|
|
+ if (!attachments.length && !crashFileStripped) {
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
- togglePreview(attachment: IssueAttachment) {
|
|
|
- this.setState(({attachmentPreviews}) => ({
|
|
|
- attachmentPreviews: {
|
|
|
- ...attachmentPreviews,
|
|
|
- [attachment.id]: !attachmentPreviews[attachment.id],
|
|
|
- },
|
|
|
+ const title = t('Attachments (%s)', attachments.length);
|
|
|
+
|
|
|
+ const lastAttachment = attachments.at(-1);
|
|
|
+ const lastAttachmentPreviewed =
|
|
|
+ lastAttachment && attachmentPreviewIsOpen(attachmentPreviews, lastAttachment);
|
|
|
+
|
|
|
+ const togglePreview = (attachment: IssueAttachment) => {
|
|
|
+ setAttachmentPreviews(previewsMap => ({
|
|
|
+ ...previewsMap,
|
|
|
+ [attachment.id]: !previewsMap[attachment.id],
|
|
|
}));
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
- render() {
|
|
|
- const {event, projectSlug, 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 (
|
|
|
- <EventDataSection type="attachments" title={title}>
|
|
|
- {crashFileStripped && (
|
|
|
- <EventAttachmentsCrashReportsNotice
|
|
|
- orgSlug={orgId}
|
|
|
- projectSlug={projectSlug}
|
|
|
- groupId={event.groupID!}
|
|
|
- location={location}
|
|
|
- />
|
|
|
- )}
|
|
|
-
|
|
|
- {attachments.length > 0 && (
|
|
|
- <StyledPanelTable
|
|
|
- headers={[
|
|
|
- <Name key="name">{t('File Name')}</Name>,
|
|
|
- <Size key="size">{t('Size')}</Size>,
|
|
|
- t('Actions'),
|
|
|
- ]}
|
|
|
- >
|
|
|
- {attachments.map(attachment => (
|
|
|
- <Fragment key={attachment.id}>
|
|
|
- <Name>{attachment.name}</Name>
|
|
|
- <Size>
|
|
|
- <FileSize bytes={attachment.size} />
|
|
|
- </Size>
|
|
|
- <AttachmentUrl
|
|
|
- projectSlug={projectSlug}
|
|
|
- eventId={event.id}
|
|
|
- attachment={attachment}
|
|
|
- >
|
|
|
- {url => (
|
|
|
- <div>
|
|
|
- <EventAttachmentActions
|
|
|
- url={url}
|
|
|
- onDelete={onDeleteAttachment}
|
|
|
- onPreview={_attachmentId => this.togglePreview(attachment)}
|
|
|
- withPreviewButton
|
|
|
- previewIsOpen={this.attachmentPreviewIsOpen(attachment)}
|
|
|
- hasPreview={this.hasInlineAttachmentRenderer(attachment)}
|
|
|
- attachmentId={attachment.id}
|
|
|
- />
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </AttachmentUrl>
|
|
|
- {this.renderInlineAttachment(attachment)}
|
|
|
- {/* XXX: hack to deal with table grid borders */}
|
|
|
- {lastAttachmentPreviewed && (
|
|
|
- <Fragment>
|
|
|
- <div style={{display: 'none'}} />
|
|
|
- <div style={{display: 'none'}} />
|
|
|
- </Fragment>
|
|
|
+ return (
|
|
|
+ <EventDataSection type="attachments" title={title}>
|
|
|
+ {crashFileStripped && (
|
|
|
+ <EventAttachmentsCrashReportsNotice
|
|
|
+ orgSlug={organization.slug}
|
|
|
+ projectSlug={projectSlug}
|
|
|
+ groupId={event.groupID!}
|
|
|
+ location={location}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+
|
|
|
+ {attachments.length > 0 && (
|
|
|
+ <StyledPanelTable
|
|
|
+ headers={[
|
|
|
+ <Name key="name">{t('File Name')}</Name>,
|
|
|
+ <Size key="size">{t('Size')}</Size>,
|
|
|
+ t('Actions'),
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ {attachments.map(attachment => (
|
|
|
+ <Fragment key={attachment.id}>
|
|
|
+ <Name>{attachment.name}</Name>
|
|
|
+ <Size>
|
|
|
+ <FileSize bytes={attachment.size} />
|
|
|
+ </Size>
|
|
|
+ <AttachmentUrl
|
|
|
+ projectSlug={projectSlug}
|
|
|
+ eventId={event.id}
|
|
|
+ attachment={attachment}
|
|
|
+ >
|
|
|
+ {url => (
|
|
|
+ <div>
|
|
|
+ <EventAttachmentActions
|
|
|
+ url={url}
|
|
|
+ onDelete={onDeleteAttachment}
|
|
|
+ onPreview={_attachmentId => togglePreview(attachment)}
|
|
|
+ withPreviewButton
|
|
|
+ previewIsOpen={attachmentPreviewIsOpen(
|
|
|
+ attachmentPreviews,
|
|
|
+ attachment
|
|
|
+ )}
|
|
|
+ hasPreview={hasInlineAttachmentRenderer(attachment)}
|
|
|
+ attachmentId={attachment.id}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
)}
|
|
|
- </Fragment>
|
|
|
- ))}
|
|
|
- </StyledPanelTable>
|
|
|
- )}
|
|
|
- </EventDataSection>
|
|
|
- );
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export default EventAttachments;
|
|
|
+ </AttachmentUrl>
|
|
|
+ <InlineEventAttachment
|
|
|
+ {...{attachment, attachmentPreviews, event, projectSlug}}
|
|
|
+ />
|
|
|
+ {/* XXX: hack to deal with table grid borders */}
|
|
|
+ {lastAttachmentPreviewed && (
|
|
|
+ <Fragment>
|
|
|
+ <div style={{display: 'none'}} />
|
|
|
+ <div style={{display: 'none'}} />
|
|
|
+ </Fragment>
|
|
|
+ )}
|
|
|
+ </Fragment>
|
|
|
+ ))}
|
|
|
+ </StyledPanelTable>
|
|
|
+ )}
|
|
|
+ </EventDataSection>
|
|
|
+ );
|
|
|
+};
|
|
|
|
|
|
const StyledPanelTable = styled(PanelTable)`
|
|
|
grid-template-columns: 1fr auto auto;
|