releaseHeader.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import pick from 'lodash/pick';
  5. import Badge from 'sentry/components/badge/badge';
  6. import {Breadcrumbs} from 'sentry/components/breadcrumbs';
  7. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  8. import IdBadge from 'sentry/components/idBadge';
  9. import * as Layout from 'sentry/components/layouts/thirds';
  10. import ExternalLink from 'sentry/components/links/externalLink';
  11. import ListLink from 'sentry/components/links/listLink';
  12. import NavTabs from 'sentry/components/navTabs';
  13. import {Tooltip} from 'sentry/components/tooltip';
  14. import Version from 'sentry/components/version';
  15. import {URL_PARAM} from 'sentry/constants/pageFilters';
  16. import {IconOpen} from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import type {Organization} from 'sentry/types/organization';
  19. import type {Release, ReleaseMeta, ReleaseProject} from 'sentry/types/release';
  20. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  21. import ReleaseActions from './releaseActions';
  22. type Props = {
  23. location: Location;
  24. organization: Organization;
  25. project: Required<ReleaseProject>;
  26. refetchData: () => void;
  27. release: Release;
  28. releaseMeta: ReleaseMeta;
  29. };
  30. function ReleaseHeader({
  31. location,
  32. organization,
  33. release,
  34. project,
  35. releaseMeta,
  36. refetchData,
  37. }: Props) {
  38. const {version, url} = release;
  39. const {commitCount, commitFilesChanged} = releaseMeta;
  40. const releasePath = `/organizations/${organization.slug}/releases/${encodeURIComponent(
  41. version
  42. )}/`;
  43. const tabs = [
  44. {title: t('Overview'), to: ''},
  45. {
  46. title: (
  47. <Fragment>
  48. {t('Commits')} <NavTabsBadge text={formatAbbreviatedNumber(commitCount)} />
  49. </Fragment>
  50. ),
  51. to: `commits/`,
  52. },
  53. {
  54. title: (
  55. <Fragment>
  56. {t('Files Changed')}
  57. <NavTabsBadge text={formatAbbreviatedNumber(commitFilesChanged)} />
  58. </Fragment>
  59. ),
  60. to: `files-changed/`,
  61. },
  62. ];
  63. const getTabUrl = (path: string) => ({
  64. pathname: releasePath + path,
  65. query: pick(location.query, Object.values(URL_PARAM)),
  66. });
  67. const getActiveTabTo = () => {
  68. // We are not doing strict version check because there would be a tiny page shift when switching between releases with paginator
  69. const activeTab = tabs
  70. .filter(tab => tab.to.length) // remove home 'Overview' from consideration
  71. .find(tab => location.pathname.endsWith(tab.to));
  72. if (activeTab) {
  73. return activeTab.to;
  74. }
  75. return tabs[0].to; // default to 'Overview'
  76. };
  77. return (
  78. <Layout.Header>
  79. <Layout.HeaderContent>
  80. <Breadcrumbs
  81. crumbs={[
  82. {
  83. to: `/organizations/${organization.slug}/releases/`,
  84. label: t('Releases'),
  85. preservePageFilters: true,
  86. },
  87. {label: t('Release Details')},
  88. ]}
  89. />
  90. <Layout.Title>
  91. <IdBadge project={project} avatarSize={28} hideName />
  92. <Version version={version} anchor={false} truncate />
  93. <IconWrapper>
  94. <CopyToClipboardButton
  95. borderless
  96. size="zero"
  97. text={version}
  98. title={version}
  99. />
  100. </IconWrapper>
  101. {!!url && (
  102. <IconWrapper>
  103. <Tooltip title={url}>
  104. <ExternalLink href={url}>
  105. <IconOpen />
  106. </ExternalLink>
  107. </Tooltip>
  108. </IconWrapper>
  109. )}
  110. </Layout.Title>
  111. </Layout.HeaderContent>
  112. <Layout.HeaderActions>
  113. <ReleaseActions
  114. organization={organization}
  115. projectSlug={project.slug}
  116. release={release}
  117. releaseMeta={releaseMeta}
  118. refetchData={refetchData}
  119. location={location}
  120. />
  121. </Layout.HeaderActions>
  122. <Fragment>
  123. <StyledNavTabs>
  124. {tabs.map(tab => (
  125. <ListLink
  126. key={tab.to}
  127. to={getTabUrl(tab.to)}
  128. isActive={() => getActiveTabTo() === tab.to}
  129. >
  130. {tab.title}
  131. </ListLink>
  132. ))}
  133. </StyledNavTabs>
  134. </Fragment>
  135. </Layout.Header>
  136. );
  137. }
  138. const IconWrapper = styled('span')`
  139. transition: color 0.3s ease-in-out;
  140. &,
  141. a {
  142. color: ${p => p.theme.gray300};
  143. display: flex;
  144. &:hover {
  145. cursor: pointer;
  146. color: ${p => p.theme.textColor};
  147. }
  148. }
  149. `;
  150. const StyledNavTabs = styled(NavTabs)`
  151. margin-bottom: 0;
  152. /* Makes sure the tabs are pushed into another row */
  153. width: 100%;
  154. `;
  155. const NavTabsBadge = styled(Badge)`
  156. @media (max-width: ${p => p.theme.breakpoints.small}) {
  157. display: none;
  158. }
  159. `;
  160. export default ReleaseHeader;