autofixSetupModal.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. import {Fragment, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  4. import {Button} from 'sentry/components/button';
  5. import {AutofixCodebaseIndexingStatus} from 'sentry/components/events/autofix/types';
  6. import {useAutofixCodebaseIndexing} from 'sentry/components/events/autofix/useAutofixCodebaseIndexing';
  7. import {
  8. type AutofixSetupRepoDefinition,
  9. type AutofixSetupResponse,
  10. useAutofixSetup,
  11. } from 'sentry/components/events/autofix/useAutofixSetup';
  12. import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps';
  13. import HookOrDefault from 'sentry/components/hookOrDefault';
  14. import ExternalLink from 'sentry/components/links/externalLink';
  15. import LoadingError from 'sentry/components/loadingError';
  16. import LoadingIndicator from 'sentry/components/loadingIndicator';
  17. import {IconCheckmark, IconGithub} from 'sentry/icons';
  18. import {t, tct} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import {trackAnalytics} from 'sentry/utils/analytics';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. interface AutofixSetupModalProps extends ModalRenderProps {
  23. groupId: string;
  24. projectId: string;
  25. }
  26. const ConsentStep = HookOrDefault({
  27. hookName: 'component:autofix-setup-step-consent',
  28. defaultComponent: null,
  29. });
  30. function AutofixIntegrationStep({autofixSetup}: {autofixSetup: AutofixSetupResponse}) {
  31. if (autofixSetup.integration.ok) {
  32. return (
  33. <Fragment>
  34. {tct('The GitHub integration is already installed, [link: view in settings].', {
  35. link: <ExternalLink href={`/settings/integrations/github/`} />,
  36. })}
  37. <GuidedSteps.StepButtons />
  38. </Fragment>
  39. );
  40. }
  41. if (autofixSetup.integration.reason === 'integration_inactive') {
  42. return (
  43. <Fragment>
  44. <p>
  45. {tct(
  46. 'The GitHub integration has been installed but is not active. Navigate to the [integration settings page] and enable it to continue.',
  47. {
  48. link: <ExternalLink href={`/settings/integrations/github/`} />,
  49. }
  50. )}
  51. </p>
  52. <p>
  53. {tct(
  54. 'Once enabled, come back to this page. For more information related to installing the GitHub integration, read the [link:documentation].',
  55. {
  56. link: (
  57. <ExternalLink href="https://docs.sentry.io/product/integrations/source-code-mgmt/github/" />
  58. ),
  59. }
  60. )}
  61. </p>
  62. <GuidedSteps.StepButtons />
  63. </Fragment>
  64. );
  65. }
  66. if (autofixSetup.integration.reason === 'integration_no_code_mappings') {
  67. return (
  68. <Fragment>
  69. <p>
  70. {tct(
  71. 'You have an active GitHub installation, but no code mappings for this project. Add code mappings by visiting the [link:integration settings page] and editing your configuration.',
  72. {
  73. link: <ExternalLink href={`/settings/integrations/github/`} />,
  74. }
  75. )}
  76. </p>
  77. <p>
  78. {tct(
  79. 'Once added, come back to this page. For more information related to installing the GitHub integration, read the [link:documentation].',
  80. {
  81. link: (
  82. <ExternalLink href="https://docs.sentry.io/product/integrations/source-code-mgmt/github/" />
  83. ),
  84. }
  85. )}
  86. </p>
  87. <GuidedSteps.StepButtons />
  88. </Fragment>
  89. );
  90. }
  91. return (
  92. <Fragment>
  93. <p>
  94. {tct(
  95. 'Install the GitHub integration by navigating to the [link:integration settings page] and clicking the "Install" button. Follow the steps provided.',
  96. {
  97. link: <ExternalLink href={`/settings/integrations/github/`} />,
  98. }
  99. )}
  100. </p>
  101. <p>
  102. {tct(
  103. 'Once installed, come back to this page. For more information related to installing the GitHub integration, read the [link:documentation].',
  104. {
  105. link: (
  106. <ExternalLink href="https://docs.sentry.io/product/integrations/source-code-mgmt/github/" />
  107. ),
  108. }
  109. )}
  110. </p>
  111. <GuidedSteps.StepButtons />
  112. </Fragment>
  113. );
  114. }
  115. export function GitRepoLink({repo}: {repo: AutofixSetupRepoDefinition}) {
  116. if (repo.provider === 'github' || repo.provider.split(':')[1] === 'github') {
  117. return (
  118. <RepoLinkItem>
  119. <GithubLink>
  120. <IconGithub size="sm" />
  121. <span>
  122. {repo.owner}/{repo.name}
  123. </span>
  124. </GithubLink>
  125. {repo.ok ? <IconCheckmark color="success" isCircled /> : null}
  126. </RepoLinkItem>
  127. );
  128. }
  129. return (
  130. <li>
  131. {repo.owner}/{repo.name}
  132. </li>
  133. );
  134. }
  135. function AutofixGithubIntegrationStep({
  136. autofixSetup,
  137. }: {
  138. autofixSetup: AutofixSetupResponse;
  139. }) {
  140. const sortedRepos = useMemo(
  141. () =>
  142. autofixSetup.githubWriteIntegration.repos.toSorted((a, b) => {
  143. if (a.ok === b.ok) {
  144. return `${a.owner}/${a.name}`.localeCompare(`${b.owner}/${b.name}`);
  145. }
  146. return a.ok ? -1 : 1;
  147. }),
  148. [autofixSetup.githubWriteIntegration.repos]
  149. );
  150. if (autofixSetup.githubWriteIntegration.ok) {
  151. return (
  152. <Fragment>
  153. <p>
  154. {tct(
  155. 'The [link:Sentry Autofix GitHub App] has been installed on all required repositories:',
  156. {
  157. link: (
  158. <ExternalLink href="https://github.com/apps/sentry-autofix-experimental" />
  159. ),
  160. }
  161. )}
  162. </p>
  163. <RepoLinkUl>
  164. {sortedRepos.map(repo => (
  165. <GitRepoLink key={`${repo.owner}/${repo.name}`} repo={repo} />
  166. ))}
  167. </RepoLinkUl>
  168. <GuidedSteps.StepButtons />
  169. </Fragment>
  170. );
  171. }
  172. if (autofixSetup.githubWriteIntegration.repos.length > 0) {
  173. return (
  174. <Fragment>
  175. <p>
  176. {tct(
  177. 'Install and grant write access to the [link:Sentry Autofix Github App] for the following repositories:',
  178. {
  179. link: (
  180. <ExternalLink
  181. href={`https://github.com/apps/sentry-autofix-experimental/installations/new`}
  182. />
  183. ),
  184. }
  185. )}
  186. </p>
  187. <RepoLinkUl>
  188. {sortedRepos.map(repo => (
  189. <GitRepoLink key={`${repo.owner}/${repo.name}`} repo={repo} />
  190. ))}
  191. </RepoLinkUl>
  192. <p>
  193. {t(
  194. 'Without this, Autofix can still provide root analysis and suggested code changes.'
  195. )}
  196. </p>
  197. <GuidedSteps.StepButtons />
  198. </Fragment>
  199. );
  200. }
  201. return (
  202. <Fragment>
  203. <p>
  204. {tct(
  205. 'Install and grant write access to the [link:Sentry Autofix Github App] for the relevant repositories.',
  206. {
  207. link: (
  208. <ExternalLink
  209. href={`https://github.com/apps/sentry-autofix-experimental/installations/new`}
  210. />
  211. ),
  212. }
  213. )}
  214. </p>
  215. <p>
  216. {t(
  217. 'Without this, Autofix can still provide root analysis and suggested code changes.'
  218. )}
  219. </p>
  220. <GuidedSteps.StepButtons />
  221. </Fragment>
  222. );
  223. }
  224. function AutofixCodebaseIndexingStep({
  225. autofixSetup,
  226. projectId,
  227. groupId,
  228. closeModal,
  229. }: {
  230. autofixSetup: AutofixSetupResponse;
  231. closeModal: () => void;
  232. groupId: string;
  233. projectId: string;
  234. }) {
  235. const {startIndexing, status, reason} = useAutofixCodebaseIndexing({
  236. projectId,
  237. groupId,
  238. });
  239. const canIndex = autofixSetup.genAIConsent.ok && autofixSetup.integration.ok;
  240. return (
  241. <Fragment>
  242. <p>
  243. {t(
  244. 'Sentry will index your repositories to enable Autofix. This process may take a few minutes.'
  245. )}
  246. </p>
  247. {status === AutofixCodebaseIndexingStatus.ERRORED && reason ? (
  248. <LoadingError message={t('Failed to index repositories: %s', reason)} />
  249. ) : null}
  250. <GuidedSteps.StepButtons>
  251. <Button
  252. priority="primary"
  253. size="sm"
  254. disabled={!canIndex}
  255. analyticsEventKey="autofix.index_repositories_clicked"
  256. analyticsEventName="Autofix: Index Repositories Clicked"
  257. onClick={() => {
  258. startIndexing();
  259. closeModal();
  260. }}
  261. >
  262. {t('Index Repositories & Enable Autofix')}
  263. </Button>
  264. </GuidedSteps.StepButtons>
  265. </Fragment>
  266. );
  267. }
  268. function AutofixSetupSteps({
  269. projectId,
  270. groupId,
  271. autofixSetup,
  272. closeModal,
  273. }: {
  274. autofixSetup: AutofixSetupResponse;
  275. closeModal: () => void;
  276. groupId: string;
  277. projectId: string;
  278. }) {
  279. return (
  280. <GuidedSteps>
  281. <ConsentStep hasConsented={autofixSetup.genAIConsent.ok} />
  282. <GuidedSteps.Step
  283. stepKey="integration"
  284. title={t('Install the GitHub Integration')}
  285. isCompleted={autofixSetup.integration.ok}
  286. >
  287. <AutofixIntegrationStep autofixSetup={autofixSetup} />
  288. </GuidedSteps.Step>
  289. <GuidedSteps.Step
  290. stepKey="repoWriteAccess"
  291. title={t('Allow Autofix to Make Pull Requests')}
  292. isCompleted={autofixSetup.githubWriteIntegration.ok}
  293. optional
  294. >
  295. <AutofixGithubIntegrationStep autofixSetup={autofixSetup} />
  296. </GuidedSteps.Step>
  297. <GuidedSteps.Step
  298. stepKey="codebaseIndexing"
  299. title={t('Enable Autofix')}
  300. isCompleted={autofixSetup.codebaseIndexing.ok}
  301. >
  302. <AutofixCodebaseIndexingStep
  303. groupId={groupId}
  304. projectId={projectId}
  305. autofixSetup={autofixSetup}
  306. closeModal={closeModal}
  307. />
  308. </GuidedSteps.Step>
  309. </GuidedSteps>
  310. );
  311. }
  312. function AutofixSetupContent({
  313. projectId,
  314. groupId,
  315. closeModal,
  316. }: {
  317. closeModal: () => void;
  318. groupId: string;
  319. projectId: string;
  320. }) {
  321. const organization = useOrganization();
  322. const {data, canStartAutofix, isLoading, isError} = useAutofixSetup(
  323. {groupId},
  324. // Want to check setup status whenever the user comes back to the tab
  325. {refetchOnWindowFocus: true}
  326. );
  327. useEffect(() => {
  328. if (!data) {
  329. return;
  330. }
  331. trackAnalytics('autofix.setup_modal_viewed', {
  332. groupId,
  333. projectId,
  334. organization,
  335. setup_codebase_index: data.codebaseIndexing.ok,
  336. setup_gen_ai_consent: data.genAIConsent.ok,
  337. setup_integration: data.integration.ok,
  338. setup_write_integration: data.githubWriteIntegration.ok,
  339. });
  340. }, [data, groupId, organization, projectId]);
  341. if (isLoading) {
  342. return <LoadingIndicator />;
  343. }
  344. if (isError) {
  345. return <LoadingError message={t('Failed to fetch Autofix setup progress.')} />;
  346. }
  347. if (canStartAutofix) {
  348. return (
  349. <AutofixSetupDone>
  350. <DoneIcon color="success" size="xxl" isCircled />
  351. <p>{t("You've successfully configured Autofix!")}</p>
  352. <Button onClick={closeModal} priority="primary">
  353. {t("Let's go")}
  354. </Button>
  355. </AutofixSetupDone>
  356. );
  357. }
  358. return (
  359. <AutofixSetupSteps
  360. groupId={groupId}
  361. projectId={projectId}
  362. autofixSetup={data}
  363. closeModal={closeModal}
  364. />
  365. );
  366. }
  367. export function AutofixSetupModal({
  368. Header,
  369. Body,
  370. groupId,
  371. projectId,
  372. closeModal,
  373. }: AutofixSetupModalProps) {
  374. return (
  375. <Fragment>
  376. <Header closeButton>
  377. <h3>{t('Configure Autofix')}</h3>
  378. </Header>
  379. <Body>
  380. <AutofixSetupContent
  381. projectId={projectId}
  382. groupId={groupId}
  383. closeModal={closeModal}
  384. />
  385. </Body>
  386. </Fragment>
  387. );
  388. }
  389. export const AutofixSetupDone = styled('div')`
  390. position: relative;
  391. display: flex;
  392. align-items: center;
  393. justify-content: center;
  394. flex-direction: column;
  395. padding: 40px;
  396. font-size: ${p => p.theme.fontSizeLarge};
  397. `;
  398. const DoneIcon = styled(IconCheckmark)`
  399. margin-bottom: ${space(4)};
  400. `;
  401. const RepoLinkUl = styled('ul')`
  402. display: flex;
  403. flex-direction: column;
  404. gap: ${space(0.5)};
  405. padding: 0;
  406. `;
  407. const RepoLinkItem = styled('li')`
  408. display: flex;
  409. align-items: center;
  410. gap: ${space(0.5)};
  411. `;
  412. const GithubLink = styled('div')`
  413. display: flex;
  414. align-items: center;
  415. gap: ${space(0.5)};
  416. `;