Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions static/app/components/actions/actionLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function ActionLink({
children,
shouldConfirm,
confirmPriority,
header,
...props
}: Props) {
const actionCommonProps = {
Expand All @@ -74,6 +75,7 @@ export default function ActionLink({
priority={confirmPriority}
disabled={disabled}
message={message}
header={header}
confirmText={confirmLabel}
onConfirm={onAction}
stopPropagation={disabled}
Expand Down
4 changes: 3 additions & 1 deletion static/app/components/actions/confirmableAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ type ConfirmProps = React.ComponentProps<typeof Confirm>;
type Props = {
children: React.ReactNode | ConfirmProps['children'];
shouldConfirm?: boolean;
} & Partial<Pick<ConfirmProps, 'confirmText' | 'priority' | 'stopPropagation'>> &
} & Partial<
Pick<ConfirmProps, 'confirmText' | 'priority' | 'stopPropagation' | 'header'>
> &
Pick<ConfirmProps, 'message' | 'disabled' | 'confirmText' | 'onConfirm'>;

export default function ConfirmableAction({shouldConfirm, children, ...props}: Props) {
Expand Down
7 changes: 6 additions & 1 deletion static/app/components/events/contexts/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export function getSourcePlugin(pluginContexts: Array<any>, contextType: string)

export function getRelativeTimeFromEventDateCreated(
eventDateCreated: string,
timestamp?: string
timestamp?: string,
showTimestamp = true
) {
if (!defined(timestamp)) {
return timestamp;
Expand All @@ -56,6 +57,10 @@ export function getRelativeTimeFromEventDateCreated(
'before this event'
)})`;

if (!showTimestamp) {
return <RelativeTime>{relativeTime}</RelativeTime>;
}

return (
<Fragment>
{timestamp}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ const Title = styled('h3')`
const StyledEventDataSection = styled(EventDataSection)`
${SectionContents} {
flex: 1;
overflow: hidden;
}

@media (min-width: ${p => p.theme.breakpoints[0]}) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled from '@emotion/styled';

import ImageViewer from 'app/components/events/attachmentViewers/imageViewer';

const ImageVisualization = styled(ImageViewer)`
padding: 0;
height: 100%;
img {
width: auto;
height: 100%;
object-fit: cover;
flex: 1;
}
`;

export default ImageVisualization;
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import {Fragment, useEffect, useState} from 'react';
import styled from '@emotion/styled';

import {openModal} from 'app/actionCreators/modal';
import {Client} from 'app/api';
import Role from 'app/components/acl/role';
import MenuItemActionLink from 'app/components/actions/menuItemActionLink';
import Button from 'app/components/button';
import ButtonBar from 'app/components/buttonBar';
import DropdownLink from 'app/components/dropdownLink';
import ImageViewer from 'app/components/events/attachmentViewers/imageViewer';
import LoadingIndicator from 'app/components/loadingIndicator';
import {Panel, PanelBody, PanelFooter} from 'app/components/panels';
import {IconDownload, IconEllipsis} from 'app/icons';
import {IconEllipsis} from 'app/icons';
import {t} from 'app/locale';
import space from 'app/styles/space';
import {EventAttachment, Organization, Project} from 'app/types';
Expand All @@ -19,8 +18,8 @@ import withApi from 'app/utils/withApi';

import DataSection from '../dataSection';

import EmptyState from './emptyState';
import {platformsMobileWithAttachmentsFeature} from './utils';
import ImageVisualization from './imageVisualization';
import Modal, {modalCss} from './modal';

type Props = {
event: Event;
Expand All @@ -33,7 +32,6 @@ function Screenshot({event, api, organization, projectSlug}: Props) {
const [attachments, setAttachments] = useState<EventAttachment[]>([]);
const [isLoading, setIsLoading] = useState(false);
const orgSlug = organization.slug;
const eventPlatform = event.platform;

useEffect(() => {
fetchData();
Expand All @@ -59,43 +57,71 @@ function Screenshot({event, api, organization, projectSlug}: Props) {
}
}

function hasPreview(attachment: EventAttachment) {
switch (attachment.mimetype) {
case 'image/jpeg':
case 'image/png':
case 'image/gif':
return true;
default:
return false;
}
function hasScreenshot(attachment: EventAttachment) {
const {mimetype} = attachment;
return mimetype === 'image/jpeg' || mimetype === 'image/png';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we want to have name === 'screenshot' here in the future

}

function renderContent() {
if (isLoading) {
return <LoadingIndicator mini />;
}

const firstAttachmenteWithPreview = attachments.find(hasPreview);
async function handleDelete(screenshotAttachmentId: string, downloadUrl: string) {
try {
await api.requestPromise(downloadUrl.split('/api/0')[1], {
method: 'DELETE',
});

if (!firstAttachmenteWithPreview) {
return <EmptyState platform={eventPlatform} />;
setAttachments(
attachments.filter(attachment => attachment.id !== screenshotAttachmentId)
);
} catch (_err) {
// TODO: Error-handling
}
}

const downloadUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${event.id}/attachments/${firstAttachmenteWithPreview.id}/`;
function handleOpenVisualizationModal(
eventAttachment: EventAttachment,
downloadUrl: string
) {
openModal(
modalProps => (
<Modal
{...modalProps}
event={event}
orgSlug={orgSlug}
projectSlug={projectSlug}
eventAttachment={eventAttachment}
downloadUrl={downloadUrl}
onDelete={() => handleDelete(eventAttachment.id, downloadUrl)}
/>
),
{modalCss}
);
}

function renderContent(screenshotAttachment: EventAttachment) {
const downloadUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${event.id}/attachments/${screenshotAttachment.id}/`;

return (
<Fragment>
<StyledPanelBody>
<StyledImageViewer
attachment={firstAttachmenteWithPreview}
<ImageVisualization
attachment={screenshotAttachment}
orgId={orgSlug}
projectId={projectSlug}
event={event}
/>
</StyledPanelBody>
<StyledPanelFooter>
<StyledButtonbar gap={1}>
<Button size="xsmall">{t('View screenshot')}</Button>
<Button
size="xsmall"
onClick={() =>
handleOpenVisualizationModal(
screenshotAttachment,
`${downloadUrl}?download=1`
)
}
>
{t('View screenshot')}
</Button>
<DropdownLink
caret={false}
customTitle={
Expand All @@ -109,43 +135,46 @@ function Screenshot({event, api, organization, projectSlug}: Props) {
>
<MenuItemActionLink
shouldConfirm={false}
icon={<IconDownload size="xs" />}
title={t('Download')}
href={`${downloadUrl}?download=1`}
>
{t('Download')}
</MenuItemActionLink>
<MenuItemActionLink
shouldConfirm
title={t('Delete')}
onAction={() => handleDelete(screenshotAttachment.id, downloadUrl)}
header={t(
'Screenshots help identify what the user saw when the event happened'
)}
message={t('Are you sure you wish to delete this screenshot?')}
>
{t('Delete')}
</MenuItemActionLink>
</DropdownLink>
</StyledButtonbar>
</StyledPanelFooter>
</Fragment>
);
}

// the UI should only render the screenshots feature in events with platforms that support screenshots
if (
!eventPlatform ||
!platformsMobileWithAttachmentsFeature.includes(eventPlatform as any)
) {
return null;
}

return (
<Role role={organization.attachmentsRole}>
{({hasRole}) => {
if (!hasRole) {
// if the user has no access to the attachments,
// the UI shall not display the screenshot section
const screenshotAttachment = attachments.find(hasScreenshot);

if (!hasRole || isLoading || !screenshotAttachment) {
return null;
}

return (
<DataSection
title={t('Screenshots')}
description={t(
'Screenshots help identify what the user saw when the exception happened'
'Screenshots help identify what the user saw when the event happened'
)}
>
<StyledPanel>{renderContent()}</StyledPanel>
<StyledPanel>{renderContent(screenshotAttachment)}</StyledPanel>
</DataSection>
);
}}
Expand All @@ -161,14 +190,19 @@ const StyledPanel = styled(Panel)`
justify-content: center;
align-items: center;
margin-bottom: 0;
min-width: 175px;
min-height: 200px;
min-width: 175px;
`;

const StyledPanelBody = styled(PanelBody)`
height: 175px;
width: 100%;
overflow: hidden;
border: 1px solid ${p => p.theme.border};
border-radius: ${p => p.theme.borderRadius};
margin: -1px;
width: calc(100% + 2px);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
`;

const StyledPanelFooter = styled(PanelFooter)`
Expand All @@ -182,14 +216,3 @@ const StyledButtonbar = styled(ButtonBar)`
height: 24px;
}
`;

const StyledImageViewer = styled(ImageViewer)`
padding: 0;
height: 100%;
img {
width: auto;
height: 100%;
object-fit: cover;
flex: 1;
}
`;
Loading