Browse Source

ref(ui): Move "Activity Feed" components (#12989)

Billy Vong 5 years ago
parent
commit
5909e60693

+ 25 - 32
src/sentry/static/sentry/app/components/activity/feed.jsx → src/sentry/static/sentry/app/views/organizationActivity/activityFeed.jsx

@@ -1,9 +1,9 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 
-import {logException} from 'app/utils/logging';
+import {Panel} from 'app/components/panels';
 import {t} from 'app/locale';
-import ActivityItem from 'app/components/activity/item';
+import EmptyMessage from 'app/views/settings/components/emptyMessage';
 import ErrorBoundary from 'app/components/errorBoundary';
 import LoadingError from 'app/components/loadingError';
 import LoadingIndicator from 'app/components/loadingIndicator';
@@ -12,6 +12,8 @@ import SentryTypes from 'app/sentryTypes';
 import space from 'app/styles/space';
 import withApi from 'app/utils/withApi';
 
+import ActivityFeedItem from './activityFeedItem';
+
 class ActivityFeed extends React.Component {
   static propTypes = {
     api: PropTypes.object,
@@ -92,27 +94,18 @@ class ActivityFeed extends React.Component {
       body = <LoadingError onRetry={this.fetchData} />;
     } else if (this.state.itemList.length > 0) {
       body = (
-        <div className="activity-container">
-          <ul className="activity">
-            {this.state.itemList.map(item => {
-              try {
-                return (
-                  <ErrorBoundary
-                    mini
-                    css={{marginBottom: space(1), borderRadius: 0}}
-                    key={item.id}
-                  >
-                    <ActivityItem organization={this.props.organization} item={item} />
-                  </ErrorBoundary>
-                );
-              } catch (ex) {
-                logException(ex, {
-                  itemId: item.id,
-                });
-                return null;
-              }
-            })}
-          </ul>
+        <div data-test-id="activity-feed-list" className="activity">
+          {this.state.itemList.map(item => {
+            return (
+              <ErrorBoundary
+                mini
+                css={{marginBottom: space(1), borderRadius: 0}}
+                key={item.id}
+              >
+                <ActivityFeedItem organization={this.props.organization} item={item} />
+              </ErrorBoundary>
+            );
+          })}
         </div>
       );
     } else {
@@ -123,25 +116,25 @@ class ActivityFeed extends React.Component {
   }
 
   renderLoading() {
-    return (
-      <div className="box">
-        <LoadingIndicator />
-      </div>
-    );
+    return <LoadingIndicator />;
   }
 
   renderEmpty() {
-    return <div className="box empty">{t('Nothing to show here, move along.')}</div>;
+    return (
+      <EmptyMessage icon="icon-circle-exclamation">
+        {t('Nothing to show here, move along.')}
+      </EmptyMessage>
+    );
   }
 
   render() {
     return (
-      <div>
-        {this.renderResults()}
+      <React.Fragment>
+        <Panel>{this.renderResults()}</Panel>
         {this.props.pagination && this.state.pageLinks && (
           <Pagination pageLinks={this.state.pageLinks} {...this.props} />
         )}
-      </div>
+      </React.Fragment>
     );
   }
 }

+ 28 - 19
src/sentry/static/sentry/app/components/activity/item.jsx → src/sentry/static/sentry/app/views/organizationActivity/activityFeedItem.jsx

@@ -1,22 +1,22 @@
+import {Link} from 'react-router';
 import PropTypes from 'prop-types';
 import React from 'react';
-import {Link} from 'react-router';
 import marked from 'marked';
+import styled from 'react-emotion';
 
-import PullRequestLink from 'app/components/pullRequestLink';
-
+import {t, tn, tct} from 'app/locale';
+import Avatar from 'app/components/avatar';
 import CommitLink from 'app/components/commitLink';
 import Duration from 'app/components/duration';
-import Avatar from 'app/components/avatar';
 import IssueLink from 'app/components/issueLink';
-import VersionHoverCard from 'app/components/versionHoverCard';
 import MemberListStore from 'app/stores/memberListStore';
+import PullRequestLink from 'app/components/pullRequestLink';
 import SentryTypes from 'app/sentryTypes';
 import TeamStore from 'app/stores/teamStore';
 import TimeSince from 'app/components/timeSince';
 import Version from 'app/components/version';
-
-import {t, tn, tct} from 'app/locale';
+import VersionHoverCard from 'app/components/versionHoverCard';
+import space from 'app/styles/space';
 
 class ActivityItem extends React.Component {
   static propTypes = {
@@ -348,12 +348,12 @@ class ActivityItem extends React.Component {
     if (item.type === 'note') {
       const noteBody = marked(item.data.text);
       return (
-        <li className="activity-item activity-item-compact">
+        <div data-test-id="activity-item" className="activity-item activity-item-compact">
           <div className="activity-item-content">
             {this.formatProjectActivity(
               <span>
                 {author.avatar}
-                <span className="activity-author">{author.name}</span>
+                <ActivityAuthor>{author.name}</ActivityAuthor>
               </span>,
               item
             )}
@@ -365,19 +365,19 @@ class ActivityItem extends React.Component {
             <div className="activity-meta">
               {projectLink}
               <span className="bullet" />
-              <TimeSince date={item.dateCreated} />
+              <StyledTimeSince date={item.dateCreated} />
             </div>
           </div>
-        </li>
+        </div>
       );
     } else if (item.type === 'create_issue') {
       return (
-        <li className="activity-item activity-item-compact">
+        <div data-test-id="activity-item" className="activity-item activity-item-compact">
           <div className="activity-item-content">
             {this.formatProjectActivity(
               <span>
                 {author.avatar}
-                <span className="activity-author">{author.name}</span>
+                <ActivityAuthor>{author.name}</ActivityAuthor>
               </span>,
               item
             )}
@@ -387,32 +387,41 @@ class ActivityItem extends React.Component {
             <div className="activity-meta">
               {projectLink}
               <span className="bullet" />
-              <TimeSince date={item.dateCreated} />
+              <StyledTimeSince date={item.dateCreated} />
             </div>
           </div>
-        </li>
+        </div>
       );
     } else {
       return (
-        <li className="activity-item activity-item-compact">
+        <div data-test-id="activity-item" className="activity-item activity-item-compact">
           <div className="activity-item-content">
             {this.formatProjectActivity(
               <span>
                 {author.avatar}
-                <span className="activity-author">{author.name}</span>
+                <ActivityAuthor>{author.name}</ActivityAuthor>
               </span>,
               item
             )}
             <div className="activity-meta">
               {projectLink}
               <span className="bullet" />
-              <TimeSince date={item.dateCreated} />
+              <StyledTimeSince date={item.dateCreated} />
             </div>
           </div>
-        </li>
+        </div>
       );
     }
   }
 }
 
 export default ActivityItem;
+
+const ActivityAuthor = styled('span')`
+  font-weight: 600;
+`;
+
+const StyledTimeSince = styled(TimeSince)`
+  color: ${p => p.theme.gray2};
+  padding-left: ${space(1)};
+`;

+ 6 - 6
src/sentry/static/sentry/app/views/organizationActivity.jsx → src/sentry/static/sentry/app/views/organizationActivity/index.jsx

@@ -1,16 +1,16 @@
-import React from 'react';
-import PropTypes from 'prop-types';
 import DocumentTitle from 'react-document-title';
+import PropTypes from 'prop-types';
+import React from 'react';
 
-import ActivityFeed from 'app/components/activity/feed';
+import {PageContent} from 'app/styles/organization';
+import {t} from 'app/locale';
 import OrganizationHomeContainer from 'app/components/organizations/homeContainer';
 import PageHeading from 'app/components/pageHeading';
-
-import {t} from 'app/locale';
 import SentryTypes from 'app/sentryTypes';
-import {PageContent} from 'app/styles/organization';
 import withOrganization from 'app/utils/withOrganization';
 
+import ActivityFeed from './activityFeed';
+
 class OrganizationActivityContainer extends React.Component {
   static propTypes = {
     organization: SentryTypes.Organization,

+ 2 - 3
src/sentry/static/sentry/less/organization.less

@@ -36,9 +36,8 @@
     margin: 30px 0;
   }
   .activity {
-    .box;
-
     .activity-item {
+      position: relative;
       margin: 0;
       padding: 10px 15px 10px 60px;
       border-bottom: 1px solid lighten(@trim, 5);
@@ -87,7 +86,7 @@
             right: 0;
             left: 0;
             height: 36px;
-            #gradient > .vertical(rgba(255, 255, 255, 0.15), rgba(255,255,255, 1));
+            #gradient > .vertical(rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 1));
             border-bottom: 6px solid #fff;
             border-radius: 0 0 3px 3px;
             pointer-events: none;

+ 44 - 0
tests/acceptance/test_organization_activity.py

@@ -0,0 +1,44 @@
+from __future__ import absolute_import
+
+from django.utils import timezone
+
+from sentry.models import Activity
+from sentry.testutils import AcceptanceTestCase
+
+
+class OrganizationActivityTest(AcceptanceTestCase):
+    def setUp(self):
+        super(OrganizationActivityTest, self).setUp()
+        self.org = self.create_organization(owner=self.user, name='Rowdy Tiger')
+        self.team = self.create_team(
+            organization=self.org,
+            name='Mariachi Band',
+            members=[self.user])
+        self.project = self.create_project(
+            organization=self.org,
+            teams=[self.team],
+            name='Bengal',
+        )
+        self.group = self.create_group(project=self.project)
+        self.login_as(self.user)
+        self.path = u'/organizations/{}/activity/'.format(self.org.slug)
+        self.project.update(first_event=timezone.now())
+
+    def test(self):
+        Activity.objects.create(
+            group=self.group,
+            project=self.group.project,
+            type=Activity.NOTE,
+            user=self.user,
+            data={'text': 'hello world'},
+        )
+
+        self.browser.get(self.path)
+        self.browser.wait_until_not('.loading-indicator', timeout=100000)
+        self.browser.wait_until('[data-test-id="activity-feed-list"]')
+        self.browser.snapshot('organization activity feed')
+
+    def test_empty(self):
+        self.browser.get(self.path)
+        self.browser.wait_until_not('.loading-indicator')
+        self.browser.snapshot('organization activity feed - empty')

+ 88 - 0
tests/js/fixtures/activityFeed.js

@@ -0,0 +1,88 @@
+export function ActivityFeed(params) {
+  return {
+    data: {text: 'Very interesting comment'},
+    dateCreated: '2019-04-29T21:43:32.280Z',
+    project: {
+      status: 'active',
+      features: [
+        'releases',
+        'sample-events',
+        'minidump',
+        'rate-limits',
+        'similarity-indexing',
+        'similarity-view',
+        'data-forwarding',
+      ],
+      color: '#bf873f',
+      isInternal: true,
+      isPublic: false,
+      dateCreated: '2019-03-09T06:52:19.832Z',
+      id: '1',
+      slug: 'internal',
+      name: 'Internal',
+      hasAccess: true,
+      isBookmarked: false,
+      platform: null,
+      firstEvent: '2019-03-09T06:56:15Z',
+      avatar: {avatarUuid: null, avatarType: 'letter_avatar'},
+      isMember: true,
+    },
+    user: {
+      username: 'billy@sentry.io',
+      lastLogin: '2019-04-23T00:10:19.787Z',
+      isSuperuser: true,
+      emails: [{is_verified: false, id: '1', email: 'billy@sentry.io'}],
+      isManaged: false,
+      lastActive: '2019-04-30T01:39:05.659Z',
+      identities: [],
+      id: '1',
+      isActive: true,
+      has2fa: false,
+      name: 'billy@sentry.io',
+      avatarUrl:
+        'https://secure.gravatar.com/avatar/7b544e8eb9d08ed777be5aa82121155a?s=32&d=mm',
+      dateJoined: '2019-03-09T06:52:42.836Z',
+      options: {
+        timezone: 'America/Los_Angeles',
+        stacktraceOrder: -1,
+        language: 'en',
+        clock24Hours: false,
+      },
+      flags: {newsletter_consent_prompt: false},
+      avatar: {avatarUuid: null, avatarType: 'letter_avatar'},
+      hasPasswordAuth: true,
+      email: 'billy@sentry.io',
+    },
+    type: 'note',
+    issue: {
+      platform: 'javascript',
+      lastSeen: '2019-04-26T16:34:12.288Z',
+      numComments: 3,
+      userCount: 1,
+      culprit: '/organizations/:orgId/issues/:groupId/feedback/',
+      title: 'Error: user efedback',
+      id: '524',
+      assignedTo: null,
+      logger: null,
+      type: 'error',
+      annotations: [],
+      metadata: {type: 'Error', value: 'user efedback', filename: '<anonymous>'},
+      status: 'unresolved',
+      subscriptionDetails: {reason: 'commented'},
+      isPublic: false,
+      hasSeen: true,
+      shortId: 'INTERNAL-DW',
+      shareId: null,
+      firstSeen: '2019-04-26T16:34:12.288Z',
+      count: '1',
+      permalink: 'http://localhost:8000/organizations/sentry/issues/524/?project=1',
+      level: 'error',
+      isSubscribed: true,
+      isBookmarked: false,
+      project: {platform: null, slug: 'internal', id: '1', name: 'Internal'},
+      statusDetails: {},
+    },
+    id: '48',
+    ...params,
+  };
+}

+ 47 - 0
tests/js/spec/views/organizationActivity/index.spec.jsx

@@ -0,0 +1,47 @@
+import React from 'react';
+import {mount} from 'enzyme';
+
+import {initializeOrg} from 'app-test/helpers/initializeOrg';
+import OrganizationActivity from 'app/views/organizationActivity';
+
+describe('OrganizationUserFeedback', function() {
+  const {router, organization, routerContext} = initializeOrg();
+  let params = {};
+
+  beforeEach(function() {
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/activity/',
+      body: [
+        TestStubs.ActivityFeed(),
+        TestStubs.ActivityFeed({
+          id: '49',
+          data: {},
+          type: 'set_public',
+        }),
+      ],
+    });
+    params = {
+      ...router,
+      params: {
+        orgId: organization.slug,
+      },
+    };
+  });
+
+  it('renders', function() {
+    const wrapper = mount(<OrganizationActivity {...params} />, routerContext);
+
+    expect(wrapper.find('[data-test-id="activity-item"]')).toHaveLength(2);
+  });
+
+  it('renders empty', function() {
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/activity/',
+      body: [],
+    });
+    const wrapper = mount(<OrganizationActivity {...params} />, routerContext);
+
+    expect(wrapper.find('[data-test-id="activity-item"]')).toHaveLength(0);
+    expect(wrapper.find('EmptyMessage')).toHaveLength(1);
+  });
+});