versionHoverCard.tsx 7.0 KB

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