projectLatestReleases.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import pick from 'lodash/pick';
  5. import {SectionHeading} from 'sentry/components/charts/styles';
  6. import {DateTime} from 'sentry/components/dateTime';
  7. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  8. import LoadingError from 'sentry/components/loadingError';
  9. import Placeholder from 'sentry/components/placeholder';
  10. import TextOverflow from 'sentry/components/textOverflow';
  11. import Version from 'sentry/components/version';
  12. import {URL_PARAM} from 'sentry/constants/pageFilters';
  13. import {IconOpen} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Organization} from 'sentry/types/organization';
  17. import type {Project} from 'sentry/types/project';
  18. import type {Release} from 'sentry/types/release';
  19. import {useApiQuery} from 'sentry/utils/queryClient';
  20. import {makeReleasesPathname} from 'sentry/views/releases/utils/pathnames';
  21. import MissingReleasesButtons from './missingFeatureButtons/missingReleasesButtons';
  22. import {SectionHeadingLink, SectionHeadingWrapper, SidebarSection} from './styles';
  23. const PLACEHOLDER_AND_EMPTY_HEIGHT = '160px';
  24. type Props = {
  25. isProjectStabilized: boolean;
  26. location: Location;
  27. organization: Organization;
  28. projectSlug: string;
  29. project?: Project;
  30. };
  31. type BodyProps = {
  32. isError: boolean;
  33. isLoading: boolean;
  34. isProjectStabilized: boolean;
  35. organization: Organization;
  36. project: Project | undefined;
  37. releases: Release[] | null;
  38. };
  39. function useHasOlderReleases({
  40. releases,
  41. releasesLoading,
  42. organization,
  43. project,
  44. isProjectStabilized,
  45. }: {
  46. isProjectStabilized: boolean;
  47. organization: Organization;
  48. project: Project | undefined;
  49. releases: Release[] | null;
  50. releasesLoading: boolean;
  51. }) {
  52. const skipOldReleaseCheck =
  53. releasesLoading ||
  54. (releases ?? []).length !== 0 ||
  55. !project?.id ||
  56. !isProjectStabilized;
  57. const {data: olderReleases, isPending} = useApiQuery<Release[]>(
  58. [
  59. `/organizations/${organization.slug}/releases/stats/`,
  60. {
  61. query: {
  62. statsPeriod: '90d',
  63. project: project?.id,
  64. per_page: 1,
  65. },
  66. },
  67. ],
  68. {staleTime: 0, enabled: !skipOldReleaseCheck}
  69. );
  70. if (skipOldReleaseCheck) {
  71. return true;
  72. }
  73. if (isPending) {
  74. return null;
  75. }
  76. return (olderReleases?.length ?? 0) > 0;
  77. }
  78. function ReleasesBody({
  79. organization,
  80. project,
  81. isProjectStabilized,
  82. releases,
  83. isLoading,
  84. isError,
  85. }: BodyProps) {
  86. const hasOlderReleases = useHasOlderReleases({
  87. organization,
  88. project,
  89. releases,
  90. releasesLoading: isLoading,
  91. isProjectStabilized,
  92. });
  93. const checkingForOlderReleases = !(releases ?? []).length && hasOlderReleases === null;
  94. const showLoadingIndicator =
  95. isLoading || checkingForOlderReleases || !isProjectStabilized;
  96. if (isError) {
  97. return <LoadingError />;
  98. }
  99. if (showLoadingIndicator) {
  100. return <Placeholder height={PLACEHOLDER_AND_EMPTY_HEIGHT} />;
  101. }
  102. if (!hasOlderReleases) {
  103. return (
  104. <MissingReleasesButtons
  105. organization={organization}
  106. projectId={project?.id}
  107. platform={project?.platform}
  108. />
  109. );
  110. }
  111. if (!releases || releases.length === 0) {
  112. return (
  113. <StyledEmptyStateWarning small>{t('No releases found')}</StyledEmptyStateWarning>
  114. );
  115. }
  116. return (
  117. <ReleasesTable>
  118. {releases.map(release => (
  119. <Fragment key={release.version}>
  120. <DateTime
  121. date={release.lastDeploy?.dateFinished || release.dateCreated}
  122. seconds={false}
  123. />
  124. <TextOverflow>
  125. <StyledVersion
  126. version={release.version}
  127. tooltipRawVersion
  128. projectId={project?.id}
  129. />
  130. </TextOverflow>
  131. </Fragment>
  132. ))}
  133. </ReleasesTable>
  134. );
  135. }
  136. function ProjectLatestReleases({
  137. isProjectStabilized,
  138. location,
  139. organization,
  140. projectSlug,
  141. project,
  142. }: Props) {
  143. const {
  144. data: releases = null,
  145. isLoading,
  146. isError,
  147. } = useApiQuery<Release[]>(
  148. [
  149. `/projects/${organization.slug}/${projectSlug}/releases/`,
  150. {
  151. query: {
  152. ...pick(location.query, Object.values(URL_PARAM)),
  153. per_page: 5,
  154. },
  155. },
  156. ],
  157. {
  158. staleTime: 0,
  159. enabled: isProjectStabilized,
  160. }
  161. );
  162. return (
  163. <SidebarSection>
  164. <SectionHeadingWrapper>
  165. <SectionHeading>{t('Latest Releases')}</SectionHeading>
  166. <SectionHeadingLink
  167. to={{
  168. pathname: makeReleasesPathname({
  169. organization,
  170. path: '/',
  171. }),
  172. query: {
  173. statsPeriod: undefined,
  174. start: undefined,
  175. end: undefined,
  176. utc: undefined,
  177. },
  178. }}
  179. >
  180. <IconOpen />
  181. </SectionHeadingLink>
  182. </SectionHeadingWrapper>
  183. <div>
  184. <ReleasesBody
  185. organization={organization}
  186. project={project}
  187. isProjectStabilized={isProjectStabilized}
  188. releases={releases}
  189. isLoading={isLoading}
  190. isError={isError}
  191. />
  192. </div>
  193. </SidebarSection>
  194. );
  195. }
  196. const ReleasesTable = styled('div')`
  197. display: grid;
  198. font-size: ${p => p.theme.fontSizeMedium};
  199. white-space: nowrap;
  200. grid-template-columns: 1fr auto;
  201. margin-bottom: ${space(2)};
  202. & > * {
  203. padding: ${space(0.5)} ${space(1)};
  204. height: 32px;
  205. }
  206. & > *:nth-child(2n + 2) {
  207. text-align: right;
  208. }
  209. & > *:nth-child(4n + 1),
  210. & > *:nth-child(4n + 2) {
  211. background-color: ${p => p.theme.rowBackground};
  212. }
  213. `;
  214. const StyledVersion = styled(Version)`
  215. ${p => p.theme.overflowEllipsis}
  216. line-height: 1.6;
  217. font-variant-numeric: tabular-nums;
  218. `;
  219. const StyledEmptyStateWarning = styled(EmptyStateWarning)`
  220. height: ${PLACEHOLDER_AND_EMPTY_HEIGHT};
  221. justify-content: center;
  222. `;
  223. export default ProjectLatestReleases;