releaseHeader.tsx 4.7 KB

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