versionHoverCard.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Client} from 'sentry/api';
  4. import AvatarList from 'sentry/components/avatar/avatarList';
  5. import Button from 'sentry/components/button';
  6. import Clipboard from 'sentry/components/clipboard';
  7. import {Divider, Hovercard} from 'sentry/components/hovercard';
  8. import LastCommit from 'sentry/components/lastCommit';
  9. import LoadingError from 'sentry/components/loadingError';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import RepoLabel from 'sentry/components/repoLabel';
  12. import TimeSince from 'sentry/components/timeSince';
  13. import Version from 'sentry/components/version';
  14. import {IconCopy} from 'sentry/icons';
  15. import {t} from 'sentry/locale';
  16. import space from 'sentry/styles/space';
  17. import {Deploy, Organization, Release, Repository} from 'sentry/types';
  18. import {defined} from 'sentry/utils';
  19. import withApi from 'sentry/utils/withApi';
  20. import withRelease from 'sentry/utils/withRelease';
  21. import withRepositories from 'sentry/utils/withRepositories';
  22. interface Props extends React.ComponentProps<typeof Hovercard> {
  23. api: Client;
  24. organization: Organization;
  25. projectSlug: string;
  26. releaseVersion: string;
  27. deploys?: Array<Deploy>;
  28. deploysError?: Error;
  29. deploysLoading?: boolean;
  30. release?: Release;
  31. releaseError?: Error;
  32. releaseLoading?: boolean;
  33. repositories?: Array<Repository>;
  34. repositoriesError?: Error;
  35. repositoriesLoading?: boolean;
  36. }
  37. type State = {
  38. visible: boolean;
  39. };
  40. class VersionHoverCard extends Component<Props, State> {
  41. state: State = {
  42. visible: false,
  43. };
  44. toggleHovercard() {
  45. this.setState({
  46. visible: true,
  47. });
  48. }
  49. getRepoLink() {
  50. const {organization} = this.props;
  51. const orgSlug = organization.slug;
  52. return {
  53. header: null,
  54. body: (
  55. <ConnectRepo>
  56. <h5>{t('Releases are better with commit data!')}</h5>
  57. <p>
  58. {t(
  59. 'Connect a repository to see commit info, files changed, and authors involved in future releases.'
  60. )}
  61. </p>
  62. <Button href={`/organizations/${orgSlug}/repos/`} priority="primary">
  63. {t('Connect a repository')}
  64. </Button>
  65. </ConnectRepo>
  66. ),
  67. };
  68. }
  69. getBody() {
  70. const {releaseVersion, release, deploys} = this.props;
  71. if (release === undefined || !defined(deploys)) {
  72. return {header: null, body: null};
  73. }
  74. const {lastCommit} = release;
  75. const recentDeploysByEnvironment = deploys.reduce(function (dbe, deploy) {
  76. const {dateFinished, environment} = deploy;
  77. if (!dbe.hasOwnProperty(environment)) {
  78. dbe[environment] = dateFinished;
  79. }
  80. return dbe;
  81. }, {});
  82. let mostRecentDeploySlice = Object.keys(recentDeploysByEnvironment);
  83. if (Object.keys(recentDeploysByEnvironment).length > 3) {
  84. mostRecentDeploySlice = Object.keys(recentDeploysByEnvironment).slice(0, 3);
  85. }
  86. return {
  87. header: (
  88. <HeaderWrapper>
  89. {t('Release')}
  90. <VersionWrapper>
  91. <StyledVersion version={releaseVersion} truncate anchor={false} />
  92. <Clipboard value={releaseVersion}>
  93. <ClipboardIconWrapper>
  94. <IconCopy size="xs" />
  95. </ClipboardIconWrapper>
  96. </Clipboard>
  97. </VersionWrapper>
  98. </HeaderWrapper>
  99. ),
  100. body: (
  101. <div>
  102. <div className="row">
  103. <div className="col-xs-4">
  104. <h6>{t('New Issues')}</h6>
  105. <CountSince>{release.newGroups}</CountSince>
  106. </div>
  107. <div className="col-xs-8">
  108. <h6 style={{textAlign: 'right'}}>
  109. {release.commitCount}{' '}
  110. {release.commitCount !== 1 ? t('commits ') : t('commit ')} {t('by ')}{' '}
  111. {release.authors.length}{' '}
  112. {release.authors.length !== 1 ? t('authors') : t('author')}{' '}
  113. </h6>
  114. <AvatarList
  115. users={release.authors}
  116. avatarSize={25}
  117. tooltipOptions={{container: 'body'} as any}
  118. typeMembers="authors"
  119. />
  120. </div>
  121. </div>
  122. {lastCommit && <StyledLastCommit commit={lastCommit} />}
  123. {deploys.length > 0 && (
  124. <div>
  125. <Divider>
  126. <h6>{t('Deploys')}</h6>
  127. </Divider>
  128. {mostRecentDeploySlice.map((env, idx) => {
  129. const dateFinished = recentDeploysByEnvironment[env];
  130. return (
  131. <DeployWrap key={idx}>
  132. <VersionRepoLabel>{env}</VersionRepoLabel>
  133. {dateFinished && <StyledTimeSince date={dateFinished} />}
  134. </DeployWrap>
  135. );
  136. })}
  137. </div>
  138. )}
  139. </div>
  140. ),
  141. };
  142. }
  143. render() {
  144. const {
  145. deploysLoading,
  146. deploysError,
  147. release,
  148. releaseLoading,
  149. releaseError,
  150. repositories,
  151. repositoriesLoading,
  152. repositoriesError,
  153. } = this.props;
  154. let header: React.ReactNode = null;
  155. let body: React.ReactNode = null;
  156. const loading = !!(deploysLoading || releaseLoading || repositoriesLoading);
  157. const error = deploysError ?? releaseError ?? repositoriesError;
  158. const hasRepos = repositories && repositories.length > 0;
  159. if (loading) {
  160. body = <LoadingIndicator mini />;
  161. } else if (error) {
  162. body = <LoadingError />;
  163. } else {
  164. const renderObj: {[key: string]: React.ReactNode} =
  165. hasRepos && release ? this.getBody() : this.getRepoLink();
  166. header = renderObj.header;
  167. body = renderObj.body;
  168. }
  169. return (
  170. <Hovercard {...this.props} header={header} body={body}>
  171. {this.props.children}
  172. </Hovercard>
  173. );
  174. }
  175. }
  176. export {VersionHoverCard};
  177. export default withApi(withRelease(withRepositories(VersionHoverCard)));
  178. const ConnectRepo = styled('div')`
  179. padding: ${space(2)};
  180. text-align: center;
  181. `;
  182. const VersionRepoLabel = styled(RepoLabel)`
  183. width: 86px;
  184. `;
  185. const StyledTimeSince = styled(TimeSince)`
  186. color: ${p => p.theme.gray300};
  187. font-size: ${p => p.theme.fontSizeSmall};
  188. `;
  189. const HeaderWrapper = styled('div')`
  190. display: flex;
  191. align-items: center;
  192. justify-content: space-between;
  193. `;
  194. const VersionWrapper = styled('div')`
  195. display: flex;
  196. flex: 1;
  197. align-items: center;
  198. justify-content: flex-end;
  199. `;
  200. const StyledVersion = styled(Version)`
  201. margin-right: ${space(0.5)};
  202. max-width: 190px;
  203. `;
  204. const ClipboardIconWrapper = styled('span')`
  205. &:hover {
  206. cursor: pointer;
  207. }
  208. `;
  209. const CountSince = styled('div')`
  210. color: ${p => p.theme.headingColor};
  211. font-size: ${p => p.theme.headerFontSize};
  212. `;
  213. const StyledLastCommit = styled(LastCommit)`
  214. margin-top: ${space(2)};
  215. `;
  216. const DeployWrap = styled('div')`
  217. display: grid;
  218. grid-template-columns: max-content minmax(0, 1fr);
  219. gap: ${space(1)};
  220. justify-items: start;
  221. align-items: center;
  222. `;