projectVelocityScoreCard.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
  2. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  3. import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils';
  4. import {t} from 'sentry/locale';
  5. import type {PageFilters} from 'sentry/types/core';
  6. import type {Organization} from 'sentry/types/organization';
  7. import {getPeriod} from 'sentry/utils/duration/getPeriod';
  8. import {useApiQuery} from 'sentry/utils/queryClient';
  9. import {BigNumberWidget} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidget';
  10. import {WidgetFrame} from 'sentry/views/dashboards/widgets/common/widgetFrame';
  11. import MissingReleasesButtons from '../missingFeatureButtons/missingReleasesButtons';
  12. const API_LIMIT = 1000;
  13. type Release = {date: string; version: string};
  14. const useReleaseCount = (props: Props) => {
  15. const {organization, selection, isProjectStabilized, query} = props;
  16. const isEnabled = isProjectStabilized;
  17. const {projects, environments, datetime} = selection;
  18. const {period} = datetime;
  19. const {start: previousStart} = parseStatsPeriod(
  20. getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: true})
  21. .statsPeriod!
  22. );
  23. const {start: previousEnd} = parseStatsPeriod(
  24. getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: false})
  25. .statsPeriod!
  26. );
  27. const commonQuery = {
  28. environment: environments,
  29. project: projects[0],
  30. query,
  31. };
  32. const currentQuery = useApiQuery<Release[]>(
  33. [
  34. `/organizations/${organization.slug}/releases/stats/`,
  35. {
  36. query: {
  37. ...commonQuery,
  38. ...normalizeDateTimeParams(datetime),
  39. },
  40. },
  41. ],
  42. {staleTime: 0, enabled: isEnabled}
  43. );
  44. const isPreviousPeriodEnabled = shouldFetchPreviousPeriod({
  45. start: datetime.start,
  46. end: datetime.end,
  47. period: datetime.period,
  48. });
  49. const previousQuery = useApiQuery<Release[]>(
  50. [
  51. `/organizations/${organization.slug}/releases/stats/`,
  52. {
  53. query: {
  54. ...commonQuery,
  55. start: previousStart,
  56. end: previousEnd,
  57. },
  58. },
  59. ],
  60. {
  61. staleTime: 0,
  62. enabled: isEnabled && isPreviousPeriodEnabled,
  63. }
  64. );
  65. const allReleases = [...(currentQuery.data ?? []), ...(previousQuery.data ?? [])];
  66. const isAllTimePeriodEnabled =
  67. !currentQuery.isPending &&
  68. !currentQuery.error &&
  69. !previousQuery.isPending &&
  70. !previousQuery.error &&
  71. allReleases.length === 0;
  72. const allTimeQuery = useApiQuery<Release[]>(
  73. [
  74. `/organizations/${organization.slug}/releases/stats/`,
  75. {
  76. query: {
  77. ...commonQuery,
  78. statsPeriod: '90d',
  79. per_page: 1,
  80. },
  81. },
  82. ],
  83. {
  84. staleTime: 0,
  85. enabled: isEnabled && isAllTimePeriodEnabled,
  86. }
  87. );
  88. return {
  89. data: currentQuery.data,
  90. previousData: previousQuery.data,
  91. allTimeData: allTimeQuery.data,
  92. isLoading:
  93. currentQuery.isPending ||
  94. (previousQuery.isPending && isPreviousPeriodEnabled) ||
  95. (allTimeQuery.isPending && isAllTimePeriodEnabled),
  96. error: currentQuery.error || previousQuery.error || allTimeQuery.error,
  97. refetch: () => {
  98. currentQuery.refetch();
  99. previousQuery.refetch();
  100. allTimeQuery.refetch();
  101. },
  102. };
  103. };
  104. type Props = {
  105. isProjectStabilized: boolean;
  106. organization: Organization;
  107. selection: PageFilters;
  108. query?: string;
  109. };
  110. function ProjectVelocityScoreCard(props: Props) {
  111. const {organization} = props;
  112. const {
  113. data: currentReleases,
  114. previousData: previousReleases,
  115. allTimeData: allTimeReleases,
  116. isLoading,
  117. error,
  118. refetch,
  119. } = useReleaseCount(props);
  120. const noReleaseEver =
  121. [...(allTimeReleases ?? []), ...(previousReleases ?? []), ...(allTimeReleases ?? [])]
  122. .length === 0;
  123. const cardTitle = t('Number of Releases');
  124. const cardHelp = t('The number of releases for this project.');
  125. if (!isLoading && noReleaseEver) {
  126. return (
  127. <WidgetFrame title={cardTitle} description={cardHelp}>
  128. <MissingReleasesButtons organization={organization} />
  129. </WidgetFrame>
  130. );
  131. }
  132. return (
  133. <BigNumberWidget
  134. title={cardTitle}
  135. description={cardHelp}
  136. data={[
  137. {
  138. 'count()': currentReleases?.length,
  139. },
  140. ]}
  141. previousPeriodData={[
  142. {
  143. 'count()': previousReleases?.length,
  144. },
  145. ]}
  146. maximumValue={API_LIMIT}
  147. meta={{
  148. fields: {
  149. 'count()': 'number',
  150. },
  151. }}
  152. preferredPolarity="+"
  153. isLoading={isLoading}
  154. error={error ?? undefined}
  155. onRetry={refetch}
  156. />
  157. );
  158. }
  159. export default ProjectVelocityScoreCard;