Skip to content

Commit fff8f9d

Browse files
authored
ref(events-v2) Split up the EventDetails view better (#13473)
Split the logic up so that there is only one stateful component and several controlled components that focus on presentation only. Refs SEN-648
1 parent 2d766c5 commit fff8f9d

File tree

4 files changed

+240
-225
lines changed

4 files changed

+240
-225
lines changed

src/sentry/static/sentry/app/components/asyncComponent.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ export default class AsyncComponent extends React.Component {
353353
}
354354

355355
renderError(error, disableLog = false, disableReport = false) {
356-
// 401s are captured by SudaModal, but may be passed back to AsyncComponent if they close the modal without identifying
356+
// 401s are captured by SudoModal, but may be passed back to AsyncComponent if they close the modal without identifying
357357
const unauthorizedErrors = Object.values(this.state.errors).find(
358358
resp => resp && resp.status === 401
359359
);

src/sentry/static/sentry/app/views/organizationEventsV2/eventDetails.jsx

Lines changed: 16 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -3,260 +3,54 @@ import styled from 'react-emotion';
33
import {browserHistory} from 'react-router';
44
import PropTypes from 'prop-types';
55

6-
import {t} from 'app/locale';
7-
import SentryTypes from 'app/sentryTypes';
8-
import LoadingIndicator from 'app/components/loadingIndicator';
6+
import AsyncComponent from 'app/components/asyncComponent';
97
import Button from 'app/components/button';
10-
import DateTime from 'app/components/dateTime';
11-
import ErrorBoundary from 'app/components/errorBoundary';
12-
import ExternalLink from 'app/components/links/externalLink';
13-
import EventDataSection from 'app/components/events/eventDataSection';
14-
import EventDevice from 'app/components/events/device';
15-
import EventExtraData from 'app/components/events/extraData';
16-
import EventPackageData from 'app/components/events/packageData';
17-
import FileSize from 'app/components/fileSize';
18-
import NavTabs from 'app/components/navTabs';
19-
import NotFound from 'app/components/errors/notFound';
208
import withApi from 'app/utils/withApi';
219
import space from 'app/styles/space';
22-
import getDynamicText from 'app/utils/getDynamicText';
23-
import utils from 'app/utils';
24-
import {getMessage, getTitle} from 'app/utils/events';
2510

26-
import {INTERFACES} from 'app/components/events/eventEntries';
27-
import TagsTable from './tagsTable';
11+
import EventModalContent from './eventModalContent';
2812

29-
const OTHER_SECTIONS = {
30-
context: EventExtraData,
31-
packages: EventPackageData,
32-
device: EventDevice,
33-
};
34-
35-
class EventDetails extends React.Component {
13+
class EventDetails extends AsyncComponent {
3614
static propTypes = {
37-
api: PropTypes.object,
3815
params: PropTypes.object,
3916
eventSlug: PropTypes.string.isRequired,
4017
};
4118

42-
state = {
43-
loading: true,
44-
error: false,
45-
event: null,
46-
activeTab: null,
47-
};
19+
getEndpoints() {
20+
const {orgId} = this.props.params;
21+
const [projectId, eventId] = this.props.eventSlug.toString().split(':');
4822

49-
componentDidMount() {
50-
this.fetchData();
23+
return [['event', `/projects/${orgId}/${projectId}/events/${eventId}/`]];
5124
}
5225

53-
componentDidUpdate(prevProps) {
54-
if (prevProps.eventSlug != this.props.eventSlug) {
55-
this.fetchData();
56-
}
57-
}
58-
59-
async fetchData() {
60-
this.setState({loading: true, error: false});
61-
const {orgId} = this.props.params;
62-
const [projectId, eventId] = this.props.eventSlug.split(':');
63-
try {
64-
if (!projectId || !eventId) {
65-
throw new Error('Invalid eventSlug.');
66-
}
67-
const response = await this.props.api.requestPromise(
68-
`/projects/${orgId}/${projectId}/events/${eventId}/`
69-
);
70-
this.setState({
71-
activeTab: response.entries[0].type,
72-
event: response,
73-
loading: false,
74-
});
75-
} catch (e) {
76-
this.setState({error: true});
77-
}
26+
onRequestSuccess({data}) {
27+
// Select the first interface as the active sub-tab
28+
this.setState({activeTab: data.entries[0].type});
7829
}
7930

8031
handleClose = event => {
8132
event.preventDefault();
82-
8333
browserHistory.goBack();
8434
};
8535

8636
handleTabChange = tab => this.setState({activeTab: tab});
8737

8838
renderBody() {
89-
if (this.state.loading) {
90-
return <LoadingIndicator />;
91-
}
92-
if (this.state.error) {
93-
return <NotFound />;
94-
}
95-
const {event, activeTab} = this.state;
96-
97-
return (
98-
<ColumnGrid>
99-
<ContentColumn>
100-
<EventHeader event={this.state.event} />
101-
<NavTabs underlined={true}>
102-
{event.entries.map(entry => {
103-
if (!INTERFACES.hasOwnProperty(entry.type)) {
104-
return null;
105-
}
106-
const type = entry.type;
107-
const classname = type === activeTab ? 'active' : null;
108-
return (
109-
<li key={type} className={classname}>
110-
<a
111-
href="#"
112-
onClick={evt => {
113-
evt.preventDefault();
114-
this.handleTabChange(type);
115-
}}
116-
>
117-
{utils.toTitleCase(type)}
118-
</a>
119-
</li>
120-
);
121-
})}
122-
{Object.keys(OTHER_SECTIONS).map(section => {
123-
if (utils.objectIsEmpty(event[section])) {
124-
return null;
125-
}
126-
const classname = section === activeTab ? 'active' : null;
127-
return (
128-
<li key={section} className={classname}>
129-
<a
130-
href="#"
131-
onClick={() => {
132-
this.handleTabChange(section);
133-
}}
134-
>
135-
{utils.toTitleCase(section)}
136-
</a>
137-
</li>
138-
);
139-
})}
140-
</NavTabs>
141-
<ErrorBoundary message={t('Could not render event details')}>
142-
{this.renderActiveTab(event, activeTab)}
143-
</ErrorBoundary>
144-
</ContentColumn>
145-
<SidebarColumn>
146-
<EventMetadata event={event} />
147-
<SidebarBlock>
148-
<TagsTable tags={event.tags} />
149-
</SidebarBlock>
150-
</SidebarColumn>
151-
</ColumnGrid>
152-
);
153-
}
154-
155-
renderActiveTab(event, activeTab) {
156-
const entry = event.entries.find(item => item.type === activeTab);
15739
const [projectId, _] = this.props.eventSlug.split(':');
158-
if (INTERFACES[activeTab]) {
159-
const Component = INTERFACES[activeTab];
160-
return (
161-
<Component
162-
projectId={projectId}
163-
event={event}
164-
type={entry.type}
165-
data={entry.data}
166-
isShare={false}
167-
/>
168-
);
169-
} else if (OTHER_SECTIONS[activeTab]) {
170-
const Component = OTHER_SECTIONS[activeTab];
171-
return <Component event={event} isShare={false} />;
172-
} else {
173-
/*eslint no-console:0*/
174-
window.console &&
175-
console.error &&
176-
console.error('Unregistered interface: ' + entry.type);
177-
178-
return (
179-
<EventDataSection event={event} type={entry.type} title={entry.type}>
180-
<p>{t('There was an error rendering this data.')}</p>
181-
</EventDataSection>
182-
);
183-
}
184-
}
185-
186-
render() {
18740
return (
18841
<ModalContainer>
18942
<CloseButton onClick={this.handleClose} size="zero" icon="icon-close" />
190-
{this.renderBody()}
43+
<EventModalContent
44+
onTabChange={this.handleTabChange}
45+
event={this.state.event}
46+
activeTab={this.state.activeTab}
47+
projectId={projectId}
48+
/>
19149
</ModalContainer>
19250
);
19351
}
19452
}
19553

196-
const EventHeader = props => {
197-
const {title} = getTitle(props.event);
198-
return (
199-
<div>
200-
<h2>{title}</h2>
201-
<p>{getMessage(props.event)}</p>
202-
</div>
203-
);
204-
};
205-
EventHeader.propTypes = {
206-
event: SentryTypes.Event.isRequired,
207-
};
208-
209-
const EventMetadata = props => {
210-
const jsonUrl = 'TODO build this';
211-
const {event} = props;
212-
213-
return (
214-
<SidebarBlock withSeparator>
215-
<MetadataContainer>ID {event.eventID}</MetadataContainer>
216-
<MetadataContainer>
217-
<DateTime
218-
date={getDynamicText({value: event.dateCreated, fixed: 'Dummy timestamp'})}
219-
/>
220-
<ExternalLink href={jsonUrl} className="json-link">
221-
JSON (<FileSize bytes={event.size} />)
222-
</ExternalLink>
223-
</MetadataContainer>
224-
</SidebarBlock>
225-
);
226-
};
227-
EventMetadata.propTypes = {
228-
event: SentryTypes.Event.isRequired,
229-
};
230-
231-
const MetadataContainer = styled('div')`
232-
display: flex;
233-
justify-content: space-between;
234-
235-
color: ${p => p.theme.gray3};
236-
font-size: ${p => p.theme.fontSizeMedium};
237-
`;
238-
239-
const ColumnGrid = styled('div')`
240-
display: grid;
241-
grid-template-columns: 70% 1fr;
242-
grid-template-rows: auto;
243-
grid-column-gap: ${space(3)};
244-
`;
245-
246-
const ContentColumn = styled('div')`
247-
grid-column: 1 / 2;
248-
`;
249-
250-
const SidebarColumn = styled('div')`
251-
grid-column: 2 / 3;
252-
`;
253-
254-
const SidebarBlock = styled('div')`
255-
margin: 0 0 ${space(2)} 0;
256-
padding: 0 0 ${space(2)} 0;
257-
${p => (p.withSeparator ? `border-bottom: 1px solid ${p.theme.borderLight};` : '')}
258-
`;
259-
26054
const ModalContainer = styled('div')`
26155
position: absolute;
26256
top: 0px;

0 commit comments

Comments
 (0)