-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(assignee-selector): Create storybook component for new assignee …
…selector trigger (#70561) This PR creates a new component `<AssigneeBadge/>` which will eventually replace the current assignee selector trigger in the issue stream. This PR does **not** make any changes to the selector, it merely creates the component and a corresponding storybook entry so it can be iterated on independent of any changes made to the issue stream. More details of the project can be found [here](#69827) (#69827) At a glance (as of 5/9): <img width="206" alt="image" src="https://github.com/getsentry/sentry/assets/55160142/9da5fa59-4cc0-4dcf-be27-b7fcc5c686e3"> Note that unlike the previous assignee selector, there is no longer a "Suggested Assignee" state for this new assignee selector, there is only an Assigned and Unassigned state. TODO: - [x] Figure out what the loading state looks like - [x] Chevron weight and style match border - [x] Add tests
- Loading branch information
1 parent
5a883ee
commit d17225a
Showing
2 changed files
with
262 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import {Fragment, useState} from 'react'; | ||
|
||
import {AssigneeBadge} from 'sentry/components/assigneeBadge'; | ||
import storyBook from 'sentry/stories/storyBook'; | ||
import type {Actor} from 'sentry/types'; | ||
import {useUser} from 'sentry/utils/useUser'; | ||
import {useUserTeams} from 'sentry/utils/useUserTeams'; | ||
|
||
export default storyBook(AssigneeBadge, story => { | ||
story('User Assignee', () => { | ||
const user = useUser(); | ||
const [chevron1Toggle, setChevron1Toggle] = useState<'up' | 'down'>('down'); | ||
const [chevron2Toggle, setChevron2Toggle] = useState<'up' | 'down'>('down'); | ||
const userActor: Actor = { | ||
type: 'user', | ||
id: user.id, | ||
name: user.name, | ||
email: user.email, | ||
}; | ||
|
||
return ( | ||
<Fragment> | ||
<p onClick={() => setChevron1Toggle(chevron1Toggle === 'up' ? 'down' : 'up')}> | ||
<AssigneeBadge assignedTo={userActor} chevronDirection={chevron1Toggle} /> | ||
</p> | ||
<p onClick={() => setChevron2Toggle(chevron2Toggle === 'up' ? 'down' : 'up')}> | ||
<AssigneeBadge | ||
showLabel | ||
assignedTo={userActor} | ||
chevronDirection={chevron2Toggle} | ||
/> | ||
</p> | ||
</Fragment> | ||
); | ||
}); | ||
|
||
story('Team Assignee', () => { | ||
const {teams} = useUserTeams(); | ||
const [chevron1Toggle, setChevron1Toggle] = useState<'up' | 'down'>('down'); | ||
const [chevron2Toggle, setChevron2Toggle] = useState<'up' | 'down'>('down'); | ||
|
||
const teamActor: Actor = { | ||
type: 'team', | ||
id: teams[0].id, | ||
name: teams[0].name, | ||
}; | ||
|
||
return ( | ||
<Fragment> | ||
<p onClick={() => setChevron1Toggle(chevron1Toggle === 'up' ? 'down' : 'up')}> | ||
<AssigneeBadge | ||
assignmentReason="suspectCommit" | ||
assignedTo={teamActor} | ||
chevronDirection={chevron1Toggle} | ||
/> | ||
</p> | ||
<p onClick={() => setChevron2Toggle(chevron2Toggle === 'up' ? 'down' : 'up')}> | ||
<AssigneeBadge | ||
assignedTo={teamActor} | ||
showLabel | ||
chevronDirection={chevron2Toggle} | ||
/> | ||
</p> | ||
</Fragment> | ||
); | ||
}); | ||
|
||
story('Unassigned', () => { | ||
const [chevron1Toggle, setChevron1Toggle] = useState<'up' | 'down'>('down'); | ||
const [chevron2Toggle, setChevron2Toggle] = useState<'up' | 'down'>('down'); | ||
|
||
return ( | ||
<Fragment> | ||
<p onClick={() => setChevron1Toggle(chevron1Toggle === 'up' ? 'down' : 'up')}> | ||
<AssigneeBadge assignedTo={undefined} chevronDirection={chevron1Toggle} /> | ||
</p> | ||
<p onClick={() => setChevron2Toggle(chevron2Toggle === 'up' ? 'down' : 'up')}> | ||
<AssigneeBadge | ||
showLabel | ||
assignedTo={undefined} | ||
chevronDirection={chevron2Toggle} | ||
/> | ||
</p> | ||
</Fragment> | ||
); | ||
}); | ||
|
||
story('Loading', () => { | ||
return ( | ||
<Fragment> | ||
<p> | ||
<AssigneeBadge loading /> | ||
</p> | ||
<p> | ||
<AssigneeBadge showLabel loading /> | ||
</p> | ||
</Fragment> | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import {Fragment} from 'react'; | ||
import styled from '@emotion/styled'; | ||
|
||
import ActorAvatar from 'sentry/components/avatar/actorAvatar'; | ||
import Tag from 'sentry/components/badge/tag'; | ||
import {Chevron} from 'sentry/components/chevron'; | ||
import ExternalLink from 'sentry/components/links/externalLink'; | ||
import LoadingIndicator from 'sentry/components/loadingIndicator'; | ||
import Placeholder from 'sentry/components/placeholder'; | ||
import {Tooltip} from 'sentry/components/tooltip'; | ||
import {t, tct} from 'sentry/locale'; | ||
import {space} from 'sentry/styles/space'; | ||
import type {Actor, SuggestedOwnerReason} from 'sentry/types'; | ||
import {lightTheme as theme} from 'sentry/utils/theme'; | ||
|
||
type AssigneeBadgeProps = { | ||
assignedTo?: Actor | undefined; | ||
assignmentReason?: SuggestedOwnerReason; | ||
chevronDirection?: 'up' | 'down'; | ||
loading?: boolean; | ||
showLabel?: boolean; | ||
}; | ||
|
||
const AVATAR_SIZE = 16; | ||
|
||
export function AssigneeBadge({ | ||
assignedTo, | ||
assignmentReason, | ||
showLabel = false, | ||
chevronDirection = 'down', | ||
loading = false, | ||
}: AssigneeBadgeProps) { | ||
const suggestedReasons: Record<SuggestedOwnerReason, React.ReactNode> = { | ||
suspectCommit: tct('Based on [commit:commit data]', { | ||
commit: ( | ||
<TooltipSubExternalLink href="https://docs.sentry.io/product/sentry-basics/integrate-frontend/configure-scms/" /> | ||
), | ||
}), | ||
ownershipRule: t('Matching Issue Owners Rule'), | ||
projectOwnership: t('Matching Issue Owners Rule'), | ||
codeowners: t('Matching Codeowners Rule'), | ||
}; | ||
|
||
const makeAssignedIcon = (actor: Actor) => { | ||
return ( | ||
<Fragment> | ||
<ActorAvatar | ||
actor={actor} | ||
className="avatar" | ||
size={AVATAR_SIZE} | ||
hasTooltip={false} | ||
data-test-id="assigned-avatar" | ||
// Team avatars need extra left margin since the | ||
// square team avatar is being fit into a rounded borders | ||
style={{ | ||
marginLeft: actor.type === 'team' ? space(0.5) : space(0), | ||
}} | ||
/> | ||
{showLabel && ( | ||
<div | ||
style={{color: theme.textColor}} | ||
>{`${actor.type === 'team' ? '#' : ''}${actor.name}`}</div> | ||
)} | ||
<Chevron direction={chevronDirection} size="small" /> | ||
</Fragment> | ||
); | ||
}; | ||
|
||
const loadingIcon = ( | ||
<Fragment> | ||
<StyledLoadingIndicator mini hideMessage relative size={AVATAR_SIZE} /> | ||
{showLabel && 'Loading...'} | ||
<Chevron direction={chevronDirection} size="small" /> | ||
</Fragment> | ||
); | ||
|
||
const unassignedIcon = ( | ||
<Fragment> | ||
<Placeholder | ||
shape="circle" | ||
width={`${AVATAR_SIZE}px`} | ||
height={`${AVATAR_SIZE}px`} | ||
/> | ||
{showLabel && <Fragment>Unassigned</Fragment>} | ||
<Chevron direction={chevronDirection} size="small" /> | ||
</Fragment> | ||
); | ||
|
||
return loading ? ( | ||
<StyledTag icon={loadingIcon} /> | ||
) : assignedTo ? ( | ||
<Tooltip | ||
title={ | ||
<TooltipWrapper> | ||
{t('Assigned to ')} | ||
{assignedTo.type === 'team' ? `#${assignedTo.name}` : assignedTo.name} | ||
{assignmentReason && ( | ||
<TooltipSubtext>{suggestedReasons[assignmentReason]}</TooltipSubtext> | ||
)} | ||
</TooltipWrapper> | ||
} | ||
> | ||
<StyledTag icon={makeAssignedIcon(assignedTo)} /> | ||
</Tooltip> | ||
) : ( | ||
<Tooltip | ||
title={ | ||
<TooltipWrapper> | ||
<div>{t('Unassigned')}</div> | ||
<TooltipSubtext> | ||
{tct( | ||
'You can auto-assign issues by adding [issueOwners:Issue Owner rules].', | ||
{ | ||
issueOwners: ( | ||
<TooltipSubExternalLink href="https://docs.sentry.io/product/error-monitoring/issue-owners/" /> | ||
), | ||
} | ||
)} | ||
</TooltipSubtext> | ||
</TooltipWrapper> | ||
} | ||
> | ||
<StyledTag icon={unassignedIcon} borderStyle="dashed" /> | ||
</Tooltip> | ||
); | ||
} | ||
|
||
const StyledLoadingIndicator = styled(LoadingIndicator)` | ||
display: inline-flex; | ||
align-items: center; | ||
`; | ||
|
||
const TooltipWrapper = styled('div')` | ||
text-align: left; | ||
`; | ||
|
||
const StyledTag = styled(Tag)` | ||
span { | ||
display: flex; | ||
align-items: center; | ||
gap: ${space(0.5)}; | ||
} | ||
& > div { | ||
height: 24px; | ||
padding: ${space(0.5)}; | ||
} | ||
color: ${p => p.theme.subText}; | ||
cursor: pointer; | ||
`; | ||
|
||
const TooltipSubtext = styled('div')` | ||
color: ${p => p.theme.subText}; | ||
`; | ||
|
||
const TooltipSubExternalLink = styled(ExternalLink)` | ||
color: ${p => p.theme.subText}; | ||
text-decoration: underline; | ||
:hover { | ||
color: ${p => p.theme.subText}; | ||
} | ||
`; |