diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/index.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/index.jsx
index e9e7af960fada1..b86462c950e8c2 100644
--- a/src/sentry/static/sentry/app/views/organizationEventsV2/index.jsx
+++ b/src/sentry/static/sentry/app/views/organizationEventsV2/index.jsx
@@ -20,7 +20,7 @@ import {getCurrentView} from './utils';
export default class OrganizationEventsV2 extends React.Component {
static propTypes = {
organization: SentryTypes.Organization.isRequired,
- location: PropTypes.object,
+ location: PropTypes.object.isRequired,
};
renderTabs() {
diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.jsx
index b559977a8f0639..50d8f9ab9978f9 100644
--- a/src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.jsx
+++ b/src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.jsx
@@ -1,8 +1,12 @@
import React from 'react';
import styled from 'react-emotion';
import PropTypes from 'prop-types';
+import {withRouter} from 'react-router';
+
+import Link from 'app/components/links/link';
import {t} from 'app/locale';
import space from 'app/styles/space';
+import {eventTagSearchUrl} from './utils';
const TagsTable = props => {
return (
@@ -13,7 +17,9 @@ const TagsTable = props => {
{props.tags.map(tag => (
{tag.key}
- {tag.value}
+
+ {tag.value}
+
))}
@@ -23,6 +29,7 @@ const TagsTable = props => {
};
TagsTable.propTypes = {
tags: PropTypes.array.isRequired,
+ location: PropTypes.object,
};
const TagHeading = styled('h5')`
@@ -48,4 +55,4 @@ const TagValue = styled(TagKey)`
text-align: right;
`;
-export default TagsTable;
+export default withRouter(TagsTable);
diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx
index 28c67f9d315a9e..0c7c59a6e9ecae 100644
--- a/src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx
+++ b/src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx
@@ -32,3 +32,28 @@ export function getQuery(view) {
return data;
}
+
+/**
+ * Return a location object for the current pathname
+ * with a query string reflected the provided tag.
+ *
+ * @param {Object} tag containing key/value properties
+ * @param {Object} browser location object.
+ * @return {Object} router target
+ */
+export function eventTagSearchUrl(tag, location) {
+ const query = {...location.query};
+ // Add tag key/value to search
+ if (query.query) {
+ query.query += ` ${tag.key}:"${tag.value}"`;
+ } else {
+ query.query = `${tag.key}:"${tag.value}"`;
+ }
+ // Remove the event slug so the user sees new search results.
+ delete query.eventSlug;
+
+ return {
+ pathname: location.pathname,
+ query,
+ };
+}
diff --git a/tests/js/spec/views/organizationEventsV2/index.spec.jsx b/tests/js/spec/views/organizationEventsV2/index.spec.jsx
index 3fce18c7937f4a..6a9980701de980 100644
--- a/tests/js/spec/views/organizationEventsV2/index.spec.jsx
+++ b/tests/js/spec/views/organizationEventsV2/index.spec.jsx
@@ -1,5 +1,6 @@
import React from 'react';
import {mount} from 'enzyme';
+import {initializeOrg} from 'app-test/helpers/initializeOrg';
import OrganizationEventsV2 from 'app/views/organizationEventsV2';
@@ -18,6 +19,7 @@ describe('OrganizationEventsV2', function() {
});
MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/events/deadbeef/',
+ method: 'GET',
body: {
id: '1234',
size: 1200,
@@ -89,4 +91,73 @@ describe('OrganizationEventsV2', function() {
const modal = wrapper.find('EventDetails');
expect(modal).toHaveLength(1);
});
+
+ it('navigates when tag values are clicked', async function() {
+ const {organization, routerContext} = initializeOrg({
+ organization: TestStubs.Organization({projects: [TestStubs.Project()]}),
+ router: {
+ location: {
+ pathname: '/organizations/org-slug/events/',
+ query: {
+ eventSlug: 'project-slug:deadbeef',
+ },
+ },
+ },
+ });
+ const wrapper = mount(
+ ,
+ routerContext
+ );
+ await tick();
+ await wrapper.update();
+
+ // Get the first link as we wrap react-router's link
+ const tagLink = wrapper.find('EventDetails TagsTable TagValue Link').first();
+
+ // Should remove eventSlug and append new tag value causing
+ // the view to re-render
+ expect(tagLink.props().to).toEqual({
+ pathname: '/organizations/org-slug/events/',
+ query: {query: 'browser:"Firefox"'},
+ });
+ });
+
+ it('appends tag value to existing query when clicked', async function() {
+ const {organization, routerContext} = initializeOrg({
+ organization: TestStubs.Organization({projects: [TestStubs.Project()]}),
+ router: {
+ location: {
+ pathname: '/organizations/org-slug/events/',
+ query: {
+ query: 'Dumpster',
+ eventSlug: 'project-slug:deadbeef',
+ },
+ },
+ },
+ });
+ const wrapper = mount(
+ ,
+ routerContext
+ );
+ await tick();
+ await wrapper.update();
+
+ // Get the first link as we wrap react-router's link
+ const tagLink = wrapper.find('EventDetails TagsTable TagValue Link').first();
+
+ // Should remove eventSlug and append new tag value causing
+ // the view to re-render
+ expect(tagLink.props().to).toEqual({
+ pathname: '/organizations/org-slug/events/',
+ query: {query: 'Dumpster browser:"Firefox"'},
+ });
+ });
});
diff --git a/tests/js/spec/views/organizationEventsV2/utils.spec.jsx b/tests/js/spec/views/organizationEventsV2/utils.spec.jsx
index ff223f32678ece..7be03dc0efa700 100644
--- a/tests/js/spec/views/organizationEventsV2/utils.spec.jsx
+++ b/tests/js/spec/views/organizationEventsV2/utils.spec.jsx
@@ -1,4 +1,8 @@
-import {getCurrentView, getQuery} from 'app/views/organizationEventsV2/utils';
+import {
+ getCurrentView,
+ getQuery,
+ eventTagSearchUrl,
+} from 'app/views/organizationEventsV2/utils';
import {ALL_VIEWS} from 'app/views/organizationEventsV2/data';
describe('getCurrentView()', function() {
@@ -38,3 +42,36 @@ describe('getQuery()', function() {
]);
});
});
+
+describe('eventTagSearchUrl()', function() {
+ let location;
+ beforeEach(function() {
+ location = {
+ pathname: '/organization/org-slug/events/',
+ query: {},
+ };
+ });
+
+ it('adds a query', function() {
+ expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
+ pathname: location.pathname,
+ query: {query: 'browser:"firefox"'},
+ });
+ });
+
+ it('removes eventSlug', function() {
+ location.query.eventSlug = 'project-slug:deadbeef';
+ expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
+ pathname: location.pathname,
+ query: {query: 'browser:"firefox"'},
+ });
+ });
+
+ it('appends to an existing query', function() {
+ location.query.query = 'failure';
+ expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
+ pathname: location.pathname,
+ query: {query: 'failure browser:"firefox"'},
+ });
+ });
+});