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"'}, + }); + }); +});