Browse Source

feat(autofix): Add setup modal (#68013)

Adds a "Setup Autofix" modal which helps the user through the
dependencies required to use the feature.
Malachi Willey 11 months ago
parent
commit
f5b0cc2929

+ 10 - 4
static/app/components/events/autofix/autofixBanner.spec.tsx

@@ -18,10 +18,16 @@ describe('AutofixBanner', () => {
     jest.resetAllMocks();
   });
 
+  const defaultProps = {
+    groupId: '1',
+    hasSuccessfulSetup: true,
+    triggerAutofix: jest.fn(),
+  };
+
   it('shows PII check for sentry employee users', () => {
     mockIsSentryEmployee(true);
 
-    render(<AutofixBanner triggerAutofix={() => {}} />);
+    render(<AutofixBanner {...defaultProps} />);
     expect(
       screen.getByText(
         'By clicking the button above, you confirm that there is no PII in this event.'
@@ -32,7 +38,7 @@ describe('AutofixBanner', () => {
   it('does not show PII check for non sentry employee users', () => {
     mockIsSentryEmployee(false);
 
-    render(<AutofixBanner triggerAutofix={() => {}} />);
+    render(<AutofixBanner {...defaultProps} />);
     expect(
       screen.queryByText(
         'By clicking the button above, you confirm that there is no PII in this event.'
@@ -43,7 +49,7 @@ describe('AutofixBanner', () => {
   it('can run without instructions', async () => {
     const mockTriggerAutofix = jest.fn();
 
-    render(<AutofixBanner triggerAutofix={mockTriggerAutofix} />);
+    render(<AutofixBanner {...defaultProps} triggerAutofix={mockTriggerAutofix} />);
     renderGlobalModal();
 
     await userEvent.click(screen.getByRole('button', {name: 'Gimme Fix'}));
@@ -53,7 +59,7 @@ describe('AutofixBanner', () => {
   it('can provide instructions', async () => {
     const mockTriggerAutofix = jest.fn();
 
-    render(<AutofixBanner triggerAutofix={mockTriggerAutofix} />);
+    render(<AutofixBanner {...defaultProps} triggerAutofix={mockTriggerAutofix} />);
     renderGlobalModal();
 
     await userEvent.click(screen.getByRole('button', {name: 'Give Instructions'}));

+ 30 - 12
static/app/components/events/autofix/autofixBanner.tsx

@@ -8,6 +8,7 @@ import bannerStars from 'sentry-images/spot/ai-suggestion-banner-stars.svg';
 import {openModal} from 'sentry/actionCreators/modal';
 import {Button} from 'sentry/components/button';
 import {AutofixInstructionsModal} from 'sentry/components/events/autofix/autofixInstructionsModal';
+import {AutofixSetupModal} from 'sentry/components/modals/autofixSetupModal';
 import Panel from 'sentry/components/panels/panel';
 import PanelBody from 'sentry/components/panels/panelBody';
 import {t} from 'sentry/locale';
@@ -15,10 +16,12 @@ import {space} from 'sentry/styles/space';
 import {useIsSentryEmployee} from 'sentry/utils/useIsSentryEmployee';
 
 type Props = {
+  groupId: string;
+  hasSuccessfulSetup: boolean;
   triggerAutofix: (value: string) => void;
 };
 
-export function AutofixBanner({triggerAutofix}: Props) {
+export function AutofixBanner({groupId, triggerAutofix, hasSuccessfulSetup}: Props) {
   const isSentryEmployee = useIsSentryEmployee();
   const onClickGiveInstructions = () => {
     openModal(deps => (
@@ -39,14 +42,29 @@ export function AutofixBanner({triggerAutofix}: Props) {
           <SubTitle>{t('You might get lucky, but then again, maybe not...')}</SubTitle>
         </div>
         <ContextArea>
-          <Button onClick={() => triggerAutofix('')} size="sm">
-            {t('Gimme Fix')}
-          </Button>
-          <Button onClick={onClickGiveInstructions} size="sm">
-            {t('Give Instructions')}
-          </Button>
+          {hasSuccessfulSetup ? (
+            <Fragment>
+              <Button onClick={() => triggerAutofix('')} size="sm">
+                {t('Gimme Fix')}
+              </Button>
+              <Button onClick={onClickGiveInstructions} size="sm">
+                {t('Give Instructions')}
+              </Button>
+            </Fragment>
+          ) : (
+            <Button
+              analyticsEventKey="autofix.setup_clicked"
+              analyticsEventName="Autofix: Setup Clicked"
+              onClick={() => {
+                openModal(deps => <AutofixSetupModal {...deps} groupId={groupId} />);
+              }}
+              size="sm"
+            >
+              Setup Autofix
+            </Button>
+          )}
         </ContextArea>
-        {isSentryEmployee && (
+        {isSentryEmployee && hasSuccessfulSetup && (
           <Fragment>
             <Separator />
             <PiiMessage>
@@ -96,6 +114,7 @@ const ContextArea = styled('div')`
 
 const IllustrationContainer = styled('div')`
   display: none;
+  pointer-events: none;
 
   @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
     display: block;
@@ -110,12 +129,11 @@ const IllustrationContainer = styled('div')`
 `;
 
 const Sentaur = styled('img')`
-  height: 125px;
+  height: 110px;
   position: absolute;
   bottom: 0;
   right: 185px;
   z-index: 1;
-  pointer-events: none;
 `;
 
 const Background = styled('img')`
@@ -128,9 +146,9 @@ const Background = styled('img')`
 const Stars = styled('img')`
   pointer-events: none;
   position: absolute;
-  right: -140px;
+  right: -120px;
   bottom: 40px;
-  height: 120px;
+  height: 90px;
 `;
 
 const Separator = styled('hr')`

+ 8 - 1
static/app/components/events/autofix/index.spec.tsx

@@ -13,11 +13,18 @@ const group = GroupFixture();
 const event = EventFixture();
 
 describe('AiAutofix', () => {
-  beforeAll(() => {
+  beforeEach(() => {
     MockApiClient.addMockResponse({
       url: `/issues/${group.id}/ai-autofix/`,
       body: null,
     });
+    MockApiClient.addMockResponse({
+      url: `/issues/${group.id}/autofix/setup/`,
+      body: {
+        genAIConsent: {ok: true},
+        integration: {ok: true},
+      },
+    });
   });
 
   it('renders the Banner component when autofixData is null', () => {

+ 10 - 1
static/app/components/events/autofix/index.tsx

@@ -3,6 +3,7 @@ import {AutofixBanner} from 'sentry/components/events/autofix/autofixBanner';
 import {AutofixCard} from 'sentry/components/events/autofix/autofixCard';
 import type {GroupWithAutofix} from 'sentry/components/events/autofix/types';
 import {useAiAutofix} from 'sentry/components/events/autofix/useAutofix';
+import {useAutofixSetup} from 'sentry/components/events/autofix/useAutofixSetup';
 import type {Event} from 'sentry/types';
 
 interface Props {
@@ -13,13 +14,21 @@ interface Props {
 export function Autofix({event, group}: Props) {
   const {autofixData, triggerAutofix, reset} = useAiAutofix(group, event);
 
+  const {hasSuccessfulSetup} = useAutofixSetup({
+    groupId: group.id,
+  });
+
   return (
     <ErrorBoundary mini>
       <div>
         {autofixData ? (
           <AutofixCard data={autofixData} onRetry={reset} />
         ) : (
-          <AutofixBanner triggerAutofix={triggerAutofix} />
+          <AutofixBanner
+            groupId={group.id}
+            triggerAutofix={triggerAutofix}
+            hasSuccessfulSetup={hasSuccessfulSetup}
+          />
         )}
       </div>
     </ErrorBoundary>

+ 33 - 0
static/app/components/events/autofix/useAutofixSetup.tsx

@@ -0,0 +1,33 @@
+import {useApiQuery, type UseApiQueryOptions} from 'sentry/utils/queryClient';
+import type RequestError from 'sentry/utils/requestError/requestError';
+
+export type AutofixSetupResponse = {
+  genAIConsent: {
+    ok: boolean;
+  };
+  integration: {
+    ok: boolean;
+    reason: string | null;
+  };
+  subprocessorConsent: {
+    ok: boolean;
+  };
+};
+
+export function useAutofixSetup(
+  {groupId}: {groupId: string},
+  options: Omit<UseApiQueryOptions<AutofixSetupResponse, RequestError>, 'staleTime'> = {}
+) {
+  const queryData = useApiQuery<AutofixSetupResponse>(
+    [`/issues/${groupId}/autofix/setup/`],
+    {enabled: Boolean(groupId), staleTime: 0, retry: false, ...options}
+  );
+
+  return {
+    ...queryData,
+    hasSuccessfulSetup: Boolean(
+      // TODO: Add other checks here when we can actually configure them
+      queryData.data?.integration.ok
+    ),
+  };
+}

+ 6 - 6
static/app/components/guidedSteps/guidedSteps.spec.tsx

@@ -6,15 +6,15 @@ describe('GuidedSteps', function () {
   it('can navigate through steps and shows previous ones as completed', async function () {
     render(
       <GuidedSteps>
-        <GuidedSteps.Step title="Step 1 Title">
+        <GuidedSteps.Step stepKey="step-1" title="Step 1 Title">
           This is the first step.
           <GuidedSteps.StepButtons />
         </GuidedSteps.Step>
-        <GuidedSteps.Step title="Step 2 Title">
+        <GuidedSteps.Step stepKey="step-2" title="Step 2 Title">
           This is the second step.
           <GuidedSteps.StepButtons />
         </GuidedSteps.Step>
-        <GuidedSteps.Step title="Step 3 Title">
+        <GuidedSteps.Step stepKey="step-3" title="Step 3 Title">
           This is the third step.
           <GuidedSteps.StepButtons />
         </GuidedSteps.Step>
@@ -40,15 +40,15 @@ describe('GuidedSteps', function () {
   it('starts at the first incomplete step', function () {
     render(
       <GuidedSteps>
-        <GuidedSteps.Step title="Step 1 Title" isCompleted>
+        <GuidedSteps.Step stepKey="step-1" title="Step 1 Title" isCompleted>
           This is the first step.
           <GuidedSteps.StepButtons />
         </GuidedSteps.Step>
-        <GuidedSteps.Step title="Step 2 Title" isCompleted={false}>
+        <GuidedSteps.Step stepKey="step-2" title="Step 2 Title" isCompleted={false}>
           This is the second step.
           <GuidedSteps.StepButtons />
         </GuidedSteps.Step>
-        <GuidedSteps.Step title="Step 3 Title" isCompleted={false}>
+        <GuidedSteps.Step stepKey="step-3" title="Step 3 Title" isCompleted={false}>
           This is the third step.
           <GuidedSteps.StepButtons />
         </GuidedSteps.Step>

+ 9 - 9
static/app/components/guidedSteps/guidedSteps.stories.tsx

@@ -23,15 +23,15 @@ export default storyBook(GuidedSteps, story => {
       </p>
       <SizingWindow display="block">
         <GuidedSteps>
-          <GuidedSteps.Step title="Step 1 Title">
+          <GuidedSteps.Step title="Step 1 Title" stepKey="step-1">
             This is the first step.
             <GuidedSteps.StepButtons />
           </GuidedSteps.Step>
-          <GuidedSteps.Step title="Step 2 Title">
+          <GuidedSteps.Step title="Step 2 Title" stepKey="step-2">
             This is the second step.
             <GuidedSteps.StepButtons />
           </GuidedSteps.Step>
-          <GuidedSteps.Step title="Step 3 Title">
+          <GuidedSteps.Step title="Step 3 Title" stepKey="step-3">
             This is the third step.
             <GuidedSteps.StepButtons />
           </GuidedSteps.Step>
@@ -61,15 +61,15 @@ export default storyBook(GuidedSteps, story => {
         </p>
         <SizingWindow display="block">
           <GuidedSteps>
-            <GuidedSteps.Step title="Step 1 Title">
+            <GuidedSteps.Step title="Step 1 Title" stepKey="step-1">
               This is the first step.
               <SkipToLastButton />
             </GuidedSteps.Step>
-            <GuidedSteps.Step title="Step 2 Title">
+            <GuidedSteps.Step title="Step 2 Title" stepKey="step-2">
               This is the second step.
               <GuidedSteps.StepButtons />
             </GuidedSteps.Step>
-            <GuidedSteps.Step title="Step 3 Title">
+            <GuidedSteps.Step title="Step 3 Title" stepKey="step-3">
               This is the third step.
               <GuidedSteps.StepButtons />
             </GuidedSteps.Step>
@@ -90,17 +90,17 @@ export default storyBook(GuidedSteps, story => {
         </p>
         <SizingWindow display="block">
           <GuidedSteps>
-            <GuidedSteps.Step title="Step 1 Title" isCompleted>
+            <GuidedSteps.Step title="Step 1 Title" stepKey="step-1" isCompleted>
               Congrats, you finished the first step!
               <GuidedSteps.StepButtons />
             </GuidedSteps.Step>
-            <GuidedSteps.Step title="Step 2 Title" isCompleted={false}>
+            <GuidedSteps.Step title="Step 2 Title" stepKey="step-2" isCompleted={false}>
               You haven't completed the second step yet, here's how you do it.
               <GuidedSteps.ButtonWrapper>
                 <GuidedSteps.BackButton />
               </GuidedSteps.ButtonWrapper>
             </GuidedSteps.Step>
-            <GuidedSteps.Step title="Step 3 Title" isCompleted={false}>
+            <GuidedSteps.Step title="Step 3 Title" stepKey="step-3" isCompleted={false}>
               You haven't completed the third step yet, here's how you do it.
               <GuidedSteps.ButtonWrapper>
                 <GuidedSteps.BackButton />

+ 85 - 48
static/app/components/guidedSteps/guidedSteps.tsx

@@ -1,13 +1,14 @@
 import {
-  Children,
   createContext,
-  isValidElement,
   useCallback,
   useContext,
+  useEffect,
   useMemo,
+  useRef,
   useState,
 } from 'react';
 import styled from '@emotion/styled';
+import orderBy from 'lodash/orderBy';
 
 import {type BaseButtonProps, Button} from 'sentry/components/button';
 import {IconCheckmark} from 'sentry/icons';
@@ -22,46 +23,111 @@ type GuidedStepsProps = {
 
 interface GuidedStepsContextState {
   currentStep: number;
+  getStepNumber: (stepKey: string) => number;
+  registerStep: (step: RegisterStepInfo) => void;
   setCurrentStep: (step: number) => void;
   totalSteps: number;
 }
 
 interface StepProps {
   children: React.ReactNode;
+  stepKey: string;
   title: string;
   isCompleted?: boolean;
-  stepNumber?: number;
 }
 
+type RegisterStepInfo = Pick<StepProps, 'stepKey' | 'isCompleted'>;
+type RegisteredSteps = {[key: string]: {stepNumber: number; isCompleted?: boolean}};
+
 const GuidedStepsContext = createContext<GuidedStepsContextState>({
   currentStep: 0,
   setCurrentStep: () => {},
   totalSteps: 0,
+  registerStep: () => 0,
+  getStepNumber: () => 0,
 });
 
 export function useGuidedStepsContext() {
   return useContext(GuidedStepsContext);
 }
 
-function Step({
-  stepNumber = 1,
-  title,
-  children,
-  isCompleted: completedOverride,
-}: StepProps) {
-  const {currentStep} = useGuidedStepsContext();
+function useGuidedStepsContentValue({
+  onStepChange,
+}: Pick<GuidedStepsProps, 'onStepChange'>): GuidedStepsContextState {
+  const registeredStepsRef = useRef<RegisteredSteps>({});
+  const [totalSteps, setTotalSteps] = useState<number>(0);
+  const [currentStep, setCurrentStep] = useState<number>(1);
+
+  // Steps are registered on initial render to determine the step order and which step to start on.
+  // This allows Steps to be wrapped in other components, but does require that they exist on first
+  // render and that step order does not change.
+  const registerStep = useCallback((props: RegisterStepInfo) => {
+    if (registeredStepsRef.current[props.stepKey]) {
+      return;
+    }
+    const numRegisteredSteps = Object.keys(registeredStepsRef.current).length + 1;
+    registeredStepsRef.current[props.stepKey] = {
+      isCompleted: props.isCompleted,
+      stepNumber: numRegisteredSteps,
+    };
+    setTotalSteps(numRegisteredSteps);
+  }, []);
+
+  const getStepNumber = useCallback((stepKey: string) => {
+    return registeredStepsRef.current[stepKey]?.stepNumber ?? 1;
+  }, []);
+
+  // On initial load, set the current step to the first incomplete step
+  useEffect(() => {
+    const firstIncompleteStep = orderBy(
+      Object.values(registeredStepsRef.current),
+      'stepNumber'
+    ).find(step => step.isCompleted !== true);
+
+    setCurrentStep(firstIncompleteStep?.stepNumber ?? 1);
+  }, []);
+
+  const handleSetCurrentStep = useCallback(
+    (step: number) => {
+      setCurrentStep(step);
+      onStepChange?.(step);
+    },
+    [onStepChange]
+  );
+
+  return useMemo(
+    () => ({
+      currentStep,
+      setCurrentStep: handleSetCurrentStep,
+      totalSteps,
+      registerStep,
+      getStepNumber,
+    }),
+    [currentStep, getStepNumber, handleSetCurrentStep, registerStep, totalSteps]
+  );
+}
+
+function Step(props: StepProps) {
+  const {currentStep, registerStep, getStepNumber} = useGuidedStepsContext();
+  const stepNumber = getStepNumber(props.stepKey);
   const isActive = currentStep === stepNumber;
-  const isCompleted = completedOverride ?? currentStep > stepNumber;
+  const isCompleted = props.isCompleted ?? currentStep > stepNumber;
+
+  useEffect(() => {
+    registerStep({isCompleted: props.isCompleted, stepKey: props.stepKey});
+  }, [props.isCompleted, props.stepKey, registerStep]);
 
   return (
     <StepWrapper data-test-id={`guided-step-${stepNumber}`}>
       <StepNumber isActive={isActive}>{stepNumber}</StepNumber>
       <div>
         <StepHeading isActive={isActive}>
-          {title}
+          {props.title}
           {isCompleted && <StepDoneIcon isActive={isActive} size="sm" />}
         </StepHeading>
-        {isActive && <ChildrenWrapper isActive={isActive}>{children}</ChildrenWrapper>}
+        {isActive && (
+          <ChildrenWrapper isActive={isActive}>{props.children}</ChildrenWrapper>
+        )}
       </div>
     </StepWrapper>
   );
@@ -105,44 +171,11 @@ function StepButtons() {
 }
 
 export function GuidedSteps({className, children, onStepChange}: GuidedStepsProps) {
-  const [currentStep, setCurrentStep] = useState<number>(() => {
-    // If `isCompleted` has been passed in, we should start at the first incomplete step
-    const firstIncompleteStepIndex = Children.toArray(children).findIndex(child =>
-      isValidElement(child) ? child.props.isCompleted !== true : false
-    );
-
-    return Math.max(1, firstIncompleteStepIndex + 1);
-  });
-
-  const totalSteps = Children.count(children);
-  const handleSetCurrentStep = useCallback(
-    (step: number) => {
-      setCurrentStep(step);
-      onStepChange?.(step);
-    },
-    [onStepChange]
-  );
-
-  const value = useMemo(
-    () => ({
-      currentStep,
-      setCurrentStep: handleSetCurrentStep,
-      totalSteps,
-    }),
-    [currentStep, handleSetCurrentStep, totalSteps]
-  );
+  const value = useGuidedStepsContentValue({onStepChange});
 
   return (
     <GuidedStepsContext.Provider value={value}>
-      <StepsWrapper className={className}>
-        {Children.map(children, (child, index) => {
-          if (!child) {
-            return null;
-          }
-
-          return <Step stepNumber={index + 1} {...child.props} />;
-        })}
-      </StepsWrapper>
+      <StepsWrapper className={className}>{children}</StepsWrapper>
     </GuidedStepsContext.Provider>
   );
 }
@@ -212,6 +245,10 @@ const StepDoneIcon = styled(IconCheckmark, {
 
 const ChildrenWrapper = styled('div')<{isActive: boolean}>`
   color: ${p => (p.isActive ? p.theme.textColor : p.theme.subText)};
+
+  p {
+    margin-bottom: ${space(1)};
+  }
 `;
 
 GuidedSteps.Step = Step;

+ 83 - 0
static/app/components/modals/autofixSetupModal.spec.tsx

@@ -0,0 +1,83 @@
+import {act, renderGlobalModal, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import {openModal} from 'sentry/actionCreators/modal';
+import {AutofixSetupModal} from 'sentry/components/modals/autofixSetupModal';
+
+describe('AutofixSetupModal', function () {
+  it('renders the integration setup instructions', async function () {
+    MockApiClient.addMockResponse({
+      url: '/issues/1/autofix/setup/',
+      body: {
+        genAIConsent: {ok: true},
+        integration: {ok: false},
+      },
+    });
+
+    const closeModal = jest.fn();
+
+    renderGlobalModal();
+
+    act(() => {
+      openModal(modalProps => <AutofixSetupModal {...modalProps} groupId="1" />, {
+        onClose: closeModal,
+      });
+    });
+
+    expect(await screen.findByText('Install the GitHub Integration')).toBeInTheDocument();
+    expect(
+      screen.getByText(/Install the GitHub integration by navigating to/)
+    ).toBeInTheDocument();
+  });
+
+  it('displays successful integration text when it is installed', async function () {
+    MockApiClient.addMockResponse({
+      url: '/issues/1/autofix/setup/',
+      body: {
+        genAIConsent: {ok: false},
+        integration: {ok: true},
+      },
+    });
+
+    const closeModal = jest.fn();
+
+    renderGlobalModal();
+
+    act(() => {
+      openModal(modalProps => <AutofixSetupModal {...modalProps} groupId="1" />, {
+        onClose: closeModal,
+      });
+    });
+
+    expect(
+      await screen.findByText(/The GitHub integration is already installed/)
+    ).toBeInTheDocument();
+  });
+
+  it('shows success text when steps are done', async function () {
+    MockApiClient.addMockResponse({
+      url: '/issues/1/autofix/setup/',
+      body: {
+        genAIConsent: {ok: true},
+        integration: {ok: true},
+      },
+    });
+
+    const closeModal = jest.fn();
+
+    renderGlobalModal();
+
+    act(() => {
+      openModal(modalProps => <AutofixSetupModal {...modalProps} groupId="1" />, {
+        onClose: closeModal,
+      });
+    });
+
+    expect(
+      await screen.findByText("You've successfully configured Autofix!")
+    ).toBeInTheDocument();
+
+    await userEvent.click(screen.getByRole('button', {name: "Let's go"}));
+
+    expect(closeModal).toHaveBeenCalled();
+  });
+});

+ 199 - 0
static/app/components/modals/autofixSetupModal.tsx

@@ -0,0 +1,199 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import type {ModalRenderProps} from 'sentry/actionCreators/modal';
+import {Button} from 'sentry/components/button';
+import {
+  type AutofixSetupResponse,
+  useAutofixSetup,
+} from 'sentry/components/events/autofix/useAutofixSetup';
+import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps';
+import HookOrDefault from 'sentry/components/hookOrDefault';
+import ExternalLink from 'sentry/components/links/externalLink';
+import LoadingError from 'sentry/components/loadingError';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
+import {IconCheckmark} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+
+interface AutofixSetupModalProps extends ModalRenderProps {
+  groupId: string;
+}
+
+const ConsentStep = HookOrDefault({
+  hookName: 'component:autofix-setup-step-consent',
+  defaultComponent: null,
+});
+
+function AutofixIntegrationStep({autofixSetup}: {autofixSetup: AutofixSetupResponse}) {
+  if (autofixSetup.integration.ok) {
+    return (
+      <Fragment>
+        {tct('The GitHub integration is already installed, [link: view in settings].', {
+          link: <ExternalLink href={`/settings/integrations/github/`} />,
+        })}
+        <GuidedSteps.StepButtons />
+      </Fragment>
+    );
+  }
+
+  if (autofixSetup.integration.reason === 'integration_inactive') {
+    return (
+      <Fragment>
+        <p>
+          {tct(
+            'The GitHub integration has been installed but is not active. Navigate to the [integration settings page] and enable it to continue.',
+            {
+              link: <ExternalLink href={`/settings/integrations/github/`} />,
+            }
+          )}
+        </p>
+        <p>
+          {tct(
+            'Once enabled, come back to this page. For more information related to installing the GitHub integration, read the [link:documentation].',
+            {
+              link: (
+                <ExternalLink href="https://docs.sentry.io/product/integrations/source-code-mgmt/github/" />
+              ),
+            }
+          )}
+        </p>
+        <GuidedSteps.StepButtons />
+      </Fragment>
+    );
+  }
+
+  if (autofixSetup.integration.reason === 'integration_no_code_mappings') {
+    return (
+      <Fragment>
+        <p>
+          {tct(
+            'You have an active GitHub installation, but no linked repositories. Add repositories to the integration on the [integration settings page].',
+            {
+              link: <ExternalLink href={`/settings/integrations/github/`} />,
+            }
+          )}
+        </p>
+        <p>
+          {tct(
+            'Once added, come back to this page. For more information related to installing the GitHub integration, read the [link:documentation].',
+            {
+              link: (
+                <ExternalLink href="https://docs.sentry.io/product/integrations/source-code-mgmt/github/" />
+              ),
+            }
+          )}
+        </p>
+        <GuidedSteps.StepButtons />
+      </Fragment>
+    );
+  }
+
+  return (
+    <Fragment>
+      <p>
+        {tct(
+          'Install the GitHub integration by navigating to the [link:integration settings page] and clicking the "Install" button. Follow the steps provided.',
+          {
+            link: <ExternalLink href={`/settings/integrations/github/`} />,
+          }
+        )}
+      </p>
+      <p>
+        {tct(
+          'Once installed, come back to this page. For more information related to installing the GitHub integration, read the [link:documentation].',
+          {
+            link: (
+              <ExternalLink href="https://docs.sentry.io/product/integrations/source-code-mgmt/github/" />
+            ),
+          }
+        )}
+      </p>
+      <GuidedSteps.StepButtons />
+    </Fragment>
+  );
+}
+
+function AutofixSetupSteps({autofixSetup}: {autofixSetup: AutofixSetupResponse}) {
+  return (
+    <GuidedSteps>
+      <ConsentStep hasConsented={autofixSetup.genAIConsent.ok} />
+      <GuidedSteps.Step
+        stepKey="integration"
+        title={t('Install the GitHub Integration')}
+        isCompleted={autofixSetup.integration.ok}
+      >
+        <AutofixIntegrationStep autofixSetup={autofixSetup} />
+      </GuidedSteps.Step>
+    </GuidedSteps>
+  );
+}
+
+function AutofixSetupContent({
+  groupId,
+  closeModal,
+}: {
+  closeModal: () => void;
+  groupId: string;
+}) {
+  const {data, isLoading, isError} = useAutofixSetup(
+    {groupId},
+    // Want to check setup status whenever the user comes back to the tab
+    {refetchOnWindowFocus: true}
+  );
+
+  if (isLoading) {
+    return <LoadingIndicator />;
+  }
+
+  if (isError) {
+    return <LoadingError message={t('Failed to fetch Autofix setup progress.')} />;
+  }
+
+  if (data.genAIConsent.ok && data.integration.ok) {
+    return (
+      <AutofixSetupDone>
+        <DoneIcon size="xxl" isCircled />
+        <p>{t("You've successfully configured Autofix!")}</p>
+        <Button onClick={closeModal} priority="primary">
+          {t("Let's go")}
+        </Button>
+      </AutofixSetupDone>
+    );
+  }
+
+  return <AutofixSetupSteps autofixSetup={data} />;
+}
+
+export function AutofixSetupModal({
+  Header,
+  Body,
+  groupId,
+  closeModal,
+}: AutofixSetupModalProps) {
+  return (
+    <Fragment>
+      <Header closeButton>
+        <h3>{t('Configure Autofix')}</h3>
+      </Header>
+      <Body>
+        <AutofixSetupContent groupId={groupId} closeModal={closeModal} />
+      </Body>
+    </Fragment>
+  );
+}
+
+const AutofixSetupDone = styled('div')`
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  padding: 40px;
+  font-size: ${p => p.theme.fontSizeLarge};
+`;
+
+const DoneIcon = styled(IconCheckmark)`
+  color: ${p => p.theme.success};
+  margin-bottom: ${space(4)};
+`;

Some files were not shown because too many files changed in this diff