versionHoverCard.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import {Client} from 'app/api';
  4. import AvatarList from 'app/components/avatar/avatarList';
  5. import Button from 'app/components/button';
  6. import Clipboard from 'app/components/clipboard';
  7. import Hovercard from 'app/components/hovercard';
  8. import LastCommit from 'app/components/lastCommit';
  9. import LoadingError from 'app/components/loadingError';
  10. import LoadingIndicator from 'app/components/loadingIndicator';
  11. import RepoLabel from 'app/components/repoLabel';
  12. import TimeSince from 'app/components/timeSince';
  13. import Version from 'app/components/version';
  14. import {IconCopy} from 'app/icons';
  15. import {t} from 'app/locale';
  16. import space from 'app/styles/space';
  17. import {Deploy, Organization, Release, Repository} from 'app/types';
  18. import {defined} from 'app/utils';
  19. import withApi from 'app/utils/withApi';
  20. import withRelease from 'app/utils/withRelease';
  21. import withRepositories from 'app/utils/withRepositories';
  22. type Props = {
  23. api: Client;
  24. projectSlug: string;
  25. releaseVersion: string;
  26. organization: Organization;
  27. release?: Release;
  28. releaseLoading?: boolean;
  29. releaseError?: Error;
  30. deploys?: Array<Deploy>;
  31. deploysLoading?: boolean;
  32. deploysError?: Error;
  33. repositories?: Array<Repository>;
  34. repositoriesLoading?: boolean;
  35. repositoriesError?: Error;
  36. };
  37. type State = {
  38. visible: boolean;
  39. };
  40. class VersionHoverCard extends React.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 row-flex">
  103. <div className="col-xs-4">
  104. <h6>{t('New Issues')}</h6>
  105. <div className="count-since">{release.newGroups}</div>
  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 && <LastCommit commit={lastCommit} headerClass="commit-heading" />}
  123. {deploys.length > 0 && (
  124. <div>
  125. <div className="divider">
  126. <h6 className="deploy-heading">{t('Deploys')}</h6>
  127. </div>
  128. {mostRecentDeploySlice.map((env, idx) => {
  129. const dateFinished = recentDeploysByEnvironment[env];
  130. return (
  131. <div className="deploy" key={idx}>
  132. <div className="deploy-meta" style={{position: 'relative'}}>
  133. <VersionRepoLabel>{env}</VersionRepoLabel>
  134. {dateFinished && <StyledTimeSince date={dateFinished} />}
  135. </div>
  136. </div>
  137. );
  138. })}
  139. </div>
  140. )}
  141. </div>
  142. ),
  143. };
  144. }
  145. render() {
  146. const {
  147. deploysLoading,
  148. deploysError,
  149. release,
  150. releaseLoading,
  151. releaseError,
  152. repositories,
  153. repositoriesLoading,
  154. repositoriesError,
  155. } = this.props;
  156. let header: React.ReactNode = null;
  157. let body: React.ReactNode = null;
  158. const loading = !!(deploysLoading || releaseLoading || repositoriesLoading);
  159. const error = deploysError ?? releaseError ?? repositoriesError;
  160. const hasRepos = repositories && repositories.length > 0;
  161. if (loading) {
  162. body = <LoadingIndicator mini />;
  163. } else if (error) {
  164. body = <LoadingError />;
  165. } else {
  166. const renderObj: {[key: string]: React.ReactNode} =
  167. hasRepos && release ? this.getBody() : this.getRepoLink();
  168. header = renderObj.header;
  169. body = renderObj.body;
  170. }
  171. return (
  172. <Hovercard {...this.props} header={header} body={body}>
  173. {this.props.children}
  174. </Hovercard>
  175. );
  176. }
  177. }
  178. export {VersionHoverCard};
  179. export default withApi(withRelease(withRepositories(VersionHoverCard)));
  180. const ConnectRepo = styled('div')`
  181. padding: ${space(2)};
  182. text-align: center;
  183. `;
  184. const VersionRepoLabel = styled(RepoLabel)`
  185. width: 86px;
  186. `;
  187. const StyledTimeSince = styled(TimeSince)`
  188. color: ${p => p.theme.gray300};
  189. position: absolute;
  190. left: 98px;
  191. width: 50%;
  192. padding: 3px 0;
  193. `;
  194. const HeaderWrapper = styled('div')`
  195. display: flex;
  196. align-items: center;
  197. justify-content: space-between;
  198. `;
  199. const VersionWrapper = styled('div')`
  200. display: flex;
  201. flex: 1;
  202. align-items: center;
  203. justify-content: flex-end;
  204. `;
  205. const StyledVersion = styled(Version)`
  206. margin-right: ${space(0.5)};
  207. max-width: 190px;
  208. `;
  209. const ClipboardIconWrapper = styled('span')`
  210. &:hover {
  211. cursor: pointer;
  212. }
  213. `;