Browse Source

feat(environments): Environment filtering for group details (#12511)

Ref: SEN-238
Lyn Nagara 6 years ago
parent
commit
0460d36400

+ 23 - 10
src/sentry/static/sentry/app/components/errors/groupEventDetailsLoadingError.jsx

@@ -1,35 +1,48 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 
+import SentryTypes from 'app/sentryTypes';
 import {t} from 'app/locale';
 import DetailedError from 'app/components/errors/detailedError';
 
-const GroupEventDetailsLoadingError = ({onRetry}) => {
+const GroupEventDetailsLoadingError = ({onRetry, environments}) => {
   const reasons = [
     t('The events are still processing and are on their way'),
     t('The events have been deleted'),
     t('There is an internal systems error or active issue'),
   ];
 
+  let message;
+
+  if (environments.length === 0) {
+    // All Environments case
+    message = (
+      <div>
+        <p>{t('This could be due to a handful of reasons:')}</p>
+        <ol className="detailed-error-list">
+          {reasons.map((reason, i) => <li key={i}>{reason}</li>)}
+        </ol>
+      </div>
+    );
+  } else {
+    message = (
+      <div>{t('No events were found for the currently selected environments')}</div>
+    );
+  }
+
   return (
     <DetailedError
       className="group-event-details-error"
-      onRetry={onRetry}
+      onRetry={environments.length === 0 ? onRetry : null}
       heading={t('Sorry, the events for this issue could not be found.')}
-      message={
-        <div>
-          <p>{t('This could be due to a handful of reasons:')}</p>
-          <ol className="detailed-error-list">
-            {reasons.map((reason, i) => <li key={i}>{reason}</li>)}
-          </ol>
-        </div>
-      }
+      message={message}
     />
   );
 };
 
 GroupEventDetailsLoadingError.propTypes = {
   onRetry: PropTypes.func,
+  environments: PropTypes.arrayOf(SentryTypes.Environment).isRequired,
 };
 
 export default GroupEventDetailsLoadingError;

+ 40 - 6
src/sentry/static/sentry/app/views/groupDetails/shared/groupEventDetails.jsx

@@ -1,5 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import {isEqual} from 'lodash';
+import {browserHistory} from 'react-router';
 
 import SentryTypes from 'app/sentryTypes';
 import {withMeta} from 'app/components/events/meta/metaProxy';
@@ -14,7 +16,7 @@ import withOrganization from 'app/utils/withOrganization';
 import fetchSentryAppInstallations from 'app/utils/fetchSentryAppInstallations';
 
 import GroupEventToolbar from './eventToolbar';
-import {fetchGroupEventAndMarkSeen} from './utils';
+import {fetchGroupEventAndMarkSeen, getEventEnvironment} from './utils';
 
 class GroupEventDetails extends React.Component {
   static propTypes = {
@@ -39,8 +41,34 @@ class GroupEventDetails extends React.Component {
     this.fetchData();
   }
 
-  componentDidUpdate(prevProps) {
-    if (prevProps.params.eventId !== this.props.params.eventId) {
+  componentDidUpdate(prevProps, prevState) {
+    const {environments, params, location} = this.props;
+
+    const eventHasChanged = prevProps.params.eventId !== params.eventId;
+    const environmentsHaveChanged = !isEqual(prevProps.environments, environments);
+
+    // If environments are being actively changed and will no longer contain the
+    // current event's environment, redirect to latest
+    if (
+      environmentsHaveChanged &&
+      prevState.event &&
+      params.eventId &&
+      !['latest', 'oldest'].includes(params.eventId)
+    ) {
+      const shouldRedirect =
+        environments.length > 0 &&
+        !environments.find(env => env.name === getEventEnvironment(prevState.event));
+
+      if (shouldRedirect) {
+        browserHistory.replace({
+          pathname: `/organizations/${params.orgId}/issues/${params.groupId}/`,
+          query: location.query,
+        });
+        return;
+      }
+    }
+
+    if (eventHasChanged || environmentsHaveChanged) {
       this.fetchData();
     }
   }
@@ -50,7 +78,7 @@ class GroupEventDetails extends React.Component {
   }
 
   fetchData = () => {
-    const {api, group, project, organization, params} = this.props;
+    const {api, group, project, organization, params, environments} = this.props;
     const eventId = params.eventId || 'latest';
     const groupId = group.id;
     const orgSlug = organization.slug;
@@ -61,7 +89,9 @@ class GroupEventDetails extends React.Component {
       error: false,
     });
 
-    fetchGroupEventAndMarkSeen(api, orgSlug, projSlug, groupId, eventId)
+    const envNames = environments.map(e => e.name);
+
+    fetchGroupEventAndMarkSeen(api, orgSlug, projSlug, groupId, eventId, envNames)
       .then(data => {
         this.setState({
           event: data,
@@ -71,6 +101,7 @@ class GroupEventDetails extends React.Component {
       })
       .catch(() => {
         this.setState({
+          event: null,
           error: true,
           loading: false,
         });
@@ -119,7 +150,10 @@ class GroupEventDetails extends React.Component {
             {this.state.loading ? (
               <LoadingIndicator />
             ) : this.state.error ? (
-              <GroupEventDetailsLoadingError onRetry={this.fetchData} />
+              <GroupEventDetailsLoadingError
+                environments={environments}
+                onRetry={this.fetchData}
+              />
             ) : (
               <EventEntries
                 group={group}

+ 26 - 2
src/sentry/static/sentry/app/views/groupDetails/shared/utils.jsx

@@ -9,13 +9,25 @@ import {Client} from 'app/api';
  * @param {String} eventId eventId or "latest" or "oldest"
  * @returns {Promise<Object>}
  */
-export function fetchGroupEventAndMarkSeen(api, orgId, projectId, groupId, eventId) {
+export function fetchGroupEventAndMarkSeen(
+  api,
+  orgId,
+  projectId,
+  groupId,
+  eventId,
+  envNames
+) {
   const url =
     eventId === 'latest' || eventId === 'oldest'
       ? `/issues/${groupId}/events/${eventId}/`
       : `/projects/${orgId}/${projectId}/events/${eventId}/`;
 
-  const promise = api.requestPromise(url);
+  const query = {};
+  if (envNames.length !== 0) {
+    query.environment = envNames;
+  }
+
+  const promise = api.requestPromise(url, {query});
 
   promise.then(data => {
     api.bulkUpdate({
@@ -39,3 +51,15 @@ export function fetchGroupUserReports(groupId, query) {
     query,
   });
 }
+
+/**
+ * Returns the environment name for an event or null
+ *
+ * @param {Object} event
+ * @returns {String|Void}
+ */
+export function getEventEnvironment(event) {
+  const tag = event.tags.find(({key}) => key === 'environment');
+
+  return tag ? tag.value : null;
+}

+ 57 - 0
tests/js/spec/views/groupDetails/groupEventDetails.spec.jsx

@@ -1,5 +1,6 @@
 import React from 'react';
 import {mount} from 'enzyme';
+import {browserHistory} from 'react-router';
 
 import {initializeOrg} from 'app-test/helpers/initializeOrg';
 import GroupEventDetails from 'app/views/groupDetails/shared/groupEventDetails';
@@ -24,6 +25,7 @@ describe('groupEventDetails', () => {
       dateCreated: '2019-03-20T00:00:00.000Z',
       errors: [],
       entries: [],
+      tags: [{key: 'environment', value: 'dev'}],
     });
 
     MockApiClient.addMockResponse({
@@ -77,6 +79,59 @@ describe('groupEventDetails', () => {
     });
   });
 
+  afterEach(function() {
+    MockApiClient.clearMockResponses();
+    browserHistory.replace.mockClear();
+  });
+
+  it('redirects on switching to an invalid environment selection for event', async function() {
+    MockApiClient.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/events/1/`,
+      body: event,
+    });
+    const wrapper = mount(
+      <GroupEventDetails
+        group={group}
+        project={project}
+        organization={org}
+        environments={[{id: '1', name: 'dev', displayName: 'Dev'}]}
+        params={{orgId: org.slug, groupId: group.id, eventId: '1'}}
+        location={{}}
+      />,
+      routerContext
+    );
+    await tick();
+    expect(browserHistory.replace).not.toHaveBeenCalled();
+    wrapper.setProps({environments: [{id: '1', name: 'prod', displayName: 'Prod'}]});
+    await tick();
+
+    expect(browserHistory.replace).toHaveBeenCalled();
+  });
+
+  it('does not redirect when switching to a valid environment selection for event', async function() {
+    MockApiClient.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/events/1/`,
+      body: event,
+    });
+    const wrapper = mount(
+      <GroupEventDetails
+        group={group}
+        project={project}
+        organization={org}
+        environments={[{id: '1', name: 'dev', displayName: 'Dev'}]}
+        params={{orgId: org.slug, group: group.id, eventId: '1'}}
+        location={{}}
+      />,
+      routerContext
+    );
+    await tick();
+    expect(browserHistory.replace).not.toHaveBeenCalled();
+    wrapper.setProps({environments: []});
+    await tick();
+
+    expect(browserHistory.replace).not.toHaveBeenCalled();
+  });
+
   it("doesn't load Sentry Apps without being flagged in", () => {
     const request = MockApiClient.addMockResponse({
       url: '/sentry-apps/',
@@ -90,6 +145,7 @@ describe('groupEventDetails', () => {
         organization={org}
         environments={[{id: '1', name: 'dev', displayName: 'Dev'}]}
         params={{}}
+        location={{}}
       />,
       routerContext
     );
@@ -113,6 +169,7 @@ describe('groupEventDetails', () => {
         organization={org}
         environments={[{id: '1', name: 'dev', displayName: 'Dev'}]}
         params={{}}
+        location={{}}
       />,
       routerContext
     );