releaseCommit.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {openInviteMembersModal} from 'sentry/actionCreators/modal';
  5. import UserAvatar from 'sentry/components/avatar/userAvatar';
  6. import {LinkButton} from 'sentry/components/button';
  7. import CommitLink from 'sentry/components/commitLink';
  8. import Link from 'sentry/components/links/link';
  9. import PanelItem from 'sentry/components/panels/panelItem';
  10. import TextOverflow from 'sentry/components/textOverflow';
  11. import TimeSince from 'sentry/components/timeSince';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import {IconQuestion} from 'sentry/icons';
  14. import {t, tct} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Commit} from 'sentry/types/integrations';
  17. import {useUser} from 'sentry/utils/useUser';
  18. function formatCommitMessage(message: string | null) {
  19. if (!message) {
  20. return t('No message provided');
  21. }
  22. return message.split(/\n/)[0];
  23. }
  24. export interface ReleaseCommitProps {
  25. commit: Commit;
  26. }
  27. export function ReleaseCommit({commit}: ReleaseCommitProps) {
  28. const user = useUser();
  29. const handleInviteClick = useCallback(
  30. (event: React.MouseEvent<HTMLAnchorElement>) => {
  31. // Prevent link behavior
  32. event?.preventDefault();
  33. if (!commit.author?.email) {
  34. Sentry.captureException(
  35. new Error(`Commit author has no email or id, invite flow is broken.`)
  36. );
  37. return;
  38. }
  39. openInviteMembersModal({
  40. initialData: [
  41. {
  42. emails: new Set([commit.author.email]),
  43. },
  44. ],
  45. source: 'suspect_commit',
  46. });
  47. },
  48. [commit.author]
  49. );
  50. const isUser = user?.id === commit.author?.id;
  51. return (
  52. <StyledPanelItem key={commit.id} data-test-id="commit-row">
  53. <CommitContent>
  54. <Message>{formatCommitMessage(commit.message)}</Message>
  55. <MetaWrapper>
  56. <UserAvatar size={16} user={commit.author} />
  57. <Meta>
  58. <Tooltip
  59. title={tct(
  60. 'The email [actorEmail] is not a member of your organization. [inviteUser:Invite] them or link additional emails in [accountSettings:account settings].',
  61. {
  62. actorEmail: <BoldEmail>{commit.author?.email}</BoldEmail>,
  63. accountSettings: <StyledLink to="/settings/account/emails/" />,
  64. inviteUser: (
  65. <StyledLink
  66. to=""
  67. onClick={handleInviteClick}
  68. aria-label={t('Invite user')}
  69. />
  70. ),
  71. }
  72. )}
  73. disabled={!commit.author || commit.author.id !== undefined}
  74. overlayStyle={{maxWidth: '350px'}}
  75. skipWrapper
  76. isHoverable
  77. >
  78. <AuthorWrapper>
  79. {isUser ? t('You') : commit.author?.name ?? t('Unknown author')}
  80. {commit.author && commit.author.id === undefined && (
  81. <IconQuestion size="xs" />
  82. )}
  83. </AuthorWrapper>
  84. </Tooltip>
  85. {tct(' committed [commitLink] ', {
  86. commitLink: (
  87. <CommitLink
  88. inline
  89. showIcon={false}
  90. commitId={commit.id}
  91. repository={commit.repository}
  92. />
  93. ),
  94. })}
  95. <TimeSince date={commit.dateCreated} tooltipUnderlineColor="background" />
  96. </Meta>
  97. </MetaWrapper>
  98. </CommitContent>
  99. {commit.pullRequest?.externalUrl && (
  100. <LinkButton external href={commit.pullRequest.externalUrl}>
  101. {t('View Pull Request')}
  102. </LinkButton>
  103. )}
  104. </StyledPanelItem>
  105. );
  106. }
  107. const StyledPanelItem = styled(PanelItem)`
  108. display: flex;
  109. align-items: center;
  110. justify-content: space-between;
  111. gap: ${space(2)};
  112. `;
  113. const BoldEmail = styled('strong')`
  114. font-weight: bold;
  115. word-break: break-all;
  116. `;
  117. const StyledLink = styled(Link)`
  118. color: ${p => p.theme.textColor};
  119. border-bottom: 1px dotted ${p => p.theme.textColor};
  120. &:hover {
  121. color: ${p => p.theme.textColor};
  122. }
  123. `;
  124. const Message = styled(TextOverflow)`
  125. font-size: ${p => p.theme.fontSizeLarge};
  126. line-height: 1.2;
  127. `;
  128. const Meta = styled(TextOverflow)`
  129. line-height: 1.5;
  130. margin: 0;
  131. color: ${p => p.theme.subText};
  132. a {
  133. color: ${p => p.theme.subText};
  134. text-decoration: underline;
  135. text-decoration-style: dotted;
  136. }
  137. a:hover {
  138. color: ${p => p.theme.textColor};
  139. }
  140. `;
  141. const CommitContent = styled('div')`
  142. display: flex;
  143. flex-direction: column;
  144. gap: ${space(0.25)};
  145. ${p => p.theme.overflowEllipsis};
  146. `;
  147. const MetaWrapper = styled('div')`
  148. display: flex;
  149. align-items: center;
  150. gap: ${space(0.5)};
  151. color: ${p => p.theme.subText};
  152. font-size: ${p => p.theme.fontSizeMedium};
  153. line-height: 1.2;
  154. `;
  155. const AuthorWrapper = styled('span')`
  156. display: inline-flex;
  157. align-items: center;
  158. gap: ${space(0.25)};
  159. color: ${p => p.theme.subText};
  160. & svg {
  161. transition: 120ms opacity;
  162. opacity: 0.6;
  163. }
  164. &:has(svg):hover {
  165. color: ${p => p.theme.textColor};
  166. & svg {
  167. opacity: 1;
  168. }
  169. }
  170. `;