123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- import {Fragment} from 'react';
- import styled from '@emotion/styled';
- import moment from 'moment';
- import CommitLink from 'sentry/components/commitLink';
- import Duration from 'sentry/components/duration';
- import ExternalLink from 'sentry/components/links/externalLink';
- import Link from 'sentry/components/links/link';
- import PullRequestLink from 'sentry/components/pullRequestLink';
- import Version from 'sentry/components/version';
- import {t, tct, tn} from 'sentry/locale';
- import TeamStore from 'sentry/stores/teamStore';
- import {
- GroupActivity,
- GroupActivityAssigned,
- GroupActivitySetIgnored,
- GroupActivityType,
- Organization,
- Project,
- User,
- } from 'sentry/types';
- type Props = {
- activity: GroupActivity;
- author: React.ReactNode;
- orgSlug: Organization['slug'];
- projectId: Project['id'];
- };
- function GroupActivityItem({activity, orgSlug, projectId, author}: Props) {
- const issuesLink = `/organizations/${orgSlug}/issues/`;
- function getIgnoredMessage(data: GroupActivitySetIgnored['data']) {
- if (data.ignoreDuration) {
- return tct('[author] ignored this issue for [duration]', {
- author,
- duration: <Duration seconds={data.ignoreDuration * 60} />,
- });
- }
- if (data.ignoreCount && data.ignoreWindow) {
- return tct(
- '[author] ignored this issue until it happens [count] time(s) in [duration]',
- {
- author,
- count: data.ignoreCount,
- duration: <Duration seconds={data.ignoreWindow * 60} />,
- }
- );
- }
- if (data.ignoreCount) {
- return tct('[author] ignored this issue until it happens [count] time(s)', {
- author,
- count: data.ignoreCount,
- });
- }
- if (data.ignoreUserCount && data.ignoreUserWindow) {
- return tct(
- '[author] ignored this issue until it affects [count] user(s) in [duration]',
- {
- author,
- count: data.ignoreUserCount,
- duration: <Duration seconds={data.ignoreUserWindow * 60} />,
- }
- );
- }
- if (data.ignoreUserCount) {
- return tct('[author] ignored this issue until it affects [count] user(s)', {
- author,
- count: data.ignoreUserCount,
- });
- }
- return tct('[author] ignored this issue', {author});
- }
- function getAssignedMessage(data: GroupActivityAssigned['data']) {
- let assignee: string | User | undefined = undefined;
- if (data.assigneeType === 'team') {
- const team = TeamStore.getById(data.assignee);
- assignee = team ? `#${team.slug}` : '<unknown-team>';
- } else if (activity.user && data.assignee === activity.user.id) {
- assignee = t('themselves');
- } else if (data.assigneeType === 'user' && data.assigneeEmail) {
- assignee = data.assigneeEmail;
- } else {
- assignee = t('an unknown user');
- }
- const isAutoAssigned = ['projectOwnership', 'codeowners'].includes(
- data.integration as string
- );
- const integrationName: Record<
- NonNullable<GroupActivityAssigned['data']['integration']>,
- string
- > = {
- msteams: t('Microsoft Teams'),
- slack: t('Slack'),
- projectOwnership: t('Ownership Rule'),
- codeowners: t('Codeowners Rule'),
- };
- return (
- <Fragment>
- <div>
- {tct('[author] [action] this issue to [assignee]', {
- action: isAutoAssigned ? t('auto-assigned') : t('assigned'),
- author,
- assignee,
- })}
- </div>
- {data.integration && (
- <CodeWrapper>
- {t('Assigned via %s', integrationName[data.integration])}
- {data.rule && (
- <Fragment>
- : <StyledRuleSpan>{data.rule}</StyledRuleSpan>
- </Fragment>
- )}
- </CodeWrapper>
- )}
- </Fragment>
- );
- }
- function renderContent() {
- switch (activity.type) {
- case GroupActivityType.NOTE:
- return tct('[author] left a comment', {author});
- case GroupActivityType.SET_RESOLVED:
- return tct('[author] marked this issue as resolved', {author});
- case GroupActivityType.SET_RESOLVED_BY_AGE:
- return tct('[author] marked this issue as resolved due to inactivity', {
- author,
- });
- case GroupActivityType.SET_RESOLVED_IN_RELEASE:
- const {current_release_version, version} = activity.data;
- if (current_release_version) {
- return tct(
- '[author] marked this issue as resolved in releases greater than [version]',
- {
- author,
- version: (
- <Version
- version={current_release_version}
- projectId={projectId}
- tooltipRawVersion
- />
- ),
- }
- );
- }
- return version
- ? tct('[author] marked this issue as resolved in [version]', {
- author,
- version: (
- <Version version={version} projectId={projectId} tooltipRawVersion />
- ),
- })
- : tct('[author] marked this issue as resolved in the upcoming release', {
- author,
- });
- case GroupActivityType.SET_RESOLVED_IN_COMMIT:
- const deployedReleases = (activity.data.commit?.releases || [])
- .filter(r => r.dateReleased !== null)
- .sort(
- (a, b) => moment(a.dateReleased).valueOf() - moment(b.dateReleased).valueOf()
- );
- if (deployedReleases.length === 1) {
- return tct(
- '[author] marked this issue as resolved in [version] [break]This commit was released in [release]',
- {
- author,
- version: (
- <CommitLink
- inline
- commitId={activity.data.commit.id}
- repository={activity.data.commit.repository}
- />
- ),
- break: <br />,
- release: (
- <Version
- version={deployedReleases[0].version}
- projectId={projectId}
- tooltipRawVersion
- />
- ),
- }
- );
- }
- if (deployedReleases.length > 1) {
- return tct(
- '[author] marked this issue as resolved in [version] [break]This commit was released in [release] and [otherCount] others',
- {
- author,
- otherCount: deployedReleases.length - 1,
- version: (
- <CommitLink
- inline
- commitId={activity.data.commit.id}
- repository={activity.data.commit.repository}
- />
- ),
- break: <br />,
- release: (
- <Version
- version={deployedReleases[0].version}
- projectId={projectId}
- tooltipRawVersion
- />
- ),
- }
- );
- }
- return tct('[author] marked this issue as resolved in [version]', {
- author,
- version: (
- <CommitLink
- inline
- commitId={activity.data.commit.id}
- repository={activity.data.commit.repository}
- />
- ),
- });
- case GroupActivityType.SET_RESOLVED_IN_PULL_REQUEST: {
- const {data} = activity;
- const {pullRequest} = data;
- return tct('[author] has created a PR for this issue: [version]', {
- author,
- version: (
- <PullRequestLink
- inline
- pullRequest={pullRequest}
- repository={pullRequest.repository}
- />
- ),
- });
- }
- case GroupActivityType.SET_UNRESOLVED:
- return tct('[author] marked this issue as unresolved', {author});
- case GroupActivityType.SET_IGNORED: {
- const {data} = activity;
- return getIgnoredMessage(data);
- }
- case GroupActivityType.SET_PUBLIC:
- return tct('[author] made this issue public', {author});
- case GroupActivityType.SET_PRIVATE:
- return tct('[author] made this issue private', {author});
- case GroupActivityType.SET_REGRESSION: {
- const {data} = activity;
- return data.version
- ? tct('[author] marked this issue as a regression in [version]', {
- author,
- version: (
- <Version version={data.version} projectId={projectId} tooltipRawVersion />
- ),
- })
- : tct('[author] marked this issue as a regression', {author});
- }
- case GroupActivityType.CREATE_ISSUE: {
- const {data} = activity;
- return tct('[author] created an issue on [provider] titled [title]', {
- author,
- provider: data.provider,
- title: <ExternalLink href={data.location}>{data.title}</ExternalLink>,
- });
- }
- case GroupActivityType.UNMERGE_SOURCE: {
- const {data} = activity;
- const {destination, fingerprints} = data;
- return tn(
- '%2$s migrated %1$s fingerprint to %3$s',
- '%2$s migrated %1$s fingerprints to %3$s',
- fingerprints.length,
- author,
- destination ? (
- <Link
- to={`${issuesLink}${destination.id}?referrer=group-activity-unmerged-source`}
- >
- {destination.shortId}
- </Link>
- ) : (
- t('a group')
- )
- );
- }
- case GroupActivityType.UNMERGE_DESTINATION: {
- const {data} = activity;
- const {source, fingerprints} = data;
- return tn(
- '%2$s migrated %1$s fingerprint from %3$s',
- '%2$s migrated %1$s fingerprints from %3$s',
- fingerprints.length,
- author,
- source ? (
- <Link
- to={`${issuesLink}${source.id}?referrer=group-activity-unmerged-destination`}
- >
- {source.shortId}
- </Link>
- ) : (
- t('a group')
- )
- );
- }
- case GroupActivityType.FIRST_SEEN:
- return tct('[author] first saw this issue', {author});
- case GroupActivityType.ASSIGNED: {
- const {data} = activity;
- return getAssignedMessage(data);
- }
- case GroupActivityType.UNASSIGNED:
- return tct('[author] unassigned this issue', {author});
- case GroupActivityType.MERGE:
- return tn(
- '%2$s merged %1$s issue into this issue',
- '%2$s merged %1$s issues into this issue',
- activity.data.issues.length,
- author
- );
- case GroupActivityType.REPROCESS: {
- const {data} = activity;
- const {oldGroupId, eventCount} = data;
- return tct('[author] reprocessed the events in this issue. [new-events]', {
- author,
- ['new-events']: (
- <Link
- to={`/organizations/${orgSlug}/issues/?query=reprocessing.original_issue_id:${oldGroupId}&referrer=group-activity-reprocesses`}
- >
- {tn('See %s new event', 'See %s new events', eventCount)}
- </Link>
- ),
- });
- }
- case GroupActivityType.MARK_REVIEWED: {
- return tct('[author] marked this issue as reviewed', {
- author,
- });
- }
- default:
- return ''; // should never hit (?)
- }
- }
- return <Fragment>{renderContent()}</Fragment>;
- }
- export default GroupActivityItem;
- const CodeWrapper = styled('div')`
- overflow-wrap: anywhere;
- font-size: ${p => p.theme.fontSizeSmall};
- `;
- const StyledRuleSpan = styled('span')`
- font-family: ${p => p.theme.text.familyMono};
- `;
|