version.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. // eslint-disable-next-line no-restricted-imports
  2. import {withRouter, WithRouterProps} from 'react-router';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import Clipboard from 'sentry/components/clipboard';
  6. import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
  7. import Link from 'sentry/components/links/link';
  8. import Tooltip from 'sentry/components/tooltip';
  9. import {IconCopy} from 'sentry/icons';
  10. import space from 'sentry/styles/space';
  11. import {Organization} from 'sentry/types';
  12. import {formatVersion} from 'sentry/utils/formatters';
  13. import theme from 'sentry/utils/theme';
  14. import withOrganization from 'sentry/utils/withOrganization';
  15. type Props = {
  16. /**
  17. * Organization injected by withOrganization HOC
  18. */
  19. organization: Organization;
  20. /**
  21. * Raw version (canonical release identifier)
  22. */
  23. version: string;
  24. /**
  25. * Should the version be a link to the release page
  26. */
  27. anchor?: boolean;
  28. className?: string;
  29. /**
  30. * Should link to release page preserve user's page filter values
  31. */
  32. preservePageFilters?: boolean;
  33. /**
  34. * Will add project ID to the linked url (can be overridden by preservePageFilters).
  35. * If not provided and user does not have global-views enabled, it will try to take it from current url query.
  36. */
  37. projectId?: string;
  38. /**
  39. * Should there be a tooltip with raw version on hover
  40. */
  41. tooltipRawVersion?: boolean;
  42. /**
  43. * Ellipsis on overflow
  44. */
  45. truncate?: boolean;
  46. /**
  47. * Should we also show package name
  48. */
  49. withPackage?: boolean;
  50. };
  51. const Version = ({
  52. version,
  53. organization,
  54. anchor = true,
  55. preservePageFilters,
  56. tooltipRawVersion,
  57. withPackage,
  58. projectId,
  59. truncate,
  60. className,
  61. location,
  62. }: WithRouterProps & Props) => {
  63. const versionToDisplay = formatVersion(version, withPackage);
  64. let releaseDetailProjectId: null | undefined | string | string[];
  65. if (projectId) {
  66. // we can override preservePageFilters's project id
  67. releaseDetailProjectId = projectId;
  68. } else if (!organization?.features.includes('global-views')) {
  69. // we need this for users without global-views, otherwise they might get `This release may not be in your selected project`
  70. releaseDetailProjectId = location?.query.project;
  71. }
  72. const renderVersion = () => {
  73. if (anchor && organization?.slug) {
  74. const props = {
  75. to: {
  76. pathname: `/organizations/${organization?.slug}/releases/${encodeURIComponent(
  77. version
  78. )}/`,
  79. query: releaseDetailProjectId ? {project: releaseDetailProjectId} : undefined,
  80. },
  81. className,
  82. };
  83. if (preservePageFilters) {
  84. return (
  85. <GlobalSelectionLink {...props}>
  86. <VersionText truncate={truncate}>{versionToDisplay}</VersionText>
  87. </GlobalSelectionLink>
  88. );
  89. }
  90. return (
  91. <Link {...props}>
  92. <VersionText truncate={truncate}>{versionToDisplay}</VersionText>
  93. </Link>
  94. );
  95. }
  96. return (
  97. <VersionText className={className} truncate={truncate}>
  98. {versionToDisplay}
  99. </VersionText>
  100. );
  101. };
  102. const renderTooltipContent = () => (
  103. <TooltipContent
  104. onClick={e => {
  105. e.stopPropagation();
  106. }}
  107. >
  108. <TooltipVersionWrapper>{version}</TooltipVersionWrapper>
  109. <Clipboard value={version}>
  110. <TooltipClipboardIconWrapper>
  111. <IconCopy size="xs" />
  112. </TooltipClipboardIconWrapper>
  113. </Clipboard>
  114. </TooltipContent>
  115. );
  116. const getOverlayStyle = () => {
  117. // if the version name is not a hash (sha1 or sha265) and we are not on
  118. // mobile, allow tooltip to be as wide as 500px
  119. if (/(^[a-f0-9]{40}$)|(^[a-f0-9]{64}$)/.test(version)) {
  120. return undefined;
  121. }
  122. return css`
  123. @media (min-width: ${theme.breakpoints.small}) {
  124. max-width: 500px;
  125. }
  126. `;
  127. };
  128. return (
  129. <Tooltip
  130. title={renderTooltipContent()}
  131. disabled={!tooltipRawVersion}
  132. isHoverable
  133. containerDisplayMode={truncate ? 'block' : 'inline-block'}
  134. overlayStyle={getOverlayStyle()}
  135. >
  136. {renderVersion()}
  137. </Tooltip>
  138. );
  139. };
  140. // TODO(matej): try to wrap version with this when truncate prop is true (in separate PR)
  141. // const VersionWrapper = styled('div')`
  142. // ${p => p.theme.overflowEllipsis};
  143. // max-width: 100%;
  144. // width: auto;
  145. // display: inline-block;
  146. // `;
  147. const VersionText = styled('span')<{truncate?: boolean}>`
  148. ${p =>
  149. p.truncate &&
  150. `max-width: 100%;
  151. display: block;
  152. overflow: hidden;
  153. font-variant-numeric: tabular-nums;
  154. text-overflow: ellipsis;
  155. white-space: nowrap;`}
  156. `;
  157. const TooltipContent = styled('span')`
  158. display: flex;
  159. align-items: center;
  160. `;
  161. const TooltipVersionWrapper = styled('span')`
  162. ${p => p.theme.overflowEllipsis}
  163. `;
  164. const TooltipClipboardIconWrapper = styled('span')`
  165. margin-left: ${space(0.5)};
  166. position: relative;
  167. bottom: -${space(0.25)};
  168. &:hover {
  169. cursor: pointer;
  170. }
  171. `;
  172. type PropsWithoutOrg = Omit<Props, 'organization'>;
  173. export default withOrganization(
  174. withRouter(Version)
  175. ) as React.ComponentClass<PropsWithoutOrg>;