releaseHeader.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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 Clipboard from 'sentry/components/clipboard';
  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 {IconCopy, 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. const 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. <Tooltip title={version} containerDisplayMode="flex">
  94. <Clipboard value={version}>
  95. <IconCopy />
  96. </Clipboard>
  97. </Tooltip>
  98. </IconWrapper>
  99. {!!url && (
  100. <IconWrapper>
  101. <Tooltip title={url}>
  102. <ExternalLink href={url}>
  103. <IconOpen />
  104. </ExternalLink>
  105. </Tooltip>
  106. </IconWrapper>
  107. )}
  108. </Layout.Title>
  109. </Layout.HeaderContent>
  110. <Layout.HeaderActions>
  111. <ReleaseActions
  112. organization={organization}
  113. projectSlug={project.slug}
  114. release={release}
  115. releaseMeta={releaseMeta}
  116. refetchData={refetchData}
  117. location={location}
  118. />
  119. </Layout.HeaderActions>
  120. <Fragment>
  121. <StyledNavTabs>
  122. {tabs.map(tab => (
  123. <ListLink
  124. key={tab.to}
  125. to={getTabUrl(tab.to)}
  126. isActive={() => getActiveTabTo() === tab.to}
  127. >
  128. {tab.title}
  129. </ListLink>
  130. ))}
  131. </StyledNavTabs>
  132. </Fragment>
  133. </Layout.Header>
  134. );
  135. };
  136. const IconWrapper = styled('span')`
  137. transition: color 0.3s ease-in-out;
  138. &,
  139. a {
  140. color: ${p => p.theme.gray300};
  141. display: flex;
  142. &:hover {
  143. cursor: pointer;
  144. color: ${p => p.theme.textColor};
  145. }
  146. }
  147. `;
  148. const StyledNavTabs = styled(NavTabs)`
  149. margin-bottom: 0;
  150. /* Makes sure the tabs are pushed into another row */
  151. width: 100%;
  152. `;
  153. const NavTabsBadge = styled(Badge)`
  154. @media (max-width: ${p => p.theme.breakpoints.small}) {
  155. display: none;
  156. }
  157. `;
  158. export default ReleaseHeader;