Browse Source

feat(issues) Add waiting/sample state to org issue list (#11647)

Add the ErrorRobot waiting state to the org issue list. I added tests
for ErrorRobot as there were none before.

Fixes APP-993
Mark Story 6 years ago
parent
commit
9068d4986c

+ 40 - 22
src/sentry/static/sentry/app/components/errorRobot.jsx

@@ -7,6 +7,7 @@ import styled from 'react-emotion';
 import {addErrorMessage} from 'app/actionCreators/indicator';
 import {analytics} from 'app/utils/analytics';
 import {sendSampleEvent} from 'app/actionCreators/projects';
+import Button from 'app/components/button';
 import {t} from 'app/locale';
 import ApiMixin from 'app/mixins/apiMixin';
 
@@ -15,7 +16,7 @@ const ErrorRobot = createReactClass({
 
   propTypes: {
     org: PropTypes.object.isRequired,
-    project: PropTypes.object.isRequired,
+    project: PropTypes.object,
     // sampleIssueId can have 3 values:
     // - empty string to indicate it doesn't exist (render "create sample event")
     // - non-empty string to indicate it exists (render "see sample event")
@@ -29,8 +30,7 @@ const ErrorRobot = createReactClass({
   getInitialState() {
     return {
       error: false,
-      loading:
-        this.props.sampleIssueId === null || this.props.sampleIssueId === undefined,
+      loading: false,
       sampleIssueId: this.props.sampleIssueId,
     };
   },
@@ -43,9 +43,15 @@ const ErrorRobot = createReactClass({
     let {org, project} = this.props;
     let {sampleIssueId} = this.state;
 
+    if (!project) {
+      return;
+    }
+
     if (sampleIssueId === null || sampleIssueId === undefined) {
       let url = '/projects/' + org.slug + '/' + project.slug + '/issues/';
       let requestParams = {limit: 1};
+
+      this.setState({loading: true});
       this.api.request(url, {
         method: 'GET',
         data: requestParams,
@@ -89,19 +95,27 @@ const ErrorRobot = createReactClass({
     let sampleLink;
 
     if (!loading && !error) {
-      sampleLink =
-        sampleIssueId === '' ? (
-          <p>
-            <a onClick={this.createSampleEvent}>{t('Create a sample event')}</a>
-          </p>
-        ) : (
-          <p>
-            <Link to={`/${org.slug}/${project.slug}/issues/${sampleIssueId}/?sample`}>
-              {t('Or see your sample event')}
-            </Link>
-          </p>
-        );
+      sampleLink = sampleIssueId ? (
+        <p>
+          <Link to={`/${org.slug}/${project.slug}/issues/${sampleIssueId}/?sample`}>
+            {t('Or see your sample event')}
+          </Link>
+        </p>
+      ) : (
+        <p>
+          <Button
+            priority="link"
+            borderless
+            size="large"
+            disabled={!project}
+            onClick={this.createSampleEvent}
+          >
+            {t('Create a sample event')}
+          </Button>
+        </p>
+      );
     }
+
     return (
       <ErrorRobotWrapper
         data-test-id="awaiting-events"
@@ -123,13 +137,17 @@ const ErrorRobot = createReactClass({
             </span>
           </p>
           <p>
-            <Link
-              to={`/${org.slug}/${project.slug}/getting-started/${project.platform ||
-                ''}`}
-              className="btn btn-primary btn-lg"
-            >
-              {t('Installation Instructions')}
-            </Link>
+            {project && (
+              <Button
+                data-test-id="install-instructions"
+                priority="primary"
+                size="large"
+                to={`/${org.slug}/${project.slug}/getting-started/${project.platform ||
+                  ''}`}
+              >
+                {t('Installation Instructions')}
+              </Button>
+            )}
           </p>
           {sampleLink}
         </div>

+ 34 - 0
src/sentry/static/sentry/app/views/organizationStream/overview.jsx

@@ -11,6 +11,7 @@ import {Client} from 'app/api';
 import {Panel, PanelBody} from 'app/components/panels';
 import {analytics} from 'app/utils/analytics';
 import {t} from 'app/locale';
+import ErrorRobot from 'app/components/errorRobot';
 import {fetchProject} from 'app/actionCreators/projects';
 import {fetchTags} from 'app/actionCreators/tags';
 import {fetchOrgMembers} from 'app/actionCreators/members';
@@ -199,6 +200,16 @@ const OrganizationStream = createReactClass({
     return new Set(this.props.organization.access);
   },
 
+  /**
+   * Get the projects that are selected in the global filters
+   */
+  getGlobalSearchProjects() {
+    let {projects} = this.props.selection;
+    projects = projects.map(p => p.toString());
+
+    return this.props.organization.projects.filter(p => projects.indexOf(p.id) > -1);
+  },
+
   fetchData() {
     this.fetchProcessingIssues();
     GroupStore.loadInitialData([]);
@@ -417,6 +428,10 @@ const OrganizationStream = createReactClass({
     this.fetchProject(uniqProjects[0]);
   },
 
+  /**
+   * Fetch the selected project from the API
+   * We need to do this as `props.organization.projects` lacks the `latestRelease` property
+   */
   fetchProject(projectSlug) {
     if (projectSlug in this.projectCache) {
       this.setState({selectedProject: this.projectCache[projectSlug]});
@@ -517,6 +532,8 @@ const OrganizationStream = createReactClass({
 
   renderStreamBody() {
     let body;
+    let selectedProjects = this.getGlobalSearchProjects();
+    let noEvents = selectedProjects.filter(p => !p.firstEvent).length > 0;
 
     if (this.state.loading) {
       body = this.renderLoading();
@@ -524,6 +541,8 @@ const OrganizationStream = createReactClass({
       body = <LoadingError message={this.state.error} onRetry={this.fetchData} />;
     } else if (this.state.groupIds.length > 0) {
       body = this.renderGroupNodes(this.state.groupIds, this.getGroupStatsPeriod());
+    } else if (noEvents) {
+      body = this.renderAwaitingEvents(selectedProjects);
     } else {
       body = this.renderEmpty();
     }
@@ -573,6 +592,21 @@ const OrganizationStream = createReactClass({
     });
   },
 
+  renderAwaitingEvents(projects) {
+    let {organization} = this.props;
+    let project = projects.length > 0 ? projects[0] : null;
+
+    let sampleIssueId = this.state.groupIds.length > 0 ? this.state.groupIds[0] : '';
+    return (
+      <ErrorRobot
+        org={organization}
+        project={project}
+        sampleIssueId={sampleIssueId}
+        gradient={true}
+      />
+    );
+  },
+
   render() {
     if (this.state.loading) {
       return this.renderLoading();

+ 1 - 1
tests/acceptance/test_onboarding.py

@@ -47,4 +47,4 @@ class OrganizationOnboardingTest(AcceptanceTestCase):
         self.browser.wait_until_not('.loading')
 
         assert self.browser.element_exists('.robot')
-        assert self.browser.element_exists('.btn-primary')
+        assert self.browser.element_exists('[data-test-id="install-instructions"]')

+ 73 - 0
tests/js/spec/components/errorRobot.spec.jsx

@@ -0,0 +1,73 @@
+import React from 'react';
+import {browserHistory} from 'react-router';
+import {shallow} from 'enzyme';
+import {Client} from 'app/api';
+import ErrorRobot from 'app/components/errorRobot';
+
+describe('ErrorRobot', function() {
+  let getIssues;
+
+  beforeEach(function() {
+    Client.clearMockResponses();
+    getIssues = Client.addMockResponse({
+      url: '/projects/org-slug/project-slug/issues/',
+      method: 'GET',
+      body: [],
+    });
+  });
+
+  describe('with a project', function() {
+    let wrapper;
+    beforeEach(function() {
+      wrapper = shallow(
+        <ErrorRobot org={TestStubs.Organization()} project={TestStubs.Project()} />
+      );
+    });
+
+    it('Renders a button for creating an event', function() {
+      let button = wrapper.find('Button[priority="link"]');
+      expect(button).toHaveLength(1);
+      expect(button.props().onClick).toBeDefined();
+      expect(button.props().disabled).toBeFalsy();
+      expect(getIssues).toHaveBeenCalled();
+    });
+
+    it('Renders installation instructions', function() {
+      let button = wrapper.find('Button[priority="primary"]');
+      expect(button).toHaveLength(1);
+      expect(button.props().to).toEqual(expect.stringContaining('getting-started'));
+    });
+
+    it('can create a sample event', async function() {
+      Client.addMockResponse({
+        url: '/projects/org-slug/project-slug/create-sample/',
+        method: 'POST',
+        body: {groupID: 999},
+      });
+      wrapper.find('Button[priority="link"]').simulate('click');
+      await wrapper.update();
+
+      expect(browserHistory.push).toHaveBeenCalled();
+    });
+  });
+
+  describe('without a project', function() {
+    let wrapper;
+
+    beforeEach(function() {
+      wrapper = shallow(<ErrorRobot org={TestStubs.Organization()} />);
+    });
+
+    it('Renders a disabled create event button', function() {
+      let button = wrapper.find('Button[priority="link"]');
+      expect(button).toHaveLength(1);
+      expect(button.props().disabled).toBeTruthy();
+      expect(getIssues).toHaveBeenCalledTimes(0);
+    });
+
+    it('does not display install instructions', function() {
+      let button = wrapper.find('Button[priority="primary"]');
+      expect(button).toHaveLength(0);
+    });
+  });
+});