Skip to content

ref(events-v2) Split up the EventDetails view better #13473

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 31, 2019
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: 1 addition & 1 deletion src/sentry/static/sentry/app/components/asyncComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export default class AsyncComponent extends React.Component {
}

renderError(error, disableLog = false, disableReport = false) {
// 401s are captured by SudaModal, but may be passed back to AsyncComponent if they close the modal without identifying
// 401s are captured by SudoModal, but may be passed back to AsyncComponent if they close the modal without identifying
const unauthorizedErrors = Object.values(this.state.errors).find(
resp => resp && resp.status === 401
);
Expand Down
238 changes: 16 additions & 222 deletions src/sentry/static/sentry/app/views/organizationEventsV2/eventDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,260 +3,54 @@ import styled from 'react-emotion';
import {browserHistory} from 'react-router';
import PropTypes from 'prop-types';

import {t} from 'app/locale';
import SentryTypes from 'app/sentryTypes';
import LoadingIndicator from 'app/components/loadingIndicator';
import AsyncComponent from 'app/components/asyncComponent';
import Button from 'app/components/button';
import DateTime from 'app/components/dateTime';
import ErrorBoundary from 'app/components/errorBoundary';
import ExternalLink from 'app/components/links/externalLink';
import EventDataSection from 'app/components/events/eventDataSection';
import EventDevice from 'app/components/events/device';
import EventExtraData from 'app/components/events/extraData';
import EventPackageData from 'app/components/events/packageData';
import FileSize from 'app/components/fileSize';
import NavTabs from 'app/components/navTabs';
import NotFound from 'app/components/errors/notFound';
import withApi from 'app/utils/withApi';
import space from 'app/styles/space';
import getDynamicText from 'app/utils/getDynamicText';
import utils from 'app/utils';
import {getMessage, getTitle} from 'app/utils/events';

import {INTERFACES} from 'app/components/events/eventEntries';
import TagsTable from './tagsTable';
import EventModalContent from './eventModalContent';

const OTHER_SECTIONS = {
context: EventExtraData,
packages: EventPackageData,
device: EventDevice,
};

class EventDetails extends React.Component {
class EventDetails extends AsyncComponent {
static propTypes = {
api: PropTypes.object,
params: PropTypes.object,
eventSlug: PropTypes.string.isRequired,
};

state = {
loading: true,
error: false,
event: null,
activeTab: null,
};
getEndpoints() {
const {orgId} = this.props.params;
const [projectId, eventId] = this.props.eventSlug.toString().split(':');

componentDidMount() {
this.fetchData();
return [['event', `/projects/${orgId}/${projectId}/events/${eventId}/`]];
}

componentDidUpdate(prevProps) {
if (prevProps.eventSlug != this.props.eventSlug) {
this.fetchData();
}
}

async fetchData() {
this.setState({loading: true, error: false});
const {orgId} = this.props.params;
const [projectId, eventId] = this.props.eventSlug.split(':');
try {
if (!projectId || !eventId) {
throw new Error('Invalid eventSlug.');
}
const response = await this.props.api.requestPromise(
`/projects/${orgId}/${projectId}/events/${eventId}/`
);
this.setState({
activeTab: response.entries[0].type,
event: response,
loading: false,
});
} catch (e) {
this.setState({error: true});
}
onRequestSuccess({data}) {
// Select the first interface as the active sub-tab
this.setState({activeTab: data.entries[0].type});
}

handleClose = event => {
event.preventDefault();

browserHistory.goBack();
};

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

renderBody() {
if (this.state.loading) {
return <LoadingIndicator />;
}
if (this.state.error) {
return <NotFound />;
}
const {event, activeTab} = this.state;

return (
<ColumnGrid>
<ContentColumn>
<EventHeader event={this.state.event} />
<NavTabs underlined={true}>
{event.entries.map(entry => {
if (!INTERFACES.hasOwnProperty(entry.type)) {
return null;
}
const type = entry.type;
const classname = type === activeTab ? 'active' : null;
return (
<li key={type} className={classname}>
<a
href="#"
onClick={evt => {
evt.preventDefault();
this.handleTabChange(type);
}}
>
{utils.toTitleCase(type)}
</a>
</li>
);
})}
{Object.keys(OTHER_SECTIONS).map(section => {
if (utils.objectIsEmpty(event[section])) {
return null;
}
const classname = section === activeTab ? 'active' : null;
return (
<li key={section} className={classname}>
<a
href="#"
onClick={() => {
this.handleTabChange(section);
}}
>
{utils.toTitleCase(section)}
</a>
</li>
);
})}
</NavTabs>
<ErrorBoundary message={t('Could not render event details')}>
{this.renderActiveTab(event, activeTab)}
</ErrorBoundary>
</ContentColumn>
<SidebarColumn>
<EventMetadata event={event} />
<SidebarBlock>
<TagsTable tags={event.tags} />
</SidebarBlock>
</SidebarColumn>
</ColumnGrid>
);
}

renderActiveTab(event, activeTab) {
const entry = event.entries.find(item => item.type === activeTab);
const [projectId, _] = this.props.eventSlug.split(':');
if (INTERFACES[activeTab]) {
const Component = INTERFACES[activeTab];
return (
<Component
projectId={projectId}
event={event}
type={entry.type}
data={entry.data}
isShare={false}
/>
);
} else if (OTHER_SECTIONS[activeTab]) {
const Component = OTHER_SECTIONS[activeTab];
return <Component event={event} isShare={false} />;
} else {
/*eslint no-console:0*/
window.console &&
console.error &&
console.error('Unregistered interface: ' + entry.type);

return (
<EventDataSection event={event} type={entry.type} title={entry.type}>
<p>{t('There was an error rendering this data.')}</p>
</EventDataSection>
);
}
}

render() {
return (
<ModalContainer>
<CloseButton onClick={this.handleClose} size="zero" icon="icon-close" />
{this.renderBody()}
<EventModalContent
onTabChange={this.handleTabChange}
event={this.state.event}
activeTab={this.state.activeTab}
projectId={projectId}
/>
</ModalContainer>
);
}
}

const EventHeader = props => {
const {title} = getTitle(props.event);
return (
<div>
<h2>{title}</h2>
<p>{getMessage(props.event)}</p>
</div>
);
};
EventHeader.propTypes = {
event: SentryTypes.Event.isRequired,
};

const EventMetadata = props => {
const jsonUrl = 'TODO build this';
const {event} = props;

return (
<SidebarBlock withSeparator>
<MetadataContainer>ID {event.eventID}</MetadataContainer>
<MetadataContainer>
<DateTime
date={getDynamicText({value: event.dateCreated, fixed: 'Dummy timestamp'})}
/>
<ExternalLink href={jsonUrl} className="json-link">
JSON (<FileSize bytes={event.size} />)
</ExternalLink>
</MetadataContainer>
</SidebarBlock>
);
};
EventMetadata.propTypes = {
event: SentryTypes.Event.isRequired,
};

const MetadataContainer = styled('div')`
display: flex;
justify-content: space-between;

color: ${p => p.theme.gray3};
font-size: ${p => p.theme.fontSizeMedium};
`;

const ColumnGrid = styled('div')`
display: grid;
grid-template-columns: 70% 1fr;
grid-template-rows: auto;
grid-column-gap: ${space(3)};
`;

const ContentColumn = styled('div')`
grid-column: 1 / 2;
`;

const SidebarColumn = styled('div')`
grid-column: 2 / 3;
`;

const SidebarBlock = styled('div')`
margin: 0 0 ${space(2)} 0;
padding: 0 0 ${space(2)} 0;
${p => (p.withSeparator ? `border-bottom: 1px solid ${p.theme.borderLight};` : '')}
`;

const ModalContainer = styled('div')`
position: absolute;
top: 0px;
Expand Down
Loading