From 45194dc5eacf5bcb288396d43033e0d75216f52e Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Mon, 26 Feb 2018 13:20:38 -0800 Subject: [PATCH 1/5] feat(environments): Add group events environment filtering --- .../sentry/app/components/searchBar.jsx | 17 ++-- .../stream/utils.js => utils/queryString.jsx} | 8 +- .../static/sentry/app/views/groupEvents.jsx | 88 +++++++++++++------ src/sentry/static/sentry/app/views/stream.jsx | 10 +-- .../queryString.spec.js} | 2 +- 5 files changed, 84 insertions(+), 41 deletions(-) rename src/sentry/static/sentry/app/{views/stream/utils.js => utils/queryString.jsx} (71%) rename tests/js/spec/{views/stream/utils.spec.js => utils/queryString.spec.js} (97%) diff --git a/src/sentry/static/sentry/app/components/searchBar.jsx b/src/sentry/static/sentry/app/components/searchBar.jsx index bbdae7159f57d4..8152ceaf5c75c5 100644 --- a/src/sentry/static/sentry/app/components/searchBar.jsx +++ b/src/sentry/static/sentry/app/components/searchBar.jsx @@ -1,13 +1,11 @@ import PropTypes from 'prop-types'; import React from 'react'; -import ReactDOM from 'react-dom'; class SearchBar extends React.PureComponent { static propTypes = { query: PropTypes.string, defaultQuery: PropTypes.string, onSearch: PropTypes.func, - onQueryChange: PropTypes.func, placeholder: PropTypes.string, }; @@ -15,7 +13,6 @@ class SearchBar extends React.PureComponent { defaultQuery: '', query: '', onSearch: function() {}, - onQueryChange: function() {}, }; constructor(...args) { @@ -25,8 +22,16 @@ class SearchBar extends React.PureComponent { }; } + componentWillReceiveProps(nextProps) { + if (nextProps.query !== this.props.query) { + this.setState({ + query: nextProps.query, + }); + } + } + blur = () => { - ReactDOM.findDOMNode(this.refs.searchInput).blur(); + this.searchInput.blur(); }; onSubmit = evt => { @@ -58,14 +63,14 @@ class SearchBar extends React.PureComponent { render() { return (
-
+
(this.searchInput = el)} autoComplete="off" value={this.state.query} onBlur={this.onQueryBlur} diff --git a/src/sentry/static/sentry/app/views/stream/utils.js b/src/sentry/static/sentry/app/utils/queryString.jsx similarity index 71% rename from src/sentry/static/sentry/app/views/stream/utils.js rename to src/sentry/static/sentry/app/utils/queryString.jsx index 5b4ae479059dc3..22c3d0e13c2ee5 100644 --- a/src/sentry/static/sentry/app/views/stream/utils.js +++ b/src/sentry/static/sentry/app/utils/queryString.jsx @@ -1,19 +1,19 @@ // remove leading and trailing whitespace and remove double spaces -function formatQueryString(qs) { +export function formatQueryString(qs) { return qs.trim().replace(/\s+/g, ' '); } // returns environment name from query or null if not specified // Any charater can be valid in an environment name -function getQueryEnvironment(qs) { +export function getQueryEnvironment(qs) { const match = qs.match(/environment:([^\s]*)/); return match ? match[1] : null; } -function getQueryStringWithEnvironment(qs, env) { +export function getQueryStringWithEnvironment(qs, env) { const qsWithoutEnv = qs.replace(/environment:[^\s]*/g, ''); return formatQueryString( - env === null ? qsWithoutEnv : qsWithoutEnv + ` environment:${env}` + env === null ? qsWithoutEnv : `${qsWithoutEnv} environment:${env}` ); } diff --git a/src/sentry/static/sentry/app/views/groupEvents.jsx b/src/sentry/static/sentry/app/views/groupEvents.jsx index ea3a7a8abb534a..855758055d6f86 100644 --- a/src/sentry/static/sentry/app/views/groupEvents.jsx +++ b/src/sentry/static/sentry/app/views/groupEvents.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import {browserHistory} from 'react-router'; @@ -10,20 +11,47 @@ import Pagination from '../components/pagination'; import SearchBar from '../components/searchBar'; import EventsTable from '../components/eventsTable/eventsTable'; import {t} from '../locale'; +import withEnvironment from '../utils/withEnvironment'; +import {getQueryEnvironment, getQueryStringWithEnvironment} from '../utils/queryString'; +import EnvironmentStore from '../stores/environmentStore'; +import {setActiveEnvironment} from '../actionCreators/environments'; const GroupEvents = createReactClass({ displayName: 'GroupEvents', + + propTypes: { + environment: PropTypes.object, + }, + mixins: [ApiMixin, GroupState], getInitialState() { - let queryParams = this.props.location.query; - return { + const queryParams = this.props.location.query; + + const initialState = { eventList: [], loading: true, error: false, pageLinks: '', query: queryParams.query || '', }; + + // If an environment is specified in the query, update the global environment + // Otherwise if a global environment is present update the query + const queryEnvironment = EnvironmentStore.getByName( + getQueryEnvironment(queryParams.query || '') + ); + + if (queryEnvironment) { + setActiveEnvironment(queryEnvironment); + } else if (this.props.environment) { + initialState.query = getQueryStringWithEnvironment( + initialState.query, + this.props.environment.name + ); + } + + return initialState; }, componentWillMount() { @@ -31,11 +59,18 @@ const GroupEvents = createReactClass({ }, componentWillReceiveProps(nextProps) { - if ( - nextProps.params.groupId !== this.props.params.groupId || - nextProps.location.search !== this.props.location.search - ) { - let queryParams = nextProps.location.query; + // If query has changed, update the environment with the query environment + if (nextProps.location.search !== this.props.location.search) { + const queryParams = nextProps.location.query; + + const queryEnvironment = EnvironmentStore.getByName( + getQueryEnvironment(queryParams.query || '') + ); + + if (queryEnvironment) { + setActiveEnvironment(queryEnvironment); + } + this.setState( { query: queryParams.query, @@ -43,9 +78,23 @@ const GroupEvents = createReactClass({ this.fetchData ); } + + // If environment has changed, update query with new environment + if (nextProps.environment !== this.props.environment) { + const newQueryString = getQueryStringWithEnvironment( + nextProps.location.query.query || '', + nextProps.environment ? nextProps.environment.name : null + ); + this.setState( + { + query: newQueryString, + }, + this.handleSearch(newQueryString) + ); + } }, - onSearch(query) { + handleSearch(query) { let targetQueryParams = {}; if (query !== '') targetQueryParams.query = query; @@ -56,28 +105,17 @@ const GroupEvents = createReactClass({ }); }, - getEndpoint() { - let params = this.props.params; - let queryParams = { - ...this.props.location.query, - limit: 50, - query: this.state.query, - }; - - return `/issues/${params.groupId}/events/?${jQuery.param(queryParams)}`; - }, - fetchData() { - let queryParams = this.props.location.query; - this.setState({ loading: true, error: false, }); - this.api.request(this.getEndpoint(), { + const query = {limit: 50, query: this.state.query}; + + this.api.request(`/issues/${this.props.params.groupId}/events/`, { + query, method: 'GET', - data: queryParams, success: (data, _, jqXHR) => { this.setState({ eventList: data, @@ -155,7 +193,7 @@ const GroupEvents = createReactClass({ defaultQuery="" placeholder={t('search event id, message, or tags')} query={this.state.query} - onSearch={this.onSearch} + onSearch={this.handleSearch} />
{this.renderBody()} @@ -164,4 +202,4 @@ const GroupEvents = createReactClass({ }, }); -export default GroupEvents; +export default withEnvironment(GroupEvents); diff --git a/src/sentry/static/sentry/app/views/stream.jsx b/src/sentry/static/sentry/app/views/stream.jsx index ef558aa69455ff..57860c8468c1d5 100644 --- a/src/sentry/static/sentry/app/views/stream.jsx +++ b/src/sentry/static/sentry/app/views/stream.jsx @@ -25,7 +25,7 @@ import StreamFilters from './stream/filters'; import StreamSidebar from './stream/sidebar'; import TimeSince from '../components/timeSince'; import utils from '../utils'; -import streamUtils from './stream/utils'; +import qsUtils from '../utils/queryString'; import {logAjaxError} from '../utils/logging'; import parseLinkHeader from '../utils/parseLinkHeader'; import {t, tn, tct} from '../locale'; @@ -370,7 +370,7 @@ const Stream = createReactClass({ let url = this.getGroupListEndpoint(); // Remove leading and trailing whitespace - let query = streamUtils.formatQueryString(this.state.query); + let query = qsUtils.formatQueryString(this.state.query); let activeEnvironment = this.state.activeEnvironment; let activeEnvName = activeEnvironment ? activeEnvironment.name : null; @@ -385,7 +385,7 @@ const Stream = createReactClass({ // Always keep the global active environment in sync with the queried environment // The global environment wins unless there one is specified by the saved search - const queryEnvironment = streamUtils.getQueryEnvironment(query); + const queryEnvironment = qsUtils.getQueryEnvironment(query); if (queryEnvironment !== null) { // Set the global environment to the one specified by the saved search @@ -396,7 +396,7 @@ const Stream = createReactClass({ requestParams.environment = queryEnvironment; } else if (activeEnvironment) { // Set the environment of the query to match the global settings - query = streamUtils.getQueryStringWithEnvironment(query, activeEnvironment.name); + query = qsUtils.getQueryStringWithEnvironment(query, activeEnvironment.name); requestParams.query = query; requestParams.environment = activeEnvironment.name; } @@ -536,7 +536,7 @@ const Stream = createReactClass({ // the environment parameter is part of the saved search let environment = context.environment; - let query = streamUtils.getQueryStringWithEnvironment( + let query = qsUtils.getQueryStringWithEnvironment( this.state.query, environment === null ? null : environment.name ); diff --git a/tests/js/spec/views/stream/utils.spec.js b/tests/js/spec/utils/queryString.spec.js similarity index 97% rename from tests/js/spec/views/stream/utils.spec.js rename to tests/js/spec/utils/queryString.spec.js index d630bd61779162..c002ddf3d40651 100644 --- a/tests/js/spec/views/stream/utils.spec.js +++ b/tests/js/spec/utils/queryString.spec.js @@ -1,4 +1,4 @@ -import utils from 'app/views/stream/utils'; +import utils from 'app/utils/queryString'; describe('getQueryEnvironment()', function() { it('returns environment name', function() { From e48ff71e650f0a50ecafb4803c97e929ea752aae Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Mon, 26 Feb 2018 17:08:39 -0800 Subject: [PATCH 2/5] fixup! feat(environments): Add group events environment filtering --- src/sentry/static/sentry/app/views/groupEvents.jsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/sentry/static/sentry/app/views/groupEvents.jsx b/src/sentry/static/sentry/app/views/groupEvents.jsx index 855758055d6f86..f4a6b83a2533ba 100644 --- a/src/sentry/static/sentry/app/views/groupEvents.jsx +++ b/src/sentry/static/sentry/app/views/groupEvents.jsx @@ -45,10 +45,11 @@ const GroupEvents = createReactClass({ if (queryEnvironment) { setActiveEnvironment(queryEnvironment); } else if (this.props.environment) { - initialState.query = getQueryStringWithEnvironment( + const newQuery = getQueryStringWithEnvironment( initialState.query, this.props.environment.name ); + this.handleSearch(newQuery); } return initialState; @@ -85,12 +86,7 @@ const GroupEvents = createReactClass({ nextProps.location.query.query || '', nextProps.environment ? nextProps.environment.name : null ); - this.setState( - { - query: newQueryString, - }, - this.handleSearch(newQueryString) - ); + this.handleSearch(newQueryString); } }, From efba6cb4f69804fb0865cf4b3d49f45a9fa13db3 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Mon, 26 Feb 2018 17:44:23 -0800 Subject: [PATCH 3/5] test(ui): Add snapshot test for group events --- .../static/sentry/app/views/groupEvents.jsx | 1 + tests/js/setup.js | 8 +++ .../__snapshots__/groupEvents.spec.jsx.snap | 56 +++++++++++++++++++ tests/js/spec/views/groupEvents.spec.jsx | 30 ++++++++++ 4 files changed, 95 insertions(+) create mode 100644 tests/js/spec/views/__snapshots__/groupEvents.spec.jsx.snap create mode 100644 tests/js/spec/views/groupEvents.spec.jsx diff --git a/src/sentry/static/sentry/app/views/groupEvents.jsx b/src/sentry/static/sentry/app/views/groupEvents.jsx index f4a6b83a2533ba..4bab2cdb0fbd3d 100644 --- a/src/sentry/static/sentry/app/views/groupEvents.jsx +++ b/src/sentry/static/sentry/app/views/groupEvents.jsx @@ -198,4 +198,5 @@ const GroupEvents = createReactClass({ }, }); +export {GroupEvents}; // For tests export default withEnvironment(GroupEvents); diff --git a/tests/js/setup.js b/tests/js/setup.js index 5da47e22307410..0d066b2c127dff 100644 --- a/tests/js/setup.js +++ b/tests/js/setup.js @@ -367,6 +367,13 @@ window.TestStubs = { } }, + Events: () => { + return [ + {eventID: '12345', id: '1', message: 'ApiException', groupID: '1'}, + {eventID: '12346', id: '2', message: 'TestException', groupID: '1'}, + ]; + }, + GitHubRepositoryProvider: params => { return { id: 'github', @@ -402,6 +409,7 @@ window.TestStubs = { '24h': [[1517281200, 2], [1517310000, 1]], '30d': [[1514764800, 1], [1515024000, 122]], }, + tags: [], }; }, diff --git a/tests/js/spec/views/__snapshots__/groupEvents.spec.jsx.snap b/tests/js/spec/views/__snapshots__/groupEvents.spec.jsx.snap new file mode 100644 index 00000000000000..6b6ff222000611 --- /dev/null +++ b/tests/js/spec/views/__snapshots__/groupEvents.spec.jsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`groupEvents renders 1`] = ` +
+
+ +
+
+
+ +
+ +
+
+`; diff --git a/tests/js/spec/views/groupEvents.spec.jsx b/tests/js/spec/views/groupEvents.spec.jsx new file mode 100644 index 00000000000000..1c7e787f0fa560 --- /dev/null +++ b/tests/js/spec/views/groupEvents.spec.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {shallow} from 'enzyme'; + +import {GroupEvents} from 'app/views/groupEvents'; + +describe('groupEvents', function() { + beforeEach(function() { + MockApiClient.addMockResponse({ + url: '/issues/1/events/', + body: TestStubs.Events(), + }); + }); + + it('renders', function() { + const component = shallow( + , + { + context: {...TestStubs.router(), group: TestStubs.Group()}, + childContextTypes: { + router: PropTypes.object, + }, + } + ); + expect(component).toMatchSnapshot(); + }); +}); From daca22d99710791ed7ce1100f1ab5424d2c8bc92 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Mon, 26 Feb 2018 17:49:04 -0800 Subject: [PATCH 4/5] Rename import --- src/sentry/static/sentry/app/views/stream.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sentry/static/sentry/app/views/stream.jsx b/src/sentry/static/sentry/app/views/stream.jsx index 57860c8468c1d5..7a5e335948582b 100644 --- a/src/sentry/static/sentry/app/views/stream.jsx +++ b/src/sentry/static/sentry/app/views/stream.jsx @@ -25,7 +25,7 @@ import StreamFilters from './stream/filters'; import StreamSidebar from './stream/sidebar'; import TimeSince from '../components/timeSince'; import utils from '../utils'; -import qsUtils from '../utils/queryString'; +import queryString from '../utils/queryString'; import {logAjaxError} from '../utils/logging'; import parseLinkHeader from '../utils/parseLinkHeader'; import {t, tn, tct} from '../locale'; @@ -370,7 +370,7 @@ const Stream = createReactClass({ let url = this.getGroupListEndpoint(); // Remove leading and trailing whitespace - let query = qsUtils.formatQueryString(this.state.query); + let query = queryString.formatQueryString(this.state.query); let activeEnvironment = this.state.activeEnvironment; let activeEnvName = activeEnvironment ? activeEnvironment.name : null; @@ -385,7 +385,7 @@ const Stream = createReactClass({ // Always keep the global active environment in sync with the queried environment // The global environment wins unless there one is specified by the saved search - const queryEnvironment = qsUtils.getQueryEnvironment(query); + const queryEnvironment = queryString.getQueryEnvironment(query); if (queryEnvironment !== null) { // Set the global environment to the one specified by the saved search @@ -396,7 +396,7 @@ const Stream = createReactClass({ requestParams.environment = queryEnvironment; } else if (activeEnvironment) { // Set the environment of the query to match the global settings - query = qsUtils.getQueryStringWithEnvironment(query, activeEnvironment.name); + query = queryString.getQueryStringWithEnvironment(query, activeEnvironment.name); requestParams.query = query; requestParams.environment = activeEnvironment.name; } @@ -536,7 +536,7 @@ const Stream = createReactClass({ // the environment parameter is part of the saved search let environment = context.environment; - let query = qsUtils.getQueryStringWithEnvironment( + let query = queryString.getQueryStringWithEnvironment( this.state.query, environment === null ? null : environment.name ); From e1695e3cd67a1d6e78814359d68654182c4747ad Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Mon, 26 Feb 2018 18:31:26 -0800 Subject: [PATCH 5/5] update required snapshot --- .../components/group/__snapshots__/releaseStats.spec.jsx.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/js/spec/components/group/__snapshots__/releaseStats.spec.jsx.snap b/tests/js/spec/components/group/__snapshots__/releaseStats.spec.jsx.snap index 5b13e3cf9a7d79..69e0c5c758ba97 100644 --- a/tests/js/spec/components/group/__snapshots__/releaseStats.spec.jsx.snap +++ b/tests/js/spec/components/group/__snapshots__/releaseStats.spec.jsx.snap @@ -27,6 +27,7 @@ exports[`GroupReleaseStats renders 1`] = ` ], ], }, + "tags": Array [], } } location={ @@ -156,6 +157,7 @@ exports[`GroupReleaseStats renders 1`] = ` ], ], }, + "tags": Array [], } } release={null} @@ -393,6 +395,7 @@ exports[`GroupReleaseStats renders 1`] = ` ], ], }, + "tags": Array [], } } release={null}