releaseHeader.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {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 {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. title: <Fragment>{t('View Bundle')}</Fragment>,
  63. to: `bundle/`,
  64. },
  65. {
  66. title: <Fragment>{t('Bundle Details')}</Fragment>,
  67. to: `bundle-details/`,
  68. },
  69. ];
  70. const getTabUrl = (path: string) => ({
  71. pathname: releasePath + path,
  72. query: pick(location.query, Object.values(URL_PARAM)),
  73. });
  74. const getActiveTabTo = () => {
  75. // We are not doing strict version check because there would be a tiny page shift when switching between releases with paginator
  76. const activeTab = tabs
  77. .filter(tab => tab.to.length) // remove home 'Overview' from consideration
  78. .find(tab => location.pathname.endsWith(tab.to));
  79. if (activeTab) {
  80. return activeTab.to;
  81. }
  82. return tabs[0].to; // default to 'Overview'
  83. };
  84. return (
  85. <Layout.Header>
  86. <Layout.HeaderContent>
  87. <Breadcrumbs
  88. crumbs={[
  89. {
  90. to: `/organizations/${organization.slug}/releases/`,
  91. label: t('Releases'),
  92. preservePageFilters: true,
  93. },
  94. {label: t('Release Details')},
  95. ]}
  96. />
  97. <Layout.Title>
  98. <IdBadge project={project} avatarSize={28} hideName />
  99. <Version version={version} anchor={false} truncate />
  100. <IconWrapper>
  101. <CopyToClipboardButton
  102. borderless
  103. size="zero"
  104. text={version}
  105. title={version}
  106. />
  107. </IconWrapper>
  108. {!!url && (
  109. <IconWrapper>
  110. <Tooltip title={url}>
  111. <ExternalLink href={url}>
  112. <IconOpen />
  113. </ExternalLink>
  114. </Tooltip>
  115. </IconWrapper>
  116. )}
  117. </Layout.Title>
  118. </Layout.HeaderContent>
  119. <Layout.HeaderActions>
  120. <ReleaseActions
  121. organization={organization}
  122. projectSlug={project.slug}
  123. release={release}
  124. releaseMeta={releaseMeta}
  125. refetchData={refetchData}
  126. location={location}
  127. />
  128. </Layout.HeaderActions>
  129. <Fragment>
  130. <StyledNavTabs>
  131. {tabs.map(tab => (
  132. <ListLink
  133. key={tab.to}
  134. to={getTabUrl(tab.to)}
  135. isActive={() => getActiveTabTo() === tab.to}
  136. >
  137. {tab.title}
  138. </ListLink>
  139. ))}
  140. </StyledNavTabs>
  141. </Fragment>
  142. </Layout.Header>
  143. );
  144. }
  145. const IconWrapper = styled('span')`
  146. transition: color 0.3s ease-in-out;
  147. &,
  148. a {
  149. color: ${p => p.theme.gray300};
  150. display: flex;
  151. &:hover {
  152. cursor: pointer;
  153. color: ${p => p.theme.textColor};
  154. }
  155. }
  156. `;
  157. const StyledNavTabs = styled(NavTabs)`
  158. margin-bottom: 0;
  159. /* Makes sure the tabs are pushed into another row */
  160. width: 100%;
  161. `;
  162. const NavTabsBadge = styled(Badge)`
  163. @media (max-width: ${p => p.theme.breakpoints.small}) {
  164. display: none;
  165. }
  166. `;
  167. export default ReleaseHeader;