suggestedAssignees.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import {useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import ActorAvatar from 'sentry/components/avatar/actorAvatar';
  4. import Button from 'sentry/components/button';
  5. import SuggestedOwnerHovercard from 'sentry/components/group/suggestedOwnerHovercard';
  6. import * as SidebarSection from 'sentry/components/sidebarSection';
  7. import {IconCheckmark} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import space from 'sentry/styles/space';
  10. import type {Actor, Commit, Group, Organization, Release} from 'sentry/types';
  11. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  12. type Owner = {
  13. actor: Actor;
  14. source: 'codeowners' | 'projectOwnership' | 'suspectCommit';
  15. commits?: Array<Commit>;
  16. release?: Release;
  17. rules?: Array<any> | null;
  18. };
  19. type Props = {
  20. group: Group;
  21. onAssign: (actor: Actor) => void;
  22. organization: Organization;
  23. owners: Array<Owner>;
  24. projectId?: string;
  25. };
  26. const SuggestedAssignees = ({
  27. group,
  28. owners,
  29. projectId,
  30. organization,
  31. onAssign,
  32. }: Props) => {
  33. const handleAssign = useCallback(
  34. (owner: Owner) => {
  35. onAssign(owner.actor);
  36. trackAdvancedAnalyticsEvent('issue_details.action_clicked', {
  37. organization,
  38. project_id: parseInt(projectId!, 10),
  39. group_id: parseInt(group.id, 10),
  40. issue_category: group.issueCategory,
  41. action_type: 'assign',
  42. assigned_suggestion_reason: owner.source,
  43. });
  44. },
  45. [onAssign, group.id, group.issueCategory, projectId, organization]
  46. );
  47. return (
  48. <SidebarSection.Wrap>
  49. <SidebarSection.Title>{t('Suggested Assignees')}</SidebarSection.Title>
  50. <SidebarSection.Content>
  51. <Content>
  52. {owners.map((owner, i) => (
  53. <SuggestionRow
  54. key={`${owner.actor.id}:${owner.actor.email}:${owner.actor.name}:${i}`}
  55. >
  56. <ActorMaxWidth>
  57. <SuggestedOwnerHovercard
  58. projectId={projectId}
  59. organization={organization}
  60. {...owner}
  61. >
  62. <ActorWrapper>
  63. <ActorAvatar
  64. hasTooltip={false}
  65. actor={owner.actor}
  66. data-test-id="suggested-assignee"
  67. size={20}
  68. />
  69. <ActorName>
  70. {owner.actor.type === 'team'
  71. ? `#${owner.actor.name}`
  72. : owner.actor.name}
  73. </ActorName>
  74. </ActorWrapper>
  75. </SuggestedOwnerHovercard>
  76. </ActorMaxWidth>
  77. <StyledButton
  78. role="button"
  79. size="zero"
  80. borderless
  81. aria-label={t('Assign')}
  82. icon={<IconCheckmark />}
  83. disabled={owner.actor.id === undefined}
  84. onClick={() => handleAssign(owner)}
  85. />
  86. </SuggestionRow>
  87. ))}
  88. </Content>
  89. </SidebarSection.Content>
  90. </SidebarSection.Wrap>
  91. );
  92. };
  93. export {SuggestedAssignees};
  94. const Content = styled('div')`
  95. display: flex;
  96. gap: ${space(1)};
  97. flex-direction: column;
  98. `;
  99. const ActorMaxWidth = styled('div')`
  100. max-width: 85%;
  101. `;
  102. const ActorWrapper = styled('div')`
  103. display: flex;
  104. align-items: center;
  105. gap: ${space(1)};
  106. `;
  107. const ActorName = styled('div')`
  108. ${p => p.theme.overflowEllipsis}
  109. `;
  110. const SuggestionRow = styled('div')`
  111. display: flex;
  112. align-items: center;
  113. justify-content: space-between;
  114. `;
  115. const StyledButton = styled(Button)`
  116. padding-right: 0;
  117. `;