commitAuthorBreakdown.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import styled from '@emotion/styled';
  2. import UserAvatar from 'sentry/components/avatar/userAvatar';
  3. import {Button} from 'sentry/components/button';
  4. import Collapsible from 'sentry/components/collapsible';
  5. import LoadingError from 'sentry/components/loadingError';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import * as SidebarSection from 'sentry/components/sidebarSection';
  8. import {t, tn} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {Commit} from 'sentry/types/integrations';
  11. import type {User} from 'sentry/types/user';
  12. import {percent} from 'sentry/utils';
  13. import {userDisplayName} from 'sentry/utils/formatters';
  14. import {useApiQuery} from 'sentry/utils/queryClient';
  15. type GroupedAuthorCommits = {
  16. [key: string]: {author: User | undefined; commitCount: number};
  17. };
  18. type Props = {
  19. orgId: string;
  20. projectSlug: string;
  21. version: string;
  22. };
  23. function CommitAuthorBreakdown({orgId, projectSlug, version}: Props) {
  24. const commitsEndpoint = `/projects/${orgId}/${projectSlug}/releases/${encodeURIComponent(
  25. version
  26. )}/commits/`;
  27. const {
  28. data: commits,
  29. isPending,
  30. isError,
  31. } = useApiQuery<Commit[]>([commitsEndpoint], {staleTime: 0});
  32. if (isPending) {
  33. return <LoadingIndicator />;
  34. }
  35. if (isError) {
  36. return <LoadingError />;
  37. }
  38. function getDisplayPercent(authorCommitCount: number) {
  39. if (commits) {
  40. const calculatedPercent = Math.round(percent(authorCommitCount, commits.length));
  41. return `${calculatedPercent < 1 ? '<1' : calculatedPercent}%`;
  42. }
  43. return '';
  44. }
  45. // group commits by author
  46. const groupedAuthorCommits = commits?.reduce<GroupedAuthorCommits>(
  47. (authorCommitsAccumulator, commit) => {
  48. const email = commit.author?.email ?? 'unknown';
  49. if (authorCommitsAccumulator.hasOwnProperty(email)) {
  50. authorCommitsAccumulator[email].commitCount += 1;
  51. } else {
  52. authorCommitsAccumulator[email] = {
  53. commitCount: 1,
  54. author: commit.author,
  55. };
  56. }
  57. return authorCommitsAccumulator;
  58. },
  59. {}
  60. );
  61. // sort authors by number of commits
  62. const sortedAuthorsByNumberOfCommits = Object.values(groupedAuthorCommits).sort(
  63. (a, b) => b.commitCount - a.commitCount
  64. );
  65. if (!sortedAuthorsByNumberOfCommits.length) {
  66. return null;
  67. }
  68. return (
  69. <SidebarSection.Wrap>
  70. <SidebarSection.Title>{t('Commit Author Breakdown')}</SidebarSection.Title>
  71. <SidebarSection.Content>
  72. <Collapsible
  73. expandButton={({onExpand, numberOfHiddenItems}) => (
  74. <Button priority="link" onClick={onExpand}>
  75. {tn('Show %s other author', 'Show %s other authors', numberOfHiddenItems)}
  76. </Button>
  77. )}
  78. >
  79. {sortedAuthorsByNumberOfCommits.map(({commitCount, author}, index) => (
  80. <AuthorLine key={author?.email ?? index}>
  81. <UserAvatar user={author} size={20} hasTooltip />
  82. <AuthorName>{userDisplayName(author || {}, false)}</AuthorName>
  83. <Commits>{tn('%s commit', '%s commits', commitCount)}</Commits>
  84. <Percent>{getDisplayPercent(commitCount)}</Percent>
  85. </AuthorLine>
  86. ))}
  87. </Collapsible>
  88. </SidebarSection.Content>
  89. </SidebarSection.Wrap>
  90. );
  91. }
  92. const AuthorLine = styled('div')`
  93. display: inline-grid;
  94. grid-template-columns: 30px 2fr 1fr 40px;
  95. width: 100%;
  96. margin-bottom: ${space(1)};
  97. font-size: ${p => p.theme.fontSizeMedium};
  98. `;
  99. const AuthorName = styled('div')`
  100. color: ${p => p.theme.textColor};
  101. ${p => p.theme.overflowEllipsis};
  102. `;
  103. const Commits = styled('div')`
  104. color: ${p => p.theme.subText};
  105. text-align: right;
  106. `;
  107. const Percent = styled('div')`
  108. min-width: 40px;
  109. text-align: right;
  110. `;
  111. export default CommitAuthorBreakdown;