releaseHeader.tsx 5.0 KB

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