projectVelocityScoreCard.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import {Fragment} from 'react';
  2. import {fetchAnyReleaseExistence} from 'sentry/actionCreators/projects';
  3. import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
  4. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  5. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  6. import ScoreCard from 'sentry/components/scoreCard';
  7. import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils';
  8. import {IconArrow} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import type {PageFilters} from 'sentry/types/core';
  11. import type {Organization} from 'sentry/types/organization';
  12. import {defined} from 'sentry/utils';
  13. import {getPeriod} from 'sentry/utils/duration/getPeriod';
  14. import MissingReleasesButtons from '../missingFeatureButtons/missingReleasesButtons';
  15. const API_LIMIT = 1000;
  16. type Release = {date: string; version: string};
  17. type Props = DeprecatedAsyncComponent['props'] & {
  18. isProjectStabilized: boolean;
  19. organization: Organization;
  20. selection: PageFilters;
  21. query?: string;
  22. };
  23. type State = DeprecatedAsyncComponent['state'] & {
  24. currentReleases: Release[] | null;
  25. noReleaseEver: boolean;
  26. previousReleases: Release[] | null;
  27. };
  28. class ProjectVelocityScoreCard extends DeprecatedAsyncComponent<Props, State> {
  29. shouldRenderBadRequests = true;
  30. getDefaultState() {
  31. return {
  32. ...super.getDefaultState(),
  33. currentReleases: null,
  34. previousReleases: null,
  35. noReleaseEver: false,
  36. };
  37. }
  38. getEndpoints() {
  39. const {organization, selection, isProjectStabilized, query} = this.props;
  40. if (!isProjectStabilized) {
  41. return [];
  42. }
  43. const {projects, environments, datetime} = selection;
  44. const {period} = datetime;
  45. const commonQuery = {
  46. environment: environments,
  47. project: projects[0],
  48. query,
  49. };
  50. const endpoints: ReturnType<DeprecatedAsyncComponent['getEndpoints']> = [
  51. [
  52. 'currentReleases',
  53. `/organizations/${organization.slug}/releases/stats/`,
  54. {
  55. includeAllArgs: true,
  56. method: 'GET',
  57. query: {
  58. ...commonQuery,
  59. ...normalizeDateTimeParams(datetime),
  60. },
  61. },
  62. ],
  63. ];
  64. if (
  65. shouldFetchPreviousPeriod({
  66. start: datetime.start,
  67. end: datetime.end,
  68. period: datetime.period,
  69. })
  70. ) {
  71. const {start: previousStart} = parseStatsPeriod(
  72. getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: true})
  73. .statsPeriod!
  74. );
  75. const {start: previousEnd} = parseStatsPeriod(
  76. getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: false})
  77. .statsPeriod!
  78. );
  79. endpoints.push([
  80. 'previousReleases',
  81. `/organizations/${organization.slug}/releases/stats/`,
  82. {
  83. query: {
  84. ...commonQuery,
  85. start: previousStart,
  86. end: previousEnd,
  87. },
  88. },
  89. ]);
  90. }
  91. return endpoints;
  92. }
  93. /**
  94. * If our releases are empty, determine if we had a release in the last 90 days (empty message differs then)
  95. */
  96. async onLoadAllEndpointsSuccess() {
  97. const {currentReleases, previousReleases} = this.state;
  98. const {organization, selection, isProjectStabilized} = this.props;
  99. if (!isProjectStabilized) {
  100. return;
  101. }
  102. if ([...(currentReleases ?? []), ...(previousReleases ?? [])].length !== 0) {
  103. this.setState({noReleaseEver: false});
  104. return;
  105. }
  106. this.setState({loading: true});
  107. const hasOlderReleases = await fetchAnyReleaseExistence(
  108. this.api,
  109. organization.slug,
  110. selection.projects[0]
  111. );
  112. this.setState({noReleaseEver: !hasOlderReleases, loading: false});
  113. }
  114. get cardTitle() {
  115. return t('Number of Releases');
  116. }
  117. get cardHelp() {
  118. return this.trend
  119. ? t(
  120. 'The number of releases for this project and how it has changed since the last period.'
  121. )
  122. : t('The number of releases for this project.');
  123. }
  124. get trend() {
  125. const {currentReleases, previousReleases} = this.state;
  126. if (!defined(currentReleases) || !defined(previousReleases)) {
  127. return null;
  128. }
  129. return currentReleases.length - previousReleases.length;
  130. }
  131. get trendStatus(): React.ComponentProps<typeof ScoreCard>['trendStatus'] {
  132. if (!this.trend) {
  133. return undefined;
  134. }
  135. return this.trend > 0 ? 'good' : 'bad';
  136. }
  137. componentDidUpdate(prevProps: Props) {
  138. const {selection, isProjectStabilized, query} = this.props;
  139. if (
  140. prevProps.selection !== selection ||
  141. prevProps.isProjectStabilized !== isProjectStabilized ||
  142. prevProps.query !== query
  143. ) {
  144. this.remountComponent();
  145. }
  146. }
  147. renderLoading() {
  148. return this.renderBody();
  149. }
  150. renderMissingFeatureCard() {
  151. const {organization} = this.props;
  152. return (
  153. <ScoreCard
  154. title={this.cardTitle}
  155. help={this.cardHelp}
  156. score={<MissingReleasesButtons organization={organization} />}
  157. />
  158. );
  159. }
  160. renderScore() {
  161. const {currentReleases, loading} = this.state;
  162. if (loading || !defined(currentReleases)) {
  163. return '\u2014';
  164. }
  165. return currentReleases.length === API_LIMIT
  166. ? `${API_LIMIT - 1}+`
  167. : currentReleases.length;
  168. }
  169. renderTrend() {
  170. const {loading, currentReleases} = this.state;
  171. if (loading || !defined(this.trend) || currentReleases?.length === API_LIMIT) {
  172. return null;
  173. }
  174. return (
  175. <Fragment>
  176. {this.trend >= 0 ? (
  177. <IconArrow direction="up" size="xs" />
  178. ) : (
  179. <IconArrow direction="down" size="xs" />
  180. )}
  181. {Math.abs(this.trend)}
  182. </Fragment>
  183. );
  184. }
  185. renderBody() {
  186. const {noReleaseEver} = this.state;
  187. if (noReleaseEver) {
  188. return this.renderMissingFeatureCard();
  189. }
  190. return (
  191. <ScoreCard
  192. title={this.cardTitle}
  193. help={this.cardHelp}
  194. score={this.renderScore()}
  195. trend={this.renderTrend()}
  196. trendStatus={this.trendStatus}
  197. />
  198. );
  199. }
  200. }
  201. export default ProjectVelocityScoreCard;