Skip to content

Commit 511e6dd

Browse files
feat(issue-details): Add screenshot section (#27274)
1 parent 07fe05e commit 511e6dd

File tree

18 files changed

+540
-28
lines changed

18 files changed

+540
-28
lines changed

static/app/components/events/attachmentViewers/imageViewer.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import {
66
} from 'app/components/events/attachmentViewers/utils';
77
import {PanelItem} from 'app/components/panels';
88

9-
export default function ImageViewer(props: ViewerProps) {
9+
function ImageViewer({className, ...props}: ViewerProps) {
1010
return (
11-
<Container>
11+
<Container className={className}>
1212
<img src={getAttachmentUrl(props, true)} />
1313
</Container>
1414
);
1515
}
1616

17+
export default ImageViewer;
18+
1719
const Container = styled(PanelItem)`
1820
justify-content: center;
1921
`;

static/app/components/events/attachmentViewers/utils.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type ViewerProps = {
66
orgId: string;
77
projectId: string;
88
attachment: EventAttachment;
9+
className?: string;
910
};
1011

1112
export function getAttachmentUrl(props: ViewerProps, withPrefix?: boolean): string {

static/app/components/events/eventAttachments.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,6 @@ class EventAttachments extends React.Component<Props, State> {
205205
)}
206206
</AttachmentUrl>
207207
{this.renderInlineAttachment(attachment)}
208-
209208
{/* XXX: hack to deal with table grid borders */}
210209
{lastAttachmentPreviewed && (
211210
<React.Fragment>

static/app/components/events/eventDataSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ const defaultProps = {
2020
type DefaultProps = Readonly<typeof defaultProps>;
2121

2222
type Props = {
23-
className?: string;
2423
title: React.ReactNode;
2524
type: string;
2625
toggleRaw?: (enable: boolean) => void;
2726
actions?: React.ReactNode;
27+
className?: string;
2828
} & DefaultProps;
2929

3030
class EventDataSection extends React.Component<Props> {
@@ -191,7 +191,7 @@ const SectionHeader = styled('div')<{isCentered?: boolean}>`
191191
}
192192
`;
193193

194-
const SectionContents = styled('div')`
194+
export const SectionContents = styled('div')`
195195
position: relative;
196196
`;
197197

static/app/components/events/eventEntries.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {projectProcessingIssuesMessages} from 'app/views/settings/project/projec
4646
import findBestThread from './interfaces/threads/threadSelector/findBestThread';
4747
import getThreadException from './interfaces/threads/threadSelector/getThreadException';
4848
import EventEntry from './eventEntry';
49+
import EventAndScreenshot from './eventTagsAndScreenshot';
4950

5051
const MINIFIED_DATA_JAVA_EVENT_REGEX_MATCH =
5152
/^(([\w\$]\.[\w\$]{1,2})|([\w\$]{2}\.[\w\$]\.[\w\$]))(\.|$)/g;
@@ -54,6 +55,7 @@ const defaultProps = {
5455
isShare: false,
5556
showExampleCommit: false,
5657
showTagSummary: true,
58+
isBorderless: false,
5759
};
5860

5961
type ProGuardErrors = Array<Error>;
@@ -322,11 +324,13 @@ class EventEntries extends Component<Props, State> {
322324
showExampleCommit,
323325
showTagSummary,
324326
location,
327+
isBorderless,
325328
} = this.props;
326329
const {proGuardErrors, isLoading} = this.state;
327330

328331
const features = new Set(organization?.features);
329332
const hasQueryFeature = features.has('discover-query');
333+
const hasMobileScreenshotsFeature = features.has('mobile-screenshots');
330334

331335
if (!event) {
332336
return (
@@ -373,18 +377,32 @@ class EventEntries extends Component<Props, State> {
373377
includeBorder={!hasErrors}
374378
/>
375379
)}
376-
{showTagSummary && (
377-
<StyledEventDataSection title={t('Tags')} type="tags">
378-
{hasContext && <EventContextSummary event={event} />}
379-
<EventTags
380+
{showTagSummary &&
381+
(hasMobileScreenshotsFeature ? (
382+
<EventAndScreenshot
380383
event={event}
381384
organization={organization as Organization}
382385
projectId={project.slug}
383386
location={location}
384387
hasQueryFeature={hasQueryFeature}
388+
isShare={isShare}
389+
hasContext={hasContext}
390+
isBorderless={isBorderless}
385391
/>
386-
</StyledEventDataSection>
387-
)}
392+
) : (
393+
(!!(event.tags ?? []).length || hasContext) && (
394+
<StyledEventDataSection title={t('Tags')} type="tags">
395+
{hasContext && <EventContextSummary event={event} />}
396+
<EventTags
397+
event={event}
398+
organization={organization as Organization}
399+
projectId={project.slug}
400+
location={location}
401+
hasQueryFeature={hasQueryFeature}
402+
/>
403+
</StyledEventDataSection>
404+
)
405+
))}
388406
{this.renderEntries(event)}
389407
{hasContext && <EventContexts group={group} event={event} />}
390408
{event && !objectIsEmpty(event.context) && <EventExtraData event={event} />}

static/app/components/events/eventTags/eventTags.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {Location} from 'history';
2-
import isEmpty from 'lodash/isEmpty';
32

43
import Pills from 'app/components/pills';
54
import {Organization} from 'app/types';
@@ -17,13 +16,13 @@ type Props = {
1716
};
1817

1918
const EventTags = ({
20-
event: {tags},
19+
event: {tags = []},
2120
organization,
2221
projectId,
2322
location,
2423
hasQueryFeature,
2524
}: Props) => {
26-
if (isEmpty(tags)) {
25+
if (!tags.length) {
2726
return null;
2827
}
2928

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import styled from '@emotion/styled';
2+
import kebabCase from 'lodash/kebabCase';
3+
4+
import GuideAnchor from 'app/components/assistant/guideAnchor';
5+
import EventDataSection, {SectionContents} from 'app/components/events/eventDataSection';
6+
import QuestionTooltip from 'app/components/questionTooltip';
7+
import space from 'app/styles/space';
8+
9+
type Props = {
10+
title: string;
11+
description: string;
12+
children: React.ReactNode;
13+
};
14+
15+
function DataSection({title, description, children}: Props) {
16+
const type = kebabCase(title);
17+
return (
18+
<StyledEventDataSection
19+
type={type}
20+
title={
21+
<TitleWrapper>
22+
<GuideAnchor target={type} position="bottom">
23+
<Title>{title}</Title>
24+
</GuideAnchor>
25+
<QuestionTooltip size="xs" position="top" title={description} />
26+
</TitleWrapper>
27+
}
28+
wrapTitle={false}
29+
>
30+
{children}
31+
</StyledEventDataSection>
32+
);
33+
}
34+
35+
export default DataSection;
36+
37+
const TitleWrapper = styled('div')`
38+
display: grid;
39+
grid-template-columns: repeat(2, max-content);
40+
grid-gap: ${space(0.5)};
41+
align-items: center;
42+
padding: ${space(0.75)} 0;
43+
`;
44+
45+
const Title = styled('h3')`
46+
margin-bottom: 0;
47+
padding: 0 !important;
48+
height: 14px;
49+
`;
50+
51+
const StyledEventDataSection = styled(EventDataSection)`
52+
${SectionContents} {
53+
flex: 1;
54+
overflow: hidden;
55+
}
56+
57+
@media (min-width: ${p => p.theme.breakpoints[0]}) {
58+
&& {
59+
padding: 0;
60+
border: 0;
61+
}
62+
}
63+
`;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import styled from '@emotion/styled';
2+
3+
import {DataSection} from 'app/components/events/styles';
4+
import space from 'app/styles/space';
5+
6+
import Screenshot from './screenshot';
7+
import Tags from './tags';
8+
9+
type Props = Omit<React.ComponentProps<typeof Tags>, 'projectSlug'> & {
10+
projectId: string;
11+
isShare: boolean;
12+
hasContext: boolean;
13+
isBorderless: boolean;
14+
};
15+
16+
function EventTagsAndScreenshots({
17+
projectId: projectSlug,
18+
isShare,
19+
hasContext,
20+
hasQueryFeature,
21+
location,
22+
isBorderless,
23+
event,
24+
...props
25+
}: Props) {
26+
const {tags = []} = event;
27+
28+
if (!tags.length && !hasContext && isShare) {
29+
return null;
30+
}
31+
32+
return (
33+
<Wrapper isBorderless={isBorderless}>
34+
{!isShare && <Screenshot {...props} event={event} projectSlug={projectSlug} />}
35+
<Tags
36+
{...props}
37+
event={event}
38+
projectSlug={projectSlug}
39+
hasContext={hasContext}
40+
hasQueryFeature={hasQueryFeature}
41+
location={location}
42+
/>
43+
</Wrapper>
44+
);
45+
}
46+
47+
export default EventTagsAndScreenshots;
48+
49+
const Wrapper = styled(DataSection)<{isBorderless: boolean}>`
50+
display: grid;
51+
grid-gap: ${space(3)};
52+
53+
@media (max-width: ${p => p.theme.breakpoints[0]}) {
54+
&& {
55+
padding: 0;
56+
border: 0;
57+
}
58+
}
59+
60+
@media (min-width: ${p => p.theme.breakpoints[0]}) {
61+
padding-bottom: ${space(2)};
62+
grid-template-columns: auto minmax(0, 1fr);
63+
grid-gap: ${space(4)};
64+
65+
> *:first-child {
66+
border-bottom: 0;
67+
padding-bottom: 0;
68+
}
69+
}
70+
71+
${p =>
72+
p.isBorderless &&
73+
`
74+
&& {
75+
padding: ${space(3)} 0 0 0;
76+
:first-child {
77+
padding-top: 0;
78+
border-top: 0;
79+
}
80+
}
81+
`}
82+
`;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import styled from '@emotion/styled';
2+
3+
import emptyStateImg from 'sentry-images/spot/feedback-empty-state.svg';
4+
5+
import Button, {ButtonLabel} from 'app/components/button';
6+
import ButtonBar from 'app/components/buttonBar';
7+
import {t} from 'app/locale';
8+
import space from 'app/styles/space';
9+
import {PlatformType} from 'app/types';
10+
11+
import {getConfigureAttachmentsDocsLink} from './utils';
12+
13+
type Props = {
14+
platform?: PlatformType;
15+
};
16+
17+
function EmptyState({platform}: Props) {
18+
const configureAttachmentsDocsLink = getConfigureAttachmentsDocsLink(platform);
19+
20+
return (
21+
<Wrapper>
22+
<img src={emptyStateImg} />
23+
<StyledButtonbar gap={1}>
24+
<Button priority="link" size="xsmall" to={configureAttachmentsDocsLink} external>
25+
{t('Setup screenshot')}
26+
</Button>
27+
{'|'}
28+
<Button priority="link" size="xsmall">
29+
{t('Dismiss')}
30+
</Button>
31+
</StyledButtonbar>
32+
</Wrapper>
33+
);
34+
}
35+
36+
export default EmptyState;
37+
38+
const Wrapper = styled('div')`
39+
width: 100%;
40+
overflow: hidden;
41+
padding: ${space(2)} ${space(2)} ${space(1)} ${space(2)};
42+
display: flex;
43+
flex-direction: column;
44+
&,
45+
img {
46+
flex: 1;
47+
}
48+
img {
49+
height: 100%;
50+
width: auto;
51+
overflow: hidden;
52+
max-height: 100%;
53+
}
54+
`;
55+
56+
const StyledButtonbar = styled(ButtonBar)`
57+
color: ${p => p.theme.gray200};
58+
justify-content: flex-start;
59+
margin-top: ${space(2)};
60+
${ButtonLabel} {
61+
font-size: ${p => p.theme.fontSizeMedium};
62+
white-space: nowrap;
63+
}
64+
`;

0 commit comments

Comments
 (0)