diff --git a/static/app/components/timeline/index.tsx b/static/app/components/timeline/index.tsx index d425cc24e98d44..79c0a4c6ead755 100644 --- a/static/app/components/timeline/index.tsx +++ b/static/app/components/timeline/index.tsx @@ -19,6 +19,7 @@ export interface TimelineItemProps { 'data-index'?: number; icon?: React.ReactNode; isActive?: boolean; + marker?: React.ReactNode; onClick?: React.MouseEventHandler; onMouseEnter?: React.MouseEventHandler; onMouseLeave?: React.MouseEventHandler; @@ -33,6 +34,7 @@ function Item({ title, children, icon, + marker, colorConfig, timestamp, isActive = false, @@ -42,9 +44,11 @@ function Item({ }: TimelineItemProps) { const theme = useTheme(); const config = colorConfig ?? makeDefaultColorConfig(theme); + const hasMarker = marker !== undefined; return ( - + + {hasMarker && {marker}} {icon ? ( {timestamp ??
} - {children} + {children} ); } @@ -77,12 +81,14 @@ function makeDefaultColorConfig(theme: Theme) { }; } -const Row = styled('div')<{showLastLine?: boolean}>` +const Row = styled('div')<{hasMarker: boolean; showLastLine?: boolean}>` position: relative; color: ${p => p.theme.tokens.content.secondary}; display: grid; align-items: start; - grid-template: auto auto / 22px 1fr auto; + grid-template-rows: auto auto; + grid-template-columns: ${p => + p.hasMarker ? '22px 22px minmax(50px, 1fr) auto' : '22px minmax(50px, 1fr) auto'}; grid-column-gap: ${p => p.theme.space.md}; margin: ${p => p.theme.space.md} 0; &:first-child { @@ -96,6 +102,15 @@ const Row = styled('div')<{showLastLine?: boolean}>` } `; +const MarkerWrapper = styled('div')` + grid-column: span 1; + z-index: 10; + display: grid; + place-items: center; + min-width: 22px; + min-height: 22px; +`; + const IconWrapper = styled('div')` grid-column: span 1; border-radius: 100%; @@ -115,9 +130,9 @@ const Title = styled('div')` font-size: ${p => p.theme.font.size.md}; `; -const Content = styled('div')` +const Content = styled('div')<{hasMarker: boolean}>` text-align: left; - grid-column: span 2; + grid-column: ${p => (p.hasMarker ? '3 / -1' : 'span 2')}; color: ${p => p.theme.tokens.content.secondary}; margin: ${p => p.theme.space['2xs']} 0 0; font-size: ${p => p.theme.font.size.sm}; diff --git a/static/app/views/issueDetails/activitySection/index.spec.tsx b/static/app/views/issueDetails/activitySection/index.spec.tsx index 9d3acf064aa81b..a801e182bd08b8 100644 --- a/static/app/views/issueDetails/activitySection/index.spec.tsx +++ b/static/app/views/issueDetails/activitySection/index.spec.tsx @@ -174,6 +174,58 @@ describe('ActivitySection', () => { expect(screen.queryByText('Test Note')).not.toBeInTheDocument(); }); + it('renders activity actor markers', async () => { + const activityGroup = GroupFixture({ + id: '1338', + activity: [ + { + type: GroupActivityType.NOTE, + id: 'note-1', + data: {text: 'User note'}, + dateCreated: '2020-01-01T00:00:00', + user, + }, + { + type: GroupActivityType.SET_RESOLVED, + id: 'resolved-1', + data: {}, + dateCreated: '2020-01-02T00:00:00', + user: null, + }, + ], + project, + }); + + render(, { + organization: OrganizationFixture({features: ['issue-activity-feed-v2']}), + }); + + expect(await screen.findByText('User note')).toBeInTheDocument(); + expect(screen.getByTestId('user-activity-marker')).toBeInTheDocument(); + expect(screen.getByTestId('sentry-activity-marker')).toBeInTheDocument(); + }); + + it('does not render activity actor markers when the feature is disabled', async () => { + const activityGroup = GroupFixture({ + id: '1338', + activity: [ + { + type: GroupActivityType.NOTE, + id: 'note-1', + data: {text: 'User note'}, + dateCreated: '2020-01-01T00:00:00', + user, + }, + ], + project, + }); + + render(); + + expect(await screen.findByText('User note')).toBeInTheDocument(); + expect(screen.queryByTestId('user-activity-marker')).not.toBeInTheDocument(); + }); + it('renders note and allows for edit', async () => { jest.spyOn(indicators, 'addSuccessMessage'); diff --git a/static/app/views/issueDetails/activitySection/index.tsx b/static/app/views/issueDetails/activitySection/index.tsx index b79f8da5d6d13e..f27843506575ad 100644 --- a/static/app/views/issueDetails/activitySection/index.tsx +++ b/static/app/views/issueDetails/activitySection/index.tsx @@ -1,7 +1,8 @@ import {Fragment, useCallback, useState} from 'react'; -import {useTheme} from '@emotion/react'; +import {useTheme, type Theme} from '@emotion/react'; import styled from '@emotion/styled'; +import {SentryAppAvatar, UserAvatar} from '@sentry/scraps/avatar'; import {LinkButton} from '@sentry/scraps/button'; import {Flex, Grid} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; @@ -47,6 +48,73 @@ function getAuthorName(item: GroupActivity) { return 'Sentry'; } +function getActivityMarker(item: GroupActivity, color: string) { + if (item.sentry_app) { + return ( + + + + ); + } + if (item.user) { + return ( + + + + ); + } + return ; +} + +function getActivityColorConfig(theme: Theme, type: GroupActivityType) { + const defaultConfig = { + title: theme.tokens.content.primary, + icon: theme.tokens.content.secondary, + iconBorder: theme.tokens.content.secondary, + }; + + switch (type) { + case GroupActivityType.SET_RESOLVED: + case GroupActivityType.SET_RESOLVED_BY_AGE: + case GroupActivityType.SET_RESOLVED_IN_RELEASE: + case GroupActivityType.SET_RESOLVED_IN_COMMIT: + case GroupActivityType.MARK_REVIEWED: + case GroupActivityType.SEER_RCA_COMPLETED: + case GroupActivityType.SEER_SOLUTION_COMPLETED: + case GroupActivityType.SEER_CODING_COMPLETED: + case GroupActivityType.SEER_PR_CREATED: + return { + ...defaultConfig, + icon: theme.tokens.graphics.success.vibrant, + iconBorder: theme.tokens.border.success.vibrant, + }; + case GroupActivityType.SET_UNRESOLVED: + case GroupActivityType.SET_REGRESSION: + return { + ...defaultConfig, + icon: theme.tokens.graphics.danger.vibrant, + iconBorder: theme.tokens.border.danger.vibrant, + }; + case GroupActivityType.SET_ESCALATING: + case GroupActivityType.SET_PRIORITY: + return { + ...defaultConfig, + icon: theme.tokens.graphics.warning.vibrant, + iconBorder: theme.tokens.border.warning.vibrant, + }; + case GroupActivityType.SET_IGNORED: + return { + ...defaultConfig, + }; + default: + return defaultConfig; + } +} + function TimelineItem({ item, handleDelete, @@ -65,8 +133,11 @@ function TimelineItem({ teams: Team[]; }) { const organization = useOrganization(); + const theme = useTheme(); const [editing, setEditing] = useState(false); + const useTwoColumnLayout = organization.features.includes('issue-activity-feed-v2'); const authorName = getAuthorName(item); + const colorConfig = getActivityColorConfig(theme, item.type); const {title, message} = getGroupActivityItem( item, organization, @@ -77,13 +148,14 @@ function TimelineItem({ ); const iconMapping = groupActivityTypeIconMapping[item.type]; - const Icon = iconMapping?.componentFunction - ? iconMapping.componentFunction({ - data: item.data, - user: item.user, - sentry_app: item.sentry_app, - }) - : (iconMapping?.Component ?? null); + const Icon = + !useTwoColumnLayout && iconMapping?.componentFunction + ? iconMapping.componentFunction({ + data: item.data, + user: item.user, + sentry_app: item.sentry_app, + }) + : (iconMapping?.Component ?? null); return ( } timestamp={} + marker={useTwoColumnLayout ? getActivityMarker(item, colorConfig.icon) : undefined} + colorConfig={useTwoColumnLayout ? colorConfig : undefined} icon={ Icon && ( ` const ActivityInputFrame = styled('div')` color: ${p => p.theme.tokens.content.primary}; `; + +const AvatarMarker = styled('span')<{color: string}>` + display: block; + position: relative; + border-radius: 100%; + line-height: 0; + + &::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 100%; + box-shadow: inset 0 0 0 2px ${p => p.color}; + pointer-events: none; + } +`; + +const SentryMarker = styled('span')<{color: string}>` + width: 12px; + height: 12px; + border-radius: 100%; + background: ${p => p.theme.tokens.background.primary}; + display: grid; + place-items: center; + + &::after { + content: ''; + width: 6px; + height: 6px; + border-radius: 100%; + background: ${p => p.color}; + } +`;