projectVelocityScoreCard.tsx 4.7 KB

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