releaseHeader.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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/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 {TabList} from 'sentry/components/tabs';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import Version from 'sentry/components/version';
  14. import {URL_PARAM} from 'sentry/constants/pageFilters';
  15. import {IconOpen} from 'sentry/icons';
  16. import {t} from 'sentry/locale';
  17. import type {Organization} from 'sentry/types/organization';
  18. import type {Release, ReleaseMeta, ReleaseProject} from 'sentry/types/release';
  19. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  20. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  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. function 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. normalizeUrl({
  65. pathname: releasePath + path,
  66. query: pick(location.query, Object.values(URL_PARAM)),
  67. });
  68. const getActiveTabTo = () => {
  69. // We are not doing strict version check because there would be a tiny page shift when switching between releases with paginator
  70. const activeTab = tabs
  71. .filter(tab => tab.to.length) // remove home 'Overview' from consideration
  72. .find(tab => location.pathname.endsWith(tab.to));
  73. if (activeTab) {
  74. return activeTab.to;
  75. }
  76. return tabs[0]!.to; // default to 'Overview'
  77. };
  78. return (
  79. <Layout.Header>
  80. <Layout.HeaderContent>
  81. <Breadcrumbs
  82. crumbs={[
  83. {
  84. to: `/organizations/${organization.slug}/releases/`,
  85. label: t('Releases'),
  86. preservePageFilters: true,
  87. },
  88. {label: t('Release Details')},
  89. ]}
  90. />
  91. <Layout.Title>
  92. <IdBadge project={project} avatarSize={28} hideName />
  93. <Version version={version} anchor={false} truncate />
  94. <IconWrapper>
  95. <CopyToClipboardButton
  96. borderless
  97. size="zero"
  98. text={version}
  99. title={version}
  100. />
  101. </IconWrapper>
  102. {!!url && (
  103. <IconWrapper>
  104. <Tooltip title={url}>
  105. <ExternalLink href={url}>
  106. <IconOpen />
  107. </ExternalLink>
  108. </Tooltip>
  109. </IconWrapper>
  110. )}
  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. <Layout.HeaderTabs value={getActiveTabTo()}>
  124. <TabList hideBorder>
  125. {tabs.map(tab => (
  126. <TabList.Item key={tab.to} to={getTabUrl(tab.to)}>
  127. {tab.title}
  128. </TabList.Item>
  129. ))}
  130. </TabList>
  131. </Layout.HeaderTabs>
  132. </Layout.Header>
  133. );
  134. }
  135. const IconWrapper = styled('span')`
  136. transition: color 0.3s ease-in-out;
  137. &,
  138. a {
  139. color: ${p => p.theme.gray300};
  140. display: flex;
  141. &:hover {
  142. cursor: pointer;
  143. color: ${p => p.theme.textColor};
  144. }
  145. }
  146. `;
  147. const NavTabsBadge = styled(Badge)`
  148. @media (max-width: ${p => p.theme.breakpoints.small}) {
  149. display: none;
  150. }
  151. `;
  152. export default ReleaseHeader;