releaseHeader.tsx 5.4 KB

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