filesChanged.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import {Fragment, useContext} from 'react';
  2. import * as Layout from 'sentry/components/layouts/thirds';
  3. import LoadingError from 'sentry/components/loadingError';
  4. import LoadingIndicator from 'sentry/components/loadingIndicator';
  5. import Pagination from 'sentry/components/pagination';
  6. import Panel from 'sentry/components/panels/panel';
  7. import PanelBody from 'sentry/components/panels/panelBody';
  8. import PanelHeader from 'sentry/components/panels/panelHeader';
  9. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  10. import {t, tn} from 'sentry/locale';
  11. import type {CommitFile, Repository} from 'sentry/types/integrations';
  12. import type {Organization} from 'sentry/types/organization';
  13. import type {Project} from 'sentry/types/project';
  14. import {useApiQuery} from 'sentry/utils/queryClient';
  15. import routeTitleGen from 'sentry/utils/routeTitle';
  16. import {useLocation} from 'sentry/utils/useLocation';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {useParams} from 'sentry/utils/useParams';
  19. import {useReleaseRepositories} from 'sentry/utils/useReleaseRepositories';
  20. import {useRepositories} from 'sentry/utils/useRepositories';
  21. import {formatVersion} from 'sentry/utils/versions/formatVersion';
  22. import {ReleaseContext} from 'sentry/views/releases/detail';
  23. import {getFilesByRepository, getQuery, getReposToRender} from '../utils';
  24. import {EmptyState, NoReleaseRepos, NoRepositories} from './emptyState';
  25. import FileChange from './fileChange';
  26. import RepositorySwitcher from './repositorySwitcher';
  27. interface FilesChangedProps {
  28. organization: Organization;
  29. projectSlug: Project['slug'];
  30. releaseRepos: Repository[];
  31. }
  32. function FilesChangedList({organization, releaseRepos, projectSlug}: FilesChangedProps) {
  33. const location = useLocation();
  34. const params = useParams<{release: string}>();
  35. const activeReleaseRepo =
  36. releaseRepos.find(repo => repo.name === location.query.activeRepo) ?? releaseRepos[0];
  37. const query = getQuery({location, activeRepository: activeReleaseRepo});
  38. const {
  39. data: fileList = [],
  40. isPending: isLoadingFileList,
  41. error: fileListError,
  42. refetch,
  43. getResponseHeader,
  44. } = useApiQuery<CommitFile[]>(
  45. [
  46. `/organizations/${organization.slug}/releases/${encodeURIComponent(
  47. params.release
  48. )}/commitfiles/`,
  49. {query},
  50. ],
  51. {
  52. staleTime: Infinity,
  53. }
  54. );
  55. const filesByRepository = getFilesByRepository(fileList);
  56. const reposToRender = getReposToRender(Object.keys(filesByRepository));
  57. const fileListPageLinks = getResponseHeader?.('Link');
  58. return (
  59. <Fragment>
  60. <SentryDocumentTitle
  61. title={routeTitleGen(
  62. t('Files Changed - Release %s', formatVersion(params.release)),
  63. organization.slug,
  64. false,
  65. projectSlug
  66. )}
  67. />
  68. <Layout.Body>
  69. {releaseRepos.length > 1 && (
  70. <Layout.Main fullWidth>
  71. <RepositorySwitcher
  72. repositories={releaseRepos}
  73. activeRepository={activeReleaseRepo}
  74. />
  75. </Layout.Main>
  76. )}
  77. <Layout.Main fullWidth>
  78. {fileListError && <LoadingError onRetry={refetch} />}
  79. {isLoadingFileList ? (
  80. <LoadingIndicator />
  81. ) : fileList.length ? (
  82. <Fragment>
  83. {reposToRender.map(repoName => {
  84. const repoData = filesByRepository[repoName]!;
  85. const files = Object.keys(repoData);
  86. const fileCount = files.length;
  87. return (
  88. <Panel key={repoName}>
  89. <PanelHeader>
  90. <span>{repoName}</span>
  91. <span>{tn('%s file changed', '%s files changed', fileCount)}</span>
  92. </PanelHeader>
  93. <PanelBody>
  94. {files.map(filename => {
  95. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  96. const {authors} = repoData[filename];
  97. return (
  98. <FileChange
  99. key={filename}
  100. filename={filename}
  101. authors={Object.values(authors)}
  102. />
  103. );
  104. })}
  105. </PanelBody>
  106. </Panel>
  107. );
  108. })}
  109. <Pagination pageLinks={fileListPageLinks} />
  110. </Fragment>
  111. ) : (
  112. <EmptyState>
  113. {activeReleaseRepo
  114. ? t(
  115. 'There are no changed files associated with this release in the %s repository.',
  116. activeReleaseRepo.name
  117. )
  118. : t('There are no changed files associated with this release.')}
  119. </EmptyState>
  120. )}
  121. </Layout.Main>
  122. </Layout.Body>
  123. </Fragment>
  124. );
  125. }
  126. function FilesChanged() {
  127. const organization = useOrganization();
  128. const params = useParams<{release: string}>();
  129. const releaseContext = useContext(ReleaseContext);
  130. const {
  131. data: repositories,
  132. isLoading: isLoadingRepositories,
  133. isError: isRepositoriesError,
  134. refetch: refetchRepositories,
  135. } = useRepositories({
  136. orgSlug: organization.slug,
  137. });
  138. const {
  139. data: releaseRepos,
  140. isLoading: isLoadingReleaseRepos,
  141. isError: isReleaseReposError,
  142. refetch: refetchReleaseRepos,
  143. } = useReleaseRepositories({
  144. orgSlug: organization.slug,
  145. projectSlug: releaseContext.project.slug,
  146. release: params.release,
  147. options: {
  148. enabled: !!releaseContext.project.slug,
  149. },
  150. });
  151. if (isLoadingReleaseRepos || isLoadingRepositories) {
  152. return <LoadingIndicator />;
  153. }
  154. if (isRepositoriesError || isReleaseReposError) {
  155. return (
  156. <LoadingError
  157. onRetry={() => {
  158. refetchRepositories();
  159. refetchReleaseRepos();
  160. }}
  161. />
  162. );
  163. }
  164. const noReleaseReposFound = !releaseRepos?.length;
  165. if (noReleaseReposFound) {
  166. return <NoReleaseRepos />;
  167. }
  168. const noRepositoryOrgRelatedFound = !repositories?.length;
  169. if (noRepositoryOrgRelatedFound) {
  170. return <NoRepositories orgSlug={organization.slug} />;
  171. }
  172. return (
  173. <FilesChangedList
  174. releaseRepos={releaseRepos}
  175. organization={organization}
  176. projectSlug={releaseContext.project.slug}
  177. />
  178. );
  179. }
  180. export default FilesChanged;