Skip to content

feat(events-v2) Add a event graph to the event modal #13568

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 14 commits into from
Jun 11, 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: 2 additions & 0 deletions src/sentry/static/sentry/app/actionCreators/events.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const doEventsRequest = (
limit,
query,
yAxis,
groupId,
}
) => {
const shouldDoublePeriod = canIncludePreviousPeriod(includePrevious, period);
Expand All @@ -40,6 +41,7 @@ export const doEventsRequest = (
environment,
query,
yAxis,
group: groupId,
};

// Doubling period for absolute dates is not accurate unless starting and
Expand Down
1 change: 1 addition & 0 deletions src/sentry/static/sentry/app/icons/icon-next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/sentry/static/sentry/app/icons/icon-prev.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,18 @@ class EventsRequest extends React.PureComponent {
* The yAxis being plotted
*/
yAxis: PropTypes.string,

/**
* issue group id or groupids to filter results by.
*/
groupId: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
};

static defaultProps = {
period: null,
start: null,
end: null,
groupId: null,
interval: '1d',
limit: 15,
getCategory: i => i,
Expand Down
38 changes: 34 additions & 4 deletions src/sentry/static/sentry/app/views/organizationEventsV2/data.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,42 @@ export const SPECIAL_FIELDS = {
),
},
error: {
fields: ['issue_title'],
renderFunc: data => <Container>{data.issue_title}</Container>,
fields: ['issue_title', 'project.name', 'issue.id'],
renderFunc: (data, {organization, location}) => {
const target = {
pathname: `/organizations/${organization.slug}/events/`,
query: {
...location.query,
groupSlug: `${data['project.name']}:${data['issue.id']}:latest`,
},
};
return (
<Container>
<Link css={overflowEllipsis} to={target} data-test-id="event-title">
{data.issue_title}
</Link>
</Container>
);
},
},
csp: {
fields: ['issue_title'],
renderFunc: data => <Container>{data.issue_title}</Container>,
fields: ['issue_title', 'project.name', 'issue.id'],
renderFunc: (data, {organization, location}) => {
const target = {
pathname: `/organizations/${organization.slug}/events/`,
query: {
...location.query,
groupSlug: `${data['project.name']}:${data['issue.id']}:latest`,
},
};
return (
<Container>
<Link css={overflowEllipsis} to={target} data-test-id="event-title">
{data.issue_title}
</Link>
</Container>
);
},
},
event_count: {
title: 'events',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,75 @@ import React from 'react';
import styled from 'react-emotion';
import {browserHistory} from 'react-router';
import PropTypes from 'prop-types';
import {omit} from 'lodash';

import SentryTypes from 'app/sentryTypes';
import AsyncComponent from 'app/components/asyncComponent';
import InlineSvg from 'app/components/inlineSvg';
import withApi from 'app/utils/withApi';
import space from 'app/styles/space';

import EventModalContent from './eventModalContent';
import {getQuery} from './utils';

const slugValidator = function(props, propName, componentName) {
const value = props[propName];
// Accept slugs that look like:
// * project-slug:123:latest
// * project-slug:123:oldest
// * project-slug:123:deadbeef
// * project-slug:deadbeef
if (value && !/^(?:[^:]+:)?(?:[^:]+):(?:[a-f0-9]+|latest|oldest)$/.test(value)) {
Copy link
Member

Choose a reason for hiding this comment

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

:nice:regex:

It'd help to have a comment with what a valid slug looks like. Are special characters (non alphanumerics) allowed in the first two groups?

return new Error(`Invalid value for ${propName} provided to ${componentName}.`);
}
return null;
};

class EventDetails extends AsyncComponent {
static propTypes = {
params: PropTypes.object,
eventSlug: PropTypes.string.isRequired,
organization: SentryTypes.Organization.isRequired,
eventSlug: slugValidator,
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to validate these at runtime as well? (I guess receiving a 404 makes sense)

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the 404 we display in the modal could use some work, but I can do that separately.

groupSlug: slugValidator,
location: PropTypes.object.isRequired,
view: PropTypes.object.isRequired,
};

getEndpoints() {
const {orgId} = this.props.params;
const [projectId, eventId] = this.props.eventSlug.toString().split(':');

return [['event', `/projects/${orgId}/${projectId}/events/${eventId}/`]];
const {organization, eventSlug, groupSlug, view, location} = this.props;
const query = getQuery(view, location);

// If we're getting an issue/group use the latest endpoint.
// We pass the current query/view state to the API so we get an
// event that matches the current list filters.
if (groupSlug) {
const [projectId, groupId, eventId] = groupSlug.toString().split(':');

let url = `/organizations/${organization.slug}/events/`;
// latest / oldest have dedicated endpoints
if (['latest', 'oldest'].includes(eventId)) {
url += 'latest/';
} else {
url += `${projectId}:${eventId}/`;
}
if (query.query) {
query.query += ` issue.id:${groupId}`;
} else {
query.query = `issue.id:${groupId}`;
}

return [['event', url, {query}]];
}

// Get a specific event. This could be coming from
// a paginated group or standalone event.
const [projectId, eventId] = eventSlug.toString().split(':');
return [
[
'event',
`/organizations/${organization.slug}/events/${projectId}:${eventId}/`,
{query},
],
];
}

onRequestSuccess({data}) {
Expand All @@ -30,23 +80,45 @@ class EventDetails extends AsyncComponent {

handleClose = event => {
event.preventDefault();
browserHistory.goBack();
const {location} = this.props;
// Remove modal related query parameters.
const query = omit(location.query, ['groupSlug', 'eventSlug']);

browserHistory.push({
pathname: location.pathname,
query,
});
};

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

get projectId() {
if (this.props.eventSlug) {
const [projectId] = this.props.eventSlug.split(':');
return projectId;
}
if (this.props.groupSlug) {
const [projectId] = this.props.groupSlug.split(':');
return projectId;
}
throw new Error('Could not determine projectId');
}

renderBody() {
const {orgId} = this.props.params;
const [projectId, _] = this.props.eventSlug.split(':');
const {organization, view, location} = this.props;
const {event, activeTab} = this.state;

return (
<ModalContainer>
<CloseButton onClick={this.handleClose} size={30} />
<EventModalContent
onTabChange={this.handleTabChange}
event={this.state.event}
activeTab={this.state.activeTab}
projectId={projectId}
orgId={orgId}
event={event}
activeTab={activeTab}
projectId={this.projectId}
organization={organization}
view={view}
location={location}
/>
</ModalContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import getDynamicText from 'app/utils/getDynamicText';
import space from 'app/styles/space';

import LinkedIssuePreview from './linkedIssuePreview';
import ModalPagination from './modalPagination';
import ModalLineGraph from './modalLineGraph';
import TagsTable from './tagsTable';

const OTHER_SECTIONS = {
Expand Down Expand Up @@ -76,15 +78,30 @@ ActiveTab.propTypes = {
* Controlled by the EventDetails View.
*/
const EventModalContent = props => {
const {event, activeTab, projectId, orgId, onTabChange} = props;
const eventJsonUrl = `/api/0/projects/${orgId}/${projectId}/events/${
const {event, activeTab, projectId, organization, onTabChange, location, view} = props;
const isGroupedView = !!view.data.groupby;
const eventJsonUrl = `/api/0/projects/${organization.slug}/${projectId}/events/${
event.eventID
}/json/`;

return (
<ColumnGrid>
<ContentColumn>
<HeaderBox>
<EventHeader event={event} />
{isGroupedView && <ModalPagination event={event} location={location} />}
{isGroupedView &&
getDynamicText({
value: (
<ModalLineGraph
organization={organization}
groupId={event.groupID}
location={location}
/>
),
fixed: 'events chart',
})}
</HeaderBox>
<ContentColumn>
<NavTabs underlined={true}>
{event.entries.map(entry => {
if (!INTERFACES.hasOwnProperty(entry.type)) {
Expand Down Expand Up @@ -143,7 +160,9 @@ const EventModalContent = props => {
EventModalContent.propTypes = {
...ActiveTab.propTypes,
onTabChange: PropTypes.func.isRequired,
orgId: PropTypes.string.isRequired,
organization: SentryTypes.Organization.isRequired,
view: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
};

/**
Expand Down Expand Up @@ -197,11 +216,14 @@ const MetadataContainer = styled('div')`

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

const HeaderBox = styled('div')`
grid-column: 1 / 3;
`;
const ContentColumn = styled('div')`
grid-column: 1 / 2;
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export default class OrganizationEventsV2 extends React.Component {

render() {
const {organization, location, router} = this.props;
const {eventSlug} = location.query;
const {eventSlug, groupSlug} = location.query;
const currentView = getCurrentView(location.query.view);
const showModal = groupSlug || eventSlug;

return (
<DocumentTitle title={`Events - ${organization.slug} - Sentry`}>
Expand All @@ -64,16 +66,19 @@ export default class OrganizationEventsV2 extends React.Component {
{this.renderTabs()}
<Events
organization={organization}
view={getCurrentView(location.query.view)}
view={currentView}
location={location}
router={router}
/>
</NoProjectMessage>
{eventSlug && (
{showModal && (
<EventDetails
orgId={organization.slug}
organization={organization}
params={this.props.params}
eventSlug={eventSlug}
groupSlug={groupSlug}
view={currentView}
location={location}
/>
)}
</PageContent>
Expand Down
Loading