version.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import * as React from 'react';
  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 overflowEllipsis from 'sentry/styles/overflowEllipsis';
  11. import space from 'sentry/styles/space';
  12. import {Organization} from 'sentry/types';
  13. import {formatVersion} from 'sentry/utils/formatters';
  14. import theme from 'sentry/utils/theme';
  15. import withOrganization from 'sentry/utils/withOrganization';
  16. type Props = {
  17. /**
  18. * Raw version (canonical release identifier)
  19. */
  20. version: string;
  21. /**
  22. * Organization injected by withOrganization HOC
  23. */
  24. organization: Organization;
  25. /**
  26. * Should the version be a link to the release page
  27. */
  28. anchor?: boolean;
  29. /**
  30. * Should link to release page preserve user's global selection values
  31. */
  32. preserveGlobalSelection?: boolean;
  33. /**
  34. * Should there be a tooltip with raw version on hover
  35. */
  36. tooltipRawVersion?: boolean;
  37. /**
  38. * Should we also show package name
  39. */
  40. withPackage?: boolean;
  41. /**
  42. * Will add project ID to the linked url (can be overridden by preserveGlobalSelection).
  43. * If not provided and user does not have global-views enabled, it will try to take it from current url query.
  44. */
  45. projectId?: string;
  46. /**
  47. * Ellipsis on overflow
  48. */
  49. truncate?: boolean;
  50. className?: string;
  51. };
  52. const Version = ({
  53. version,
  54. organization,
  55. anchor = true,
  56. preserveGlobalSelection,
  57. tooltipRawVersion,
  58. withPackage,
  59. projectId,
  60. truncate,
  61. className,
  62. location,
  63. }: WithRouterProps & Props) => {
  64. const versionToDisplay = formatVersion(version, withPackage);
  65. let releaseDetailProjectId: null | undefined | string | string[];
  66. if (projectId) {
  67. // we can override preserveGlobalSelection's project id
  68. releaseDetailProjectId = projectId;
  69. } else if (!organization?.features.includes('global-views')) {
  70. // we need this for users without global-views, otherwise they might get `This release may not be in your selected project`
  71. releaseDetailProjectId = location?.query.project;
  72. }
  73. const renderVersion = () => {
  74. if (anchor && organization?.slug) {
  75. const props = {
  76. to: {
  77. pathname: `/organizations/${organization?.slug}/releases/${encodeURIComponent(
  78. version
  79. )}/`,
  80. query: releaseDetailProjectId ? {project: releaseDetailProjectId} : undefined,
  81. },
  82. className,
  83. };
  84. if (preserveGlobalSelection) {
  85. return (
  86. <GlobalSelectionLink {...props}>
  87. <VersionText truncate={truncate}>{versionToDisplay}</VersionText>
  88. </GlobalSelectionLink>
  89. );
  90. }
  91. return (
  92. <Link {...props}>
  93. <VersionText truncate={truncate}>{versionToDisplay}</VersionText>
  94. </Link>
  95. );
  96. }
  97. return (
  98. <VersionText className={className} truncate={truncate}>
  99. {versionToDisplay}
  100. </VersionText>
  101. );
  102. };
  103. const renderTooltipContent = () => (
  104. <TooltipContent
  105. onClick={e => {
  106. e.stopPropagation();
  107. }}
  108. >
  109. <TooltipVersionWrapper>{version}</TooltipVersionWrapper>
  110. <Clipboard value={version}>
  111. <TooltipClipboardIconWrapper>
  112. <IconCopy size="xs" color="white" />
  113. </TooltipClipboardIconWrapper>
  114. </Clipboard>
  115. </TooltipContent>
  116. );
  117. const getPopperStyles = () => {
  118. // if the version name is not a hash (sha1 or sha265) and we are not on 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[0]}) {
  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. popperStyle={getPopperStyles()}
  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. // ${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. ${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>;