Browse Source

chore(issues): activity removal, FE only (#74590)

Split FE and BE apart from
https://github.com/getsentry/sentry/pull/74587
Bartek Ogryczak 7 months ago
parent
commit
2e72dff4ea

+ 0 - 9
static/app/routes.tsx

@@ -1372,14 +1372,6 @@ function buildRoutes() {
     </Fragment>
   );
 
-  const activityRoutes = (
-    <Route
-      path="/activity/"
-      component={make(() => import('sentry/views/organizationActivity'))}
-      withOrgPath
-    />
-  );
-
   const statsRoutes = (
     <Fragment>
       <Route path="/stats/" withOrgPath>
@@ -2105,7 +2097,6 @@ function buildRoutes() {
       {cronsRoutes}
       {replayRoutes}
       {releasesRoutes}
-      {activityRoutes}
       {statsRoutes}
       {discoverRoutes}
       {performanceRoutes}

+ 0 - 470
static/app/views/organizationActivity/activityFeedItem.tsx

@@ -1,470 +0,0 @@
-import {Component, createRef} from 'react';
-import styled from '@emotion/styled';
-
-import {ActivityAvatar} from 'sentry/components/activity/item/avatar';
-import CommitLink from 'sentry/components/commitLink';
-import Duration from 'sentry/components/duration';
-import IssueLink from 'sentry/components/issueLink';
-import ExternalLink from 'sentry/components/links/externalLink';
-import Link from 'sentry/components/links/link';
-import PullRequestLink from 'sentry/components/pullRequestLink';
-import TimeSince from 'sentry/components/timeSince';
-import Version from 'sentry/components/version';
-import VersionHoverCard from 'sentry/components/versionHoverCard';
-import {t, tct, tn} from 'sentry/locale';
-import MemberListStore from 'sentry/stores/memberListStore';
-import TeamStore from 'sentry/stores/teamStore';
-import {space} from 'sentry/styles/space';
-import type {Activity, GroupActivity, Organization} from 'sentry/types';
-import marked from 'sentry/utils/marked';
-
-const defaultProps = {
-  defaultClipped: false,
-  clipHeight: 68,
-};
-type DefaultProps = typeof defaultProps;
-
-type Props = {
-  item: Activity;
-  organization: Organization;
-  className?: string;
-} & DefaultProps;
-
-type State = {
-  clipped: Props['defaultClipped'];
-};
-
-class ActivityItem extends Component<Props, State> {
-  static defaultProps = defaultProps;
-
-  state: State = {
-    clipped: this.props.defaultClipped,
-  };
-
-  componentDidMount() {
-    if (this.activityBubbleRef.current) {
-      const bubbleHeight = this.activityBubbleRef.current.offsetHeight;
-
-      if (bubbleHeight > this.props.clipHeight) {
-        // okay if this causes re-render; cannot determine until
-        // rendered first anyways
-        // eslint-disable-next-line react/no-did-mount-set-state
-        this.setState({clipped: true});
-      }
-    }
-  }
-
-  renderVersionLink(version: string, item: GroupActivity) {
-    const {organization} = this.props;
-    const {project} = item;
-    return version ? (
-      <VersionHoverCard
-        organization={organization}
-        projectSlug={project.slug}
-        releaseVersion={version}
-      >
-        <Version version={version} projectId={project.id} />
-      </VersionHoverCard>
-    ) : null;
-  }
-
-  activityBubbleRef = createRef<HTMLDivElement>();
-
-  formatProjectActivity = (author, item) => {
-    const data = item.data;
-    const {organization} = this.props;
-    const orgId = organization.slug;
-    const issue = item.issue;
-    const basePath = `/organizations/${orgId}/issues/`;
-
-    const issueLink = issue ? (
-      <IssueLink
-        orgId={orgId}
-        issue={issue}
-        to={`${basePath}${issue.id}/?referrer=activity-feed-issue-link`}
-        card
-      >
-        {issue.shortId}
-      </IssueLink>
-    ) : null;
-
-    const versionLink = this.renderVersionLink(data.version, item);
-
-    switch (item.type) {
-      case 'note':
-        return tct('[author] commented on [issue]', {
-          author,
-          issue: (
-            <IssueLink
-              card
-              orgId={orgId}
-              issue={issue}
-              to={`${basePath}${issue.id}/activity/?referrer=activity-comment#event_${item.id}`}
-            >
-              {issue.shortId}
-            </IssueLink>
-          ),
-        });
-      case 'set_resolved':
-        return tct('[author] marked [issue] as resolved', {
-          author,
-          issue: issueLink,
-        });
-      case 'set_resolved_by_age':
-        return tct('[author] marked [issue] as resolved due to age', {
-          author,
-          issue: issueLink,
-        });
-      case 'set_resolved_in_release':
-        const {current_release_version, version} = item.data;
-        if (current_release_version) {
-          return tct(
-            '[author] marked [issue] as resolved in releases greater than [version]',
-            {
-              author,
-              version: this.renderVersionLink(current_release_version, item),
-              issue: issueLink,
-            }
-          );
-        }
-        if (version) {
-          return tct('[author] marked [issue] as resolved in [version]', {
-            author,
-            version: versionLink,
-            issue: issueLink,
-          });
-        }
-        return tct('[author] marked [issue] as resolved in the upcoming release', {
-          author,
-          issue: issueLink,
-        });
-      case 'set_resolved_in_commit':
-        if (data.commit) {
-          return tct('[author] marked [issue] as resolved in [commit]', {
-            author,
-            commit: (
-              <CommitLink
-                inline
-                commitId={data.commit.id}
-                repository={data.commit.repository}
-              />
-            ),
-            issue: issueLink,
-          });
-        }
-        return tct('[author] marked [issue] as resolved in a commit', {
-          author,
-          issue: issueLink,
-        });
-      case 'set_resolved_in_pull_request':
-        return tct('[author] marked [issue] as resolved in [pullRequest]', {
-          author,
-          pullRequest: data.pullRequest ? (
-            <PullRequestLink
-              inline
-              pullRequest={data.pullRequest}
-              repository={data.pullRequest.repository}
-            />
-          ) : (
-            t('PR not available')
-          ),
-          issue: issueLink,
-        });
-      case 'set_unresolved':
-        return tct('[author] marked [issue] as unresolved', {
-          author,
-          issue: issueLink,
-        });
-      case 'set_ignored':
-        if (data.ignoreDuration) {
-          return tct('[author] ignored [issue] for [duration]', {
-            author,
-            duration: <Duration seconds={data.ignoreDuration * 60} />,
-            issue: issueLink,
-          });
-        }
-        if (data.ignoreCount && data.ignoreWindow) {
-          return tct(
-            '[author] ignored [issue] until it happens [count] time(s) in [duration]',
-            {
-              author,
-              count: data.ignoreCount,
-              duration: <Duration seconds={data.ignoreWindow * 60} />,
-              issue: issueLink,
-            }
-          );
-        }
-        if (data.ignoreCount) {
-          return tct('[author] ignored [issue] until it happens [count] time(s)', {
-            author,
-            count: data.ignoreCount,
-            issue: issueLink,
-          });
-        }
-        if (data.ignoreUserCount && data.ignoreUserWindow) {
-          return tct(
-            '[author] ignored [issue] until it affects [count] user(s) in [duration]',
-            {
-              author,
-              count: data.ignoreUserCount,
-              duration: <Duration seconds={data.ignoreUserWindow * 60} />,
-              issue: issueLink,
-            }
-          );
-        }
-        if (data.ignoreUserCount) {
-          return tct('[author] ignored [issue] until it affects [count] user(s)', {
-            author,
-            count: data.ignoreUserCount,
-            issue: issueLink,
-          });
-        }
-        return tct('[author] ignored [issue]', {
-          author,
-          issue: issueLink,
-        });
-      case 'set_public':
-        return tct('[author] made [issue] public', {
-          author,
-          issue: issueLink,
-        });
-      case 'set_private':
-        return tct('[author] made [issue] private', {
-          author,
-          issue: issueLink,
-        });
-      case 'set_regression':
-        if (data.version) {
-          return tct('[author] marked [issue] as a regression in [version]', {
-            author,
-            version: versionLink,
-            issue: issueLink,
-          });
-        }
-        return tct('[author] marked [issue] as a regression', {
-          author,
-          issue: issueLink,
-        });
-      case 'create_issue':
-        return tct('[author] linked [issue] on [provider]', {
-          author,
-          provider: data.provider,
-          issue: issueLink,
-        });
-      case 'unmerge_destination':
-        return tn(
-          '%2$s migrated %1$s fingerprint from %3$s to %4$s',
-          '%2$s migrated %1$s fingerprints from %3$s to %4$s',
-          data.fingerprints.length,
-          author,
-          data.source ? (
-            <a href={`${basePath}${data.source.id}`}>{data.source.shortId}</a>
-          ) : (
-            t('a group')
-          ),
-          issueLink
-        );
-      case 'first_seen':
-        return tct('[author] saw [link:issue]', {
-          author,
-          issue: issueLink,
-        });
-      case 'assigned':
-        let assignee;
-
-        if (data.assigneeType === 'team') {
-          const team = TeamStore.getById(data.assignee);
-          assignee = team ? team.slug : '<unknown-team>';
-
-          return tct('[author] assigned [issue] to #[assignee]', {
-            author,
-            issue: issueLink,
-            assignee,
-          });
-        }
-
-        if (item.user && data.assignee === item.user.id) {
-          return tct('[author] assigned [issue] to themselves', {
-            author,
-            issue: issueLink,
-          });
-        }
-        assignee = MemberListStore.getById(data.assignee);
-        if (assignee?.email) {
-          return tct('[author] assigned [issue] to [assignee]', {
-            author,
-            assignee: <span title={assignee.email}>{assignee.name}</span>,
-            issue: issueLink,
-          });
-        }
-        if (data.assigneeEmail) {
-          return tct('[author] assigned [issue] to [assignee]', {
-            author,
-            assignee: data.assigneeEmail,
-            issue: issueLink,
-          });
-        }
-        return tct('[author] assigned [issue] to an [help:unknown user]', {
-          author,
-          help: <span title={data.assignee} />,
-          issue: issueLink,
-        });
-      case 'unassigned':
-        return tct('[author] unassigned [issue]', {
-          author,
-          issue: issueLink,
-        });
-      case 'merge':
-        return tct('[author] merged [count] [link:issues]', {
-          author,
-          count: data.issues.length + 1,
-          link: <Link to={`${basePath}${issue.id}/?referrer=activity-feed-merge`} />,
-        });
-      case 'release':
-        return tct('[author] released version [version]', {
-          author,
-          version: versionLink,
-        });
-      case 'deploy':
-        return tct('[author] deployed version [version] to [environment].', {
-          author,
-          version: versionLink,
-          environment: data.environment || 'Default Environment',
-        });
-      case 'mark_reviewed':
-        return tct('[author] marked [issue] as reviewed', {
-          author,
-          issue: issueLink,
-        });
-      default:
-        return ''; // should never hit (?)
-    }
-  };
-
-  render() {
-    const {className, item} = this.props;
-
-    const avatar = (
-      <ActivityAvatar
-        type={!item.user ? 'system' : 'user'}
-        user={item.user ?? undefined}
-        size={36}
-      />
-    );
-    const author = {
-      name: item.user ? item.user.name : 'Sentry',
-      avatar,
-    };
-
-    const hasBubble = ['note', 'create_issue'].includes(item.type);
-    const bubbleProps = {
-      ...(item.type === 'note'
-        ? {dangerouslySetInnerHTML: {__html: marked(item.data.text)}}
-        : {}),
-      ...(item.type === 'create_issue'
-        ? {
-            children: (
-              <ExternalLink href={item.data.location}>{item.data.title}</ExternalLink>
-            ),
-          }
-        : {}),
-    };
-
-    return (
-      <div data-test-id="activity-feed-item" className={className}>
-        {author.avatar}
-        <div>
-          {this.formatProjectActivity(
-            <span>
-              <ActivityAuthor>{author.name}</ActivityAuthor>
-            </span>,
-            item
-          )}
-          {hasBubble && (
-            <Bubble
-              ref={this.activityBubbleRef}
-              clipped={this.state.clipped}
-              {...bubbleProps}
-            />
-          )}
-          <Meta>
-            <Project>{item.project.slug}</Project>
-            <StyledTimeSince date={item.dateCreated} />
-          </Meta>
-        </div>
-      </div>
-    );
-  }
-}
-
-export default styled(ActivityItem)`
-  display: grid;
-  gap: ${space(1)};
-  grid-template-columns: max-content auto;
-  position: relative;
-  margin: 0;
-  padding: ${space(1)};
-  border-bottom: 1px solid ${p => p.theme.innerBorder};
-  line-height: 1.4;
-  font-size: ${p => p.theme.fontSizeMedium};
-`;
-
-const ActivityAuthor = styled('span')`
-  font-weight: ${p => p.theme.fontWeightBold};
-`;
-
-const Meta = styled('div')`
-  color: ${p => p.theme.textColor};
-  font-size: ${p => p.theme.fontSizeRelativeSmall};
-`;
-const Project = styled('span')`
-  font-weight: ${p => p.theme.fontWeightBold};
-`;
-
-const Bubble = styled('div')<{clipped: boolean}>`
-  background: ${p => p.theme.backgroundSecondary};
-  margin: ${space(0.5)} 0;
-  padding: ${space(1)} ${space(2)};
-  border: 1px solid ${p => p.theme.border};
-  border-radius: 3px;
-  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
-  position: relative;
-  overflow: hidden;
-
-  a {
-    max-width: 100%;
-    overflow-x: hidden;
-    text-overflow: ellipsis;
-  }
-
-  p {
-    &:last-child {
-      margin-bottom: 0;
-    }
-  }
-
-  ${p =>
-    p.clipped &&
-    `
-    max-height: 68px;
-
-    &:after {
-      position: absolute;
-      content: '';
-      display: block;
-      bottom: 0;
-      right: 0;
-      left: 0;
-      height: 36px;
-      background-image: linear-gradient(180deg, 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;
-    }
-  `}
-`;
-
-const StyledTimeSince = styled(TimeSince)`
-  color: ${p => p.theme.gray300};
-  padding-left: ${space(1)};
-`;

+ 0 - 61
static/app/views/organizationActivity/index.spec.tsx

@@ -1,61 +0,0 @@
-import {ActivityFeedFixture} from 'sentry-fixture/activityFeed';
-
-import {initializeOrg} from 'sentry-test/initializeOrg';
-import {render, screen} from 'sentry-test/reactTestingLibrary';
-
-import {GroupActivityType} from 'sentry/types/group';
-import OrganizationActivity from 'sentry/views/organizationActivity';
-
-describe('OrganizationActivity', function () {
-  const {router, organization} = initializeOrg();
-  let params = {};
-
-  beforeEach(function () {
-    MockApiClient.addMockResponse({
-      url: '/organizations/org-slug/activity/',
-      body: [
-        ActivityFeedFixture(),
-        ActivityFeedFixture({
-          id: '49',
-          data: {},
-          type: GroupActivityType.SET_PUBLIC,
-        }),
-      ],
-    });
-    params = {
-      ...router,
-      params: {
-        orgId: organization.slug,
-      },
-    };
-  });
-
-  it('renders', async function () {
-    render(<OrganizationActivity {...params} />, {router});
-
-    expect(await screen.findAllByTestId('activity-feed-item')).toHaveLength(2);
-  });
-
-  it('renders empty', async function () {
-    MockApiClient.addMockResponse({
-      url: '/organizations/org-slug/activity/',
-      body: [],
-    });
-    render(<OrganizationActivity {...params} />, {router});
-
-    expect(screen.queryByTestId('activity-feed-item')).not.toBeInTheDocument();
-    expect(await screen.findByTestId('empty-state')).toBeInTheDocument();
-  });
-
-  it('renders not found', async function () {
-    MockApiClient.addMockResponse({
-      url: '/organizations/org-slug/activity/',
-      body: [],
-      statusCode: 404,
-    });
-    render(<OrganizationActivity {...params} />, {router});
-
-    expect(screen.queryByTestId('activity-feed-item')).not.toBeInTheDocument();
-    expect(await screen.findByTestId('empty-state')).toBeInTheDocument();
-  });
-});

+ 0 - 85
static/app/views/organizationActivity/index.tsx

@@ -1,85 +0,0 @@
-import EmptyStateWarning from 'sentry/components/emptyStateWarning';
-import ErrorBoundary from 'sentry/components/errorBoundary';
-import * as Layout from 'sentry/components/layouts/thirds';
-import LoadingIndicator from 'sentry/components/loadingIndicator';
-import Pagination from 'sentry/components/pagination';
-import Panel from 'sentry/components/panels/panel';
-import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
-import {t} from 'sentry/locale';
-import AlertStore from 'sentry/stores/alertStore';
-import {space} from 'sentry/styles/space';
-import type {Activity} from 'sentry/types';
-import {useApiQuery} from 'sentry/utils/queryClient';
-import routeTitle from 'sentry/utils/routeTitle';
-import useOrganization from 'sentry/utils/useOrganization';
-
-import ActivityFeedItem from './activityFeedItem';
-
-function OrganizationActivity() {
-  const organization = useOrganization();
-  const {
-    data: activity,
-    isLoading,
-    isError,
-    getResponseHeader,
-  } = useApiQuery<Activity[]>([`/organizations/${organization.slug}/activity/`], {
-    staleTime: 0,
-  });
-
-  AlertStore.addAlert({
-    id: 'organization-activity-deprecation-notice',
-    message: t('This page is deprecated and will be removed in a future release.'),
-    type: 'warning',
-    opaque: true,
-    neverExpire: true,
-    noDuplicates: true,
-  });
-
-  if (isLoading) {
-    return <LoadingIndicator />;
-  }
-
-  if (isError || (!isLoading && !activity.length)) {
-    return (
-      <EmptyStateWarning>
-        <p>{t('Nothing to show here, move along.')}</p>
-      </EmptyStateWarning>
-    );
-  }
-
-  const activityPageLinks = getResponseHeader?.('Link');
-
-  return (
-    <SentryDocumentTitle title={routeTitle(t('Activity'), organization.slug, false)}>
-      <Layout.Page>
-        <Layout.Header>
-          <Layout.HeaderContent>
-            <Layout.Title>{t('Activity')}</Layout.Title>
-          </Layout.HeaderContent>
-        </Layout.Header>
-        <Layout.Body>
-          <Layout.Main fullWidth>
-            <Panel>
-              {!isLoading && (
-                <div data-test-id="activity-feed-list">
-                  {activity.map(item => (
-                    <ErrorBoundary
-                      mini
-                      css={{marginBottom: space(1), borderRadius: 0}}
-                      key={item.id}
-                    >
-                      <ActivityFeedItem organization={organization} item={item} />
-                    </ErrorBoundary>
-                  ))}
-                </div>
-              )}
-            </Panel>
-            {activityPageLinks && <Pagination pageLinks={activityPageLinks} />}
-          </Layout.Main>
-        </Layout.Body>
-      </Layout.Page>
-    </SentryDocumentTitle>
-  );
-}
-
-export default OrganizationActivity;

+ 0 - 38
tests/acceptance/test_organization_activity.py

@@ -1,38 +0,0 @@
-from django.utils import timezone
-
-from sentry.models.activity import Activity
-from sentry.testutils.cases import AcceptanceTestCase
-from sentry.testutils.silo import no_silo_test
-from sentry.types.activity import ActivityType
-
-
-@no_silo_test
-class OrganizationActivityTest(AcceptanceTestCase):
-    def setUp(self):
-        super().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 = f"/organizations/{self.org.slug}/activity/"
-        self.project.update(first_event=timezone.now())
-
-    def test(self):
-        Activity.objects.create(
-            group=self.group,
-            project=self.group.project,
-            type=ActivityType.NOTE.value,
-            user_id=self.user.id,
-            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"]')
-
-    def test_empty(self):
-        self.browser.get(self.path)
-        self.browser.wait_until_not('[data-test-id="loading-indicator"]')