version.tsx 5.1 KB

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